Version: 1.0
Last Updated: 2025-02-17
Author(s): samarixum
The .txd (Texture Dictionary) format is a custom EA/RenderWare variant used by The Simpsons Game on PS3. While it carries the name from standard RenderWare texture containers, it uses bespoke markers and structure instead of canonical RW chunk headers. The format stores textures with various compression and swizzling schemes.
Endianness: Mixed (see details per section).
Character Encoding: UTF-8 for texture names.
16 00 00 00 (0x00000016 LE, or "TexDict" identifier)03 00 00 00 14 00 00 00 2D 00 02 1C00 00)01 <format_code>)| Offset | Size | Type | Endian | Description |
|---|---|---|---|---|
| 0x00 | 4 | UInt32 | LE | Magic marker: 0x00000016. Identifies as Texture Dictionary. |
| 0x04 | ... | Variable | - | Scan forward for texture records (see Texture Record section). |
| Marker (Hex) | Purpose | Notes |
|---|---|---|
| 03 00 00 00 14 00 00 00 2D 00 02 1C 2F EA 00 00 08 00 00 00 2D 00 02 1C | SIG_BLOCK_START + variable data | Marks start of a texture block or section transition. Variable 8-byte area may contain timestamps/metadata. |
| 2D 00 02 1C 00 00 00 0A | Texture Name Signature | Precedes texture name string. Followed by 12 bytes of padding/data, then UTF-8 name terminated by 00 00. |
| 03 00 00 00 00 00 00 00 2D 00 02 1C | EOF_SUFFIX | Marks end of file. Appears exactly once at the end of valid TXD files. |
Scanning algorithm: Locate the texture name signature (2D 00 02 1C 00 00 00 0A) in the file. When found, extract the texture name and locate its associated metadata.
2D 00 02 1C 00 00 00 0A (8 bytes)00 00)._-texture_at_0x<OFFSET>Example: File contains 2D 00 02 1C 00 00 00 0A 00 00 00 00 00 00 00 00 00 00 00 00 48 65 6C 6C 6F 00 00
Extract: offset +20 bytes reads "Hello" (48 65 6C 6C 6F), then double null (00 00) terminates.
01 <format_code>
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 1 | UInt8 | - | Meta0 | Unknown/reserved. Usually 0x00 or carries flag bits. |
| 0x01 | 1 | UInt8 | - | Meta1 | Unknown/reserved. Usually 0x00. |
| 0x02 | 1 | UInt8 | - | Preamble_0x01 | Must be 0x01. This is the preamble marker we searched for. |
| 0x03 | 1 | UInt8 | - | Format Code | Pixel format identifier. See Format Code Table below. |
| 0x04 | 2 | UInt16 | BE | Width | Image width in pixels. Big-Endian. Validate: > 0. |
| 0x06 | 2 | UInt16 | BE | Height | Image height in pixels. Big-Endian. Validate: > 0. |
| 0x08 | 1 | UInt8 | - | MetaA | Unknown. Often 0x1 or related to mipmaps. |
| 0x09 | 1 | UInt8 | - | Mip Map Count | Number of mipmap levels. If 0, assume 1 (base only). Used for DDS header. |
| 0x0A | 1 | UInt8 | - | MetaB | Unknown. Usually 0x00. |
| 0x0B | 1 | UInt8 | - | Padding | Unused. Usually 0x00. |
| 0x0C | 4 | UInt32 | LE | Total Pixel Data Size | Size in bytes of the pixel data blob following this metadata. Must be > 0 and fit within segment. |
Location: Immediately after the 16-byte metadata block (at offset metadata_block_start + 16).
Length: Total Pixel Data Size bytes (from metadata).
| Format Code (Hex) | Format Name | Compression | Bytes Per Pixel (if uncompressed) | Handling |
|---|---|---|---|---|
| 0x52 | DXT1 | Block-Compressed (4:1 ratio) | N/A | Copy raw data to DDS with DXT1 FourCC. No further processing. |
| 0x53 | DXT3 | Block-Compressed (2:1 ratio) | N/A | Copy raw data to DDS with DXT3 FourCC. No further processing. |
| 0x54 | DXT5 | Block-Compressed (2:1 ratio) | N/A | Copy raw data to DDS with DXT5 FourCC. No further processing. |
| 0x86 | Swizzled BGRA | Uncompressed (Morton-swizzled) | 4 | Unswizzle using Morton/Z-order decoding. Output is 32-bit BGRA. Export as DDS RGBA8888. |
| 0x02 | Swizzled Indexed (A8 or P8A8) | Uncompressed (Morton-swizzled) | 1 or 2 | Unswizzle. Output is palettized or alpha-only. May require palette lookup. |
Purpose: PS3 stores textures in Morton/Z-order to improve cache coherency. Data must be reordered to linear memory layout.
Morton Encoding Function (pseudo-code):
function morton_encode_2d(x, y) {
// Interleave bits of x and y
x = (x | (x << 8)) & 0x00FF00FF;
x = (x | (x << 4)) & 0x0F0F0F0F;
x = (x | (x << 2)) & 0x33333333;
x = (x | (x << 1)) & 0x55555555;
y = (y | (y << 8)) & 0x00FF00FF;
y = (y | (y << 4)) & 0x0F0F0F0F;
y = (y | (y << 2)) & 0x33333333;
y = (y | (y << 1)) & 0x55555555;
return x | (y << 1);
}
Unswizzling Algorithm:
Current Implementation: The pixel data blob contains only the base mipmap level (highest resolution). Additional mipmap levels are not separately parsed or extracted.
DDS Export: If Mip Map Count > 0, set the DDSD_MIPMAPCOUNT flag in the DDS header to inform readers that mipmaps should exist (even though only base level is exported). This maintains compatibility with engines that expect mipmap headers.
For DXT Formats (0x52, 0x53, 0x54):
44 44 53 20 ("DDS ")(width/4) * (height/4) * block_size where block_size = 8 (DXT1) or 16 (DXT3/DXT5)For RGBA Formats (0x86 after unswizzle):
function parse_txd(file_data) {
textures = []
// Validate header
if (read_u32_le(0) != 0x16) return []
// Scan entire file for texture name signatures
pos = 0
while (pos < len(file_data)) {
if (matches_pattern(file_data, pos, "2D 00 02 1C 00 00 00 0A")) {
pos += 8
pos += 12 // Skip known padding
// Read texture name
name_start = pos
while (file_data[pos:pos+2] != "00 00") {
pos += 1
}
name = sanitize(file_data[name_start:pos])
pos += 2 // Skip terminator
// Find metadata
while (pos < len(file_data) && file_data[pos] == 0x00) {
pos += 1
}
search_pos = pos
while (search_pos < min(pos + 4096, len(file_data))) {
if (file_data[search_pos] == 0x01) {
format_code = file_data[search_pos + 1]
meta_start = search_pos - 2
break
}
search_pos += 1
}
if (meta_start not found) continue // Malformed
// Parse metadata
metadata = read_metadata_block(file_data, meta_start)
width = metadata.width
height = metadata.height
fmt = metadata.format_code
data_size = metadata.total_pixel_data_size
if (width == 0 || height == 0) continue // Placeholder
// Extract pixel data
pixel_data_start = meta_start + 16
pixel_data = file_data[pixel_data_start : pixel_data_start + data_size]
// Unswizzle if needed
if (fmt == 0x86) { // Swizzled BGRA
pixel_data = unswizzle_morton(pixel_data, width, height, 4)
} else if (fmt == 0x02) { // Swizzled indexed
pixel_data = unswizzle_morton(pixel_data, width, height, 1)
}
// For DXT formats (0x52/0x53/0x54), use as-is
textures.append({
name: name,
width: width,
height: height,
format: fmt,
data: pixel_data
})
pos = pixel_data_start + data_size
} else {
pos += 1
}
}
return textures
}