RenderWare Texture Dictionary (.txd)

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.

Parsing Algorithm Overview

  1. Verify file starts with magic marker: 16 00 00 00 (0x00000016 LE, or "TexDict" identifier)
  2. Scan entire file linearly for segment markers:
  3. Extract texture names (null-terminated UTF-8, terminated by 00 00)
  4. Locate metadata block (search for preamble 01 <format_code>)
  5. Parse 16-byte metadata block
  6. Extract and unswizzle/decompress pixel data
  7. Export to standard image format (DDS)

File Header

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).

Segment Markers

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.

Texture Record Structure

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.

Step 1: Texture Name Extraction

  1. Find pattern: 2D 00 02 1C 00 00 00 0A (8 bytes)
  2. Append 12 byte skip (usually zeros)
  3. Read UTF-8 characters until double null terminator (00 00)
  4. Sanitize string: remove non-alphanumeric chars except ._-
  5. Validate: if empty after sanitization, use fallback name 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.

Step 2: Metadata Block Location and Parsing

  1. After texture name, skip zeros until a non-zero byte is encountered
  2. Search forward from this point for the preamble pattern: 01 <format_code>
  3. Read exactly 16 bytes starting from (X - 2) as the metadata block

16-Byte Metadata Block (Little-Endian)

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.

Validation Rules

Pixel Data Blob

Location: Immediately after the 16-byte metadata block (at offset metadata_block_start + 16).
Length: Total Pixel Data Size bytes (from metadata).

Format Codes

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.

Morton Unswizzling (for Swizzled Formats)

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:

  1. Allocate output buffer: width * height * bytes_per_pixel
  2. For each y from 0 to (height - 1):
    1. For each x from 0 to (width - 1):
      1. Compute morton_index = morton_encode_2d(x, y)
      2. Source offset = morton_index * bytes_per_pixel
      3. Dest offset = (y * width + x) * bytes_per_pixel
      4. Copy bytes_per_pixel bytes from source to dest
  3. Return unswizzled buffer

Mipmaps

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.

DDS Export Helper

For DXT Formats (0x52, 0x53, 0x54):

For RGBA Formats (0x86 after unswizzle):

Complete Parsing Example (Pseudocode)


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
}