This document provides a complete technical specification for the Analogue 3D labels.db file format, which stores cartridge label artwork for the N64 game carousel UI.
The labels.db file is a binary database that maps cartridge IDs to label artwork. This file is user-generated - you must create your own or obtain one from the community. The Analogue 3D does not ship with a pre-populated labels.db.
| Property | Value |
|---|---|
| Location on SD Card | /Library/N64/Images/labels.db |
| Image Dimensions | 74 × 86 pixels |
| Bytes Per Image Slot | 25,600 (25,456 image data + 144 padding) |
| Color Format | BGRA (Blue, Green, Red, Alpha) |
| Endianness | Little-endian |
Stock Labels: GitHub user retrogamecorps has shared a collection of stock image labels that were provided to Analogue 3D reviewers: https://github.com/retrogamecorps/Analogue-3D-Images
This is an excellent starting point for building your own labels.db or obtaining artwork for known N64 titles.
The file consists of four sections. The size of the file varies based on how many cartridge entries it contains.
┌─────────────────────────────────────────────────────────────┐
│ HEADER (256 bytes) 0x000 - 0x0FF│
├─────────────────────────────────────────────────────────────┤
│ CARTRIDGE ID TABLE (variable) 0x100 - ... │
│ N entries × 4 bytes each │
├─────────────────────────────────────────────────────────────┤
│ PADDING (variable) ... - 0x40FF │
│ Filled with 0xFF │
├─────────────────────────────────────────────────────────────┤
│ IMAGE DATA (variable) 0x4100 - EOF │
│ N images × 25,600 bytes each │
└─────────────────────────────────────────────────────────────┘
For a labels.db with N cartridge entries:
Header: 256 bytes (fixed)
ID Table: N × 4 bytes
Padding: (0x4100 - 0x100) - (N × 4) bytes
Images: N × 25,600 bytes
─────────────────────────────────────────
Total: 0x4100 + (N × 25,600) bytes
The header is exactly 256 bytes (0x100).
| Offset | Size | Content | Description |
|---|---|---|---|
| 0x00 | 1 | 0x07 |
Magic byte |
| 0x01 | 11 | Analogue-Co |
Identifier string (ASCII) |
| 0x0C | 20 | 0x00... |
Null padding |
| 0x20 | 18 | Analogue-3D.labels |
File type identifier (ASCII) |
| 0x32 | 14 | 0x00... |
Null padding to 32 bytes |
| 0x40 | 4 | 0x00 0x00 0x02 0x00 |
Version: 2.0 (little-endian) |
| 0x44 | 188 | 0x00... |
Reserved (zeros) |
To verify a valid labels.db file:
Byte 0x00 must equal 0x07
Bytes 0x01-0x0B must equal "Analogue-Co" (ASCII)
00000000: 07 41 6e 61 6c 6f 67 75 65 2d 43 6f 00 00 00 00 .Analogue-Co....
00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000020: 41 6e 61 6c 6f 67 75 65 2d 33 44 2e 6c 61 62 65 Analogue-3D.labe
00000030: 6c 73 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ls..............
00000040: 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
... (remaining header bytes 0x50-0xFF are 0x00) ...
The cartridge ID table contains one 4-byte entry per cartridge in the database.
| Property | Value |
|---|---|
| Start Offset | 0x100 (256) |
| Entry Size | 4 bytes |
| Format | 32-bit unsigned integer, little-endian |
| Sorting | Ascending numerical order (required) |
| Maximum Entries | 4,096 (limited by padding space before 0x4100) |
Each cartridge ID is a 32-bit value stored in little-endian format:
| Display Format | Stored Bytes | Example Game |
|---|---|---|
0x03cc04ee |
ee 04 cc 03 |
Mario Kart 64 |
0xac631da0 |
a0 1d 63 ac |
GoldenEye 007 |
0xb393776d |
6d 77 93 b3 |
Super Mario 64 |
The index position of a cartridge ID in this sorted table directly corresponds to its image position in the image data section.
Image Offset = 0x4100 + (table_index × 25,600)
To determine how many entries exist in a labels.db file:
entry_count = (file_size - 0x4100) / 25,600
Or by scanning the ID table from 0x100 until you encounter 0xFFFFFFFF (padding).
| Property | Value |
|---|---|
| End Offset | 0x40FF (16,639) |
| Fill Value | 0xFF |
The padding fills the space between the end of the ID table and the start of image data at 0x4100. All bytes in this range are 0xFF.
As entries are added to the database, the ID table grows and the padding shrinks. The image data always starts at 0x4100.
The image data section contains consecutive images with no gaps between them.
| Property | Value |
|---|---|
| Start Offset | 0x4100 (16,640) |
| Bytes Per Image | 25,600 |
| Property | Value |
|---|---|
| Width | 74 pixels |
| Height | 86 pixels |
| Color Depth | 32-bit (4 bytes per pixel) |
| Pixel Count | 6,364 (74 × 86) |
| Bytes Per Image | 25,456 (6,364 × 4) |
| Slot Padding | 144 bytes (filled with 0xFF) |
| Total Slot Size | 25,600 bytes |
Critical: Pixels are stored in BGRA order, not RGBA.
| Byte Offset | Channel | Description |
|---|---|---|
| +0 | Blue | Blue component (0-255) |
| +1 | Green | Green component (0-255) |
| +2 | Red | Red component (0-255) |
| +3 | Alpha | Alpha/transparency (0-255, typically 0xFF for opaque) |
When reading images for display (BGRA → RGBA):
rgba[0] = bgra[2]; // Red from byte 2
rgba[1] = bgra[1]; // Green stays
rgba[2] = bgra[0]; // Blue from byte 0
rgba[3] = bgra[3]; // Alpha stays
When writing images to file (RGBA → BGRA):
bgra[0] = rgba[2]; // Blue from byte 2
bgra[1] = rgba[1]; // Green stays
bgra[2] = rgba[0]; // Red from byte 0
bgra[3] = rgba[3]; // Alpha stays
Pixels are stored in row-major order (left-to-right, top-to-bottom):
Row 0: pixels 0-73 (offset +0 to +295)
Row 1: pixels 74-147 (offset +296 to +591)
...
Row 85: pixels 6290-6363 (offset +25160 to +25455)
Padding: 144 bytes of 0xFF (offset +25456 to +25599)
To find the byte offset for image at index i:
offset = 0x4100 + (i × 25,600)
offset = 0x4100 + (i × 0x6400)
- Parse ID Table: Read cartridge IDs from offset 0x100 until 0xFFFFFFFF
- Search for Cart ID: Find the target cartridge ID in the sorted table
- Get Index: Note the index position (0-based)
- Calculate Offset:
offset = 0x4100 + (index × 25,600) - Read Image: Extract 25,456 bytes of pixel data from calculated offset (ignore 144-byte padding)
- Convert BGRA→RGBA: Swap blue and red channels for display
- Find Cart ID Index: Locate cartridge in sorted ID table
- Prepare Image: Resize to 74×86 pixels
- Convert RGBA→BGRA: Swap red and blue channels
- Calculate Offset:
offset = 0x4100 + (index × 25,600) - Write Image: Write 25,456 bytes of BGRA pixel data at calculated offset
- Write Padding: Fill remaining 144 bytes with 0xFF
Adding a new cartridge requires modifying both the ID table and image data:
- Find Insertion Point: Binary search for correct sorted position in ID table
- Expand ID Table: Insert new 4-byte ID at sorted position
- Shift Existing IDs: Move all subsequent IDs down by 4 bytes
- Expand Image Section: Allocate new 25,600 bytes at corresponding image position
- Shift Existing Images: Move all subsequent images down by 25,600 bytes
- Write New Image: Insert new image data at calculated position
- Update File Size: File grows by 25,604 bytes (4 + 25,600)
Note: After adding entries, the padding between the ID table and 0x40FF decreases accordingly.
To create a labels.db from scratch:
- Write Header: 256 bytes as specified above
- Write ID Table: Sorted list of cartridge IDs at 0x100
- Fill Padding: 0xFF bytes from end of ID table to 0x40FF
- Write Images: Sequential 25,600-byte BGRA images starting at 0x4100
A labels.db with zero entries would be:
- Header (256 bytes)
- Padding filled with 0xFF from 0x100 to 0x40FF
- No image data
- Total size: 16,640 bytes (0x4100)
Cartridge IDs are CRC32 checksums of the first 8 KiB (8,192 bytes) of ROM data in big-endian (Z64) format.
For complete details on the algorithm, ROM format conversion, and a utility script to compute IDs from ROM files, see Cart ID Algorithm.
| Hex ID | Game Title |
|---|---|
03cc04ee |
Mario Kart 64 |
04079b93 |
Super Smash Bros. |
ac631da0 |
GoldenEye 007 |
b04b4109 |
Star Fox 64 |
b393776d |
Super Mario 64 |
e5240d18 |
The Legend of Zelda: Ocarina of Time |
fffffffe |
Unknown Cartridge (placeholder) |
// labels.db format constants
const HEADER_SIZE = 0x100; // 256 bytes
const ID_TABLE_START = 0x100; // 256
const DATA_START = 0x4100; // 16,640
const IMAGE_WIDTH = 74; // pixels
const IMAGE_HEIGHT = 86; // pixels
const BYTES_PER_PIXEL = 4; // BGRA
const IMAGE_DATA_SIZE = 25456; // 74 × 86 × 4 (actual pixel data)
const IMAGE_SLOT_SIZE = 25600; // Total slot including 144 bytes padding
const SLOT_PADDING = 144; // 0xFF padding at end of each slotOffset Description Size Value/Format
───────────────────────────────────────────────────────────────────────────────────
0x000 Magic byte 1 0x07
0x001 Identifier 11 "Analogue-Co"
0x00C Padding 20 0x00
0x020 File type 32 "Analogue-3D.labels\0..."
0x040 Version 4 0x00020000 (LE)
0x044 Reserved 188 0x00
0x100 Cartridge ID table N × 4 uint32_le (sorted ascending)
... Padding variable 0xFF
0x4100 Image data N × 25600 BGRA pixels (74×86) + 144 bytes padding
───────────────────────────────────────────────────────────────────────────────────
Image Slot Structure (25,600 bytes each):
┌────────────────────────────────────────┬──────────────┐
│ BGRA Pixel Data (74×86×4 = 25,456 B) │ Padding (144)│
└────────────────────────────────────────┴──────────────┘
When writing labels.db to an SD card, transfer performance varies significantly based on chunk size and sync behavior. A3D Manager includes a Chunk Size Benchmark on the Settings page to test your specific SD card.
Testing on a typical SD card showed:
| Configuration | Avg Speed | Duration (22MB) |
|---|---|---|
| 4MB (no fsync) | ~822 KB/s | ~27s |
| 256KB (no fsync) | ~806 KB/s | ~28s |
| 2MB + fsync | ~784 KB/s | ~29s |
| 1MB + fsync | ~752 KB/s | ~30s |
| 256KB + fsync | ~620 KB/s | ~36s |
| 64KB + fsync | ~360 KB/s | ~63s |
Key findings:
- Larger chunks are faster: 4MB chunks outperform 64KB by ~2.3x
- fsync per chunk is slower: Syncing after each write adds significant overhead
- No fsync mode: Buffers writes and syncs once at the end (fastest, but progress bar won't be accurate)
Transfer settings can be configured via environment variables in .env:
# Chunk size in bytes (default: 2097152 = 2MB)
TRANSFER_CHUNK_SIZE=2097152
# Whether to fsync after each chunk (default: true)
# true = accurate progress bar (recommended)
# false = ~5% faster but progress won't reflect actual disk writes
TRANSFER_FSYNC_PER_CHUNK=true| Setting | Pros | Cons |
|---|---|---|
| Large chunks + fsync | Fast with accurate progress | Slightly slower than no-fsync |
| Large chunks + no fsync | Absolute fastest | Progress bar jumps to 100% immediately |
| Small chunks + fsync | Most granular progress | Significantly slower |
Recommendation: Use the defaults (2MB + fsync) for the best balance of speed and accurate progress tracking. Only set TRANSFER_FSYNC_PER_CHUNK=false if you don't need real-time progress feedback.
This specification is validated by a comprehensive test suite. See TESTING.md for details on running the tests.
The test suite includes:
- Pixel-perfect round-trip verification (write → read → compare)
- Binary format compliance checks
- CRUD operation tests (Create, Read, Update, Delete)
- Edge case handling (empty database, sorted insertion)