The streamed world¶
Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4). No runtime claims: PCSX2 was
not running when this page was written. The disc-side checks (2026-10-04) read the NTSC-U disc's WAD with throwaway
scripts outside the repository and are reported as names, counts and sizes only; those marked "Coney's disc test" come
from coney_tests "[world]" (tests/platform/disc_world_test.cpp), which anyone can run on their own disc.
Purpose¶
A level's buildings, streets and props that never move are not in the level file. They are a streamed world: a RenderWare BSP world that holds only the layout (sectors with bounding boxes and no triangles) and its textures, plus the geometry of each sector as one RenderWare atomic, packed into numbered parts that the game reads from the disc and throws away again as the camera moves. This page is the format of those files, how the game picks what to load and unload, the memory it gives them, and how one frame draws them. How a level as a whole is loaded, and in which order, is on Level loading.
Every level has two streamed worlds, drawn at different points of the frame: <level>s, the solid scenery, and
<level>d, the detail layer (graffiti, fences, road markings, vegetation), drawn after the level's objects. The page
calls them the s world and the d world.
Original structure¶
c:/Warriors/Source/World/ps2/WorldPS2.cpp holds the world class (path string at 0x00588dc0; source map
0x004101f0-0x004124f8), World/ps2/WorldManagerPS2.cpp the world manager that owns the two worlds
(Source map). The two RenderWare plugins are registered from
Graphics/Devices/Renderware/DevRWGeneric.cpp (Graphics). Names are ours.
| Address | Name | Role | Evidence |
|---|---|---|---|
0x00410308 |
World::World |
constructor of the 0x2188-byte world object | confirmed (code) |
0x00410e08 |
World_ReadManifest(world, name) |
reads <name>_sec.mem |
confirmed (code) |
0x00410648 |
World_LoadStream(world, name) |
reads <name>_sec.wld into its own clump |
confirmed (code) |
0x00410a50 |
World_ReadStream(world, rwStream) |
part count, texture dictionary, RenderWare world, callbacks | confirmed (code) |
0x00411010 |
World_RegisterSector |
per-sector callback at load: index the sector by its plugin data | confirmed (code) |
0x00412310 |
World_RequestPart(world, sector) |
starts the asynchronous read of the sector's part | confirmed (code) |
0x004114d8 |
World_PartLoaded (file-manager callback) |
wraps the read buffer in a RenderWare stream and calls 0x004110c0 |
confirmed (code) |
0x004110c0 |
World_ReadPart(world, rwStream) |
reads one <name>_ms<i>.sec |
confirmed (code) |
0x004115d0 |
World_UnloadPart(world, part) |
destroys a part's atomics, dictionary and clump | confirmed (code) |
0x004118b8 |
WorldSector_ReleaseAtomic |
one sector's atomic | confirmed (code) |
0x004101f0 |
World_CameraDistanceSq(bbox) |
squared distance from the nearest camera to a sector box | confirmed (code) |
0x00411eb0 |
World_FindPartToLoad |
nearest sector without its atomic | confirmed (code) |
0x004120a8 |
World_FindPartToUnload |
farthest part that nobody sees | confirmed (code) |
0x00411880 |
World_PendingDistance |
distance to the sector found by 0x00411eb0; FLT_MAX when there is none |
confirmed (code) |
0x0040e100 |
WorldManager_NearestPendingDistance |
the smaller of the two worlds' World_PendingDistance; no new search |
confirmed (code) |
0x00426c78 |
Atomic_AssignGamePipelines |
the game's PS2 pipelines for an atomic and its materials (Pipelines) | confirmed (code) |
0x0017d640 |
LightManager constructor |
the light manager (0x0050cce4, 0xc0 bytes) (Lighting) |
confirmed (code) |
0x0017de10 |
LightManager_SelectLights |
the lights for one atomic or sphere | confirmed (code) |
0x00124778 |
Cam_Follow::Cam_Follow |
the player camera: field of view, near and far clip (The player camera) | confirmed (code) |
0x00411d10 |
World_FindVisibleSectors(world, firstViewport) |
visibility pass for one camera | confirmed (code) |
0x00411b98 |
World_VisibilitySectorCallback |
PVS, occluders, mark visible | confirmed (code) |
0x00411b20 |
World_CollectSector (the world's sector render callback) |
queues a sector whose atomic is loaded | confirmed (code) |
0x00411990 |
World_RenderSectorAtomic(atomic, fadeEnd) |
fade-in, lights, default atomic render | confirmed (code) |
0x004123e8 |
World_ResetVisibility |
clears the visible bits, sets the PVS view point | confirmed (code); file inferred |
0x00410b70 |
World_Unload |
every part, the world, its dictionary and clump | confirmed (code) |
0x0040f8a0 |
WorldManager_Update |
the streaming decision, once a frame | confirmed (code); file inferred |
0x0040e8d8 |
WorldManager_Render(viewport) |
the world part of a viewport's frame | confirmed (code) |
0x00198e20 |
sector plugin 0x3F1 stream reader |
20 bytes into the sector's plugin data | confirmed (code) |
0x00192688 |
atomic plugin 0x3F0 stream reader |
12 bytes into the atomic's plugin data | confirmed (code) |
RenderWare function names (RpWorldRender, RpWorldSetSectorRenderCallBack, RpPVSHook and so on) are inferred from
their arguments and their place in the RenderWare block, as on Graphics.
Small functions of World/¶
The rest of World/ (0x0040c5e0-0x004124f8) besides the level loader's functions on
Level loading. Names are ours and match the local Ghidra project; all
confirmed (code) at the address.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x0040c628 / 0x0040c648 / 0x0040c668 |
Wad_FindEntry / Wad_GetEntrySize / Wad_HasEntry |
the WAD object's lookups (its opener is Wad_Open, File I/O): the index entry for a name, its size field (+0x04, no null check), whether it exists |
confirmed (code) |
0x0040c938 |
WorldManager_SetLoadPriority(scale) |
sets 0x005147cc, the factor the streaming update applies to the resource manager's distances (LoadLevel sets 0.001, the Rumble menu 10.0) |
confirmed (code) |
0x0040c948 |
World_Precache(name, radius, budget) |
pauses the frame pacer (0x0050b734) if it runs, then WorldManager_Preload around the named position when a level object exists, and resumes |
confirmed (code) |
0x0040ca18 |
Water_Set (anchor of World/WorldManagerLua.cpp) |
first call: allocates the level's WaterEffect (0x100 bytes, the water surface) into world manager +0x5c; later calls set its frame, wave height and speed, width, length and colour |
confirmed (code) |
0x0040cc40 |
World_QueuePackToPrecache(name) |
WorldManager_QueuePack (0x0040e1a0) on the world manager: appends a pack name to the queue at +0x0c-+0x24 that the preload loads at its end (The world manager) |
confirmed (code) |
0x0040cc70 |
ScreenFx_SetMotionAlpha(a) |
unless the screen effect 0x0051489c is running, sets the motion-blur colour (white, alpha a) of both screen managers (0x005fdeb8, 0x005fdebc, +0x1a4 and +0x1a8) |
confirmed (code) |
0x0040cce8 / 0x0040cd28 |
ScreenFx_QueueMotionBlurAlpha / ScreenFx_QueueMotionBlurColour(time, rgba) |
queue a blur colour change on the screen manager 0x005fdeb8 (0x0018c8c8) |
confirmed (code) |
0x0040cd78 / 0x0040cda8 |
ResourceManager_SetDynamicAnimation / _SetCharacterModel |
forward to the resource manager 0x0050cd4c (0x0018abd0, 0x0018ad40) |
confirmed (code) |
0x0040cc68 |
PVS_CaptureSamplePoint |
empty: the PVS sampler was removed (Debug) | confirmed (code) |
0x0040cf18 |
LevelHeader_RegisterChunkHandler |
installs 0x0040ce30 as chunk 0x17's handler (Chunk system) |
confirmed (code) |
0x0040e0b8 |
WorldManager_ResetVisibility |
clears +0x34 and calls World_ResetVisibility on the s world (+0x44, with no null check) and on the d world (+0x48) when present |
confirmed (code) |
0x0040e8d0 |
WorldManager_GetSolidWorld |
returns the s world (+0x44) |
confirmed (code) |
0x004102f0 |
Atomic_SetModulateMaterialColour |
geometry flag 0x40 on an atomic's geometry (Part file, step 4) |
confirmed (code) |
0x00410558 |
World_Destroy |
the world's destructor: empties the 140 16-byte vectors at +0x940-+0x1200 and frees their buffers from the STL pool |
confirmed (code) |
0x00411958 |
Material_SetAlphaCallback |
a material callback that sets the alpha byte of the material colour (+0x04); World_RenderSectorAtomic runs it over the materials for the fade-in |
confirmed (code) |
Data¶
All values little-endian. File names below are the WAD names; how a level's world name is chosen is on Level loading.
File names¶
| File | Format | Evidence |
|---|---|---|
<world>_sec.mem |
the manifest | confirmed (code) at 0x00410e08 (string 0x00588e00) |
<world>_sec.wld |
the world stream | confirmed (code) at 0x00410648 (string 0x00588db0) |
<world>_ms<i>.sec, i = 1, 2, ... |
part i |
confirmed (code) at 0x00412310 (string 0x00588df0, "%s_ms%i.sec") |
<world> is <level>s and <level>d when <level>s_sec.wld exists, and <level> otherwise
(Level loading).
Disc check (corroboration), all 2,229 streamed-world entries now named: the WAD holds 159 worlds: 79 levels with
an s and a d world, and objarena with a single world. Each has a _sec.wld and a _sec.mem, and parts
numbered from 1 with no gaps: 1,911 part files. So the 2,229 entries the WAD survey
could not name are 159 + 159 + 1,911.
Manifest (_sec.mem)¶
Read by 0x00410e08 into the world object, confirmed (code):
struct WorldManifest {
uint32_t worldSize; // bytes of <world>_sec.wld -> world +0x216c
uint32_t worldHeapSize; // heap to create for the world -> world +0x2170
uint32_t partCount; // n -> world +0x2164
struct {
uint32_t fileSize; // bytes of <world>_ms<i>.sec -> part record i, +0x10
uint32_t heapSize; // heap to create for part i -> part record i, +0x14
} parts[partCount]; // i = 1 .. n
};
Disc check (corroboration): in all 159 manifests worldSize is the world stream's size, partCount equals the
world stream's own count, and every fileSize is its part's size. worldHeapSize is 1.009 to 1.575 times
worldSize; heapSize is 0.988 to 1.518 times fileSize.
Without a manifest the loader measures the files instead (confirmed (code) at 0x00410648): world heap = 135 % of
the file size, at least 716,800 bytes (0xaf000); part heap = 130 % of the file size, at least 256 KB. Every world on
the disc has a manifest, so this path is never taken on the retail disc (inferred).
World stream (_sec.wld)¶
Read by 0x00410a50, confirmed (code):
u32 partCount the same n as the manifest; stored at world +0x2164
RenderWare texture dictionary (0x16) every world texture; made current, then current = none
RenderWare world (0x0B) the BSP: plane sectors and atomic sectors
The world itself is ordinary RenderWare 3.7 (0x1C02000A), PS2 native (0x510 native data on each atomic sector),
except that no atomic sector holds triangles: the sectors are only boxes in the BSP, each carrying the game's
sector plugin. Disc check (corroboration): all 159 worlds have zero triangles and vertices in
the world header and in every one of their 16,074 atomic sectors. World format flags: 0x410200b9 (two texture
coordinate sets) in 74 s worlds and 12 d worlds, 0x4101003d (one set) in 5 s worlds, 67 d worlds and
objarena. 18 worlds carry RenderWare PVS data (3,304 extension sections of plugin 0x12A).
After reading, the loader (confirmed (code) at 0x00410a50, 0x00410648):
- Records whether the world has PVS data (
0x004bef68) at+0x2160. - Sets the world's sector render callback to
World_CollectSector(0x00411b20). - Walks every world sector with
World_RegisterSector(0x00411010), below. - Checks every atomic's textures with
0x00198190(the "Potential Crash from Missing Texture" check of Graphics). - Sets the RpWorld's word
+0x0cto 2. In RenderWare 3.7 that word is the world's render order and 2 is back-to-front (inferred from the RenderWare layout).
Sector plugin 0x3F1¶
Every atomic sector of a streamed world carries 20 bytes of extension data under plugin id 0x3F1. The plugin gives
each world sector 32 bytes (offset in 0x0050ced0). Stream reader 0x00198e20, setters 0x00198ff0-0x00199068,
confirmed (code):
| Plugin offset | Stream bytes | Meaning | Evidence |
|---|---|---|---|
+0x00 |
the sector's atomic once its part is loaded, else null | confirmed (code) at 0x004110c0, 0x004118b8 |
|
+0x04 |
0-3 | streamed-sector index k: the sector's slot in the world's sector table; 0xffffffff = the sector has no atomic |
confirmed (code) at 0x00411010 |
+0x08 |
state: 2 = atomic loaded, 3 = release pending, 4 = released | confirmed (code) at 0x004110c0, 0x004118b8 |
|
+0x0c |
4-7 | part number i: the atomic is in <world>_ms<i>.sec; 0 for a sector without an atomic |
confirmed (code) at 0x00411010, 0x00412310 |
+0x10 |
end of the fade-in: real-time milliseconds when the atomic was read, plus 1,000 | confirmed (code) at 0x004110c0 |
|
+0x14 |
8-19 | three floats: where the atomic's frame is placed (its translation, RenderWare axes) | confirmed (code) at 0x004110c0 |
Disc check (corroboration): 5,315 of the 16,074 sectors have an index; the indices of a world run 0 to k - 1
with no gap or repeat. Exactly the sectors without an index have part 0 and a zero position (10,759). Part numbers
run 1 to partCount. 4,810 of the 5,315 positions lie inside their sector's box, 4,891 within 5 % of its centre.
Part file (_ms<i>.sec)¶
Read by 0x004110c0, confirmed (code):
16 bytes header {u32 1, u32 0, u32 0, u32 nameHash}; read and ignored
RenderWare texture dictionary (0x16) made current, then current = none; kept in the part record (+0x04)
u32 count
count times:
u32 sectorIndex the streamed-sector index k (sector plugin +0x04)
RenderWare atomic (0x14) a standalone atomic with its geometry
nameHash is the CRC-32 of the file's own name, <world>_ms<i>.sec, not lowercased and without ./ee_files/
(disc check: 1,911 of 1,911). The loader does not read it.
For each atomic the loader (confirmed (code)):
- Creates a frame (
0x004841e8), translates it to the sector plugin's position, and gives it to the atomic. - Stores the atomic in sector
k(+0x00), sets the state to 2 and the fade end to now + 1,000 ms. - Saves the atomic's render callback in
0x005147e8and replaces it with a do-nothing callback (0x00411b18returns its argument). RenderWare never draws these atomics itself; the world manager does (A frame). - Sets geometry flag
0x40(modulate material colour, used for the fade) (0x004102f0). - Checks the materials' textures (
0x00198190) and runs0x00426c78, which stamps the atomic with0x005151c4and walks its materials (meaning not traced).
Disc check (corroboration): every part's atomics belong to sectors whose part number is that part: in all 159
worlds each indexed sector gets exactly one atomic from exactly one part (5,315 atomics in the 1,564 parts numbered
1..n, no mismatch). 1,153 of those parts have a non-empty texture dictionary (10,776 textures in all); the world
streams hold 11,664. All atomics read with RenderWare's PS2 native geometry (0x510) and the game's own
right-to-render pipeline ids (plugin 3, extra data 0x30082/0x30083 on atomics, 0x30084, 0x30086, 0x30088 on
materials).
Native data struct size: inside each part atomic's native data section (0x510), the struct section's header
gives a size larger than the 0x510 section that holds it (for example 878 bytes in a 422-byte section). RenderWare's
reader, like librw's, skips that header without using the size; Coney does the same. Why it is larger is not known
(Coney's disc test reads every atomic this way).
347 part files are never loaded: parts numbered above partCount (31,046,766 bytes). The loader only asks for
parts that sectors name, and no sector names them; they look like leftovers of earlier builds (inferred).
Atomic plugin 0x3F0¶
Every atomic gets 16 bytes (offset in 0x0050cd98; registered by 0x001927d8); 12 of them are streamed, confirmed
(code) at 0x00192618 (defaults), 0x00192688 (reader), 0x00192740 (writer):
| Offset | Stream | Default | Meaning | Evidence |
|---|---|---|---|---|
+0x00 |
float | 1.0 | uploaded to VU memory by the game's four custom PS2 pipelines (0x004252c8, 0x00426430, 0x004275d0, 0x00428410); the scale of the packed 16-bit vertex positions (PS2 world geometry) |
upload confirmed (code); scale confirmed (runtime) by Coney's disc test; that the microcode applies it this way inferred |
+0x04 |
float | 1.0 | uploaded next to +0x00 in the same quadword (+0x00 as x, +0x04 as z; 0x001928c0, 0x001928f0); the scale of the packed 16-bit texture coordinates |
upload confirmed (code); scale confirmed (runtime) by Coney's viewer, visually; the dual microcode applies it to both sets, confirmed (code) (Pipelines) |
+0x08 |
u32 | 0 | read by 0x004290d8 |
confirmed (code); meaning unknown |
+0x0c |
0 | not streamed: the game object that owns the atomic (getter 0x00192870, used by the object renderers) |
confirmed (code); meaning inferred |
Disc check (corroboration): in the streamed worlds' atomics the two floats are always powers of two from 2⁻⁸ to
2⁻¹⁵ (for example 2⁻¹⁰ and 2⁻¹¹), and +0x08 is always 0.
Coney's disc test, confirmed (runtime) for the disc data: with each atomic's packed positions multiplied by
+0x00 and placed at its sector's origin, all 7,563,801 vertices of the 5,315 atomics lie in their sector's box
(1 % margin), and for all 5,315 atomics the vertices' own bounds are the sector's box, every face within 1 % of the
box's largest side. Unscaled, 136 vertices lie in the box. In the 4,708 atomics whose two floats differ, scaling by
+0x04 never gives the box (0 of 4,708; the floats are equal in the other 607). So +0x00 is the position scale.
The atomic's own bounding sphere in the geometry header is in the same scaled units (seen on one atomic). +0x04 is
the texture-coordinate scale (Coney's viewer, below; scaled by it, second-set coordinates
fall in 0 to 1 for 93 % of level2s's vertices, first-set ones show tiling values).
PS2 world geometry¶
Every part atomic's geometry is RenderWare PS2 native geometry: the mesh plugin (0x50E) gives each mesh's vertex
count and material, and the native data (0x510) holds, per mesh, a DMA chain for vector unit 1 (VU1). The chains are
built for the game's own pipelines, right to render plugin 3 with data 0x30083 (5,314 atomics) or 0x30082
(1 atomic), and their layout is not RenderWare's default one. From Coney's disc test (confirmed (runtime) for the disc
data: all 5,315 atomics, 181,150 batches decode this way; what the microcode does with it is inferred):
- Tags: reference tags whose address counts 16-byte units from the chain's start (the vertex data sits after the
chain's
rettag), andcnt/rettags whose upper two words carry VIF commands. - Batches:
STCYCL 4, 1, then up to fourUNPACKs to VU addresses 0 to 3, interleaved four quadwords a vertex: positionV4_16(signed: x, y, z and an unused fourth word), texture coordinatesV4_16(two sets) orV2_16(one set), prelighting colourV4_8unsigned, normalV4_8signed. ThenITOP nand a microprogram start (MSCALFfor the first batch,MSCNTafter);FLUSHat the end. - ITOP is the batch's vertex count. Each
UNPACKmay write a few more vectors than that, as padding: positions to an even count, the byte formats to whole quadwords. With ITOP the batches add up to every mesh's count in the mesh plugin (all meshes). - Strips across batches: the meshes are triangle strips, and every batch after the first starts with the last two vertices of the batch before (all batches on the disc).
- Scale: positions are the 16-bit integers times the atomic plugin's
+0x00, in the atomic's frame, which the loader places at the sector plugin's origin (Atomic plugin 0x3F0).
Triangles: decoded and joined, the strips give 2,869,406 non-degenerate triangles against 2,870,179 in the geometry headers (5,155 of 5,315 atomics equal), and 4,882,437 distinct vertices against 4,943,282 (vertices compared in every attribute librw keeps; the unused fourth position word and the normal's padding byte are ignored). The difference is inferred to be vertices that became identical when packed to 16 bits, which merges them and turns their triangles degenerate.
Pipelines and the second texture-coordinate set¶
The pipeline ids in the files are not the whole story: when a part's atomic (and the level file's sky, cloud and
skyline models) is set up, Atomic_AssignGamePipelines (0x00426c78) gives the atomic the pipeline 0x30083
(0x005151c4, made by 0x00426d58) and chooses each material's pipeline from its MatFX effect (0x00426cc8,
reading the effect type through 0x004653d8), confirmed (code):
| Material's MatFX effect | Pipeline | Made by | Texture coordinates unpacked as | Microcode | Evidence |
|---|---|---|---|---|---|
none, geometry without flag 0x80 |
0x30084 (0x005151b4) |
0x00428f30 |
V2_16, one set (0x6500000d) |
table 0x005045a0 |
confirmed (code) |
none, geometry with flag 0x80 (two sets) |
0x30088 (0x005151b8) |
0x004298c0 |
V4_16, two sets (0x6d00000d) |
the same table 0x005045a0 |
confirmed (code) |
| 4, dual texture | 0x30086 (0x005151bc) |
0x004273f8 |
0x004fc870 |
confirmed (code) | |
| 2, environment map | 0x30087 (0x005151c0) |
0x00428080 |
0x004ff1c0 |
confirmed (code) | |
| any other | unchanged | confirmed (code) |
The set-up of these pipelines is 0x00426e28 / 0x00426c40. So 0x30084 and 0x30088 run the same microcode
and differ only in unpacking one or two texture-coordinate sets: on a plain material the second set is unpacked and,
inferred from the shared microcode, not used. 0x30086 is the dual-texture pipeline with its own microcode.
Disc check (corroboration): in the streamed worlds' part files, 9,516 materials carry MatFX effect 4 (dual), all
with the blend SRCALPHA / INVSRCALPHA and a second texture; 2,467 carry effect 1 (bump map), which keeps its stored
pipeline. So the second texture-coordinate set is, inferred, the coordinates of a dual material's second texture,
alpha-blended over the first (decals, grime, painted markings).
The dual pipeline's upload and microcode (0x004275d0, 0x004fc870), confirmed (code) from the disassembly of
the VU1 program (our own decoding of the MPG blocks of the DMA chain at 0x004fc870):
- Both coordinate sets are scaled by
0x3F0 +0x04. The upload unpacks ten quadwords to VU1 address0x3bc(956); the eighth, at 963, is the scale quadword (+0x00asx,+0x04asz). The microcode loads it (LQ.xz vf6, 963(vi0), at VU address0x0011) and, for every vertex, converts the whole texture-coordinate quadword (both sets,V4_16: s1, t1, s2, t2) to float and multiplies all four components by itsz(ITOF0.xyzw vf14, vf14, thenMULz.xyzw vf14, vf14, vf6z, at0x0023/0x0027). Positions are multiplied byxthe same way, the normals by 1/127, and the prelighting is only converted to float. - The second pass's blend comes from the material, not the microcode. The upload writes
ALPHA_2from the 64-bit word at+0x28of the material's dual effect.RpMatFXMaterialSetDualBlendModes(0x004658d0) stores the source and destination blend at the effect's+0x04/+0x08and rebuilds that word (0x00465e78) astable[dst][src]withFIX0x80, from a 6 × 6 byte table at0x0052e620over zero, one, source alpha, inverse source alpha, destination alpha and any other mode. Source alpha over inverse source alpha gives0x44(the ordinary alpha blend); one and one gives0x68(additive); unsupported pairs give0xaa. Every dual material in the streamed worlds is source alpha / inverse source alpha (disc check above), so in the retail worlds the second pass is always0x44; a faithful port can still convert the pair through the same table. - The rest of
TEST_2is the global Z test (0x00596de8&0x70000) with no alpha test (the effect's+0x38, 0). The fog colour of the second pass is the effect's+0x30unless that is0x1000000, the default (0x00465f88), when it is the current fog colour (0x00596df8); other writers of+0x30were not searched.
Runtime corroboration (the GS dumps of the reference views and the level dumps, fourteen dumps, 1,684 dual pairs): each context-2 strip draws the same screen positions as the context-1 strip before it with different texture coordinates (the second set), and in most pairs the coordinates of both passes are multiples of the same power of two, the scale (in the rest, inferred, one set's raw values were all even or its coordinates whole numbers, so its step looks coarser or finer).
Vertex colour range, confirmed (code) at the uploads 0x004252c8 and 0x00426430: the material colour is
scaled by 1/255 for an untextured material and by 0.0019700117 (about 0.5/255) for a textured one; alpha
always by 0.00197. A textured white material thus reaches the GS as 128, and the GS's texture modulate treats 128 as
1.0 (inferred from the GS's documented behaviour). The prelighting colours are unpacked unsigned (V4_8) and not
scaled by the CPU, so they too are on the GS's scale, where 0x80 is full brightness (inferred). The lit vertex colour
the GS receives never exceeds 0x80 (confirmed (runtime): the brightest channel in a level99 frame is exactly 128
on thousands of vertices), so there is no 2× overbright: min(1, prelight + light) × material, Lighting: the
maths.
The pipeline unit¶
The game's PS2 pipelines are one unit of their own, 0x00424ee8-0x00429b18, linked between Warriors/ and
Movie/, with no path string (its directory is not known). It ends with the static-initialiser stub 0x00429af8.
It holds two groups: the pipelines of the character models (0x30080-0x30082) and those of the world
(0x30083-0x30088, the table above). Each pipeline is a RenderWare PS2 all-in-one pipeline (made by 0x00461420
from a static descriptor, found again by id with 0x00461470) with two callbacks: a first that always answers 1 and
an upload callback that builds the material's data for the VU microcode (the colour scale above, the 0x3F0
words, texture and blend state). Names ours; confirmed (code) unless marked.
| Address | Name | Role | Evidence |
|---|---|---|---|
0x00429a98 |
Pipelines_CreateGame |
creates both groups at device start (from Init, Graphics); returns whether all were made |
confirmed (code) |
0x00424ee8 / 0x00426c30 |
CharPipelines_Construct / WorldPipelines_Construct |
empty constructors of the two groups' objects, which hold no data | confirmed (code) |
0x00429a60 |
Pipelines_ConstructStatic |
constructs the static object 0x006fecb0 that holds both groups |
confirmed (code) |
0x00429ac8 / 0x00429af8 |
Pipelines_StaticInit / its stub |
the unit's static initialiser and stub | confirmed (code) |
0x00424ef8 |
CharPipelines_Create |
makes 0x30081 (0x004250a8, kept in 0x00515068), 0x30082 (0x00426240, 0x00515064) and the atomic pipeline 0x30080 (0x00424fd0, 0x0051506c) |
confirmed (code) |
0x00424f68 |
CharPipelines_AssignToAtomic(atomic) |
gives an atomic 0x30080 and each of its materials 0x30081 (the material callback 0x00424fb0); called by the character model set-up 0x00174c18 (from CharacterModel.cpp's 0x00178178) |
confirmed (code); that these are the character pipelines inferred from that caller |
0x00424fb0 |
CharPipelines_SetMaterialPipe (made) |
material callback: material +0x08 = 0x30081 |
confirmed (code) |
0x00424fd0 |
CharPipelines_CreateAtomicPipe |
the atomic pipeline 0x30080, object callback 0x00425d78 |
confirmed (code) |
0x00425d78 |
CharPipelines_AtomicSetup |
the atomic's per-draw set-up: the geometry's instance data, the morph interpolation, re-instance flags when the geometry changed, a test of the bounding sphere against the camera's 6 frustum planes (inside: no clipping), the lighting mode from the geometry's flags | confirmed (code) |
0x004250a8 |
CharPipelines_CreateMaterialPipe |
the material pipeline 0x30081: microcode names PS2user1.csl-PS2user4.csl (0x0052ea50) and their unpack formats |
confirmed (code) |
0x004250a0 / 0x004273e8 / 0x004273f0 |
Pipe_AlwaysTrue (made, three copies) |
the pipelines' first callback: returns 1 | confirmed (code) |
0x004252c8 |
CharPipelines_UploadMaterial |
upload callback of 0x30081 |
confirmed (code) |
0x00426240 |
CharPipelines_CreateMaterialPipe2 |
a second material pipeline 0x30082 (32 microcode slots); nothing reads 0x00515064 back, so it is never used |
confirmed (code); unused inferred |
0x00426430 |
CharPipelines_UploadMaterial2 |
upload callback of 0x30082 |
confirmed (code) |
0x00426c40 |
WorldPipelines_Create |
the material pipelines (0x00426e28), then the atomic pipeline 0x30083 (0x00426d58) |
confirmed (code) |
0x00426e28 |
WorldPipelines_CreateMaterialPipes |
0x30084, 0x30086, 0x30087, 0x30088 |
confirmed (code) |
0x00426e70 |
WorldPipelines_AtomicSetup |
object callback of 0x30083: as 0x00425d78 |
confirmed (code) |
0x004275d0 |
WorldPipelines_UploadDual |
upload callback of the dual-texture 0x30086 |
confirmed (code) |
0x00428410 |
WorldPipelines_UploadEnvMap |
upload callback of the environment map 0x30087 |
confirmed (code) |
0x00428260 |
EnvMap_GetMatrix |
the environment map's texture matrix from the camera and the effect's frame (none: the camera alone), cached while camera, frame and frame counter stay the same | confirmed (code) |
0x004290d8 |
WorldPipelines_UploadPlain |
upload callback shared by 0x30084 and 0x30088; the only reader of 0x3F0 +0x08 |
confirmed (code) |
The world object (0x2188 bytes)¶
Built by 0x00410308, confirmed (code) for the offsets; the names are ours.
| Offset | Meaning |
|---|---|
+0x0000 |
part being read, -1 when none (set by 0x00412310, cleared by 0x004114d8) |
+0x0004 |
-1, set by the constructor and again when a part's unload completes (0x004115d0); the world manager treats >= 0 as busy, but no other value is ever written (inferred, see Open questions) |
+0x0008 |
pool the part clumps are created in (the Sector Pool) |
+0x000c |
sector whose part is being read |
+0x0010 |
set when the last search fell back to sectors nobody sees |
+0x0014 |
sector found by the last search (0x00411eb0), read by 0x00411880 |
+0x0018 |
32 bytes cleared at each first-viewport visibility pass; use not traced |
+0x0038 |
sector table: 560 pointers, indexed by the streamed-sector index |
+0x08f8 |
visible bits: 18 words, 576 bits, by streamed-sector index |
+0x0940 |
per part: a vector of its sectors (140 entries of 16 bytes, part number as index) |
+0x1200 |
per part: a 0x1c-byte record (140 entries, part number as index): +0x04 texture dictionary, +0x08 parent pool, +0x0c part clump, +0x10 file size, +0x14 heap size (after a load: the bytes the part really used) |
+0x2150 |
result of the PVS view-point update |
+0x2154 |
the world's clump |
+0x2158 |
the world's texture dictionary |
+0x215c |
the RpWorld |
+0x2160 |
world has PVS data |
+0x2164 |
part count n |
+0x2168 |
streamed-sector count (highest index + 1) |
+0x216c, +0x2170 |
world stream size and heap size (manifest) |
+0x2174 |
the world's name, 19 characters and a NUL |
The fixed tables limit a world to 560 streamed sectors and 139 parts. level70s has 593 streamed sectors: its
indices 560 to 592 would be stored over the visible bits and the first part vectors. Whether level 70 is ever played
(it has no level70.lev, see Level loading) is open. Coney should size these
tables from the data.
Behaviour¶
Loading a world¶
World_LoadStream (0x00410648), after World_ReadManifest, confirmed (code):
- If no manifest was read, take the sizes from the files (above).
- Ask the resource manager for room:
0x00187d28(resourceManager, worldHeapSize, 1)evicts the least recently used resources that nothing holds until theSector Pool's largest free block is big enough (confirmed (code) for the loop; "least recently used" inferred from the time stamps it compares). If it cannot make room, the world is not loaded. - Create a clump (Memory) of
worldHeapSizebytes named after the world in theSector Pool(0x006eb9c8), make it current, and read the whole world stream synchronously through a RenderWare stream over the file (World stream). - With no manifest, also measure every part file.
Choosing what to stream¶
Once a frame, while a level runs, the world manager's update (0x0040f8a0) makes at most one streaming decision. It
weighs the two worlds' parts against two other streamers, the resource manager (level props and characters) and the
water effect (Graphics/WaterEffect.cpp, world manager +0x5c). Confirmed (code) for the steps; the arbitration is
summarised:
- Busy? If either world is reading a part (
+0x00 >= 0) or has+0x04 >= 0, or the file manager has a read in flight, do nothing (return 2). - What is wanted. In each world,
World_FindPartToLoad(0x00411eb0) looks for the nearest sector that has an index and no loaded atomic: first among the sectors marked visible in the last frame, and, if none of those is within reach, among all of them (then+0x10is set). "Nearest" is the camera distance; "within reach" compares that squared distance with the camera's draw distance, which is not squared (confirmed (code); whether that is intended is not known). The resource manager offers its own nearest wanted resource (0x00189750, scaled by0x005147cc= 0.001, set byLoadLevel), the water effect its own. - Urgency.
0x005147c8= 1 when the nearest wanted thing is closer than 75.0 (read elsewhere; not traced). - Load the nearest. If the nearest is a world sector,
World_RequestPart(0x00412310) asks the resource manager for room for the part's heap (0x00187d28(rm, heapSize, 0)); if it gets it, it records the part as being read and queues an asynchronous read of<world>_ms<i>.secwithWorld_PartLoadedas the callback (return 2). Otherwise the resource manager or the water effect starts its read. - Make room. When nothing could be started,
World_FindPartToUnload(0x004120a8) offers, in each world, the part whose nearest sector is farthest from the cameras, among parts that are loaded and of which no sector was visible last frame and no atomic is still in use (0x00198220). The resource manager offers its farthest resource. The farthest of these is unloaded, but only if it is more than 5.0 farther than the nearest wanted thing (return 2). If there is nothing to unload, return 1 or 3. The two world searches give squared camera distances, so for scenery the 5.0 margin is added to a squared distance (unload > load + 5.0, both squared; confirmed (code) at0x0040f8a0); the resource manager's values (0x00189750, scaled by 0.001) are compared on the same scale, with a- 5.0on its own path.
World_FindPartToUnload looks at parts 1 to n - 1 only: the last part of a world is never unloaded (confirmed
(code); an off-by-one in the original, inferred).
When the read finishes, the file manager calls World_PartLoaded (0x004114d8), which wraps the read buffer in a
RenderWare memory stream and reads the part into a new clump, Sectors<i>, of the part's heap size, created in the
Sector Pool (0x004110c0, Part file). Afterwards the part record's heap size is replaced by what the
part really used. Then +0x00 = -1.
Unloading a part (0x004115d0): for each sector of the part, 0x004118b8 sets the state to 3 and, unless the
atomic is still in use, detaches and destroys the atomic and its frame and sets the state to 4. If every sector
reached 4, the part's texture dictionary is destroyed, world +0x04 is set to -1, and the part's clump is checked
empty, unregistered and given back to the Sector Pool. Otherwise the unload is retried on a later frame. While the
atomics are destroyed the clump counts frees, the only place where a clump's individual frees matter
(Memory); confirmed (code).
Camera distance¶
0x004101f0, confirmed (code). For each player camera (count 0x0011eae0, camera i from 0x0011f9b0), its
position p in game axes becomes RenderWare axes as q = (p.x, p.z, -p.y). For a sector box with corners lo and
hi:
d2 = sum over the three axes a of min((lo[a] - q[a])^2, (hi[a] - q[a])^2)
result = the smallest d2 over all cameras
This is a distance to the nearest face planes, not to the box: a camera inside a box still gets a positive value.
World_PendingDistance (0x00411880) recomputes this for the sector found by the last search (world +0x14),
against the current cameras, and returns its square root; with no sector found (+0x14 null) it returns FLT_MAX
(3.4028235e38), not infinity. 0x0040e100 returns the smaller of the two worlds' values and does not search
again (confirmed (code)). So in the draw-distance step of A frame, a fully loaded level gives FLT_MAX
and the draw distance grows to its ceiling.
Preloading¶
When a level starts, InitLevel preloads the world around the camera before the first frame
(Level loading): it runs the update above in a loop until it stops reporting work, the
time budget runs out or the nearest missing sector is beyond the camera's draw distance.
A frame¶
WorldManager_Render(viewport) (0x0040e8d8) is the "world" step of a viewport's frame.
Confirmed (code), in order. Render states are RenderWare's: 0x14 cull mode (1 none, 2 back faces), 6 Z test, 8
Z write, 0x0e fog. The world toggles 0x005e5380-0x005e5398 are all 1 (set by the static initialiser 0x00155a90).
- Nothing at all is drawn while a full-screen fade covers the screen (
0x005fdeb8 + 0x1d8 >= 1and+0x1d4set). - Viewport camera; the level's occluders are set up for it (
0x0017a610). -
Draw distance. When
0x0050c698is set (it is, in.data) and the byteW_GameState + 0x410is 0 (meaning not traced), the camera's draw distance moves each frame by at mostdt(real time, capped at 100 ms):- frame rate (
0x0050c680) above 29.5 (24.5 with device flag0x02): toward the pending distance of the nearest missing sector of either world, shrinking by up to40.5 dtand growing by up to10.5 dt, never above 300; - frame rate below it: shrink by
(threshold - rate) × 10.5 dt; - never below
60 - 10 × viewportsand never above the camera's own far clip (115 for the player camera, The player camera).
So the view closes in on scenery that is not loaded yet rather than showing holes. 4. Far clip = draw distance; fog distance = draw distance × the device's fog start. 5. The level world (the light glows in the
.lev, Level loading): culling off, Z test, Z write and fog on, rendered withRpWorldRender. 6. Thesworld: add the camera to it; culling =0x0050c69c(2, back faces); the visibility pass (below); then draw the collected sectors' atomics, last collected first, each withWorld_RenderSectorAtomic(0x00411990). 7. Fog distance = draw distance × 0.5: the humans in view (the resource manager's list+0xc78, last queued first, back-face culling; Drawing a human). Fog distance restored (× the fog start): the cars' opaque pass (list+0xc98, culling off; Drawing a car), then the object instances of list+0xc88(0x0017fd78, back-face culling) except those of type0x20. 8. Thedworld: the same as thesworld. 9. The water effect (0x00191dd8, a phase that grows by 0.16 a frame). 10. With Z write off: the instances of type0x20(last first), then the cars' glass pass (list+0xc98, last first, culling off). Then the remaining effects, each after the world camera's update has ended: the players' throw arcs (ThrowArcs_RenderAll,0x00419da0), litter (Garbage_Render,0x001712c0), ground fog (GroundFog_Render,0x00171f58) and, while0x005971acis set, embers (Embers_Render,0x001795f8) (Graphics has each). The toggles0x005e5380-0x005e5398gate steps 5-10 in the code; all are 1, so every step runs. Confirmed (code). - frame rate (
World_RenderSectorAtomic (0x00411990): during the second after the atomic was read, set the alpha of every
material colour to 255 × (1 - (fadeEnd - now) / 1000), afterwards to 255 (only when it changes); light the atomic
from the LightManager with its world bounding sphere (0x0017de10, 0x0017e810); then call the atomic's original
render callback (RenderWare's default, 0x00469ef8, when none was saved). New scenery therefore fades in over one
second.
Visibility¶
World_FindVisibleSectors (0x00411d10), confirmed (code):
- For the first viewport, reset: clear the visible bits and the 32 bytes at
+0x18, and, if the world has PVS data, set the PVS view point to player 1's camera position (0x004123e8). - Install
0x00411b98as the sector render callback and callRpWorldRender: RenderWare walks the BSP and calls the callback for each sector inside the camera's frustum (no geometry is drawn: the sectors have none). - With PVS data and PVS on (
0x005e539c, set to 1 at start-up), the walk runs with the PVS hooked in (0x004beff8,0x004beec0,0x004befa0). If no indexed sector got past the occluders (a sector the PVS hides still counts,0x005147f4), the pass is run again without PVS. - The callback, for a sector with an index: skip it if one of the level's occluders hides its box (
0x0017a738); otherwise count it, and unless the PVS says it is hidden, call the world's own sector callback,World_CollectSector(0x00411b20), which appends the sector to the world manager's draw list (+0x50-+0x58) when its atomic is loaded, and mark it visible in thesworld's+0x08f8(GetSolidWorld), whichever world is being walked, so thedworld's own bits stay clear. A sector without an index is neither counted, collected nor marked. The full callback: Physics, Unit 19.
Because the world's render order is back-to-front and the list is drawn from its end, sectors are drawn roughly front to back (inferred).
Lighting¶
The LightManager (0x0050cce4) lights the world; the whole subject is on Lighting. For the streamed
world, confirmed (code): World_RenderSectorAtomic selects the world's lights for the sector's bounding sphere
(LightManager_SelectLights, 0x0017de10, flags 2): the world ambient, any world directional lights and the nearest
overlapping world point lights, up to 8, which the PS2 pipeline adds to the prelighting
(World lighting). With no script call the world ambient is the brightness, 40/255 = 0.157 grey;
SetWorldAmbient(r, g, b) makes it rgb + 0.07 + 0.157 (0.227 grey in level99). The background is lit the same
way with a sphere far away and no point lights (Level loading).
The player camera¶
Confirmed (code) at the constructors and 0x00120a98:
| Field | Cam_ICamera base (0x00120868) |
Cam_Follow (0x00124778, vtable 0x00535d50), the player camera |
Getter slot |
|---|---|---|---|
+0x44 field of view, degrees |
60 | 65 | 0x1f4 |
+0x50 near clip |
0.3 | 0.1 | 0x204 |
+0x54 the camera's own far clip |
60 | 115.0 (0x005d91c0, set by Cam_Follow's static initialiser 0x00134ab0) |
0x20c |
+0x58 draw distance |
60 | set by 0x00122128 (which also sets the main camera's far clip when 0x0050c698 is set) |
0x214 |
+0x5c, +0x60 view window |
from the field of view, below | 0x254 |
View window (0x00120a98): half = (fov + 0x0050b178) × 0.5 degrees (0x0050b178 is 0); +0x5c = tan(half) ×
0.75 × aspect with aspect +0x4c = 0x0050b204 = 4/3, so just tan(half); +0x60 = tan(half) × 0.75. For the
player camera that is (0.637, 0.478): a 65° horizontal field of view on a 4:3 picture. Split screen halves the
values. (0x004b8d30 is inferred to be tanf.)
Each frame 0x001562c8 hands the view window, the near clip (+0x50) and the draw distance (+0x58) as the far
clip to device slot +0x28; fog starts at far clip × the device's fog start. In WorldManager_Render the draw
distance's ceiling is the camera's own far clip (+0x54, slot 0x20c): 115 for the player camera.
Fog and background colour¶
One colour is both: device +0x440, set through device slot +0x48 (Graphics). It is
white from start-up (GraphicsDevice_Open) and black after UnloadLevel; a level sets it from its script, confirmed
(code):
SetFogColor(r, g, b)(0x0036e558→Level_SetFogColour,0x0040c868): floats in 0 to 1, times 255, alpha 255 (confirmed (code)).SetFogDistance(d)(0x0036e5f8→Level_SetFogDistance,0x0040c908; confirmed (code)): the device's fog start+0x444(0.5 by default), the fraction of the far clip where fog begins.
The fog curve, confirmed (runtime) from a PCSX2 GS dump of level99 (checkpoint 3, the street at night): fog is
per vertex and linear in the camera distance w (metres). The GS fog value (255 = no fog, 0 = all fog colour) is
255 × (far − w) / (far − start) with far the draw distance and start = far × fog start: 255 up to 57.5 m and 0
at 115 m, for the 115 m draw distance and fog start 0.5 (a least-squares fit over 2,305 partly fogged world vertices
puts the ends at 57.3 m and 115.1 m). The GS blends colour × f + fogColour × (1 − f) with f = value / 255, after
texturing and before alpha blending. Fog is off for the sky, clouds and skyline (Level
loading) and for the HUD.
Disc check (corroboration): SetFogColor appears in 66 compiled Lua files, covering 62 of the 64 levels that have
a .lev; SetFogDistance in 14 levels. The colour values themselves are inside Lua bytecode and were not decoded.
Coney's implementation¶
Reading, decoding, streaming and drawing exist (2026-10-04): coney --view-world <level> streams a level's two worlds
around a free-flying camera (Running Coney). The level file, the level world,
objects, PVS and occluders do not.
Reading and decoding (2026-10-04, unchanged):
src/world/world_streams.hreads the layout of a world stream (inspectWorldStream: part count, dictionary, every atomic sector with its box and0x3F1data) and of a part file (inspectPartFile), and checks each atomic section for librw (inspectAtomicSection). librw has no RpWorld stream reader at all, so the BSP is walked here; the sectors hold no geometry, so nothing else of the world is needed. The two plugin readers are@orig-tagged.src/graphics/ps2_world_mesh.hdecodes a mesh's DMA chain (PS2 world geometry).src/platform/world_atomic.h:attachWorldPlugins()(run byRenderEngine::startbetween librw's init and open) registers librw's mesh, native data and right-to-render plugins, the0x3F0atomic plugin (so the scales stay with the atomic), and a rights callback for plugin id 3 that gives atomics with pipeline0x30082/0x30083Coney's PS2ObjPipeline, whose uninstance step decodes the chains into plain librw geometry (positions scaled by+0x00, texture coordinates by+0x04, colours, normals, triangles from the strips).WorldAtomic::readreads one part atomic: librw's atomic reader is written for clumps (it takes the geometry from a clump's geometry list), but a part atomic carries its geometry inside, so the geometry is read on its own and the atomic is then read from its struct and extension.WorldAtomic::unpack()uninstances it and hands it to the default (GL3) pipeline.
What librw does with these atomics on its own (from librw's source): it reads the geometry and keeps each mesh's
DMA chain untouched, but it cannot decode it. Its PS2 uninstance code expects RenderWare's default layout,
positions as V3_32 and texture coordinates as V2_32 inline after their UNPACK; here they are V4_16, by
reference, interleaved and scaled. With its asserts off (Coney's build) it would print "unexpected unpack" and copy the
wrong bytes. And the GL3 renderer cannot draw PS2 native geometry at all. So the smallest fix is the pipeline above,
registered from Coney's code; librw needs no patch.
Streaming (platform-neutral, src/world/, unit-tested on synthetic worlds):
world_manifest.h:readWorldManifest(0x00410e08) and the no-manifest heap rules.streamed_world.h:StreamedWorld, one world's bookkeeping, its tables sized from the data (solevel70s's 593 sectors fit):createindexes the sectors (World_RegisterSector);cameraDistanceSqis the face-plane metric;findSectorToLoad,pendingDistance,findPartToUnload,resetVisibility,findVisibleSectorsandcollectSectorsfollow0x00411eb0,0x00411880,0x004120a8,0x004123e8,0x00411d10and0x00411b20. The two quirks are kept and marked in the code: the squared distance compared with the plain draw distance, and partnnever unloaded. A consequence of the metric shows in the tests: a camera inside a sector is as near to the neighbour across the closest face, and the lower index wins the tie.pendingDistanceisFLT_MAXwhen the last search found nothing.collectSectorswalks the world's BSP back to front from the viewport camera (the far side of each plane first) and returns the sectors last collected first, as the original draws its list; the reader keeps the BSP's planes for it (world_streams.h,BspPlane).world_streamer.h:updateStreaming(WorldManager_Update) makes one decision a frame across thesanddworlds through aPartStore;requestPart(0x00412310);preloadWorlds(WorldManager_Preload, radius = the draw distance);adjustDrawDistance(step 3 of A frame). The 5.0 unload margin is added to the wanted sector's squared distance, andnearestPendingDistance(0x0040e100) reads the last searches without searching again, so the preload loads one part past its radius before it stops, as the original does.sector_budget.h:SectorBudget, theSector Poolas a number (Memory).view_frustum.h,debug_camera.h: the visibility pass's frustum test (Coney tests each sector's box, having no BSP walk) and the viewer's free-fly camera.
Loading and drawing (src/platform/):
world_set.h:WorldSetloads a name's worlds in LoadLevel's order (<name>sthen<name>d, or<name>alone): manifest, room in the budget, world stream with its dictionary (0x00410648,0x00410a50); it is thePartStorethat reads a part (0x004110c0: dictionary first, then each atomic placed at its sector's origin, unpacked and given geometry flag0x40) and frees one (0x004115d0: atomics, then dictionary); its destructor isWorld_Unload.texture_lookup.h: the global lookup. Every world and part dictionary is registered, newest first, and librw's find callback searches them all; librw's own default makes its start-up dictionary current and searches only that one, so Coney clears the current dictionary, as the game never leaves one set. No file is read and no stand-in made for a missing name.world_renderer.h:WorldManager_Renderas far as it goes: the camera at the draw distance with fog from half of it in the background colour, then thesworld's collected sectors and thedworld's in the BSP's drawing order, Z test and write, back faces culled, each atomic throughWorld_RenderSectorAtomic(material alpha for the one-second fade, then librw's render). With a level object it first draws the level's background (sky, turning clouds, skyline, then a Z-only clear), and then step 5, the glow world (Level loading).world_viewer_mode.h: the mode behind--view-world. Per frame: camera, one streaming decision (from the last frame's visibility), draw distance, visibility pass, draw. Game time, so--framesand--input-scriptgive the same run every time. The camera is Coney's free-flying debug camera, looking through the player camera's lens (src/camera/camera_lens.h: 65°, view window (0.637, 0.478), near clip 0.1, far clip 115 as the draw distance's ceiling). The documented clip planes keep the debug view useful: the draw distance still follows the missing scenery, and 115 is far enough to see down a street; beyond it the fog's colour takes over.- Second layers (
world_atomic.h,dualLayerOf): librw's MatFX plugin is not attached (it opens a driver for every backend and draws through pipelines Coney does not use), so Coney reads the MatFX material extension itself, keeping only the dual effect's texture.WorldAtomic::unpack()gives an atomic with dual materials a second atomic on the same frame: the same vertices with the second texture-coordinate set, one mesh per dual material textured with its dual texture.SceneLightingdraws it right after the atomic, as the second pass does: blended by the texture's alpha, no alpha test, Z write, at the base's fade (level99checkpoint 3: 137 dual meshes, stains and dirt over the ground and walls). - First passes' alpha test: the world, objects and humans are drawn with librw's emulation of the GS's GEQUAL
0x40test withAFAILFB_ONLY(Shared state): a fainter pixel is blended but writes no Z. - Mipmapped textures: librw's OpenGL conversion makes a whole mip chain for a texture flagged mipmapped but fills
only the levels the PS2 texture has (most have one), and OpenGL draws such a texture black; each converted texture is
limited to its source's levels (
texture_dictionary.h). Before this,level99's roller shutters drew black. - Mip level by distance: OpenGL picks a level by the texture's size on screen, the GS by the camera distance
(Rendering). Coney keeps each texture's
KandLwhen it converts it and draws atomics through its own copy of librw's default GL3 shaders, which sample atlog2(depth) × 2^L + Kfor the sectors and at level 0 for everything else (repo:src/platform/texture_lod.h). With an OpenGL 2.1 context librw's own shaders stay. - Mipmap levels unswizzled once: librw's conversion unswizzles each mipmap level twice (its level lock, then the
image's), which scrambled every level and turned the padded small ones into a lattice (
level99's far roller shutters). Coney converts PS2 mipmapped rasters level by level itself, each unswizzled once at its sent size and cropped (GS upload packets); the far shutters then show their slats.
Seen in the viewer (screenshots of level2, level14, level51, level83, level100 and objarena, checked by
eye; none kept):
- Sectors meet without cracks or overlaps, so placement at the sector origin with the
+0x00scale is right. 0x3F0 +0x04is the texture-coordinate scale: with it, road markings, crossings, tiled pavements and trees look right; with the+0x00scale instead, the same textures tile visibly wrong (trees become rows of repeated leaf patches, crossings lose their stripes). Coney's evidence: confirmed (runtime), visually.- The first texture-coordinate set is the base texture's: drawn with set 1 only, everything looks right. The
second set (in the
sworlds) is the MatFX dual texture's, below. - Prelighting is dark: over all of
level2s's vertices the colour channels average about 14 of 255 and rarely pass 128; alpha is always 255. Coney doubles red, green and blue (clamped) when it unpacks, reading 0x80 as full brightness as the GS does when it modulates a texel by a vertex colour (Coney's choice, inferred from the GS). The viewer lights the scenery with the light manager as it starts (world ambient 0.157, Lighting), as a level is lit before its script runs; play lights it with the level's scripted lights. - Winding and culling: in
level2s,level2dandlevel51s99.6 % of the triangles face the way their vertex normals point (153,453 against 525); with back-face culling, the faces that go are the backs of one-sided backdrop façades seen from outside the play area, as expected. - Billboards and signs read left to right: the image is not mirrored.
Coney's choices (marked in the code): a part's recorded heap size stays the manifest's (the original replaces it
with what the part used); reads are synchronous; a part that fails to read is marked failed and not asked for again;
freeing happens only when a wanted part does not fit; the fade uses game time; a window that is not 4:3 keeps the
player camera's view-window height; the viewer starts above the middle of the first world's part 1, looking along
+z. The choices the research contradicted (2026-10-04) now follow the original: the Sector Pool is 17,217,536 bytes
(Memory), the unload margin is added to squared distances, the preload reads the last
search, pendingDistance is FLT_MAX, the camera's far clip is 115 with near 0.1 and a 65° view, the background and
fog colour is white, the ambient 0.157, and collected sectors are drawn last collected first in the BSP's order.
Disc check (NTSC-U, 2026-10-04, counts only): coney_tests "[world_streaming]" streams all 80 levels' worlds
(79 pairs and objarena) under a camera that visits the centre of every streamed sector, three frames each (15,945
frames), with the player camera's lens and the retail Sector Pool (charged with the worlds and parts only): 1,605
part reads, 43 parts freed, 435 frames short of room, every used part read at least once, none failed. Most atomics
resident at once: 598; budget peak 17,168,058 bytes. With the budget cut to the worlds plus 2 MB, level51,
level83 and level54 stream with 723 parts freed, 752 read and 451 frames short of room, peak 3,753,423 bytes; in
both runs no frame breaks an invariant (budget never exceeded and equal to what is loaded, every loaded part's atomics
present, no freed part seen last frame, the last part never freed, the margin always kept). coney_tests
"[disc][world]" also checks the BSP: in all 15,915 planes of the 159 worlds the left subtree lies below the right
one on the plane's axis, the side collectSectors takes it to be.
The implementer's questions, answered (2026-10-04, from code; the contradictions are marked in the paragraph above):
0x3F0 +0x04: the texture-coordinate scale. The code shows both floats uploaded in one quadword; the microcode of the plain pipelines was not read, so for them the evidence stays Coney's visual check; the dual pipeline's microcode multiplies both texture-coordinate sets by it, confirmed (code) (Pipelines).- Vertex colour range: consistent with 0x80 = 1.0, so Coney's doubling stays; the CPU halves textured material colours for the same reason (Pipelines). The lit result is clamped at 1.0 before the material multiplies it (Lighting: the maths), confirmed (runtime). The world is lit by Coney's light manager (Lighting).
- The 5.0 margin: squared distances.
0x0040e100: no new search.World_PendingDistancewith nothing found:FLT_MAX(Camera distance). - The player camera: far clip 115, near clip 0.1, 65° (The player camera).
- The second texture-coordinate set and the pipelines: Pipelines.
- The background colour: the level script's fog colour (Fog); the level world: the light glows (Level loading).
- The
Sector Pool's real size: still open (runtime, Memory).
Disc counts¶
NTSC-U disc, 2026-10-04, counts and sizes only. "Parts" are those numbered 1..n, the ones the game can load;
"sectors" are sectors with a streamed atomic; sizes in bytes.
| Count | Bytes | |
|---|---|---|
Worlds (_sec.wld) |
159 | 70,177,584 (texture dictionaries 66,506,916) |
Manifests (_sec.mem) |
159 | 14,420 |
| Part files used | 1,564 | 279,667,294 (texture dictionaries 41,411,116) |
| Part files never loaded | 347 | 31,046,766 |
| World sectors / with an atomic | 16,074 / 5,315 | |
| Atomics in used parts | 5,315 | 2,870,179 triangles, 4,943,282 vertices, 88,425 materials |
| Textures | 11,664 in worlds, 10,776 in used parts |
Largest values: world stream 1,316,524 (level61s); world heap 1,594,404; part file 346,962; part heap 372,229;
parts in one world 46 (level83s, level84s); streamed sectors in one world 593 (level70s, then 144). The level
whose worlds and used parts are largest is level51: 15,538,318 bytes in all, far more than fits in memory at once,
which is why the parts stream.
Per level: world stream size, parts and streamed sectors of each world, and the bytes of all its used parts.
objarena has one world: 28,820 bytes, 1 part, 1 sector, 69,708 bytes of parts.
Every level (79)
| Level | s world |
s parts |
s sectors |
d world |
d parts |
d sectors |
Parts, both worlds |
|---|---|---|---|---|---|---|---|
level1 |
26,800 | 1 | 1 | 18,112 | 1 | 1 | 2,076 |
level2 |
811,128 | 24 | 48 | 275,956 | 16 | 57 | 6,940,964 |
level3 |
1,154,234 | 30 | 112 | 283,156 | 10 | 35 | 6,457,894 |
level5 |
1,098,068 | 23 | 67 | 297,416 | 20 | 72 | 8,297,320 |
level9 |
553,380 | 22 | 57 | 268,772 | 5 | 20 | 5,915,296 |
level11 |
1,080,879 | 30 | 71 | 269,740 | 20 | 62 | 9,437,256 |
level14 |
807,126 | 21 | 54 | 269,756 | 9 | 34 | 5,743,904 |
level20 |
1,081,816 | 36 | 94 | 278,056 | 12 | 52 | 9,784,458 |
level31 |
801,358 | 20 | 40 | 284,636 | 12 | 48 | 6,426,536 |
level34 |
1,085,436 | 23 | 68 | 265,808 | 10 | 29 | 5,535,774 |
level51 |
1,127,936 | 39 | 95 | 306,644 | 30 | 101 | 14,103,738 |
level52 |
1,117,420 | 26 | 75 | 283,492 | 16 | 55 | 8,769,378 |
level54 |
1,214,140 | 35 | 137 | 300,496 | 28 | 102 | 11,784,862 |
level55 |
1,095,196 | 19 | 46 | 271,504 | 11 | 28 | 4,941,500 |
level60 |
1,233,152 | 7 | 28 | 517,868 | 4 | 8 | 1,900,100 |
level61 |
1,316,524 | 10 | 25 | 267,664 | 5 | 14 | 2,876,126 |
level62 |
1,062,240 | 7 | 32 | 258,168 | 4 | 5 | 1,684,622 |
level63 |
723,600 | 5 | 21 | 254,880 | 3 | 8 | 1,316,814 |
level64 |
1,049,612 | 8 | 20 | 269,336 | 5 | 20 | 2,306,562 |
level70 |
1,289,380 | 31 | 593 | 214,696 | 5 | 5 | 2,732,096 |
level71 |
511,908 | 3 | 12 | 198,764 | 2 | 3 | 507,526 |
level72 |
201,076 | 1 | 1 | 48,536 | 1 | 1 | 123,322 |
level73 |
1,003,424 | 21 | 140 | 276,664 | 13 | 34 | 5,259,224 |
level74 |
351,256 | 4 | 24 | 221,172 | 2 | 5 | 1,177,106 |
level80 |
1,063,451 | 19 | 48 | 278,940 | 12 | 42 | 6,692,176 |
level81 |
845,680 | 27 | 68 | 274,316 | 10 | 31 | 5,342,364 |
level82 |
825,154 | 29 | 82 | 291,940 | 17 | 74 | 9,017,116 |
level83 |
1,104,452 | 46 | 103 | 303,164 | 33 | 104 | 13,074,120 |
level84 |
871,416 | 46 | 144 | 281,532 | 17 | 45 | 7,676,910 |
level86 |
1,051,788 | 14 | 31 | 259,300 | 5 | 20 | 3,716,592 |
level87 |
793,823 | 24 | 56 | 276,424 | 11 | 39 | 7,157,260 |
level90 |
346,412 | 1 | 2 | 221,776 | 1 | 3 | 326,300 |
level91 |
254,848 | 8 | 18 | 276,048 | 5 | 33 | 2,895,146 |
level92 |
1,089,181 | 30 | 73 | 278,396 | 14 | 40 | 7,767,202 |
level93 |
804,528 | 23 | 63 | 270,528 | 14 | 46 | 6,603,378 |
level94 |
784,504 | 8 | 20 | 254,860 | 5 | 21 | 2,753,064 |
level95 |
1,102,042 | 32 | 78 | 286,748 | 17 | 48 | 8,542,984 |
level96 |
1,132,436 | 26 | 100 | 280,264 | 17 | 38 | 9,255,666 |
level97 |
254,848 | 8 | 18 | 276,048 | 5 | 33 | 2,895,146 |
level98 |
184,508 | 1 | 2 | 98,164 | 1 | 1 | 152,946 |
level99 |
811,848 | 20 | 47 | 274,856 | 11 | 27 | 5,712,274 |
level100 |
50,756 | 19 | 51 | 30,056 | 1 | 1 | 95,700 |
level101 |
494,044 | 4 | 12 | 190,816 | 2 | 9 | 1,205,364 |
level102 |
474,088 | 3 | 9 | 257,736 | 3 | 13 | 1,213,532 |
level103 |
710,368 | 5 | 17 | 257,796 | 2 | 5 | 1,364,454 |
level104 |
413,756 | 5 | 29 | 264,276 | 5 | 21 | 1,458,342 |
level105 |
159,720 | 1 | 2 | 51,272 | 1 | 2 | 201,474 |
level106 |
630,084 | 6 | 13 | 224,600 | 2 | 8 | 1,456,838 |
level107 |
787,800 | 5 | 13 | 204,476 | 1 | 5 | 1,317,476 |
level108 |
778,096 | 8 | 18 | 269,836 | 5 | 22 | 2,501,606 |
level109 |
736,060 | 9 | 21 | 263,720 | 3 | 13 | 2,565,624 |
level110 |
86,588 | 2 | 5 | 12,948 | 1 | 1 | 360,938 |
level111 |
668,100 | 6 | 14 | 250,488 | 3 | 8 | 1,593,614 |
level112 |
1,056,084 | 15 | 48 | 268,032 | 6 | 22 | 4,524,650 |
level113 |
616,952 | 4 | 9 | 265,956 | 3 | 14 | 1,358,088 |
level114 |
788,220 | 10 | 20 | 208,556 | 2 | 9 | 2,297,158 |
level115 |
741,720 | 6 | 13 | 270,132 | 4 | 18 | 1,909,206 |
level116 |
117,344 | 1 | 2 | 91,676 | 1 | 1 | 190,452 |
level117 |
124,500 | 1 | 3 | 37,632 | 1 | 1 | 252,000 |
level118 |
468,940 | 4 | 8 | 128,004 | 1 | 5 | 935,422 |
level119 |
530,872 | 19 | 51 | 290,812 | 10 | 55 | 6,335,822 |
level120 |
530,872 | 19 | 51 | 290,812 | 10 | 55 | 6,335,822 |
level121 |
408,860 | 2 | 5 | 226,836 | 2 | 9 | 754,820 |
level122 |
367,208 | 3 | 6 | 220,292 | 1 | 5 | 554,400 |
level123 |
134,472 | 1 | 1 | 23,348 | 1 | 1 | 116,210 |
level124 |
135,412 | 2 | 4 | 58,020 | 1 | 1 | 349,650 |
level125 |
128,864 | 2 | 3 | 22,696 | 1 | 1 | 275,460 |
level126 |
755,760 | 7 | 27 | 261,628 | 2 | 7 | 1,684,436 |
level127 |
456,524 | 3 | 32 | 98,704 | 2 | 12 | 916,258 |
level128 |
396,944 | 4 | 8 | 231,468 | 2 | 8 | 946,568 |
level129 |
635,228 | 7 | 13 | 238,724 | 2 | 8 | 1,610,290 |
level130 |
117,788 | 2 | 6 | 204,748 | 2 | 13 | 746,400 |
level131 |
353,100 | 4 | 62 | 46,384 | 1 | 7 | 671,722 |
level132 |
260,628 | 2 | 4 | 81,780 | 1 | 4 | 512,874 |
level133 |
265,656 | 6 | 13 | 109,732 | 2 | 7 | 1,543,244 |
level134 |
643,996 | 6 | 14 | 267,224 | 3 | 16 | 1,783,600 |
level135 |
783,448 | 7 | 13 | 265,152 | 4 | 13 | 2,143,394 |
level136 |
338,544 | 4 | 10 | 211,940 | 3 | 13 | 1,112,152 |
level137 |
317,176 | 3 | 5 | 183,280 | 2 | 8 | 797,428 |
Some pairs are byte-identical (level91/level97, level119/level120).
Open questions¶
- The microcode: how the VU1 programs at
0x005045a0and0x004ff1c0use the0x3F0scales, the prelighting and the lights. The dual program0x004fc870scales both texture-coordinate sets by+0x04(Pipelines); its lighting part was not read. - MatFX effect 1 (bump map, 2,467 materials): which pipeline it ends up with, and whether it draws differently.
- The two atomic pipelines
0x30082and0x30083: only one atomic uses0x30082; how the two differ. - The native data struct size that exceeds its section (Part file).
0x3F0+0x08(always 0 in the worlds) and0x004290d8, its only reader.- World
+0x04(answered): nothing sets it to a value>= 0. The only writes found are the constructor's and the finished unload's -1 (0x00410308,0x004115d0); every other function ofWorldPS2.cppand the world manager's update and loaders were searched for a store to+0x04(inferred: a write from outsideWorld/is not ruled out, but the world objects are private to the world manager). The busy test on it is dead code in practice; it looks like the remains of an "unload in progress" index that the retrying unload no longer needs (speculative). 0x005147c8, the "nearest wanted thing within 75" flag, and the 32 bytes at world+0x18: their readers.level70: 593 streamed sectors overflow the 560-entry table. Is the level reachable?- Runtime confirmation with PCSX2: a breakpoint on
0x00412310while walking through a level would show the order parts are requested in; a watch on world+0x0000the reads in flight.