TRE Header Layout & Sections¶
This page covers the TRE sub-header layout, map levels (TRE1), subdivisions (TRE2), raster layer offsets (TRE7), object type parameters (TRE8), draw order, and attribution. The TRE section defines the spatial index that organizes tile data stored in Tile Storage.
5. TRE Header Layout and Section Offsets¶
5.1 TRE Header Structure (Raster Maps, 273 bytes)¶
The TRE sub-header in raster maps uses an extended 273-byte format, significantly larger than vector maps (116-188 bytes). The layout below was verified against the QMapShack wiki analysis by Alex Whiter and confirmed with IOM.img and other reference files.
Common sub-header prefix (21 bytes):
| Offset | Size | Field | Value |
|---|---|---|---|
| 0x00 | 2 | Header length | 273 (0x0111) |
| 0x02 | 10 | Signature | GARMIN TRE |
| 0x0C | 1 | Version | 1 |
| 0x0D | 1 | Lock | 0 |
| 0x0E | 7 | Date | 7-byte Garmin date |
Bounds and section descriptors:
| TRE Offset | Size | Field | Description |
|---|---|---|---|
| 0x15 | 3 | North bound | 3-byte signed LE, map units |
| 0x18 | 3 | East bound | 3-byte signed LE, map units |
| 0x1B | 3 | South bound | 3-byte signed LE, map units |
| 0x1E | 3 | West bound | 3-byte signed LE, map units |
| 0x21 | 8 | TRE1 (levels) | pos(4) + size(4) — GMP-relative offset to level data |
| 0x29 | 8 | TRE2 (subdivisions) | pos(4) + size(4) — GMP-relative offset to subdivision data |
| 0x31 | 10 | TRE3 (copyright) | pos(4) + size(4) + item_size(2) — GMP-relative |
| 0x3B | 4 | Padding | Zeros |
| 0x3F | 1 | Flags | 0x00 or 0x01 |
| 0x40 | 2 | Display priority | uint16 LE (20 for IOM and our output, 24 for some references) |
| 0x42 | 8 | Parameters | 8-byte parameter block. IOM: 10 01 08 24 00 01 00 00. Single-map: 00 01 04 24 00 01 00 00. Our output matches IOM. Byte 0x42 is a flag (0x00=single-map, 0x10=IOM). Byte 0x44 is likely bits-per-coord (4=single-map, 8=IOM). Byte 0x45=0x24 (36) is a tile size constant. |
| 0x4A | 14 | TRE4 descriptor | pos(4) + size(4) + rec_size(2) + pad(4) — GMP-relative |
| 0x58 | 14 | TRE5 descriptor | pos(4) + size(4) + rec_size(2) + pad(4) — GMP-relative |
| 0x66 | 14 | TRE6 descriptor | pos(4) + size(4) + rec_size(2) + pad(4) — GMP-relative |
| 0x74 | 4 | Map ID | uint32 LE |
| 0x78 | 4 | Padding | Zeros |
| 0x7C | 14 | TRE7 (raster layers) | pos(4) + size(4) + rec_size(2) + pad(4) — GMP-relative |
| 0x8A | 14 | TRE8 (object types) | pos(4) + size(4) + rec_size(2) + pad(6) — GMP-relative |
| 0x9A | 16 | Map ID hash | 16-byte hash value |
| 0xAA | 4 | Padding | Zeros |
| 0xAE | 14 | TRE9 descriptor | pos(4) + size(4) + rec_size(2) + pad(4) — GMP-relative |
| 0xBC | 14 | TRE10 descriptor | pos(4) + size(4) + rec_size(2) + pad(4) — GMP-relative |
| 0xCA | 5 | Padding | Zeros |
| 0xCF | 4 | Matching number | uint32 LE |
| 0xD3 | rest | Map name | Null-terminated ASCII string |
Critical: GMP-Relative Offsets. All pos values in the section descriptors above (TRE1 through TRE10) are offsets relative to the start of the GMP data, NOT relative to the TRE block start. This is different from what the 2005 Mechalas spec documents for vector maps, where positions are TRE-relative. For raster maps in GMP containers, positions are always GMP-relative.
5.2 TRE1 — Map Levels (Zoom Level Table)¶
TRE1 contains the zoom level definitions as an array of 4-byte records:
byte 0: zoom_code — determines at which map scale this level is active
byte 1: level_number (bits) — coordinate precision (shift = 24 - level_number)
bytes 2-3: number_of_subdivisions (uint16 LE)
Critical: Byte 0 is zoom_code, byte 1 is level_number. This is the OPPOSITE of what some documentation claims. Confirmed via reference binary analysis and GPXSee source (trefile.cpp:107-111):
_levels[i].level = *zoom; // byte0 = zoom_code
_levels[i].bits = *(zoom + 1); // byte1 = level_number
Level number (bits) and coordinate precision:
The level_number field determines coordinate precision for subdivision width/height and RGN2 delta encoding. The shift value is max(0, 24 - level_number). Higher level_number = less shift = better precision.
Important: For raster maps, the level_number must be high enough that the quantization step (2^shift × 360 / 2^24 degrees) is smaller than the tile size. Otherwise, GPXSee's copyPolys() boundingRect filtering will drop tiles because the single-point boundingRect (derived from delta << shift) can land outside the view rect.
Level number remapping: The writer remaps level_numbers from the actual zoom levels to the range 24 - N + 1 .. 24 (where N = number of zoom levels), ensuring the most detailed level has level_number=24 (shift=0, no quantization error). This matches patterns observed in reference files: 5 levels → level_numbers 20-24.
Example with 12 zoom levels (zooms 6-17): - Config zoom levels: 6, 7, 8, ..., 17 - Remapped level_numbers: 13, 14, 15, ..., 24 - Shift values: 11, 10, 9, ..., 0
Zoom code computation:
- Zoom code 0 = most detailed (highest zoom level)
- Higher zoom codes = less detailed (overview levels)
- Only the first (most zoomed-out) level gets the inherited flag (0x80) per mkgmap
- Pattern: level 0 gets
0x80 + (N-1), remaining levels count down fromN-2to0
Observed values from reference files:
| File | Zoom Codes (byte 0) | Level Numbers (byte 1) | Subdivisions |
|---|---|---|---|
| Single-map reference | 0x84, 0x83, 0x02, 0x01, 0x00 | 20, 21, 22, 23, 24 | 1, 3, 138, 156, 300 |
| IOM subfile 355951 | 0x87, 0x06, 0x05, ..., 0x00 | 17, 18, 19, ..., 24 | 1 each (8 total) |
Single-map reference decoded level 0: code=0x84 (inherited, bit 7 set + value 4), bits=20. GPXSee skips inherited levels for data rendering.
5.3 TRE2 — Group/Subdivision Section¶
TRE2 contains subdivision records that define the spatial index for map data. The record size depends on the zoom level: 16 bytes for non-last levels and 14 bytes for the last (most detailed) level. After all subdivision records, there are 4 trailing bytes containing the total RGN2 data extent as uint32 LE.
16-byte record (non-last zoom levels):
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | RGN offset/flags | uint32 LE: bits 31-28 = has-polygons/lines/points flags, bits 27-0 = RGN2 offset |
| 4 | 3 | Longitude center | 3-byte signed LE, map units (degrees × 2^24 / 360) |
| 7 | 3 | Latitude center | 3-byte signed LE, map units (degrees × 2^24 / 360) |
| 10 | 2 | Width | uint16 LE: bit 15 = end of chain marker, bits 14-0 = encoded width |
| 12 | 2 | Height | uint16 LE |
| 14 | 2 | Next level index | uint16 LE, 1-based global subdivision number of first child at next zoom level |
14-byte record (last zoom level — no next_level field):
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | RGN offset/flags | uint32 LE: bits 31-28 = has-polygons/lines/points flags, bits 27-0 = RGN2 offset |
| 4 | 3 | Longitude center | 3-byte signed LE, map units |
| 7 | 3 | Latitude center | 3-byte signed LE, map units |
| 10 | 2 | Width | uint16 LE (no end-of-chain bit in last level) |
| 12 | 2 | Height | uint16 LE |
Trailing bytes: 4 bytes (uint32 LE) containing total RGN2 data size. This is the sentinel value used by GPXSee to determine the end of the last subdivision's RGN2 segment.
Width/height encoding:
Width and height are encoded with a precision-reducing shift. The shift is max(0, 24 - level_number) where level_number comes from TRE1 byte 1 for this zoom level. The encoding formula:
shift = max(0, 24 - level_number)
mask = (1 << shift) - 1
width = ((2 * (center_mu - west_mu) + 1) // 2 + mask) >> shift
height = ((2 * (center_mu - south_mu) + 1) // 2 + mask) >> shift
For non-last levels: width |= 0x8000 only on the LAST subdivision in each
chain (bit 15 = end of chain marker per PDF spec)
Where center_mu, west_mu, south_mu are the subdivision bounds in 24-bit map units (degrees × 2^24 / 360). The +1 // 2 rounding ensures the encoded value rounds up to cover the full subdivision area.
Decoding (in GPXSee): The subdivision bounds are reconstructed from center + encoded width/height: - West = center_lon - (width << shift) - South = center_lat - (height << shift)
TRE2 section size: Sum of all record sizes (16 × non-last subdivs + 14 × last-level subdivs + 4 trailing bytes).
Example from a single-map reference:
Level 0 (overview): 1 subdiv, w=1, h=1, shift=4 → ~0.09° × 0.07° actual size
Level 4 (detail): 300 subdivs, larger w/h values, shift=0 → precise bounds
Total: 560 subdivisions across 5 levels
Note: The 3-byte coordinate encoding in TRE2 uses the older map units format (degrees × 2^24 / 360), distinct from the 4-byte signed int32 coordinates (degrees × 2^31 / 180) used in RGN2 compound records.
5.4 TRE7 — Raster Layer Section¶
TRE7 defines an offset table that maps subdivisions to their raster layer data in RGN2. Each entry corresponds to one subdivision and provides the byte offset into RGN2 where that subdivision's data begins. Adjacent entries form segment boundaries — subdivision N's data spans from offset[N] to offset[N+1] (see RGN2 Segment Boundaries for details).
The section descriptor at TRE+0x7C includes a rec_size field that determines the record format.
TRE7 descriptor header (at TRE+0x7C):
pos(4): GMP-relative offset to TRE7 data
size(4): Total size of TRE7 data
rec_size(2): Size of each record in bytes
pad(4): Zeros
TRE7 _flags field (at TRE+0x86):
A 4-byte flags value that determines how each TRE7 entry is parsed. The flags indicate which offset types are present in each entry:
| Flag bit | Meaning when set |
|---|---|
| 0 | Polygons — entry contains uint32 polygon offset |
| 1 | Lines — entry contains uint32 line offset |
| 2 | Points — entry contains uint32 point offset |
For extended-format references (_flags = 0x00000481), bit 0 (polygons) and bit 2 (points) are set, meaning readExtEntry() reads 4+4=8 bytes per entry. For IOM and our output (_flags = 0x00000001), only bit 0 (polygons) is set, reading just 4 bytes per entry.
Record format:
| Variant | rec_size | Format |
|---|---|---|
| Simple (IOM/ours) | 4 | uint32 LE offset into RGN2 |
| Extended (rec_size=5) | 5 | uint32 LE offset + 1 byte flag |
Extended TRE7 entry flag byte:
| Value | Meaning |
|---|---|
| 0x01 | Empty/overview subdivision (no tiles) |
| 0x00 | Data subdivision (contains tile data) |
Segment boundary interpretation:
TRE7 has N+1 entries for N subdivisions. The extra entry is a sentinel containing the total RGN2 data extent. The segment for subdivision i spans:
The sentinel is required by GPXSee's subdivision parser: it reads diff = totalSubdivs - (size / recSize) + 1 to determine which subdivisions get TRE7 entries, and then reads one extra entry after the last subdivision to call setExtEnds() on it. Without the sentinel, diff would be 1, causing the first subdivision to be skipped, and the last subdivision's segment would have no end boundary.
These offsets are relative to the RGN2 base position stored at RGN header offset 0x1D. To get absolute GMP positions: abs_pos = RGN2_base + TRE7[i].offset.
IOM subfile 00355951 example (rec_size=4):
Offset table: [0, 46, 92, 138, 184, 243, 361, 420]
→ 8 entries pointing to raster layer descriptions in RGN2 for 8 zoom levels
Single-map example (rec_size=5):
748 entries with uint32 offset + 1 byte flag each
→ Points to raster layer descriptions for 560 groups across 5 zoom levels
+1 sentinel entry (all zeros) marking end of data
5.5 TRE8 — Object Type Parameters¶
TRE8 defines object type parameters used by the renderer. The section contains 3-byte records.
TRE8 record format (3 bytes each):
Observed values:
| File | Entries | Description |
|---|---|---|
| IOM subfile 355951 | 2 entries: 06 06 13 and 0D 06 01 |
Polyline (0x06) + Polygon (0x0D) types |
| Single-map reference | 1 entry: 13 06 06 |
Raster tiles only |
| Our output | 2 entries: 06 06 13 and 0D 06 01 |
Matches IOM reference |
TRE8 entry decoding:
Each 3-byte record declares an object type: byte 0 = type code, byte 1 = parameter, byte 2 = subtype/version.
- Type
0x06(polyline): Used for raster tile polylines. Parameter0x06, subtype0x13(= 19, the raster subtype identifier). - Type
0x0D(polygon): Used for DATA_BOUNDS polygons. Parameter0x06, subtype0x01.
Both types must be declared for the Garmin device to correctly parse raster tile data.
5.6 Multi-Resolution Pyramid¶
Single-map reference files use 5 zoom levels (20-24), forming a pyramid where each level covers the same geographic area with different tile counts and resolutions. IOM uses 8 zoom levels (17-24).
For our implementation, we support configurable zoom levels with the zoom_code specified per level.
5.7 JNX Format Comparison¶
JNX (used by Garmin BirdsEye and the original format) is a simpler raster map format. Some raster IMG files were converted from JNX using Garmin tools. Understanding JNX's approach helps explain why IMG raster requires careful subdivision handling.
JNX tile positioning: Each tile stores its own 32-bit bounding rectangle (north, south, east, west as int32 LE) with NO quantization or subdivision scheme. Tiles are independently positioned at full precision, making gap-free display trivial.
IMG tile positioning: Tiles are positioned relative to subdivision centers via 16-bit deltas with shift = 24 - level_number. This introduces quantization at the subdivision level. The bitstream produces a full-tile boundingRect via 1 delta pair (+width, +height) from P0 to P1, used by copyPolys() for tile filtering. Absolute 32-bit bounds handle rendering.
Key differences:
| Aspect | JNX | IMG (raster) |
|---|---|---|
| Tile bounds | Independent 32-bit rect per tile | Subdivision-relative 16-bit deltas |
| Quantization | None | Shift = 24 - level_number |
| Spatial indexing | Per-tile bounds | TRE2 subdivision grid |
| Tile filtering | Direct bounds comparison | copyPolys() via boundingRect |
| Rendering | Direct | Absolute 32-bit from readRasterInfo |
| Gap risk | None (full precision) | Quantization at low level_numbers |
Why this matters for white lines: JNX has no subdivision concept, so tiles are always gap-free. IMG's subdivision-relative encoding can produce white lines when: (1) subdivision bounds don't cover all tile positions, (2) boundingRect quantization exceeds tile extent, or (3) subdivision centers are misaligned with tile positions. Our implementation avoids these by using tile-derived subdivision bounds and centers, and by remapping level_numbers to ensure coordinate precision exceeds tile size.
7. Draw Order and Attribution¶
7.1 Display Priority¶
The TRE sub-header contains a display priority field:
- Value: 20 (matching IOM reference, optimal for raster basemaps)
- Determines rendering order when multiple maps overlap
- Higher values are drawn on top
- Some references use 24 (drawn above vector overlays), IOM uses 20 (drawn below)
7.2 Map Metadata¶
| Field | Location | Max Length | Encoding |
|---|---|---|---|
| Map name | Header 0x49 + MPS | 20/32 bytes | ASCII |
| Description | GMP "Raster Map\0" | Variable | ASCII |
| Copyright | GMP copyright strings | Variable | CP-1252 |
| Map ID | FAT entry name | 8 bytes | Hex ASCII |
7.3 Map ID¶
- 8-character hexadecimal identifier (e.g.,
09C102B0) - Used as the GMP subfile name in the FAT directory
- Unique per map file