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:
- 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. - 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. - 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. - 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. - Retail MIPS-code analysis
The interpretation of the four bytes at road-record offset+0x24was independently confirmed at call sites in the retail executable. This conclusively disproved the earlier material-token theory. - 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. - 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
CDEUROresource tree andROOT*.PAKarchive 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-
0x11PSX 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:
| Archive | Non-empty blocks | Observed purpose |
ROOTCARS.PAK | 281 | primary car variants |
ROOTCAR2.PAK | 96 | additional LOD/mode variants |
ROOTCAR4.PAK | 66 | further 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:
- a compact descriptor or compiled metadata block;
- a PSX texture envelope followed by additional metadata;
- type-2/version-
0x11V3D 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
0x14in 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:
| Type | Record size | Corners | Purpose |
| 2 | 172 bytes | 4 | textured quad |
| 3 | 140 bytes | 3 | textured triangle |
| 4 | 52 bytes | 4 | UV-less quad |
| 5 | 84 bytes | 4 | shaded/textured quad |
| 6 | 68 bytes | 3 | shaded/textured triangle |
| 10 | 196 bytes | 4 | additional textured quad variant |
| 11 | 156 bytes | 3 | additional 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:
- Exact local material
The face points to a valid mapping in the car’s immediately associated local bank. - 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. 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:
| Offset | Meaning | |
+0x00 | packed VRAM coordinate `x | (y << 16)` |
+0x04 | PSX 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
0x0000is 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/H00has 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.
- block 102:
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 symptom | Incorrect assumption | Correct interpretation |
| Missing or crossing body faces | PSX quad written as one OBJ polygon | emit the native strip as two triangles |
| Split or lobed car | six shorts read as two linear XYZ triples | decode x0,y0,z0,z1,x1,y1 pairing |
| Textures missing after the first material | inherited 20-byte VR2 mapping stride | NFS5 mappings are 24 bytes |
| Correct UVs but wrong paint/livery colours | CLUT-table index used as palette | derive palette from VRAM coordinate and CBA |
| Wheel texture on windows | UV containment determines local material | only an exact native mapping index is locally authoritative |
| Incorrect driver texture | runtime face forced into a local rectangle | keep driver/glass in a separate runtime pass |
| Wrong roof/tail-light palette | CLUT variants overwrite a shared rectangle | pack each rectangle/CLUT pair independently |
| Ghosted or invisible surfaces | every STP colour treated as alpha | only exact 0x0000 is statically transparent |
| Car standing on its bumper | generic VR2 PSX axis conversion | NFS5-specific 180° X rotation |
| Names shifted after an archive slot | SCC row used directly as PAK block | map 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-
0x15scene 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 pair | Contents |
[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:
| Type | Size | Meaning |
| 0 | 12 bytes | textured triangle with three indices, three UV pairs, and material ID |
| 1 | 16 bytes | textured quad with four indices, four UV pairs, and material ID |
| 7 | 20 bytes | billboard/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
+0x08selected one of four values per cell; - the four bytes at
+0x24were those compact material tokens; - depending on the scene, the selected token had to be used directly, with
selector ^ 2, or astoken >> 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
u8material 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
+0x08is not the material selector; - road materials are read directly, per cell, from the parallel
u8array.
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:
- select and order the four stored UV pairs according to traversal direction and left/right half;
- convert the native PSX packet slots into OBJ grid order.
The four confirmed branch assignments are:
| Branch | Stored-pair order |
| forward, left | 3,0,2,1 |
| forward, right | 2,1,3,0 |
| reverse, left | 0,3,1,2 |
| reverse, right | 1,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 symptom | Incorrect assumption | Correct interpretation |
| Course chunks piled together | local vertices exported without object translation | add the signed XYZ translation from the 0x1a0-byte object |
| Missing groups or scenery pieces | two pointers treated as a simple start/end range | ownership ends at the next object’s first-group pointer |
| Entire driveable road missing | road expected inside scenery geometry | decode the separate 0x28-byte road grids |
| Incorrect colours and transparency | CLUT-table position used as palette | resolve native VRAM coordinate and CBA |
| Building/rock/grass texture on road | two-bit field and four tail bytes used as materials | use each stream’s parallel u8 material array |
| Scotland almost entirely grass/rock | scene-wide direct/shifted heuristic | every cell has an explicit native material ID |
| 45-degree wedges and destroyed tiles | midpoint split overreads material bytes as UVs | use the stored signed split column |
| Correct image but rotated/mirrored | native packet slots bound directly to OBJ grid | branch permutation plus TL,BL,TR,BR → TL,TR,BL,BR |
| Worse orientation after a global swap | VR2 middle swap applied without complete NFS5 branch logic | combine stream selection, traversal, branch, and packet conversion |
| Missing signs/spectators/trees | type 7 dismissed as renderer metadata | pair type-7 records with XYZ/angle placements |
| Missing road blocks/bollards/chevrons | only ordinary placement groups exported | decode runtime mesh and sprite lists at +0xac/+0xb4 |
| Whole course rotated by 90° | generic PSX axis conversion only | add 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
| Area | V-Rally 2 PC | V-Rally 2 PSX | NFS5 PSX |
| PAK sub-block record | 20 bytes | 16 bytes | 20 bytes despite PSX payload |
| Car texture model | 256×256 RGBA8 with mipmaps | 4-bpp + BGR555/STP CLUT | 4-bpp + BGR555/STP CLUT |
| Track texture model | A1R5G5B5 pages | PSX VRAM uploads + CLUT | PSX VRAM uploads + CLUT |
| Car V3D | version 0x12, floats | version 0x11, GTE shorts | version 0x11, GTE shorts |
| Car mesh stride | 0xC8 bytes | 0x74 bytes | 0x74 bytes |
| Car topology | variable triangle strips | fixed GPU triangles/quads | fixed GPU triangles/quads |
| Car material mapping | PC pages/UVs | 20 bytes, CLUT order usable | 24 bytes, CLUT from VRAM/CBA |
| Track root | type 1 / 0x24 | type 1 / 0x22 or 0x122 | type 1 / 0x15 |
| Scene object stride | 0x128 bytes | 0xF4 bytes | 0x1a0 bytes |
| World placement | float XYZ translation | X/Z origin derived from bounds; Y absolute | direct signed XYZ translation |
| Ordinary track meshes | float4, 0x28-byte mesh records | GTE pairs, 0x20-byte placements | GTE pairs, 0x24-byte placements |
| Road record | 0x20 bytes | 0x24 bytes | 0x28 bytes |
| Road material | direct u16[cell] | separate u16[cell] | parallel u8[cell] for each UV stream |
| Road UVs | four float2 pairs per cell | compact branch-dependent byte streams | branch-dependent eight-byte packets, parallel materials, stored split |
| Runtime props | different/partly open auxiliary passes | track type 7 still open in 0.6.11 | type 7 plus runtime mesh/sprite lists decoded |
| Native axes | X right, Y up, Z forward | X right, Y down, Z forward | X 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 uses0x15. - 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 parallelu8material 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.
- Scenery existed, road was absent
The initial parser handled only scenery. The hole in the driveable corridor led to the separate road table. - 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. - 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. - Scotland exposed the global misconception
_GSCOTRACK2became almost entirely grass or rock. A scene-wide heuristic could not account for its many distinct authored surfaces. - 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. - MIPS code disproved the tail-material tokens
The four bytes participate in position/grid calculations. The first duplicated value is the split column. - 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. - Native branches plus packet-slot conversion
Once stream boundaries were correct, the four traversal branches and finalTL,BL,TR,BRtoTL,TR,BL,BRexchange were confirmed. - 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:
| Datei | Inhalt |
NFS5_PSX_Asset_Explorer_v0.2.8_source/docs/DOCUMENTATION.md | Gesamtstatus, Archivinventar, Fahrzeuge, Tracks und offene Punkte |
NFS5_PSX_Asset_Explorer_v0.2.8_source/docs/FORMAT_TRACK.md | NFS5-Type-1/Version-0x15-Szenen-, Runtime- und Road-Strukturen |
NFS5_PSX_Asset_Explorer_v0.2.8_source/docs/VALIDATION.md | Korpuszahlen und Regression-Baseline |
NFS5_PSX_Asset_Explorer_v0.2.8_source/docs/CHANGELOG.md | Entwicklung der Hypothesen und Korrekturen bis 0.2.8 |
backend/nfs5psxgeo.py und backend/vr2psxgeo.py | Fahrzeuggeometrie, Materialpass-Trennung und Achsenkonvertierung |
backend/nfs5psxtex.py | NFS5-Textur-Envelope, Track-Materialien und CLUT-Auflösung |
backend/nfs5psxtrack.py | Szenengeometrie, Runtime-Props, Road-Streams und Road-UV-Orientierung |
VR2_PAK_Asset_Format_Reference_v0.6.11.md | validierte 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.