~/vannacut.com
link established posts 18 build 1.0.0 · 22:21 UTC
27 min read Development · Research

NFS5PSX file format description

1. Summary of the achieved state

Version 0.2.8 of the NFS5 PSX Asset Explorer can be regarded as the first fully usable milestone for extracting the fundamental car and track assets from the original PlayStation release of Need for Speed: Porsche Unleashed.

For cars, geometry, local vehicle textures, paint and livery palettes, detail textures, and driver textures are correct for the tested models. Geometry is reconstructed from the native PSX primitives, GTE-packed vertex pairs, object matrices, and the distinct archive variants in ROOTCARS.PAK, ROOTCAR2.PAK, and ROOTCAR4.PAK. Renderer-wide faces such as glass, drivers, and other shared texture passes are deliberately separated from the local car materials.

For tracks, all major scene components are now exported together and with coherent world placement:

  • ordinary scenery and landscape meshes;
  • the separately stored road grid;
  • local type-7 billboards;
  • instanced runtime meshes;
  • instanced runtime sprites;
  • separate high-, medium-, and low-LOD scene files;
  • material- and CLUT-correct texture atlases;
  • native per-cell road materials and UV orientation.

The user’s visual review after version 0.2.8 indicates that the tracks now appear correct as a whole, including their road textures. Scotland – Golden Era – Track 2 (_GSCOTRACK2) was the decisive reference scene for reconstructing the final road-stream format.

The remaining unknowns are predominantly renderer and gameplay details rather than fundamental static geometry:

  • exhaust-effect texture mapping;
  • vehicle light geometry and light textures;
  • runtime licence-plate placement;
  • the exact meaning of the packed two-bit field for each road cell;
  • 33 type-7 renderer variants without their own placement;
  • some animated or runtime-selected texture frames and collision/surface semantics.

2. How the formats were identified and verified

The reconstruction was not based on a single screenshot or a cosmetic rotation. Several independent forms of evidence were combined:

  1. Structural signatures and versions
    Resources were classified by envelope tags, object types, and version numbers. Similar tags were not treated as sufficient proof that two payloads used the same layout.
  2. Relative pointers and fixed record strides
    Pointer/count pairs were checked against block boundaries, record sizes, alignment, and their relationships to other tables. A proposed parser was accepted only when every referenced table remained inside the resource.
  3. Corpus-wide invariants
    Rules were tested across the retail corpus rather than only against one car or track. Examples include GTE vertex pairing, material-table strides, road-stream lengths, and the exact pairing of one road UV packet with one material ID for every cell.
  4. PSX GPU and VRAM semantics
    UV packets, texture pages, CLUT addresses, CBA values, BGR555 colours, and STP were interpreted as native PlayStation state instead of ordinary PC UV or RGBA data.
  5. Retail MIPS-code analysis
    The interpretation of the four bytes at road-record offset +0x24 was independently confirmed at call sites in the retail executable. This conclusively disproved the earlier material-token theory.
  6. Blender-versus-game comparisons
    Repeated comparisons at identical or approximately identical positions distinguished geometry errors from world-placement, material, palette, transparency, or UV-corner-order errors.
  7. Regression tests and end-to-end exports
    In addition to parser tests, production OBJ/MTL/PNG exports were checked for valid indices, complete material references, and plausible corpus totals.

The most important methodological lesson was that fields with similar value ranges must not be assumed to have the same meaning. Several early errors looked plausible because an incorrect texture still landed inside a valid atlas area. Pointer relationships, native GPU state, executable behaviour, and corpus-wide identities were needed to separate real format rules from accidental matches.

3. Shared Eden-engine foundation

NFS5 PSX belongs to the same Eden-engine family as V-Rally 2. Confirmed shared foundations include:

  • the CDEURO resource tree and ROOT*.PAK archive family;
  • the eight-byte PAK header and four-byte big-block descriptors;
  • 2048-byte CD-sector alignment;
  • stored/packed sub-block flags;
  • Eden 12:4 LZ77 compression;
  • the inverted stored CRC-32 representation
    (~zlib.crc32(stored_bytes)) & 0xffffffff;
  • PSX texture groups built from 4-bpp indices, BGR555/STP CLUTs, and VRAM upload rectangles;
  • type-2/version-0x11 PSX V3D car geometry with GTE-paired vertices;
  • native PlayStation GPU triangle and quad packets;
  • the broad principle that track scenery and the driveable road use separate geometry systems.

These similarities did not make NFS5 binary-compatible with the VR2 track parser. NFS5 combines familiar Eden components with its own root versions, larger records, different pointer tables, and additional runtime instance lists.

One especially important difference already appears in the container. The analysed NFS5 PSX release uses 20-byte sub-block records, including an overlap_size field—the record shape normally associated with VR2 PC archives. The contained assets are nevertheless unambiguously PlayStation resources. Therefore:

Container layout and payload target platform must be classified independently. A 20-byte record does not prove that the geometry or texture payload uses a PC format.

4. Car resources

4.1 Archives and block organisation

Car data is distributed across three archives:

ArchiveNon-empty blocksObserved purpose
ROOTCARS.PAK281primary car variants
ROOTCAR2.PAK96additional LOD/mode variants
ROOTCAR4.PAK66further LOD/mode variants

A total of 443 occupied car geometry blocks were validated. Every non-empty block in the reference release has three sub-blocks:

  1. a compact descriptor or compiled metadata block;
  2. a PSX texture envelope followed by additional metadata;
  3. type-2/version-0x11 V3D geometry.

ROOTCAR2 and ROOTCAR4 are not complete parallel copies of ROOTCARS. A logical block may have one, two, or three archive variants. The exporter groups variants by physical block ID but writes them to separate archive-specific folders. Absence of a rootcar2 or rootcar4 directory does not by itself indicate lost output.

4.2 Recognising car geometry

Car geometry is recognised from the following combined signature:

  • envelope tag (0,2,1,0);
  • object type 2, 5, or 6;
  • V3D version 0x11;
  • relative pointer base at object offset 0x14 in the sub-block;
  • 0x74-byte mesh records;
  • 0x24-byte draw descriptors;
  • GTE-paired vertex pools;
  • fixed-size native PSX primitive records.

The core geometry layout is closely related to VR2 PSX. NFS5 extends the material mapping, however, as described below.

4.3 GTE-paired vertices

Two signed 16-bit vertices occupy twelve bytes in an order optimised for three 32-bit GTE-related loads:

x0, y0, z0, z1, x1, y1

The actual vertices are therefore:

Vertex 0 = (x0, y0, z0)
Vertex 1 = (x1, y1, z1)

Reading the six shorts as two ordinary XYZ triples is incorrect. This exact mistake originally produced split, lobed, or torn cars in the VR2 PSX work. The corrected pairing was therefore a directly reusable, independently validated basis for NFS5.

Local points are then transformed by the signed-Q12 matrix and signed 32-bit translation stored in the mesh:

world = matrix * local / 4096 + translation

4.4 Primitive records and topology

The principal native car primitive types are:

TypeRecord sizeCornersPurpose
2172 bytes4textured quad
3140 bytes3textured triangle
452 bytes4UV-less quad
584 bytes4shaded/textured quad
668 bytes3shaded/textured triangle
10196 bytes4additional textured quad variant
11156 bytes3additional textured triangle variant

Type 8 is an additional renderer pass whose final semantics remain unresolved.

The four corners of a PSX quad are not an ordinary OBJ polygon boundary. They are a GPU triangle strip in this order:

TL, TR, BL, BR

Writing the four corners as one OBJ quad creates a self-intersecting outline. The correct explicit triangles are:

(0,1,2)
(2,1,3)

This correction removed missing and overlapping body faces. The same rule applies to VR2 PSX and NFS5 PSX.

4.5 NFS5-specific material mappings

VR2 PSX uses five-dword, 20-byte rectangle/CLUT mapping records. NFS5 extends each mapping to six dwords or 24 bytes. The sixth dword contains additional renderer metadata; the first five fields retain the familiar Eden rectangle and CLUT indirections.

The early NFS5 parser inherited the 20-byte VR2 stride. Every record after the first was consequently displaced by four bytes. The pointers often remained numerically plausible but no longer referred to matching rectangles and palettes. This caused:

  • missing textures on otherwise valid faces;
  • incorrect body details;
  • incorrect paint or livery palettes;
  • apparently random wheel, glass, or light imagery.

Using the correct 24-byte stride made the local material tables structurally coherent.

4.6 Local car materials versus runtime passes

A second class of errors arose because several texture systems reuse the same 0-to-255 UV coordinate range. A window or driver face can accidentally fit completely inside a local wheel or body rectangle. UV containment alone is therefore unsafe.

The final rule treats the native 16-bit word following UV0 as an exact local material-table index only when that index actually exists. This produces three distinct categories:

  1. Exact local material
    The face points to a valid mapping in the car’s immediately associated local bank.
  2. Baked polygon colour
    Type-5/6 faces without an exact local index retain the colour stored in their GPU packet. Many broad paint surfaces are colour-modulated rather than simple bitmap decals.
  3. runtime_shared_overlay
    Other textured faces without an exact local index are placed in a separate runtime overlay. This includes shared driver, glass, and other detail passes.

This distinction was essential. Earlier builds put wheels on windows or arbitrary local images on driver geometry even though the UV bounds appeared plausible.

4.7 Car CLUT and colour resolution

PSX textures are not ready-made RGBA images. They consist of 4-bpp pixel indices and BGR555/STP palettes. A material therefore selects both an image rectangle and a CLUT.

Each NFS5 CLUT descriptor is eight bytes:

OffsetMeaning
+0x00packed VRAM coordinate `x(y << 16)`
+0x04PSX CBA value

The table position is not the physical palette number. The local car texture upload is 64 colours wide, or four independent 16-colour palettes per row. The palette is derived as:

palette = y * 4 + x / 16

The CBA column and row are also checked against the stored coordinate. Using the table index had combined correct UVs with incorrect colours. The problem was particularly visible on paint schemes, racing liveries, wheels, tail lights, and roof textures.

All 17,928 observed local material mappings in the three car archives now pass coordinate, CBA, and palette-range validation.

4.8 Packed material atlas instead of an overwritten composite

Multiple materials may reuse one source rectangle with different CLUTs, for example for paint, damage, or livery variants. A single in-place composite cannot display all variants at once because the last palette written over the rectangle replaces the earlier one.

The exporter therefore creates a packed material atlas. Every used rectangle/CLUT combination receives an independent tile, and face UVs are rewritten to the packed location. Raw pages, composite images, and individual CLUT diagnostics remain available but are not the authoritative OBJ material representation.

4.9 Transparency and STP

Another visible failure came from treating the PSX STP bit as a fixed alpha value. NFS5 sets STP on many ordinary palette colours. Whether blending actually occurs is selected by the GPU primitive, not by the palette bit alone.

For static OBJ/PNG output, the policy is therefore:

  • BGR555 word 0x0000 is fully transparent;
  • every other colour word remains opaque, including non-zero words with STP set.

This NFS5-specific preview policy removed ghosted bodywork, false window transparency, and spurious holes. It remains an export approximation: the original renderer can select additional per-primitive blend modes.

4.10 Car orientation

Native NFS5 car axes are:

X = right
Y = down
Z = forward / vehicle longitudinal axis

The generic VR2 PSX conversion moved the longitudinal Z component to another OBJ axis. Combined with the normal Blender import convention, this made NFS5 cars stand on their bumper.

NFS5 therefore uses its own 180-degree X-axis rotation:

(X,Y,Z) -> (X,-Y,-Z)

The resulting convention is X-right, Y-up, Z-back, and the vehicle imports horizontally. Handedness and face winding are preserved.

4.11 Name and block assignment

Car names are recovered from the ordered ROOTCARS.SCC table. The sequence cannot be interpreted as the direct identity “SCC row equals PAK block number,” because it describes occupied logical resources and contains retail asymmetries.

Important corrections were:

  • SCC-only TYPE10/H00 has no distinct primary PAK block;
  • physical PAK block 262 is an internal TYPE25 variant with no SCC row;
  • the adjacent physical blocks 102 and 103 contain the opposite models from the naive SCC order:
    • block 102: TYPE23/A00, 1998 Porsche 911 GT1 Race;
    • block 103: TYPE21/A00, Porsche 917 K Short Tail.

The last pair was supported not only by labels, but independently by body silhouettes and local livery banks.

4.12 Major car problems and their causes

Visible symptomIncorrect assumptionCorrect interpretation
Missing or crossing body facesPSX quad written as one OBJ polygonemit the native strip as two triangles
Split or lobed carsix shorts read as two linear XYZ triplesdecode x0,y0,z0,z1,x1,y1 pairing
Textures missing after the first materialinherited 20-byte VR2 mapping strideNFS5 mappings are 24 bytes
Correct UVs but wrong paint/livery coloursCLUT-table index used as palettederive palette from VRAM coordinate and CBA
Wheel texture on windowsUV containment determines local materialonly an exact native mapping index is locally authoritative
Incorrect driver textureruntime face forced into a local rectanglekeep driver/glass in a separate runtime pass
Wrong roof/tail-light paletteCLUT variants overwrite a shared rectanglepack each rectangle/CLUT pair independently
Ghosted or invisible surfacesevery STP colour treated as alphaonly exact 0x0000 is statically transparent
Car standing on its bumpergeneric VR2 PSX axis conversionNFS5-specific 180° X rotation
Names shifted after an archive slotSCC row used directly as PAK blockmap occupied blocks and confirmed asymmetric slots

4.13 Current car status

Confirmed:

  • all 443 observed car geometry blocks parse;
  • local car materials are structurally validated;
  • ordinary car textures and driver textures are visually correct;
  • static colour and transparency output is correct;
  • Blender import orientation is horizontal;
  • archive variants and runtime overlays remain distinct;
  • regression exports contain valid OBJ, MTL, and PNG references.

Intentionally unresolved:

  • exhaust-effect textures are still incorrectly mapped;
  • light geometry and light textures have not been fully reconstructed;
  • licence-plate positions depend on undecoded runtime logic;
  • OBJ/MTL cannot completely reproduce PSX blend modes, palette animation, or dynamic paint modulation.

5. Track resources

5.1 Archive and scene inventory

ROOTSPEC.PAK contains 82 occupied main blocks and 498 sub-blocks. The parser identifies:

  • 82 main scenes;
  • 22 additional LOD scenes;
  • 104 type-1/version-0x15 scene roots in total.

High-, medium-, and low-detail scene passes are exported as separate OBJ files. Multiple LOD passes are not forced into one overlapping scene.

Logical names are recovered from ROOTSPEC.SCC, covering all 70 regular course resources and twelve missions, or every one of the 82 occupied main blocks. The additional 22 scenes are LOD passes inside those logical blocks rather than additional independent course names.

5.2 Recognising the NFS5 track root

The NFS5 track root is recognised by this combined signature:

Envelope:    (0,2,1,0)
Object type: 1
Version:     0x15
Root base:   sub-block +0x14

The root is a 19-dword directory. Important pointer/count pairs select:

Root pairContents
[1]/[2]shared primitive-descriptor pool
[3]/[4]placement-group table
[5]/[6]road-stream descriptors
[7]/[8]0x28-byte road table
[9]/[10]0x1a0-byte scene objects
[11]/[12]16-byte texture rectangles
[13]/[14]eight-byte CLUT descriptors
[17]/[18]24-byte material mappings

Every pointer is relative to the root at +0x14, not to the beginning of the PAK sub-block or file.

VR2 PSX tracks instead use version 0x22 or 0x122. The NFS5 parser is therefore separate; the VR2 parser was not made permissive enough to accept 0x15. This prevents similar envelope tags from selecting incorrect record strides or pointer bases.

Texture banks are not paired by a guessed filename. For each scene root, the exporter searches backwards within the same PAK block for the nearest structurally valid PSX texture group. A small type-16 support resource may sit between the texture upload and scene, so immediate sub-block adjacency is not required. The scene material table then supplies the exact primitive- or road-cell-to-rectangle, texture-page, and CLUT relationship inside that bank.

5.3 Scene objects and world placement

An NFS5 scene object is 0x1a0 bytes. Confirmed fields include:

+0x04  object_index
+0x08  first_placement_group
+0x0c  optional_secondary_group
+0x20  road_record
+0x24  translation_x (s32)
+0x28  translation_y (s32)
+0x2c  translation_z (s32)
+0xac  runtime_mesh_placements
+0xb0  runtime_mesh_count
+0xb4  runtime_sprite_placements
+0xb8  runtime_sprite_count

One important discovery was that the two stored placement pointers cannot be treated as a simple start/end pair. Placement groups are globally ordered. An object begins at its first-group pointer, and the next object’s first-group pointer is the exclusive end of the current ownership range. This preserves rare objects that own three groups.

The signed 32-bit translation is applied to the object’s ordinary meshes, local sprites, and road grid. Omitting it piles all course chunks around their local origins.

5.4 Placement groups and mesh geometry

Each placement group contains a pointer and count for fixed 0x24-byte mesh records. These reference:

  • a GTE-paired vertex pool;
  • a triangle-slot capacity;
  • an entry in the shared primitive descriptor pool.

The stored slot count is a capacity rather than an exact used-vertex count. The exporter decodes only up to the highest index referenced by a primitive. This avoids interpreting padding or immediately following packet data as extra positions.

Ordinary NFS5 track primitives are:

TypeSizeMeaning
012 bytestextured triangle with three indices, three UV pairs, and material ID
116 bytestextured quad with four indices, four UV pairs, and material ID
720 bytesbillboard/sprite descriptor

Type-1 quads use the same native strip split as PSX car quads: (0,1,2) and (2,1,3).

5.5 Why signs, lamps, and barriers were initially missing

Ordinary placement geometry is not the entire visible course. Two more data classes were identified later.

Local type-7 sprites

A type-7 record contains:

  • width and height;
  • four UV pairs;
  • one material ID;
  • a fixed terminating state.

It is paired by order with eight-byte placements:

s16 x, y, z
u16 packed_angle

The low twelve angle bits encode a full PSX turn from 0 to 4095. The exporter constructs a bottom-centred vertical billboard. This class includes vegetation, spectators, lamps, houses, signs, and local barrier graphics.

Of 128,181 validated type-7 records, 128,148 have an unambiguous placement. Only 33 surplus variants lack a direct placement row.

Runtime meshes and runtime sprites

Further missing signs, chevrons, bollards, posts, and road blocks were stored in two object-owned runtime lists.

Runtime mesh instances use 40-byte records. They reference shared mesh geometry and provide:

  • a row-major signed-4.12 3×3 matrix;
  • a local signed-16-bit translation;
  • renderer state words.

Their transformation is:

world = shared_matrix * local / 4096
      + instance_translation
      + scene_object_translation

Runtime sprite instances use 20-byte records with a shared sprite descriptor, position, renderer state, and a 12-bit angle.

Decoding these lists added across the retail corpus:

  • 18,849 runtime mesh instances;
  • 61,070 additional source faces before quad triangulation;
  • 22,507 runtime sprite instances.

For _GSCOTRACK2, this restored 292 mesh props and 455 runtime sprites. The previously missing striped road-block material 115 in _CGERTRACK1 is referenced by 14 runtime mesh faces and is now exported.

5.6 The road is a separate geometry system

As in VR2, the driveable corridor is not part of the ordinary scenery meshes. Every scene object owns one 0x28-byte road record describing an implicit row/column grid.

The basic identities are:

position_count = rows * columns
cell_count     = (rows - 1) * (columns - 1)

Positions use the same GTE pairing as other PSX geometry and receive the same object translation. Each cell addresses:

TL, TR, BL, BR

and is emitted as two strip triangles.

Important road-record fields are:

+0x00  rows / columns / cell_count / position_slots
+0x04  profile
+0x08  pointer to packed two-bit cell data
+0x0c  pointer to paired positions
+0x10  pointer to vertex colours
+0x18  pointer to first UV/material stream descriptor
+0x1c  pointer to second stream descriptor or zero
+0x20  pointer to surface flags
+0x24  four signed grid/reference indices

5.7 The decisive road-material discovery

The largest remaining track failures came from a long-lived, plausible, but incorrect interpretation of the road record.

The early theory assumed that:

  • the two-bit field at +0x08 selected one of four values per cell;
  • the four bytes at +0x24 were those compact material tokens;
  • depending on the scene, the selected token had to be used directly, with selector ^ 2, or as token >> 1.

These rules could improve individual tracks while regressing others. Scotland in particular ended up with grass or rock across almost the entire road when a supposed global shift was applied. That was strong evidence that the format had not been understood.

The actual structure is reached through root pair [5]/[6]. Every eight-byte entry contains:

u32 uv_packet_array
u32 material_id_array

For every road cell owned by a stream there are:

  • exactly eight bytes, or four native UV pairs;
  • exactly one parallel u8 material ID.

Packet i and material byte i belong together directly. In addition:

(material_pointer - uv_pointer) / 8 = number of cells owned by the stream

This identity holds without exception for all 951,676 analysed road cells. Every native UV packet lies within the rectangle selected by its parallel material ID.

The four signed bytes at +0x24 are grid/reference indices instead. Retail MIPS code uses them in calculations involving grid dimensions and positions. The first duplicated value is also the authored split column between the left and right road streams.

Therefore:

  • the former selector ^ 2, token >> 1, and direct/shifted heuristics were incorrect;
  • the two-bit field at +0x08 is not the material selector;
  • road materials are read directly, per cell, from the parallel u8 array.

Through this native structure, _GSCOTRACK2 uses 46 distinct road materials, not the six values produced by the discarded tail-token theory.

5.8 Stream ownership and the stored split column

Many road chunks use two streams. Their cell counts are not divided by a generic columns // 2. The stored split column from the first signed reference byte at +0x24 is authoritative:

left_cells  = (rows - 1) * split_column
right_cells = cell_count - left_cells

For forward traversal:

stream 1 = left half
stream 2 = right half

When profile bit 0x00010000 is set, traversal is reversed:

stream 1 = right half
stream 2 = left half

The stored split is especially important for even-width grids. The earlier midpoint calculation consumed the wrong number of UV packets in some chunks. The decoder then read beyond the UV array and into the adjacent material array. Those bytes still looked like valid UV values, creating strongly rotated, triangular, or 45-degree patterns.

5.9 Road UV orientation

Road UV orientation has two stages:

  1. select and order the four stored UV pairs according to traversal direction and left/right half;
  2. convert the native PSX packet slots into OBJ grid order.

The four confirmed branch assignments are:

BranchStored-pair order
forward, left3,0,2,1
forward, right2,1,3,0
reverse, left0,3,1,2
reverse, right1,2,0,3

These assignments first fill native PSX packet slots:

TL, BL, TR, BR

The implicit OBJ grid expects:

TL, TR, BL, BR

The two middle slots must therefore be exchanged:

(0,1,2,3) -> (0,2,1,3)

This broad packet/grid mismatch had already been discovered for VR2 road UVs. In NFS5, however, the middle swap alone is insufficient. The decoder must first choose the correct stream, traversal direction, left/right branch permutation, and authored split column.

Several partial hypotheses were tested between versions 0.2.3 and 0.2.7. Some looked better on one course while rotating transition textures by 45 or 90 degrees or damaging another course. Only after the UV-array overread into material bytes was eliminated could the final middle-slot conversion be isolated and confirmed.

The UV pairs must not be geometrically sorted or canonicalised. Their native order carries authored rotation, mirroring, trapezoidal mapping, and transition information.

5.10 Track materials, CLUTs, and texture pages

Track material mappings are also 24 bytes. They select:

  • an authored texture rectangle;
  • a CLUT descriptor;
  • an absolute PSX texture page;
  • additional renderer metadata.

The CLUT table is deliberately not sorted in physical upload order. For tracks, the palette is therefore derived from the absolute VRAM position relative to the track CLUT base:

palette = (clut_y - base_y) * 4
        + (clut_x - base_x) / 16

The CBA value is verified against the same row/column address. Using the CLUT-table index previously combined the correct geometric texture region with the wrong colour space and incorrect zero/transparent texels.

Rectangle X is local to a 256-pixel PlayStation texture page. Consecutive pages are placed horizontally for diagnostic and source atlases. For OBJ output, every used rectangle/CLUT combination receives an independent packed tile, allowing identical source rectangles with different palettes to coexist.

As with the cars, only exact BGR555 0x0000 remains transparent. Non-zero STP colours are opaque in the static export.

5.11 Whole-track orientation

The native NFS5 scene uses:

X = right
Y = down / native PSX vertical axis
Z = forward

The complete course had to be rotated 90 degrees clockwise around its native Y axis relative to the initial export. This rotation is performed before the established PSX-to-OBJ conversion and produces:

(X,Y,Z) -> (-Z, X, -Y)

One common transform is applied to:

  • scenery meshes;
  • runtime meshes;
  • type-7 sprites;
  • runtime sprites;
  • road grids.

This preserves all relative placement. VR2 export coordinates are not modified by the NFS5-specific rotation.

5.12 Major track problems and their causes

Visible symptomIncorrect assumptionCorrect interpretation
Course chunks piled togetherlocal vertices exported without object translationadd the signed XYZ translation from the 0x1a0-byte object
Missing groups or scenery piecestwo pointers treated as a simple start/end rangeownership ends at the next object’s first-group pointer
Entire driveable road missingroad expected inside scenery geometrydecode the separate 0x28-byte road grids
Incorrect colours and transparencyCLUT-table position used as paletteresolve native VRAM coordinate and CBA
Building/rock/grass texture on roadtwo-bit field and four tail bytes used as materialsuse each stream’s parallel u8 material array
Scotland almost entirely grass/rockscene-wide direct/shifted heuristicevery cell has an explicit native material ID
45-degree wedges and destroyed tilesmidpoint split overreads material bytes as UVsuse the stored signed split column
Correct image but rotated/mirrorednative packet slots bound directly to OBJ gridbranch permutation plus TL,BL,TR,BRTL,TR,BL,BR
Worse orientation after a global swapVR2 middle swap applied without complete NFS5 branch logiccombine stream selection, traversal, branch, and packet conversion
Missing signs/spectators/treestype 7 dismissed as renderer metadatapair type-7 records with XYZ/angle placements
Missing road blocks/bollards/chevronsonly ordinary placement groups exporteddecode runtime mesh and sprite lists at +0xac/+0xb4
Whole course rotated by 90°generic PSX axis conversion onlyadd NFS5 90° clockwise native-Y rotation

5.13 Current track status

Structurally validated:

  • 104/104 scene roots;
  • 38,752 ordinary placed meshes;
  • 18,849 runtime mesh instances;
  • 128,148 placed local type-7 sprites;
  • 22,507 runtime sprite instances;
  • 8,240 road chunks;
  • 951,676 road cells;
  • 754,878 ordinary scenery source faces;
  • 61,070 runtime-prop source faces;
  • all generated material atlases.

Reference courses _MFRATRACK1, all three _MJAPZONEC LODs, _MJAPTRACK1, _CGERTRACK1, and _GSCOTRACK2 export end to end. The current _GSCOTRACK2 export contains:

  • 400 ordinary meshes;
  • 292 runtime props;
  • 760 local sprites;
  • 455 runtime sprites;
  • 80 road chunks;
  • 34,839 triangles, including 15,104 road triangles;
  • 30,503 positions;
  • 71,173 valid texture-coordinate references.

After the user’s visual review of version 0.2.8, all tracks appear correct with regard to geometry, road-material assignment, and texture orientation. This version is therefore the current regression baseline.

Remaining open items:

  • the exact renderer/collision meaning of the two-bit road-cell field at +0x08;
  • structured output of surface/collision flags;
  • 33 type-7 variants without a placement row;
  • possible animated or runtime-selected material frames;
  • a full comparison against a PAL/SLES release.

6. Comparison with V-Rally 2 PC and PlayStation

6.1 Overview

AreaV-Rally 2 PCV-Rally 2 PSXNFS5 PSX
PAK sub-block record20 bytes16 bytes20 bytes despite PSX payload
Car texture model256×256 RGBA8 with mipmaps4-bpp + BGR555/STP CLUT4-bpp + BGR555/STP CLUT
Track texture modelA1R5G5B5 pagesPSX VRAM uploads + CLUTPSX VRAM uploads + CLUT
Car V3Dversion 0x12, floatsversion 0x11, GTE shortsversion 0x11, GTE shorts
Car mesh stride0xC8 bytes0x74 bytes0x74 bytes
Car topologyvariable triangle stripsfixed GPU triangles/quadsfixed GPU triangles/quads
Car material mappingPC pages/UVs20 bytes, CLUT order usable24 bytes, CLUT from VRAM/CBA
Track roottype 1 / 0x24type 1 / 0x22 or 0x122type 1 / 0x15
Scene object stride0x128 bytes0xF4 bytes0x1a0 bytes
World placementfloat XYZ translationX/Z origin derived from bounds; Y absolutedirect signed XYZ translation
Ordinary track meshesfloat4, 0x28-byte mesh recordsGTE pairs, 0x20-byte placementsGTE pairs, 0x24-byte placements
Road record0x20 bytes0x24 bytes0x28 bytes
Road materialdirect u16[cell]separate u16[cell]parallel u8[cell] for each UV stream
Road UVsfour float2 pairs per cellcompact branch-dependent byte streamsbranch-dependent eight-byte packets, parallel materials, stored split
Runtime propsdifferent/partly open auxiliary passestrack type 7 still open in 0.6.11type 7 plus runtime mesh/sprite lists decoded
Native axesX right, Y up, Z forwardX right, Y down, Z forwardX right, Y down, Z forward
Export orientation(X,-Z,Y)(X,Z,-Y)car (X,-Y,-Z); track (-Z,X,-Y)

6.2 Shared principles

Despite different records, the same engine principles recur:

  • pointers are resource-relative and must use the correct root base;
  • object placement is separate from local mesh geometry;
  • the road is its own implicit grid rather than ordinary scenery polygons;
  • PSX vertices are packed in GTE-friendly pairs;
  • PSX quads carry triangle-strip topology and require explicit triangulation;
  • material identity includes a rectangle, texture page, CLUT, and sometimes renderer state—not merely a bitmap location;
  • renderer packet order and OBJ vertex order are not identical;
  • one in-place composite cannot represent multiple CLUT variants for one rectangle;
  • similar envelope tags do not prove identical payload layouts.

6.3 Main differences from VR2 PC

VR2 PC is much closer to conventional PC 3D and texture formats:

  • cars use floating-point vertices, floating-point normals, RGBA8 colours, and variable triangle strips;
  • car textures are finished 256×256 RGBA8 pages with mipmaps and true alpha coverage;
  • track textures are 16-bit A1R5G5B5 pages rather than indexed 4-bpp pixels with a separate CLUT;
  • track and road positions are floating-point vectors;
  • road material IDs and UVs are direct arrays and far less compact;
  • object placement uses direct float translations.

The core PC road-orientation issue was nevertheless conceptually identical: UVs used renderer packet order TL,BL,TR,BR, while the grid used TL,TR,BL,BR. The middle exchange 0,2,1,3 had to be applied only to road_* geometry.

6.4 Main differences from VR2 PSX

The car formats are closely related. Both games use version 0x11, GTE pairing, Q12 matrices, and native GPU primitives. NFS5 extends the material records to 24 bytes and does not store its CLUT indirection table in physical palette order. NFS5 also requires its own static STP policy and car-axis conversion.

Track differences are more substantial:

  • VR2 PSX uses track version 0x22/0x122; NFS5 uses 0x15.
  • VR2 PSX scene objects are 0xF4 bytes; NFS5 objects are 0x1a0 bytes.
  • VR2 PSX derives X/Z placement from local and world bounds; NFS5 stores direct signed XYZ translations.
  • VR2 PSX uses 0x20-byte placements and 0x24-byte road records; NFS5 uses 0x24-byte placements and 0x28-byte road records.
  • VR2 PSX road materials live in a separate u16[cell] array. NFS5 pairs every eight-byte UV packet with a parallel u8 material byte in a stream descriptor.
  • NFS5 stores an authored split column for its two road streams.
  • NFS5 has confirmed local and runtime sprite/mesh lists that restore additional signs, barriers, and props.
  • The complete NFS5 scene requires an additional 90-degree clockwise rotation around native Y.

6.5 What transferred successfully from VR2

The validated VR2 work provided valuable starting points:

  • PAK headers, sector alignment, CRC handling, and Eden LZ77;
  • safe record/platform detection instead of filename heuristics;
  • PSX indexed texture and CLUT decoding;
  • GTE vertex pairing;
  • PSX quad triangulation;
  • the distinction between scenery and road grids;
  • the fact that PSX road packet slots do not directly match OBJ grid order;
  • packed material atlases for rectangle/CLUT variants;
  • preserving unresolved renderer passes separately instead of deleting them or assigning arbitrary textures.

6.6 What could not be copied blindly

Several persistent NFS5 errors appeared where a VR2 rule was transferred too early:

  • a 20-byte PAK record was initially associated with PC payloads;
  • VR2’s 20-byte material stride was applied to the 24-byte NFS5 table;
  • the generic VR2 PSX car-axis conversion made NFS5 cars stand on their bumper;
  • VR2 track versions and object strides do not match NFS5 0x15;
  • an isolated VR2 road middle-swap cannot solve NFS5 without branch, stream, and split logic;
  • VR2 CLUT-table order is not NFS5 physical palette order;
  • VR2’s static STP-alpha policy is too aggressive for NFS5.

The broader lesson is:

Shared engine principles are excellent sources of hypotheses, but record sizes, pointer relations, coordinate systems, and renderer state must be revalidated for every game and platform against retail data.

7. The road investigation as a case study

The road decoder best illustrates how the final interpretation emerged.

  1. Scenery existed, road was absent
    The initial parser handled only scenery. The hole in the driveable corridor led to the separate road table.
  2. Road geometry existed, materials were wrong
    Grid positions were correct, but material selection was incorrectly reconstructed from the two-bit field and four record-tail bytes.
  3. Selector heuristics improved individual courses
    selector ^ 2, token >> 1, and direct/shifted variants produced locally plausible output. Small incorrect IDs happened to resolve to existing materials, making the hypotheses look credible.
  4. Scotland exposed the global misconception
    _GSCOTRACK2 became almost entirely grass or rock. A scene-wide heuristic could not account for its many distinct authored surfaces.
  5. The road-stream table was identified as an eight-byte pair
    The UV pointer and material pointer form one descriptor. Their distance gives the exact packet count.
  6. MIPS code disproved the tail-material tokens
    The four bytes participate in position/grid calculations. The first duplicated value is the split column.
  7. The midpoint error explained 45-degree artefacts
    Incorrect stream lengths made the decoder read material bytes as UVs. Earlier permutation experiments therefore could not produce a stable result.
  8. Native branches plus packet-slot conversion
    Once stream boundaries were correct, the four traversal branches and final TL,BL,TR,BR to TL,TR,BL,BR exchange were confirmed.
  9. Corpus-wide validation
    All 951,676 cells have matching UV/material pairs and every UV packet fits its selected rectangle. Visual review then confirmed the courses as plausibly correct.

This case demonstrates why a visual “rotate every tile by 90 degrees” fix is dangerous. An apparent UV rotation may actually come from an incorrect material ID, wrong stream half, reversed traversal, or even reading bytes from the wrong array.

8. Validation state and confidence

Confirmed structurally and across the retail corpus

  • container and sub-block structure of the four NFS5 archives;
  • 1,827/1,827 stored sub-block CRCs;
  • all 443 car texture envelopes and geometry blocks;
  • all 17,928 local car material/CLUT mappings;
  • all 104 track roots;
  • all 8,240 road chunks and 951,676 road cells;
  • every native UV/material stream length;
  • every referenced road material ID;
  • type-0/1 track primitives;
  • 128,148 local type-7 placements;
  • 18,849 runtime mesh and 22,507 runtime sprite instances;
  • valid OBJ vertex and UV indices in end-to-end exports.
  • 21/21 automated core and regression tests in version 0.2.8.

Additionally confirmed visually

  • ordinary car textures;
  • driver textures;
  • car orientation in Blender;
  • course world layout;
  • road-material selection and UV orientation after version 0.2.8;
  • presence of previously missing signs and runtime props.

Export policies rather than full renderer emulation

  • non-zero STP colours are rendered opaque in static output;
  • multiple CLUT variants are packed into independent atlas tiles;
  • animated material mappings are represented as one static OBJ state;
  • runtime-shared car faces remain independently selectable;
  • collision and surface flags are not embedded as gameplay semantics in OBJ.

Still unresolved

  • final exhaust-effect texture source or mapping;
  • vehicle light and licence-plate runtime passes;
  • meaning of the NFS5 road two-bit field at +0x08;
  • unplaced type-7 variants;
  • full animated renderer state;
  • PAL/SLES comparison.

9. Conclusion

The key result is not merely that the OBJ files now “look right.” Car and track exports are now based on format-backed relationships:

  • native pointers and record strides;
  • correct GTE vertex decoding;
  • native GPU topology;
  • explicit object and runtime placements;
  • exact rectangle/CLUT material resolution;
  • PSX VRAM and CBA addresses;
  • per-cell road UV/material pairs;
  • authored stream splits and traversal branches;
  • coherent NFS5-specific axis conversion.

NFS5 PSX is therefore no longer represented by a partially adapted VR2 experiment. Reused Eden foundations are clearly separated from NFS5-specific structures. Cars and tracks have their own validated decoders, and version 0.2.8 provides a stable baseline for future research into renderer effects, lights, collision semantics, animation, or eventual repacking.


10. Technische Projektgrundlagen / Technical project references

Diese Zusammenfassung basiert auf dem bestätigten Quell- und Dokumentationsstand der beiden Werkzeuge. Die wichtigsten internen Referenzen sind:

DateiInhalt
NFS5_PSX_Asset_Explorer_v0.2.8_source/docs/DOCUMENTATION.mdGesamtstatus, Archivinventar, Fahrzeuge, Tracks und offene Punkte
NFS5_PSX_Asset_Explorer_v0.2.8_source/docs/FORMAT_TRACK.mdNFS5-Type-1/Version-0x15-Szenen-, Runtime- und Road-Strukturen
NFS5_PSX_Asset_Explorer_v0.2.8_source/docs/VALIDATION.mdKorpuszahlen und Regression-Baseline
NFS5_PSX_Asset_Explorer_v0.2.8_source/docs/CHANGELOG.mdEntwicklung der Hypothesen und Korrekturen bis 0.2.8
backend/nfs5psxgeo.py und backend/vr2psxgeo.pyFahrzeuggeometrie, Materialpass-Trennung und Achsenkonvertierung
backend/nfs5psxtex.pyNFS5-Textur-Envelope, Track-Materialien und CLUT-Auflösung
backend/nfs5psxtrack.pySzenengeometrie, Runtime-Props, Road-Streams und Road-UV-Orientierung
VR2_PAK_Asset_Format_Reference_v0.6.11.mdvalidierte Vergleichsbasis für V-Rally 2 PC und PSX

The document distinguishes confirmed binary relationships from static export policy and unresolved renderer behaviour. Historical hypotheses are included only to explain the reverse-engineering process; the native road UV/material stream interpretation implemented in version 0.2.8 supersedes the selector/token heuristics from versions 0.2.1 through 0.2.7.