Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MDX Format (Vertex Data)

The .mdx format is a companion file that always pairs with a .mdl model. It holds lists of raw 3D coordinates (vertices), and every mesh in the .mdl carries an offset into it.

InputBinary::Reset copies the whole block into an OpenGL pool at load, and nothing afterwards reads it again. The rest of the reset chain is handed the pointer and never looks inside, which is what a straight upload leaves behind: no CPU-side code has to read a buffer already sitting in GL memory. Write it correctly. See what the trace covers.

At a Glance

PropertyValue
Extension(s).mdx
Magic SignatureRaw binary stream (No explicit signature block)
TypeInterleaved Vertex Payload Array
Rust ReferenceView rakata_formats::Mdx in Rustdocs

File Layout

There is no index and no table of contents. An .mdx is one interleaved buffer: each vertex’s position, texture coordinates and normal sit next to each other in the stream, and the next vertex follows immediately. Which components a given mesh writes, and therefore how wide one vertex is, comes from the mesh node in the accompanying .mdl rather than from anything in this file.

That is the whole layout as far as reading goes. Writing needs three more rules, because a shipped .mdx is bigger than the vertex data alone and the extra bytes are structured rather than slack:

RuleWhat to emit
Terminator rowAfter each mesh’s vertices, one further row of exactly one stride, starting with three copies of a sentinel float and zero-padded to the stride
Sentinel value10000000.0 for a non-skin mesh, 1000000.0 for a skin mesh, the two distinguished by bit 0x40 of the mesh type
Inter-mesh alignmentPad up to the next 16-byte boundary after each mesh’s terminator, except after the last mesh, which gets no trailing padding

So the file size is the sum over meshes of vertex_count * stride, plus one stride per mesh, plus the alignment padding between them. A writer that emits only vertex data produces a file no community tool will match byte for byte.

The derivation of these rules, including how the sentinels were found and how the stride interacts with the alignment, is in the MDL & MDX deep dive.

Engine Audits & Decompilation

Deep Dive: For an exhaustive archive of the Ghidra decompilation notes detailing the exact byte-level layout of the binary MDL/MDX format and engine loading pipeline, refer to the MDL & MDX Deep Dive.

Read from InputBinary::Read (0x004a1230) and InputBinary::ResetMdlNode (0x004a0900) in swkotor.exe. Provenance: derived, not attested. The rows below have not been separately re-derived, so they sit on the reverse-engineering queue.

What the trace covers

InputBinary::Read loads the file into a buffer and hands it to InputBinary::Reset (0x004a1030), which performs the upload inline: GLRender::RequestPool for a pool sized by the model header’s +0xB0, LockPool, a memcpy of that many bytes out of the buffer starting at the offset in +0xAC, UnlockPool, and the handle registered for later freeing. Read then releases the CPU-side buffer, the pool having taken its copy.

Only after that does Reset pass the pointer down the rest of the chain, and nothing below it reads through the pointer: ResetTriMeshParts goes as far as overwriting its copy to reuse the register as a loop counter. That is traced through named functions, and Reset has exactly one call site, so this is the whole load path rather than one of several.

Three findings in the deep dive describe the same operation from other angles: +0xAC and +0xB0 are the copy’s source offset and length; binary models carry per-vertex bone weights nowhere but MDX, so a skinned character deforms on the strength of it; and the mesh header’s per-attribute offset slots are offsets into an MDX vertex record, with LightPartTriMesh and PartTriMesh reading at them.

Loading and Lifecycle

Pipeline EventGhidra Provenance & Engine Behavior
Memory WrappingTriggered immediately alongside the .mdl. The wrapper dynamically outlines the exact byte-count of .mdx data required (wrapper + 0x08).
Buffer LiberationThe buffer is allocated at load and freed before InputBinary::Read returns. Reset has already copied it into a GL pool by then, using the model header’s +0xAC as the source offset and +0xB0 as the length.

TriMesh Structural Addressing

There is no scan through the file block by block. Whatever reads it is driven by the MDL hierarchy, which supplies both the per-mesh offset and the record layout.

Mapped PropertyGhidra Provenance & Engine Behavior
Array SlicingEvery TriMesh carries an mdx_data_offset at TriMesh + 0x144 naming where that mesh’s records begin in this file. It is what the upload slices on, and community tools follow it too, so a wrong offset misfeeds both.
Node Alignment ConstraintsVanilla assets maintain extremely strict alignment formats. Meshes are dynamically sorted prior to hardware parsing: static rendering models fall to the top of the index chain, whereas dynamic procedural meshes (like character .Skin nodes) are specifically dumped sequentially to the rear of the .mdx.

Note

Ghost Payload Sentinels During memory extraction, the engine implicitly pads geometric mesh payloads out to distinct 16-byte aligned boundaries using Terminator Rows. Any mesh vertex iteration falling slightly out of stride will be explicitly back-filled with ghost/sentinel float arrays ([0.0, 0.0, 0.0]) to ensure OpenGL buffer calculations remain strictly uniform without overflowing pointer indexes during hardware streaming.

Proposed Linter Rules (Rakata-Lint)

Incorrectly calculated .mdx offset spans or payload array lengths can cause the engine to read misaligned bytes or overflow data bounds. Providing a linter rule to validate these payload alignments helps prevent geometry corruption and potential engine/gpu crashes.

While rakata-lint currently only evaluates GFF formats and does not yet parse .mdx buffers dynamically, the engine behaviors above hint at the foundational requirements for .mdx stability:

Planned Lint Diagnostics:

  1. Mesh Slice Verification: Enforces explicit iteration seeking. Validates .mdx vector boundaries by explicitly jumping pointers down the file according to individual mdx_data_offset assignments mapped on explicitly bound TriMesh headers, rather than assuming unverified sequential payload lengths.