Graphics device, frame and textures¶
Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4). Runtime claims were made
in PCSX2 2.9.94 (memory reads and writes over PINE, and PCSX2's own screenshots measured in scratch, never kept in
the repository); each says so where it is made. The disc-side checks were run on the NTSC-U disc (2026-10-04) and
are reported as counts only.
Purpose¶
Everything the player sees goes through one object: the game's RenderWare graphics device, a C++ wrapper around RenderWare Graphics 3.7 for PS2. It starts RenderWare, chooses the video mode, owns the cameras and frame buffers, clears and presents each frame, and reads texture dictionaries and worlds from the chunk system's streams. This page is what the "First pixels" milestone needs: how the device is set up, what one frame does on the screen, why the game runs at 30 frames a second, how textures are loaded and found by name, how the front end draws in 2D, and what the first screen after the start-up movies is.
Coney draws through librw (an open reimplementation of RenderWare 3.7) on OpenGL 3, so most RenderWare calls below have a direct librw equivalent; the PS2-specific parts (DMA packets, vertical-blank handling, GS registers) do not, and the page says what behaviour they produce instead.
Original structure¶
The device class is in c:/Warriors/Source/Graphics/Devices/Renderware/DevRWGeneric.cpp (the path string at
0x005534d0, passed by the allocation in 0x00194488; the source map attributes 0x00192908-0x00197cd0 to the
file). Its base class, with constructor 0x0017a1c8 and vtable 0x00538b40, sits in an unnamed Graphics/ file
before the stub 0x0017b1c0 (inferred). The functions after 0x00197cd0 up to 0x00198d40 (the texture and world
slots, the camera wrapper) have no anchor of their own; they are probably the end of DevRWGeneric.cpp (inferred).
RenderWare itself is the library block 0x0044e060-~0x004aa000 (Source map); the
RenderWare function names used on this page are inferred from their arguments, the structures they touch and their
place in that block.
Names are ours.
| Address | Name | Role | Evidence |
|---|---|---|---|
0x00194488 |
RwDevice_Create(progressive, widescreen) |
allocates the device (0x460 bytes), sets the globals, picks the screen mode | confirmed (code) |
0x00194d58 |
RwDevice::RwDevice |
constructor: vtable, ten camera wrappers, zeroes the mode fields | confirmed (code) |
0x00194e28 |
RwDevice_SetWidescreen(dev, on) |
flags and field-of-view constants for 4:3 or 16:9 | confirmed (code) |
0x0017a1e0 |
GraphicsDevice_Open(dev) |
base-class start-up: device slot +0x08, background colour, fog start |
confirmed (code) |
0x00194558 |
device slot +0x08 Init |
starts RenderWare and creates the cameras and rasters | confirmed (code) |
0x001958b0 |
device slot +0x30 Present |
shows the main camera's raster | confirmed (code) |
0x00195c78 |
device slot +0x88 SetUpViewport(i) |
split-screen camera for viewport i |
confirmed (code) |
0x00197f70 |
device slot +0x150 ReadTexDictionary(stream) |
RenderWare texture dictionary from a chunk stream | confirmed (code) |
0x001980e0 |
device slot +0x168 FindTexture(dict, name) |
named lookup in one dictionary | confirmed (code) |
0x00197ba0 |
device slot +0x170 ReadWorld(stream) |
RenderWare world (BSP) from a chunk stream | confirmed (code) |
0x001986b0 |
RwCam::RwCam |
camera wrapper constructor (0x60 bytes, vtable 0x00538f78) |
confirmed (code) |
0x001906e8 |
chunk 0x0B reader |
texture dictionary, pushed as 0x0B |
confirmed (code) |
0x00190770 |
chunk 0x2A reader |
texture dictionary through slot +0x150, pushed as 0x0B |
confirmed (code) |
0x00197df0 |
RwStreamCustom_Init(custom, stream) |
wraps a game stream in a RenderWare custom stream | confirmed (code) |
0x0018e898 |
texture-dictionary holder destructor (Graphics/Texture.cpp) |
destroys the dictionary's textures, the dictionary and its heap | confirmed (code); role inferred |
0x00410648 |
World_LoadStream (World/ps2/WorldPS2.cpp) |
reads %s_sec.wld |
confirmed (code) |
0x004114d8, 0x004110c0 |
World_LoadSectorAtomics |
read %s_ms%i.sec |
confirmed (code) |
0x00159c08 |
StartupScreen_Draw (mode 5) |
draws the legal screen | confirmed (code) |
0x0048bdb0 |
RenderWare PS2 vertical-blank handler | flips the display at most every second vertical blank | confirmed (code) |
Data¶
Globals¶
| Global | What | Evidence |
|---|---|---|
0x0050cdb4, 0x0050cdb8 |
the device (both set by 0x00194488) |
confirmed (code) |
0x0050cdb0 |
vertical blanks per displayed frame: 2 | confirmed (code), read by 0x00194aec |
0x0050cdac |
0x1c0000: size of the PS2 DMA packet buffer handed to the driver (0x0048e210) |
confirmed (code); meaning inferred |
0x0050b208 |
aspect of the overlay camera's view window. Its static value 1.3333 is overwritten when the device starts (0x00194634) and when the mode changes (0x00194e28): 1.45 for interlaced 4:3 (the default), 1.6667 for 16:9 (with the scale below 1.1), 1.59 for progressive |
confirmed (code) at those addresses; confirmed (runtime), PCSX2 2.9.94: 0x3fb9999a (1.45) at the front end and in a fight |
0x0050b20c |
overlay view-window scale: 1.0 (4:3) or 1.1 (16:9) | confirmed (code) |
0x0050b6f8 |
"cameras set up this frame" flag, set by 0x001448c8 in slot +0x28, cleared by Present |
confirmed (code) |
0x005fd260... |
static colour table built by 0x0017ae38 (black at 0x005fd260, white at 0x005fd268, ...) |
confirmed (code) |
The device object (0x460 bytes)¶
| Offset | Size | Meaning | Evidence |
|---|---|---|---|
+0x000 |
4 | mode flags: 0x01 interlaced, 0x02 PAL (speculative), 0x04 16:9, 0x20 progressive (480p) |
confirmed (code) at 0x00194558, 0x00194e28; names inferred |
+0x004 |
4 | pointer copied from 0x0050ce84 by 0x0017a1e0 |
confirmed (code); meaning unknown |
+0x008 |
4 | vtable (0x00538d78; the base class's is 0x00538b40) |
confirmed (code) |
+0x010 |
0x60 | main camera: the 3D scene, full screen | confirmed (code) |
+0x070 |
4 × 0x60 | viewport cameras 0-3 (split screen), sub-rasters of the main camera's | confirmed (code) |
+0x1f0 |
0x60 | overlay camera: HUD and front end, shares the main frame and Z buffers | confirmed (code) |
+0x250 |
4 × 0x60 | viewport overlay cameras 0-3 | confirmed (code) |
+0x3d0 |
0x60 | effects camera, parallel projection, 512 × 256 Z buffer | confirmed (code) |
+0x430 |
4 | frame-buffer raster: width × height, camera raster, 32-bit 8888 (type 0x502) |
confirmed (code) |
+0x434 |
4 | Z raster, same size (type 1) |
confirmed (code) |
+0x43c |
4 | 512 × 256 camera-texture raster for the blur effect (slot +0x108) |
confirmed (code) |
+0x440 |
4 | background colour, RenderWare RwRGBA byte order (R, G, B, A) |
confirmed (code) |
+0x444 |
4 | fog start as a fraction of the far clip, clamped to (0, 1); 0.5 at start-up | confirmed (code) at 0x00195168 |
+0x448 |
1 | cleared by slot +0x10 |
confirmed (code); meaning unknown |
+0x44c |
4 | screen width, float: 640.0 | confirmed (code) |
+0x450 |
4 | screen height, float: 448.0 (480.0 in progressive mode) | confirmed (code) |
+0x454 |
4 | scale, float, 1.0 | confirmed (code) |
+0x458 |
4 | progressive-scan mode on | confirmed (code) |
+0x45c |
4 | 16:9 on | confirmed (code) |
Device vtable¶
Vtable 0x00538d78, GCC 2 layout (8-byte {delta, function} entries, see Compiler); slot offsets
are from the vtable start, as on Boot and the main loop. Evidence: confirmed (code) at the function's
address for what it calls; the RenderWare call names are inferred.
| Slot | Function | What it does | Evidence |
|---|---|---|---|
+0x08 |
0x00194558 |
Init: start RenderWare, cameras, rasters (Start-up) |
confirmed (code) |
+0x10 |
0x00195030 |
clears byte +0x448 |
confirmed (code) |
+0x18 |
0x00195038 |
flush the render queue: submits the PS2 packet buffer and waits for the previous one, twice (0x0048c508) |
confirmed (code) |
+0x20 |
0x001951c0 |
screen width and height as integers | confirmed (code) |
+0x28 |
0x00195400 |
set up the cameras from the player camera: view window, near and far clip, fog distance, matrix (A frame) | confirmed (code) |
+0x30 |
0x001958b0 |
present: the main camera's show-raster, then clears 0x0050b6f8 |
confirmed (code) |
+0x38 |
0x001958f8 |
clear the main camera (colour and Z) to the background colour +0x440 |
confirmed (code) |
+0x40 |
0x00195928 |
clear the main camera to a given colour | confirmed (code) |
+0x48 |
0x00195060 |
set the background colour; it is also the fog colour (fog type, density, colour and fog-enable render states, set inside a camera update) | confirmed (code) |
+0x50 / +0x58 |
0x00195168 / 0x004e3928 |
set / get the fog start +0x444 |
confirmed (code) |
+0x60 |
0x004e3930 |
main camera (this + 0x10) |
confirmed (code) |
+0x68 |
0x00195c48 |
viewport camera i (this + 0x70 + i × 0x60) |
confirmed (code) |
+0x70 |
0x004e3938 |
overlay camera (this + 0x1f0) |
confirmed (code) |
+0x78 |
0x00195c60 |
viewport overlay camera i (this + 0x250 + i × 0x60) |
confirmed (code) |
+0x80 |
0x004e3940 |
effects camera (this + 0x3d0) |
confirmed (code) |
+0x88 |
0x00195c78 |
set up viewport i (split-screen sub-raster, clip planes, view window; creates the viewport cameras on first use) |
confirmed (code) |
+0x90 |
0x00195238 |
convert a GUI point to overlay-camera space (2D drawing) | confirmed (code) |
+0x98 |
0x00195330 |
convert a GUI width (multiplies by width / height) | confirmed (code) |
+0xa0 |
0x001953a8 |
viewport i's rectangle in pixels |
confirmed (code) |
+0xa8 / +0xb0 |
0x004e3948 / 0x004e3950 |
screen width / height (floats +0x44c, +0x450) |
confirmed (code) |
+0xb8 / +0xc8 |
0x004e3958 / 0x004e3968 |
get / set progressive mode +0x458 |
confirmed (code) |
+0xc0 |
0x004e3960 |
scale +0x454 |
confirmed (code) |
+0xd0 |
0x004e3970 |
16:9 flag +0x45c |
confirmed (code) |
+0xd8 |
0x004e3978 |
returns 0 | confirmed (code) |
+0xe0 |
0x00192c20 |
build a screen-space textured quad: destination rectangle, raster, source rectangle | confirmed (code) |
+0xe8 |
0x00192f58 |
draw that quad with a colour (2D drawing) | confirmed (code) |
+0xf0 |
0x001931f8 |
the 9 × 9 grid's index list (8 × 8 cells, two triangles each) at 0x005ff470 |
confirmed (code) |
+0xf8, +0x100 |
0x00193298, 0x001939b8 |
build and draw a 9 × 9 grid of the same, with wobbling UVs: the motion blur's form for some looks (Motion blur) | confirmed (code) |
+0x108 |
0x00193d80 |
blur through the 512 × 256 raster +0x43c and the effects camera (Motion blur) |
confirmed (code) |
+0x110 |
0x00195c00 |
destroy a raster | confirmed (code) |
+0x118 |
0x00195980 |
calls slot +0x120 |
confirmed (code) |
+0x120 |
0x00195c20 |
heat-distortion effects (0x0019c438, Heat distortion) |
confirmed (code) |
+0x128 |
0x00195be0 |
screen effects (0x0018dac0, ScreenEffectsManager.cpp) |
confirmed (code) |
+0x130 |
0x001959a8 |
fill viewport i with a colour (fades): a full-screen RwIm2D triangle strip |
confirmed (code) |
+0x138, +0x140 |
0x00196468, 0x001964a0 |
PS2 driver: +0x138 sets bit 4 in raster 0x00596e1c's plugin word before the blur and distortion passes (meaning not traced); +0x140 returns raster 0x00596e18, which the motion blur samples (inferred: the frame just shown) |
confirmed (code) |
+0x150 |
0x00197f70 |
read a texture dictionary (Loading textures) | confirmed (code) |
+0x158 |
0x00198080 |
destroy a texture dictionary | confirmed (code) |
+0x160 |
0x001980a0 |
set the current texture dictionary (or none) | confirmed (code) |
+0x168 |
0x001980e0 |
find a texture by name in one dictionary; on failure, prints the dictionary's contents to a debug buffer | confirmed (code) |
+0x170 |
0x00197ba0 |
read a world (section 0x0B) and install the game's sector render callback |
confirmed (code) |
+0x178 |
0x00197d00 |
destroy a world (detach its clumps and lights first) | confirmed (code) |
The vtable ends at +0x180; the zero entries at +0x188 start the next vtable, 0x00538f08, a small immediate-mode
drawing class in the same file (slot +0x08 0x001964c0 begins an overlay-camera update with a vertex list, slot
+0x18 0x001965d8 draws it with RwIm2D), used by particle code (confirmed (code); its users are not traced).
Camera wrapper (0x60 bytes)¶
Vtable 0x00538f78. +0x00 vtable, +0x04 the RwCamera, +0x50-+0x5c the sub-raster rectangle (x, y, w, h;
zero = whole screen). Confirmed (code) at 0x00198738-0x00198c80; the RenderWare names are inferred from the
RwCamera fields each one touches (+0x60 frame buffer, +0x64 Z buffer, +0x68 view window, +0x78 view
offset, +0x80 near, +0x84 far, +0x88 fog plane: the RenderWare 3.7 layout).
| Slot | Function | What it does | Evidence |
|---|---|---|---|
+0x08 |
0x00198750 |
create the RwCamera and its frame; perspective projection |
confirmed (code); RenderWare name inferred |
+0x10 |
0x001987b8 |
destroy | confirmed (code); RenderWare name inferred |
+0x20 |
0x00198808 |
identity matrix, near and far clip | confirmed (code); RenderWare name inferred |
+0x28 |
0x00198890 |
view offset (first pair) and view window (second pair) | confirmed (code); RenderWare name inferred |
+0x30 / +0x38 / +0x40 |
0x001988f0 / 0x00198910 / 0x00198930 |
near clip / far clip / fog distance | confirmed (code); RenderWare name inferred |
+0x48 |
0x00198980 |
projection: non-zero = perspective, zero = parallel | confirmed (code); RenderWare name inferred |
+0x50 |
0x001989b0 |
set the frame from a game matrix, converting axes (the game's x and z are negated) | confirmed (code); RenderWare name inferred |
+0x58 |
0x00198a58 |
sub-raster rectangle of the main camera's frame and Z buffers (split screen) | confirmed (code); RenderWare name inferred |
+0x60 / +0x68 |
0x00198940 / 0x00198960 |
RwCameraBeginUpdate / RwCameraEndUpdate |
confirmed (code); RenderWare name inferred |
+0x70 |
0x00198b08 |
RwCameraClear(colour, flags): colour is RwRGBA packed R in the low byte; arguments 2 and 3 select image and Z |
confirmed (code); RenderWare name inferred |
+0x78 |
0x00198b60 |
RwCameraShowRaster(camera, NULL, 0) |
confirmed (code); RenderWare name inferred |
+0x80-+0xb0 |
0x00198ba8... |
getters for view offset, view window, fog plane, near and far | confirmed (code); RenderWare name inferred |
+0xb8 |
0x00198bf8 |
frame matrix back in game axes | confirmed (code); RenderWare name inferred |
+0xc0 |
0x00198c80 |
rectangle in pixels | confirmed (code); RenderWare name inferred |
Video mode¶
Init (0x00194558, confirmed (code)) walks RenderWare's video-mode list (90 modes, a table of 24-byte records
{width, height, depth, flags, refresh, raster format} at 0x0052ec38 in the PS2 driver) and picks the first with:
| Interlaced (the default) | Progressive | |
|---|---|---|
| Width × height | 640 × 448 | 640 × 480 |
| Depth | 32 | 32 |
| Flags | 0x203 |
0x1 |
| Refresh in the table | 60 | 60 |
Confirmed (code) for the selection and the table. Flag 0x1 is RenderWare's "exclusive" and 0x2 "interlace"; 0x200
is a PS2-only flag, read-circuit anti-aliasing in the RenderWare 3.7 PS2 headers (inferred). How the driver then sends
the picture to the TV (field mode with a two-line anti-flicker mix) is on The PS2 render
driver. For Coney this means: a 640 × 448 logical screen (4:3; 16:9 when the widescreen
option is on), 32-bit colour with a Z buffer.
The camera constants per mode (RwDevice_Init, 0x001945cc-0x001946b0, and RwDevice_SetWidescreen,
0x00194e28), confirmed (code). The 3D aspect goes to every camera (Cameras_SetAspectAll, 0x00122ca0: global
0x0050b204 and each camera's +0x4c), the overlay aspect to 0x0050b208 (Camera_StoreHudAspect, 0x00122d38):
| Mode | 3D aspect | Overlay aspect | Overlay scale 0x0050b20c |
Field of view added 0x0050b178 |
Device flags |
|---|---|---|---|---|---|
| Interlaced 4:3 (default) | 1.3333 (0x3faaaa8f) |
1.45 | 1.0 | 0 | 0x01 |
| Interlaced 16:9 | 1.6667 (0x3fd5551d) |
1.6667 | 1.1 | 0 | 0x01 + 0x04 |
Progressive (480p; device +0x458) |
1.59 (0x3fcb851f) |
1.59 | 1.0 | 3° | 0x20 (+ 0x04 when 16:9 is chosen) |
A camera's view window is (tan(h) × 0.75 × aspect, tan(h) × 0.75) with h half of (field of view + the added
degrees) (The streamed world), so 16:9 widens the picture by 1.25 and keeps its height: the
player camera's 65° gives (0.637, 0.478) in 4:3 and (0.796, 0.478) in 16:9, and (0.804, 0.506) in progressive
mode. The 16:9 window is 5:3 rather than 16:9 (1.667 against 1.778), so on a 16:9 screen the original is stretched
about 6.7 % sideways (inferred from the constants). In progressive mode the 16:9 choice sets the flag but keeps the
1.59 aspect. The overlay camera's window (scale × overlay aspect × 0.5, scale × 0.5) is (0.725, 0.5) in 4:3,
(0.917, 0.55) in 16:9 and (0.795, 0.5) in progressive mode (2D drawing). The HUD's own 16:9 positions
are on HUD.
RenderWare plugins¶
Init attaches, in this order (confirmed (code); plugin identities from their RenderWare plugin ids, inferred):
PDS pipelines (id 0x131, 0x00461340, then seven pipeline registrations through 0x00461420), the world plugin
(0x00471438, ids 0x501-0x50b), HAnim (0x11E, 0x00462940), Skin (0x116, 0x00466a78), Material Effects
(0x120, 0x00464e30), PTank (0x12F, 0x0044eae8), GPVS (0x12A, 0x004bede8) and two of the game's own:
| Id | Attached to | Stream data | Read by | Evidence |
|---|---|---|---|---|
0x3F1 |
world sector (32 bytes of plugin data, offset in 0x0050ced0) |
20 bytes: streamed-sector index, part number, origin of the sector's atomic | 0x00198e20 |
confirmed (code); see The streamed world |
0x3F0 |
atomic (16 bytes, offset in 0x0050cd98) |
12 bytes: two floats for the game's PS2 pipelines, one word | 0x00192688 |
confirmed (code); the first float scales the packed positions (disc check), see The streamed world |
Disc check (corroboration): RenderWare extension chunks in the WAD with the 0x1C02000A stamp: 0x110 (PS2
sky mipmap value) 148,835; 0x11E 102,149; 0x3F0 34,857 (all 12 bytes); 0x120 19,059; 0x3F1 16,138 (all
20 bytes); 0x12A 3,304; 0x116 2,370; 0x182 40 (unidentified). librw skips extensions it has no plugin for, so
the two game plugins only matter once their data is understood.
Texture dictionaries on the disc¶
All textures are PS2 native textures (platform PS2\0) inside RenderWare texture dictionaries (section 0x16).
A throwaway disc walk (2026-10-04, outside the repository) found 20,314 dictionary sections holding 42,211 native
textures, 20,582 of them distinct. Every one parsed.
| Depth | Palette | Mipmaps flag | Occurrences | Distinct |
|---|---|---|---|---|
| 8 | 256 colours, 8888 | no | 32,333 | 13,508 |
| 4 | 16 colours, 8888 | no | 9,137 | 6,510 |
| 4 | 16 colours, 8888 | yes | 532 | 391 |
| 8 | 256 colours, 8888 | yes | 209 | 173 |
- Every texture is palettised (
PAL8orPAL4) with a 32-bit palette; there are no true-colour textures. - Sizes are all powers of two, from 4 × 4 to 512 × 512. Most common: 64 × 64 (9,807 distinct), 32 × 32 (5,938), 128 × 128 (1,835), 16 × 16 (989), 256 × 256 (365), 512 × 512 (358); 2,489 are not square.
- Filter mode: 18,431 linear-mip-linear (
6), 2,151 linear (2). Addressing: 20,559 wrap/wrap, 23 clamp/clamp. - The five older streams stamped
0x1803FFFF(WAD contents) were not counted.
Mipmap level at runtime, confirmed (runtime) from PCSX2 GS dumps of level99 (11,923 world vertices): the GS
picks a texture's level by distance, not by screen size. TEX1 has LCM 0 and L 0, so LOD = log2(1/Q) + K, and
1/Q is the vertex's camera distance in metres (it runs from 3.8 to 115.0, the far clip, over the frame). K is
the raster header's +0x3c (RenderWare formats), -2 to -7.6 on the textures
seen. With MMIN linear-mip-linear and MMAG linear, a mipmapped texture uses level 0 up to 2^(−K) m and blends
towards level 1 by 2^(1−K) m (K = −4.875: 29 m and 59 m). Most textures have no mipmaps (MXL 0) and are
simply bilinear; character textures use filter mode 2 (MMIN linear).
Evidence: inferred (data), counts as stated. In the textures' palettes PS2 colours put opaque alpha at 128, not
255; librw scales it to 0-255 when it converts a raster (Coney's implementation). Sprite
vertex colours are a different matter: they are 0-255 with 255 opaque, and the 128 / 255 that 0x001a2690 applies
is the drop shadow's half alpha, not a conversion (GUI).
Particle pages (sprite sheets)¶
Chunk 0x4C beside a texture dictionary is a sprite sheet: rectangles of texture coordinates into the
dictionary's texture, each {u0, v0, u1, v1} as floats (inferred from 0x00181e38, which returns rectangle i as
16 bytes, and from the data). The legal screen's page has one rectangle, (1/2048, 1/2048) to
(1 - 1/2048, 0.75 - 1/2048): the top 512 × 384 of its 512 × 512 texture, inset half a texel (data, inferred). The
front end and the HUD draw sprites from these pages (2D drawing). The full layout, now confirmed
(code) and checked on all 1,335 pages, the sprite batches and fonts are on GUI; how particle
systems and radar blips name a sprite (a sheet and a rectangle in one word) is on
Particles.
Screen effects¶
Two screen-effects managers (ScreenEffectsManager.cpp), 0x005fdeb8 and 0x005fdebc, one per view, 0x220 bytes
each, made by 0x0018ba10 and drawn through device slot +0x128 (0x0018dac0). Every number a script can pass is
on Screen effects. Layout, confirmed (code) at the functions cited:
| Offset | What | Written by |
|---|---|---|
+0x10 + 0x18 × k |
look k (0-12): +0x10 in seconds, +0x14 out seconds, +0x18 motion-blur alpha, +0x1c a value, +0x20 a time (the script's milliseconds × 0.06, as an unsigned integer), +0x24 tint RGBA |
CfgScrFx (0x0018b4a8, colour form) |
+0x148 / +0x14c / +0x150 |
the blur pulse's pass count n and its two UV offsets f1, f2 | CfgScrFx numeric form (0x0018b648) |
+0x154 + 0x10 × layer |
effect layer 0-3 (rain, fog, film grain, room smoke): its object and two "on" words | 0x0018bae0 / 0x0018bd48 |
+0x194 / +0x198 |
the base look and the current look | 0x0018b460, 0x0018b7d0, 0x0018b950 |
+0x1bc-+0x1cc |
the tint blend: from colour, current colour, target colour, start and end in seconds | 0x0018c988 |
+0x1d0-+0x1e4 |
the fade: colour, state, level 0-1, rate per second, delay and start in ms | 0x0018cc60 |
+0x1e8-+0x1f4 |
the letterbox: state, level 0-1, rate | 0x0018d868 |
+0x1f8-+0x210 |
the blur pulse: state, level, rate, delay, start, hold start, auto-end flag | 0x0018d058 |
+0x212 |
the view's player | |
+0x214 |
byte: when set, neither the blur pulse nor the motion blur is drawn | 0x0018d1d0, 0x0018c408 |
ScreenQueueEffect(type, seconds) runs 0x0018d450(manager, type, seconds, delay, colour) on both managers, a jump
table of six at 0x00552f70; types 6 and above do nothing. Type 0 fades in from the fade colour, type 1 fades out to
it (Fade); 2 and 3 slide the letterbox in and out; 4 starts the blur pulse over look 5's in
time and arms its auto-end; 5 ends it (at once when the seconds are 0, otherwise over look 5's out time). The letterbox
(0x0018d5f8) is two black quads, top and bottom, each level × 0.12 of the screen height. Confirmed (code).
The clocks: every start time, delay and hold is read from the real-time clock (the Timer at 0x0050b8b8, its slot
+0x30, milliseconds; Boot), so they run on while the game is paused (inferred). The tint blend
is timed on that clock alone. The fade, the letterbox and the blur pulse's level step by the dt that device slot +0x128
(0x0018dac0) is handed each frame, the frame's game time (Boot, step 8). Confirmed (code).
Tint blend¶
ScreenFx_BlendTintTo(seconds, manager, colour) (0x0018c988) blends the view's tint
(Rendering) to a packed colour (bytes red, green, blue, alpha from the low byte). Fields,
confirmed (code):
| Offset | What |
|---|---|
+0x1bc |
from colour: set to the current colour (+0x1c0) when a blend starts |
+0x1c0 |
current colour: what was last drawn |
+0x1c4 |
target colour |
+0x1c8 / +0x1cc |
start and end, in seconds on the real-time clock (now ms × 0.001; end = start + seconds) |
- Start: target = colour, from = current. With seconds > 0, start = now, end = now + seconds. With 0 or less the blend is instant: from = target and start = end = 0.
- Each frame (
ScreenFx_DrawTint,0x0018ca50, confirmed (code)): when end > start, r = (end − now) / (end − start), the fraction left; while r > 0 the colour islerp(from, target, 1 − r), otherwise (blend over, or instant) the colour is the target and from is set to it. The colour is stored in+0x1c0and filled over the viewport when its alpha byte is not 0. Nothing is drawn while the current look is 2 and+0x1b8is 0 (the current colour is still set to the target). - Lerp (
0x0017a258, confirmed (code)): linear per byte, each of the four bytes as a float,from × (1 − t) + to × t, converted back with the EE's float-to-int, which truncates (rounds toward zero). The four bytes blend together, alpha included. ScreenFx_FinishTintBlend(0x0018cb78, confirmed (code)): once now is past the end, end = start and from = target, so the next frame sees no blend running.
The death camera's blend (0xd0000014: red 20, alpha 208) is on Camera.
Fade¶
ScreenFx_StartFade(seconds, manager, out, colour, delay) (0x0018cc60, queue types 0 and 1) and
ScreenFx_DrawFade (0x0018ce58), confirmed (code):
| Offset | What |
|---|---|
+0x1d0 |
the fade colour (its red, green and blue are used) |
+0x1d4 |
state: 0 idle, 1 just started, 2 running |
+0x1d8 |
level, 0-1: the fill's alpha is level × 255, truncated |
+0x1dc |
rate per second of dt: positive fades out (toward the colour), negative fades in |
+0x1e0 / +0x1e4 |
the delay and the start, real-time ms: nothing is drawn before start + delay |
A fade out (type 1, out ≠ 0) takes 0.2 s off the time when it is longer than 0.2 s; from level 0 it starts at
−(tint alpha / 255) with rate 1 / seconds, from a level part-way it keeps the level and runs over seconds × (1 −
level); 0 s jumps to level 1. It also hides the radar on both views. A fade in (type 0) starts at 1 + tint alpha /
255 with rate −1 / seconds (0 s: level 0 at once). Each frame after the delay the level moves by rate × dt, clamped to
0-1; at 1 the state goes to 0 (fully covered, alpha 255); when a fade in drops to the tint's own alpha the level is 0
and the fade ends. The fill uses the fade colour while the level is above 0.05 and the tint's target colour below
that.
So +0x1d8 and +0x1dc are the fade's level and rate, not the tint's. CamFailed_Update (0x00123bf0)
tests level ≥ 1 and rate > 0: a fade out has fully covered the screen. It then ends the blur pulse at once
(0x0018d058 with 0 s, reverse, no delay; Blur pulse) and stops the death camera's turn
(Camera). Confirmed (code).
Blur pulse¶
A full-view blur that rises, holds and falls, drawn through the device's blur pass. Fields, confirmed
(code) at 0x0018d058 and 0x0018d1d0:
| Offset | What |
|---|---|
+0x1f8 |
state: 0 off, 1 started, 2 moving, 3 held at full |
+0x1fc |
level, 0-1 |
+0x200 |
rate per second of dt (positive rising, negative falling) |
+0x204 |
delay, real-time ms |
+0x208 |
start, real-time ms |
+0x20c |
when the level reached 1, real-time ms (0 after the auto-end) |
+0x210 |
16 bits: 1 = end by itself after the hold (set by queue type 4 only) |
Start, ScreenFx_StartBlurPulse(seconds, manager, reverse, delay) (0x0018d058, confirmed (code)): the state
becomes 1, the delay and the start (now) are stored, then:
- Forward (reverse 0), seconds > 0: from level 0 the rate is 1 / seconds. From a level part-way the level is kept, the rate is 1 / (seconds × (1 − level)) (1.0 if that time is 0), and the state goes back to what it was, except that a held pulse (3) becomes 2.
- Forward, 0 s or less: level 1 and rate 1 at once.
- Reverse (reverse ≠ 0): the auto-end flag is cleared. seconds > 0: the level is set to 1 whatever it was, rate −1 / seconds. 0 s or less: level 0, rate −1 and state 0: the pulse is off at once and nothing more is drawn.
Each frame, ScreenFx_DrawBlurPulse(dt, manager) (0x0018d1d0, confirmed (code)), from ScreenFx_Render:
- Nothing when
+0x214is set, or while the level is 0 and now < start + delay (unsigned ms), so the delay only holds back a pulse that starts from nothing. - State 3: the blur pass with all n passes. If the auto-end flag is 1 and now −
+0x20c≥+0x98(look 5's time field, read as milliseconds), queue type 5 with look 5's out time (a reverse start, no delay) and clear+0x20c. - State 0 with level 0: nothing.
- State 1: the state becomes 2; the pass is drawn (all n) only if the level is already 1. The level does not move this frame.
- Otherwise: level = clamp(level + rate ×
dt, 0, 1); passes = level × n, truncated. At level 1: state 3, passes n,+0x20c= now. At level ≤ 0 with a negative rate: level 0, auto-end flag 0, state 0, passes 0, and the global0x00510174is cleared. The pass is drawn this frame with that count, even with 0 passes.
So the level scales the number of passes, not an alpha. The hold is a units slip: +0x98 is the script's
milliseconds × 0.06 (meant as 60 Hz frames) but is compared with real-time milliseconds, so look 5's 6,000 ms hold
lasts 360 ms. Confirmed (code).
The pass (ScreenFx_DrawBlurPass, 0x0018c310, then slot +0x108, 0x00193d80) gets f1 (+0x14c), f2
(+0x150), the view's camera (slot +0x68), the source raster (slot +0x138, 0x00196468, which returns the
raster it marks; inferred the frame being drawn), the view's rectangle (slot +0xa0) and the pass count. It is
drawn only when slot +0x138 returns a raster. All three steps use linear filtering, ONE / ZERO blending (a
plain copy), no Z test or write, no fog, vertex colour (255, 255, 255, 255). Confirmed (code):
- Down: the source's rectangle (UV = (pixel + 0.5) / raster size) is drawn at half size, (0, 0)-(w/2, h/2), into
the 512 × 256 raster
+0x43c. - Passes: k times, that half-size image is drawn onto itself, UV (0.5 / 512)-((w/2 + 0.5) / 512) across and
(0.5 / 256)-((h/2 + 0.5) / 256) down, with the offset (f1 / 2, f2 / 2) in UV units (fractions of the
512 × 256 raster), its sign flipped after every pass (
0x0050ce64= 1).ScreenQuad_DrawOffset(0x00193c20) does not move the quad's UVs: a positive offset is added to the low edge only (u0 or v0), a negative one to the high edge only (u1 or v1). Each pass therefore narrows the sampled window from one side, alternately left/top and right/bottom, and resamples it bilinearly: a slight zoom toward the centre plus a blur (the look, inferred). With look 5's numbers each pass moves an edge by 0.001 in U (0.5 texel) and 0.0015 in V (0.4 texel). - Up: the view's camera draws raster
+0x43cover the whole rectangle with UV inset by one texel (0x0050ce68= 1,0x0050ce60= 1.0): U (1 / 512)-((w/2 − 1) / 512), V (1 / 256)-((h/2 − 1) / 256). WithONE/ZEROit replaces the view opaquely: while a pulse draws, the view is the half-resolution image, even at 0 passes.
Look 5's numbers (CfgScrFx numeric form in config_preload2.lua; the call's order is look, in ms, out ms,
hold ms, n, f1, f2): in 1,250 ms (+0x88 = 1.25 s), out 4,000 ms (+0x8c = 4.0 s), hold 6,000
(+0x98 = 360), n 28, f1 0.002, f2 0.003. Read from the disc's script; the order of the fields
is confirmed (code) at 0x0018b648.
Callers: queue type 4 (forward over look 5's in time, then auto-end); CamUseDeathCamera (0x0011daa8:
forward over its ms, delay ms2, no auto-end; Camera); CamFailed_Update
(off at once, above); MissionFailed_Toggle (0x00155408: off at once, with the tint put back instantly, on a
retry); Gm_Level_Update (0x00158928, 0x00158b1c; arguments not traced).
Draw order (ScreenFx_Render, 0x0018dac0), confirmed (code): with one view (or one player), for each view: blur
pulse, tint, fade, letterbox, then the motion blur (only when no rumble-mode menu is up and PM_IsDone is 1). In the
two-player one-view case: the blur pulse of whichever manager has one running (view 0 first), the letterbox, the
tint and motion blur of view 0 or view 1 by their current looks, and view 0's fade last. Since the blur pulse is
drawn first and replaces the view, the tint, fade and letterbox go over the blurred image.
Looks¶
A look is a tint and a motion-blur strength the view blends to. 0x0018b7d0(manager, look) switches to look 0, 2,
5 or 7 (blending the tint with 0x0018c988 and the blur with 0x0018c8c8 over the look's in time);
0x0018b950(manager, look) ends one, going on to its follower (0 → 4, 2 → 3) or back to the base look (look 3
over look 8's out time);
0x0018b460 sets the base look (SetLevelColour 9, EnterStore 10, ExitStore 9). QueueMotionBlurEffect
blends the blur alone (effects bindings). Confirmed (code).
Coney (effects::MotionBlur, repo:src/effects/motion_blur.h; platform::MotionBlurPass) blends the blur's colour
and strength linearly over the time given and lays the last frame over the new one at that strength, a stand-in for
the drawing above; the looks' own blur values are not applied yet. It keeps and lays over the whole
3D view (RenderEngine::viewRect), as the original lays its whole last frame: in a window wider than 4:3 that is
wider than the logical screen's 4:3 place, which, kept alone, drew a faint rectangle 12.5 % in from each side
(fixed 2026-10-08). The blur pulse's RenderEngine::blurScreen and the screen tint's and fades' wash
(RenderDevice::drawViewQuads) cover the whole view the same way.
The screen tint (effects::ScreenTint, repo:src/effects/screen_tint.h, drawn by PlayLevelEffects::drawTint)
follows Level tint: SetLevelColour stores look 9's colour (each float × 255, truncated) and
switches to it, EnterStore look 10's with a 0.25 s blend, ExitStore back to look 9 over 0.25 s; each frame,
after the HUD and before the scene's captions, a full-screen quad of the colour is blended over the screen at the
alpha byte × 128 / 255 (truncated) out of 128; an alpha of 0 is not drawn. The blend is linear, each byte
truncated (0x0017a258, confirmed (code)). ScreenTint::blendTintTo is ScreenFx_BlendTintTo (0x0018c988) for the
cameras (the game-over shot and CamUseDeathCamera blend to ScreenTint::kGameOver, 0xd0000014) and finishBlend
its finish (0x0018cb78). Coney's readings: SetLevelColour switches at once (look 9's in time is 0); the blend is
timed by the fixed steps, not the original's real-time clock (they differ only while paused or slowed); the other looks'
tints (rage, heat, the follower looks) are not modelled, so the tint is always the base look's or the last blend's
(The rage look lists what rage needs).
The blur pulse (effects::BlurPulse, repo:src/effects/blur_pulse.h, drawn by RenderEngine::blurScreen):
start(seconds, delayMs) and end(seconds) are ScreenFx_StartBlurPulse (0x0018d058, confirmed (code)) forward and
reversed, ScreenQueueEffect 4 and 5 queue look 5's (in 1.25 s, out 4 s, a 360 ms hold, 28 passes, offsets 0.002 and
0.003). Each step runs the original's draw-time update (0x0018d1d0): the delay, the rise or fall, the hold and the
auto-end; the level × 28 (truncated) is the pass count. While it runs past its delay the view is replaced, before the
tint, by the blur pass: half size, the passes, stretched back, opaque; the screen effects then go
before the HUD. Coney's readings: the delay and hold are timed by the fixed steps; the offsets are scaled from
the original's 512 × 256 texture to the same share of a half-size screen (its half-size image taken as 320 × 224);
one pulse, player 1's view's. On the front end the tint is the
front-end world's (FrontEndScene::tint(), which the level flow hands to the bindings as the context's tint), and
the menus draw it after their text and before their fade, as the title frame does
(The front end).
Ground fog and litter (effects::GroundFog, repo:src/effects/ground_fog.h; effects::CameraLitter,
repo:src/effects/camera_litter.h): Coney has one view, so one fog emitter.
Start3DFog starts it over: every 5 frames it tops the view up to 20
wisps (MaxFogParticles lowers it), at most 10 at a time, within 20 m of the camera's target and 0.5-2 m above it.
Each wisp drifts from birth toward the camera, aimed up to 2 m to either side along the camera's
x axis, at drift × 1.75-2.25 m/s, and updates every 2 ticks: its alpha rises by ⌊alpha / steps⌋ × ⌊fadeRate⌋ for
⌊9 / fadeSpeed⌋ updates (a 0 step is 1, every 30 ticks), it is hidden more than 20 m from the camera, more than 5 m
outside its view or within 4 m, and from its third update one hidden at its last update ends. PlayLevelEffects
draws the first 10 wisps of the pool each frame as camera-facing squares of rectangle 0 of part_fog_00 (or
part_fog_01 for record 531), 2 × a random 1-4 across (growing from 0 over the first update), their alpha and size
blended between updates, alpha-blended with the 3D sprites. Coney's stand-ins: the fog's own generator, not the
particles' stream; the camera flag that hides wisps within 10 m is not modelled.
StartGarbage(kind) arms the litter: 64 pieces on the 8 × 8 grid round the camera with the
kind's rectangles and sizes, a grey, a wind threshold and a 600-900-update life; at 30 Hz each falls (9.8 m/s²
edge-on, half lying flat), lands on what the collision ray meets and lies down over up to 8 updates; a ground ray every
20 updates (staggered) puts back a piece with no ground below and drops one just placed onto the ground; a piece
more than 28 m from the camera on x or y, or landed with its life over, fades out over 60 updates and comes back in
its grid cell round the camera. EndGarbage stops it. Coney's readings: the wind vector is not read, so no piece
is lifted, tumbled or pushed at the start; a piece's orientation is one tilt value; a piece that lands is put on the
hit; the pieces are not drawn yet (they are cards, not the renderer's camera-facing sprites).
The rage look¶
What the screen does while a player rages (Combat: everything rage does): look 2 on the
screen-effects manager of the player's view (0x005fdeb8[player]), a warm dark tint plus a shimmering ghost of the
last frame that is strongest at the screen's edges. Look 2's numbers come from CfgScrFx in config_preload2.lua
(look 2): tint (51, 25, 0, 80), in 1,000 ms, out 5,000 ms, blur alpha
150. Confirmed (code) at the cited functions; confirmed (runtime) where marked, PCSX2 2.9.94, a copy of quick-save
slot 6 (level99, base look 9 with the tint (7, 20, 30, 43)), the manager read every 100 ms (2026-10-10).
Start (ScreenFx_StartLook(manager, 2) 0x0018b7d0, from Human_StartRageMode): the current look becomes 2
(+0x198, +0x19c = 0; the base look +0x194 is kept), and both blends start on the real-time clock over look 2's
in time, 1.0 s:
- the tint (
ScreenFx_BlendTintTo, Tint blend) from the level's colour to0x50001933, that is (51, 25, 0) at alpha 80, drawn as As = 80 × 128 / 255 = 40 of 128: about 31 % of the way to a dark brown; - the motion blur (
ScreenFx_BlendBlurTo0x0018c8c8, confirmed (code)) from its colour to0x96ffffff, white at alpha 150.
Runtime: the tint went 0x34161512, 0x38131517, ... 0x4e011831 at 100 ms steps and was 0x50001933 0.8 s after
the first sample; the blur target was 0x96ffffff from the first.
Each frame while raging (ScreenFx_Render 0x0018dac0: blur pulse, tint, fade, letterbox, motion blur; the
screen effects go before the HUD; confirmed (code)):
- The tint: one full-view quad of the current colour,
SRCALPHA/INVSRCALPHA(Level tint).ScreenFx_DrawTint(0x0018ca50) skips it while the look is 2 and+0x1b8is 0, but onlyScreenFx_Construct(0x0018b1e0) andScreenFx_ResetAll(0x0018c058, the store at0x0018c260) write+0x1b8, both with 1 (confirmed (code) for those two stores; that no other code writes it is inferred), so the rage tint is always drawn (runtime:+0x1b8was 1 throughout). - The ghost grid (Motion blur, step 4): look 2 is one of the grid looks, so the last frame is
laid over the new one as the 9 × 9 grid, its texture coordinates wobbling by sin and cos of a phase growing 0.5 a
frame × 0.001 × ±0.9 × 3 (
0x0050cdfc,0x0050cdf8,0x0050ce1c, read by the grid0x00193298; confirmed (code)), its vertices white with alpha 0 in the centre rising to 224 at the corners, nearest filtering,SRCALPHA/INVSRCALPHA. The blur alpha of 150 only turns the grid on and hides the rain layer (111 or more). So the middle of the screen is sharp and the edges smear and swim, as in a tunnel.
End, gradual (ScreenFx_EndLook(manager, 2) 0x0018b950, the meter running out; confirmed (code)): the current
look becomes 3; the tint blends back to the base look's tint and the blur to the base look's alpha, both over look
2's out time, 5.0 s. Look 3 is still a grid look, and the grid (0x00193298) fades its wobble and alpha by
1 − elapsed / 2500 ms (0x0050ce0c = 2,500, the start kept at 0x0050ce20); past 2,500 ms the look becomes 8, which
ScreenFx_DrawMotionBlur turns back into the base look. From then on the base look (9) draws the blur as the single
full-view quad at the still-falling blur alpha, so a faint even ghost (alpha about 73 falling to 0) lingers for the
remaining 2.5 s; the tint keeps blending for the full 5 s. Runtime: from the end (game time 589.41 s) the grid fade
0x0050ce14 fell 0.973, 0.933, ... 0.039 at 100 ms steps, the corner vertex alpha from 224 to 8 with it; the look
was 3 until 591.85 s and 9 from 591.95 s; the tint went from 0x50001933 toward 0x2b1e1407 and was 0x3119140e
at 593.58 s, 4.2 s in.
End, at once (ScreenFx_EndLook(manager, 3), for the instant ends listed on
Combat, the pause menu among them): the current look becomes the base look and both blends
use look 8's out time, 0, so the tint and blur snap back on the next frame with no grid fade.
So look 3 is rage's fading grid and look 8 the hand-back to the base look; look 4 does the same for look 0 (the
flash's look, Flash_Use) over 1,500 ms (0x0050ce10; confirmed (code) in 0x00193298).
Motion blur¶
Each view, after its tint and fade, ScreenFx_DrawMotionBlur (0x0018c408) lays the last frame over the new
one, confirmed (code):
- Nothing in game mode 100's state or when
+0x214is set. The alpha blends from+0x1a4to+0x1a8over[+0x1ac, +0x1b0](0x0017a258), never below the floor+0x1b4; at 0 nothing is drawn (except in look 5). - While the alpha is 111 or more the rain layer's colour gets alpha 0, and gets its colour back below that, so rain is not drawn twice.
- The source is device slot
+0x140's raster (0x00596e18) over the view's rectangle (slot+0xa0). It is the other frame buffer, the previous frame, read in place: nothing copies it. Confirmed (runtime) from a PCSX2 GS dump of thelevel99arena (re-frame): the blur is one full-screen fan drawn after the tint, textured from the other frame buffer (CT321024 × 512,TBW10,TBP= that buffer'sFBP× 32,TCC0, nearest filtering), vertex colour0x80808080with alpha 7 (the old frame at 7/128, about 5.5 %), blend0x44, Z always, no Z write. A port with one back buffer must keep the last presented frame itself. - Looks 0, 2, 3, 4 and 7 draw it as a 9 × 9 grid (slots
+0xf8,+0x100;0x00193298,0x001939b8): each frame every vertex's texture coordinates move by sin and cos of a phase (+0.5 a frame) × 0.001 × 0.9 with a random sign × 5 (look 0) or 3 (the others), clamped to the rectangle, so the old frame shimmers. A vertex's alpha is(d × table[d]) & 0xff× the fade, withd=|column − 4| + |row − 4|(0-8) and the 9-byte table at0x0050ce30(10, 50, 180, 225, 230, 240, 240, 250, 252; themultandandi 0xffat0x00193a58), flat 200 in look 4: so byd0, 50, 104, 163, 152, 176, 160, 214, 224, clear in the centre and strongest at the corners. Confirmed (runtime): those nine values were the vertex alphas at0x005fe030 + i × 0x40 + 0x2cduring rage (The rage look). The blur colour's own alpha only switches the grid on (not 0) and hides the rain (111 or more); it does not scale the vertices. Looks 3 and 4 also fade the wobble and alpha out over 2,500 and 1,500 ms from their start; then the current look becomes 8 and the manager goes back to its base look. Texture filter nearest, vertex alpha,SRCALPHA/INVSRCALPHA. - Other looks draw one quad (slots
+0xe0,+0xe8) with the blended colour, so the old frame shows at the blur's alpha everywhere (Screen quads).
The blur pass, device slot +0x108 (0x00193d80), used by the blur pulse (0x0018d1d0) and 0x0018c310, is a
different effect: (1) the effects camera draws the source at half size into the 512 × 256 raster +0x43c (linear
filtering, ONE / ZERO, Z off); (2) as many times as asked, that half-size image is drawn onto itself with half the
given offset added to one edge of its UVs, the sign flipping each pass (0x0050ce64 = 1), each pass resampling it
bilinearly (details under Blur pulse); (3) the view's camera draws the result stretched over the whole
rectangle, inset by one texel (0x0050ce60), opaque. Confirmed (code).
Room smoke¶
Layer 3, OE_RoomSmoke (OverlayEffects/OE_RoomSmoke.cpp, 0x100 bytes, vtable 0x00539140), started by
StartRoomSmoke, is one screen-filling sprite of the sheet
room_smoke_overlay that scrolls sideways, follows the camera's tilt and slowly changes its size and alpha.
Confirmed (code) at the functions cited unless a line says otherwise.
- Texture. Sprite word
0x130000: sheet-table record 19, rectangle 0 (sprite words). Disc check (corroboration): record 19's name hash is0x834dd0f0, the CRC-32 ofroom_smoke_overlay; it is a 256 × 256 texture with one rectangle covering the whole of it (texel-inset by 1/1024), linear filtering and wrap addressing in U and V (texture filter-and-address word0x1102). The wrap matters: the texture coordinates below run past 1. (Record 18 isfog_overlay, same shape.) Its alpha (disc check, the extracted texture's statistics): a light grey (about 220 in each channel), densest in the top fifth (mean alpha about 160-173 of 255 in rows 0-40), fading down the sheet (about 37 atv0.5, under 10 by 0.62) to 0 in every texel from row 191 (v0.746) to the bottom. Across, it tiles: its left and right columns differ by under 1 on average, and no column fades towards the sides (column means 54-73). Down, it does not: the top row averages 159, the bottom row 0. - Start (
OE_RoomSmoke_Construct,0x0019add0, confirmed (code)): the overlay-effect base (0x0019ba70); the script's record (0x0019aff8: tint RGB+0x9c, alpha 0 at+0x9f, lowest and highest alpha+0xa0/+0xa1, amount+0xa4); a random U offset 0-1 (+0xd8); oneOE_Particlesprite widget (BaseWidget) set up with size 1.0, depth 10,000, GUI position (0.5, 0, 0.2, 1) (centred across,y0.2), the tint, visible, sprite word0x130000, an instance of its own, anchored at its centre. Then two drifts are picked: the first is both the "from" (+0xa8) and the current drift (+0xb8), the second the "to" (+0xc8); the blend clock (+0xe0) starts at the real-time clock's now (0x0050b8b8slot+0x30, milliseconds). - A drift (
OE_RoomSmoke_PickDrift,0x0019b198, confirmed (code)), each value uniform random: a U scroll per update of 0.0005-0.0015 × amount with a random sign, a width scale of 1.63-1.93, a height scale of 1.0-1.2, and an alpha (an integer from the lowest to the highest alpha); picking one also sets the blend time+0xe4: 4,000-20,000 ms (an integer). - One drift into the next (
OE_RoomSmoke_BlendDrift,0x0019b2a8, confirmed (code), first thing each update):t = min((now - start) / blendTime, 1)on the real-time clock, and the current scroll, width scale, height scale and alpha arefrom × (1 - t) + to × t(straight lines, no easing; the alpha rounded to a byte and written into the sprite's colour). Whentreaches 1: from = to, start = now, and a new "to" is picked with its own blend time. So the haze never rests: it moves along a chain of random targets, each reached in 4-20 s. - The camera (
OE_RoomSmoke_FollowCamera,0x0019b070), with the player camera's look vector (camera slot+0x220) of the effect's view:- tilt → height on the screen: the pitch (
0x0019b7d8:acos(look · (0, 0, -1)) - 90°, in degrees), clamped to [-50°, 10°], mapped linearly onto [-10, 10] (Math_MapRange,0x00337568), clamped, then onto [-0.25, 0.26]: the sprite's GUIy(+0xf8). Pitch -50° puts the sprite's centre aty-0.25 (above the top of the screen), 0° at 0.175, 10° at 0.26; which sign is looking up is inferred from the vector, not seen; - turn → scroll: the heading (
0x0019b6a8, the look vector's angle in the ground plane) in degrees, clamped to ±180 and mapped onto [0, 0.9999], is kept per view (0x0019b968, effect+0x20 + 4 × view); twice the change since the last update (old − new) is added to this update's scroll, so turning the camera slides the haze sideways (a whole turn, about two texture widths).
- tilt → height on the screen: the pitch (
- The sprite (
OE_RoomSmoke_Update,0x0019b4c0, the effect's slot+0x18): size (0x001a2120, without the device's width conversion) width = width scale ×*0x0050cefcand height = height scale ×*0x0050cf00, in overlay units (the screen is 1.595 wide and 1.1 high in 4:3, 2D drawing), so 2.12-2.51 × 1.0-1.2; the position (GUI (0.5,y), centred, converted by the device: overlayX0,Y= 0.5 −y); the tint with the drift's alpha; the U offset+0xd8+= scroll, wrapped into [0, 1]; texture rectangle (U, -0.1)-(U + 1, 0.9) (0x001a2190): one whole repeat of the texture across the sprite, shifted a tenth up. The two factors are the same in every mode (0x0019b500-0x0019b56c): both paths, the one for device flag0x02or0x20and the other, store 1.3 (0x3fa66666) in0x0050cefc; only the first also stores 1.0 in0x0050cf00, whose static value is already 1.0, and nothing else in the program writes either global (their only references are these). The other path computesflags & 0x04(the 16:9 flag) in a branch delay slot and never uses it, so the source probably had a 16:9 case with the same numbers (inferred). The widget's own update, which runs first in the same tick (OverlayEffect_Tick), sets the width from the rectangle's shape times*0x0050b208× 0.80357 (the effect's widget is set up with the aspect fix on,0x0019aeec), but this size call overwrites it before anything is drawn, so the overlay aspect does not reach the sprite. - Drawing: the widget's sprite in the overlay pass (2D drawing): Z test and write off, the instance's blending, source alpha over inverse source alpha (GUI), so the haze is an alpha-blended layer over the scene, with alpha (0-255) from the drift.
-
In 16:9 only the overlay camera changes:
RwDevice_SetWidescreen(0x00194e28) sets the overlay aspect 1.6667 and the view-window scale 1.1 (Video mode) and leaves the device's 640 × 448 (+0x44c,+0x450), which the GUI conversion (0x00195238) uses, alone; the sprite's size, position, texture rectangle, blending and cadence are the same as in 4:3 (confirmed (code), above). At the sprite's depth 1.1 the screen is 2 × 1.1 × 0.9167 = 2.017 overlay units wide and 2 × 1.1 × 0.55 = 1.21 high in 16:9 (1.595 × 1.1 in 4:3, 1.749 × 1.1 in progressive mode), so the same sprite looks narrower and lower on a 16:9 picture. Where its edges fall (inferred, arithmetic on the confirmed numbers; centreY0.24-0.75 from GUIy-0.25 to 0.26):Edge 4:3 16:9 width, fraction of the screen's 1.33-1.57 1.05-1.24 left and right edges 16-29 % of the width outside each side 2.5-12 % outside each side top edge ( Y+ height / 2 ≥ 0.74)above the top (0.55) above the top (0.605) the texture's wrap from v1 tov0, a tenth of the height below the top (Y≥ 0.64)above the top above the top, by at least 0.035 bottom edge ( Y− height / 2, -0.36 to 0.25)on screen, 27-83 % down on screen, 29-80 % down So the sprite covers the screen across in both modes (in 16:9 by as little as 2.5 % a side at the narrowest drift), always reaches past the top, and never reaches the bottom: the haze is a band hanging from the top whose lower part is the texture's own fade. Its bottom edge is never seen because the texture is fully transparent from
v0.746 and the sprite stops atv0.9; its sides are never seen because they are off screen and the texture tiles across. Nothing fades or clamps at the sprite's edges: the rectangle runs past the texture in V, and the sheet wraps. - Cadence (OverlayEffect_Tick,0x0019bc10, called by the manager's update0x0018bf68for each layer that is on, while the view's player camera exists): nothing while the game clock is paused (0x0050b734slot+0x20) or in game mode 10 or 12; when more than 1000 / 30 ms of game time (*(0x0050b734) + 0x48) have passed since the last tick (0x0019bd90, rate byte+0x10= 30), each visible widget's own update runs (it copies the sheet rectangle and sets the width from the height and the rectangle's shape) and then the effect's, which overrides both; then each visible widget is drawn, every frame, unless0x00512c44is set. So the drift advances at most 30 times a second of game time while the blend runs on real time. Confirmed (code) at each address. - Again while running:StartRoomSmokeon a running layer passes the new record to slot+0x20(0x0019aff8), which also picks a new "to" and sets its blend time to 1 ms, so the next update jumps to it.EndRoomSmokedeletes the effect (0x0018bd48) and its widget with it (0x0019bab8).
Coney (effects::RoomSmoke, repo:src/effects/room_smoke.h; platform::RoomSmokeOverlay) follows this, drawn after
the motion blur and before the HUD with the texture repeating. Coney's readings: its fixed step drives both the
blend and the ticks; the heading is measured from +x toward +y, and the first tick slides nothing. In a 16:9
window (fixed 2026-10-08; before, the sprite went through the 4:3 camera into the logical screen, pillarboxed in
the middle 75 % of the window while the 3D view filled it, so it spanned 0.997-1.18 of the window's width and its
height and offset were 1.1 times the original's): Coney takes a window wider than 4:3 as the 16:9 mode
(RenderEngine::widescreen, graphics::isWideView), and roomSmokeQuad then projects the sprite with the 16:9
overlay camera (OverlayCamera::forMode: scale 1.1, aspect 1.6667, a view window of 0.9167 × 0.55) across the whole
3D view (RenderEngine::drawWrappedViewQuads): screen x = 0.5 + X / 2.017 and y = 0.5 − Y / 1.21, with the
sprite's size and texture rectangle unchanged, so it spans 1.05-1.24 of the window's width as the original does.
Behaviour¶
Start-up¶
From DS_PS2Device_Init (Boot), step 6. Confirmed (code) for the order.
RwDevice_Create(0, 0)(0x00194488): allocate 0x460 bytes, construct (0x00194d58: base constructor, vtable, the ten camera wrappers), store the device in0x0050cdb4and0x0050cdb8, set progressive mode off (slot+0xc8), thenRwDevice_SetWidescreen(dev, 0): 4:3, field-of-view constants 1.3333 and 1.45 handed to the camera system (0x00122ca0,0x00122d38),0x0050b20c= 1.0.GraphicsDevice_Open(0x0017a1e0) callsInit(slot+0x08), then sets the background colour to the static white (0x005fd268,{255, 255, 255, 255}) and the fog start to 0.5.
Init (0x00194558):
- Screen 640 × 448, flags
0x01, scale 1.0; in progressive mode 640 × 480 and flags0x20; 16:9 adds0x04. - Print the largest free block ("... Before Initializing RenderWare"), set RenderWare's free-list sizes, and
RwEngineInit(0x00483530) with a 256 KB resource arena. - Attach the plugins (above).
RwEngineOpen(0x004833e8), choose the video mode (above),RwEngineSetVideoMode.- Two vertical blanks per displayed frame (
0x0048df98(2), the value from0x0050cdb0), see Presenting and the frame rate. RwEngineStart(0x00483258); on failure close and terminate RenderWare.- Install a callback the driver calls while it waits for the GPU (
0x0048df90(0x00154ee8)). - Create the main, overlay and effects cameras (camera slot
+0x08). Create the frame-buffer raster (640 × 448, camera, 8888) and the Z raster, and give both to the main and the overlay camera; give the effects camera a 512 × 256 Z raster and a parallel projection. - Background colour white, fog start 0.5; set RenderWare's texture-read callback to
0x00194550, which returns no texture (so a texture missing from every dictionary is never looked for in a file; inferred from what the callback slot is); create the game's PS2 pipelines (0x00429a98, The streamed world); create the 512 × 256 raster+0x43c.
A frame¶
What one in-game frame does on the screen, confirmed (code) at 0x0015d160 and its callees
(the GS state of each pass, measured: Rendering):
- Cameras (
0x001562c8): device slot+0x28with the player-1 camera's view window, near and far clip and fog distance and its matrix. That sets the main camera, each viewport's camera (fog distance = far clip × fog start) and the overlay cameras: view offset 0, view window(0x0050b20c × 0x0050b208 × 0.5, 0x0050b20c × 0.5)= (0.725, 0.5) in the default interlaced 4:3 mode, near 0.5, far 10,000,000, identity matrix (confirmed (runtime), PCSX2 2.9.94: the overlay camera'sRwCameraat0x00a5bea0holds view window 0.725 × 0.5 at+0x68, near 0.5 at+0x80, far 10,000,000 at+0x84). Then slot+0x38clears the frame buffer and Z to the background colour. - Simulation runs; then slot
+0x18flushes the render queue before the world update (why there: inferred, so that streaming does not free data the GPU still reads). - Each viewport (
0x00156408): slot+0x88positions viewporti's cameras on their part of the screen (columns × rows from0x0050b1a8/0x0050b1aa; one viewport covers the whole screen); lights and the level's sky, clouds and skyline (Level loading); the world (0x0040e8d8: severalRwCameraBeginUpdate/EndUpdatepasses: the level world with culling off, the streamed world's sectors with back-face culling, objects, the detail world, water, translucent objects; Z test and write on and fog on except where stated; the full order is on The streamed world); the 3D sprite batches (0x00185b38: Z write and culling off, the batches farthest first, then the glass panes near a camera, World objects: drawing a pane); these batches hold the humans' blob shadows (part_page1, Lighting), the coronas (lighting, Lighting) and the particles, so all of them come after the whole world pass, the water and the translucent objects included, in ascending key (measured in a fight: thepart_page1batches, thenlighting, GUI); the ground rings (0x0017b2e0, HUD); slot+0x118(heat distortion). - Overlays (
0x00156658): the HUD (0x001b1688) and the overlay pass0x00185d20that flushes it (2D drawing); the screen effects (slot+0x128: the tint, flashes and blur, drawn at once, not queued); the subtitles and their flush; the intro movie layer and the credits, each with a flush; the queued 2D shapes (0x0017c308). The screen effects come before the HUD instead whileW_GameState + 0x268is above 0 (meaning not traced) or a blur pulse runs in either view (screen-effects manager+0x1f8). So the level tint normally covers the HUD but not the subtitles. Confirmed (code); the tint drawn after the radar and the HUD text is also confirmed (runtime) in PCSX2 GS dumps. - Present (slot
+0x30), then file streaming.
The world pass also adjusts the draw distance to the measured frame rate: while the rate (0x0050c680) is below
29.5 (24.5 when flag 0x02 is set) the far clip shrinks, down to 60 − 10 × viewports, and otherwise grows back
toward the camera's own far clip (0x0040e8d8, confirmed (code); that 0x0050c680 is the debug counter's frames per
second is inferred). While the rate is good, the draw distance instead follows the distance of the nearest scenery
that is not loaded yet, so missing sectors stay beyond the far clip (The streamed world).
Presenting and the frame rate¶
The game runs at 30 frames a second (29.97 on NTSC) because the PS2 driver shows a new frame at most every second vertical blank. Confirmed (code):
Present(0x001958b0) calls the main camera's show-raster with flags 0 (the 1 that the device passes in is dropped by camera slot+0x78), which reaches the driver's show-raster (0x00498890, standard function 20 in the table at0x0052f538). That queues a flip token into the DMA chain (0x0048c8f0; tokens0x40and0x41alternate by frame).- The DMA handler (
0x0048b708, installed for channels 1 and 2 by0x0048d9f0) stops at a flip token and marks a flip as pending. - The vertical-blank interrupt handler (
0x0048bdb0, installed withAddIntcHandler(2, ...)by0x0048d9f0) counts vertical blanks since the last flip (0x005970f9). Until the count reaches the limit (0x00596d70, set to 2 at start-up) it only counts. Once it has, and a flip is pending, it switches the displayed buffer (GSDISPFB1andDISPLAY1), resets the count to 1 and restarts the DMA chain. - The CPU cannot run ahead: submitting the next frame's packet buffer (
0x0048c508) waits until the previous one has gone, so the game loop is held to the flip rate.
So a frame is shown for at least two vertical blanks. Evidence for the rate at runtime: confirmed (runtime), PCSX2
2.9.94 at full speed: its on-screen counters show 30 frames for 60 vertical blanks a second on the front end and in a
fight (a coarse check; the counters at 0x005970f9/0x0059708b were not watched). A frame that takes longer is shown
at the first vertical blank after it is ready (three blanks, 20 frames a second). Where the game clock is on its fixed
step (the front end, mode 8), game time still advances by 1/30 s and the game slows down rather than skipping; in a
level (mode 1) the clock runs on real time clamped to 40 ms (Boot), so a slow frame advances game time
by its real length. One exception: 0x00159468(0) sets a 33 ms limit (0x00596d9c); if that much time has passed since
the last flip when the flip token is reached, the DMA handler flips at once without waiting for a vertical blank (it
tears rather than drops to 20). 0x00159468(1) sets 65,535 ms, which turns this off. Mode 8 turns it off when it
resumes (0x0015c6f8); mode 1 turns it off when it is suspended or left and chooses per frame in its Update
(0x00158728, from a screen-effect state). Why the game allows tearing there is not known.
For Coney: the game advances in the same fixed steps of 1/30 s and slows down rather than skipping when frames run
long; drawing is decoupled from the step and may run at any rate, with --fps-cap 30 as the original's one frame per
step (Coney's implementation). The engine's test mode needs no display at all.
Loading textures¶
Texture dictionaries reach the game four ways, all through RenderWare's stream reader over the game's own stream (a
custom stream, 0x00197df0: close, read, write and skip functions over a game stream object; confirmed (code)):
| Source | Code | What it does | Evidence |
|---|---|---|---|
chunk 0x0B |
0x001906e8 |
find section 0x16, read the dictionary, push it as chunk 0x0B |
confirmed (code) |
chunk 0x2A |
0x00190770 |
device slot +0x150 reads it (the stream object is allocated in the Level Dynamic & LUA Pool heap, the dictionary in the current heap); make it current and at once current = none; push as 0x0B |
confirmed (code) |
world stream %s_sec.wld |
0x00410648 → 0x00410a50 |
u32 part count, then a dictionary (made current and back to none), then the world (section 0x0B) |
confirmed (code) |
sector atomics (world parts) %s_ms%i.sec |
0x004114d8 → 0x004110c0 |
skip the 16-byte header, a dictionary (current, then none), u32 n, then n × {u32 sector, atomic (section 0x14)}, each atomic placed in its sector |
confirmed (code); formats on The streamed world |
The world reader also sets RenderWare's sector render callback (0x00411b20) and checks every atomic's textures
(0x00198190, which prints "Potential Crash from Missing Texture" for a missing one).
The 1,911 WAD entries that are a 16-byte header {1, 0, 0, id} followed by a RenderWare stream are the sector
atomics files, read by 0x004114d8/0x004110c0 in World/ps2/WorldPS2.cpp. They are not chunk containers.
Confirmed (code) for the reader; the match to the entries is a disc check (corroboration): exactly 1,911 entries start
with {1, 0, 0, id} and a 0x16 section stamped 0x1C02000A, and all 1,911 are now named <world>_ms<i>.sec, with
id the CRC-32 of that name (The streamed world).
Each texture dictionary loaded in a resource pack has a holder object (Graphics/Texture.cpp, vtable 0x00538d60)
that, when the resource is freed, destroys every texture in the dictionary, the dictionary and the resource's heap
(0x0018e898, confirmed (code); that the holder belongs to the resource manager is inferred).
Finding textures by name¶
Materials find their textures by name while a model or world is read, through RenderWare's texture lookup. The game
leaves the current texture dictionary unset at all times except the moment right after a dictionary is read
(0x001980a0 is the only setter outside the world loaders, and every caller sets it back to none). With no current
dictionary, RenderWare's default find function (0x00489170) searches every loaded dictionary, in the order of
RenderWare's dictionary list, and returns the first texture with that name; with a current dictionary it searches
only that one. Confirmed (code) at 0x00489170. So texture names form one global name space, first match wins; the
list order (RenderWare adds a new dictionary at the head of its list, so the newest is searched first) is inferred
from RenderWare's behaviour, not read here. A name found nowhere gives no texture (the read callback, step 9 of
Init).
Game code looks up a texture by name in one particular dictionary through device slot +0x168 (used by the HUD and
effects; callers not traced).
2D drawing¶
There is no 2D layer in pixels. The front end and the HUD draw sprites through RenderWare's PTank plugin (particle tanks) seen by the overlay camera; a few effects use RwIm2D quads.
Sprites (front end, HUD, legal screen). A widget (GUI/BaseWidget.cpp) owns a resource-manager instance (id at
widget +0xc4) whose atomic is a PTank using a particle page's texture. Each frame the widget
adds one sprite to it (0x001a2690 → 0x00182de0: position, size, colour, texture rectangle), plus a drop shadow
when it has one (the same sprite offset by (0.0025, 0.004), black, at the widget's alpha × 128/255). Then the overlay
pass (0x00185d20) renders, through the overlay camera with Z test and Z write off and culling off, the overlay
world and every queued PTank in sorted order (per viewport for viewport-bound ones), and empties them again
(0x00185cc8). Confirmed (code).
Coordinates. A GUI point (x, y) with x to the right and y down, the whole screen being about [0, 1]²,
becomes overlay-camera space through slot +0x90 (0x00195238), confirmed (code):
X = (x - 0.5) * W / H # W, H = 640, 448: the device's width and height
Y = 0.5 - y
depth = 1.1 # in front of the overlay camera
Widths go through slot +0x98 (× W / H). Overlay sprites sit at z = -1.1 (seen in the batches' position arrays at
runtime), and the overlay camera has a perspective view window of (0.725, 0.5) in the default mode (A
frame) and an identity matrix, so a point projects to
screen x (fraction of the width) = 0.5 + X / (2 × 1.1 × 0.725) # 0.5 + X / 1.595
screen y (fraction of the height) = 0.5 - Y / (2 × 1.1 × 0.5) # 0.5 - Y / 1.1
and GUI x = 0 and 1 land at about 5.2 % and 94.8 % of the width, y = 0 and 1 at about 4.5 % and 95.5 % of
the height: a built-in safe-area margin of about 5 % on every side. Evidence: confirmed (runtime), PCSX2 2.9.94:
on the main menu the button glyph's sprite in the part_page0 batch sits at overlay (-0.6945, -0.3700, -1.1)
(read from the batch's position array); the formula puts it at 6.46 % across and 83.6 % down, and PCSX2's screenshot
shows the glyph centred at 6.45 % and 83.5 %. With the static 1.3333 (view window 0.6667) it would land at 2.7 %
across, which the screen rules out.
Screen quads. Device slots +0xe0/+0xe8 build four RwIm2D vertices for a destination rectangle in pixels, with
texture coordinates from a source rectangle in texels plus half a texel, and draw them as two triangles (indices
{0, 1, 2, 0, 2, 3} at 0x0050cdd8) with: nearest filtering, fog off, Z test and Z write off, vertex alpha on,
source blend SRCALPHA, destination blend INVSRCALPHA; afterwards Z test and write go back on and the raster is
unset. Slot +0x130 fills a viewport with a flat colour the same way (a four-vertex triangle strip, Z write off,
culling off). Confirmed (code).
Drawing a human¶
The humans in view are drawn in the world pass after the s world (The streamed world, step 7):
HumanAtomic_VisibilityCallback (0x00174248) queues each one on the resource manager's list +0xc78-+0xc7c, and
WorldManager_Render draws the list from its end (the last queued first) with HumanRender_Draw(human, viewport)
(0x00174320), back-face culling (0x0050c69c), Z test and Z write on, and the fog distance set to draw distance ×
0.5 instead of × the level's fog start. Confirmed (code). Per human, in order, confirmed (code) unless marked:
- Texture. The model's material is untextured on the disc (Characters).
For the draw, the geometry's first material gets the character's texture: the texture at
+0x10of the instance's texture resource (RpMaterialSetTexture,0x0046d010). After the draw it is set back to none. -
Blood. The same material is a MatFX dual-texture material. Its second texture is one of three shared blood textures, kept by the resource manager, chosen by the health percentage
h(Human_GetHealthPercentRaw,0x00222ef0: health / maximum × 100), withi = min(trunc(h / 30), 2):hiSlot Texture (confirmed (runtime), PCSX2 2.9.94, level99)60 and above 2 resource manager +0x74charblood_d130 to 60 1 +0x78charblood_d2below 30 0 +0x7ccharblood_d3(
RpMatFXMaterialSetDualTexture,0x00465810; the slot is+0x74 + ((2 - i) mod 3) × 4, the texture its object's+0x10.) The dual pass's GSALPHAregister, the 64-bit word at+0x28of the material's first MatFX effect, gets the low byte0x44whileh< 90 and the half-wordW_GameState + 0x454is 0, and0x45otherwise.0x44is(Cs − Cd) × As + Cd, an ordinary alpha blend of the blood texture over the first pass;0x45is(Cd − Cd) × As + Cd, which leaves the frame unchanged. So blood shows below 90 % health, and heavier textures replace it at 60 % and 30 % (thatd1-d3grow heavier is inferred from the names).W_GameState + 0x454is written only as 0 (by the game state's constructor and by the options' Restore Defaults,0x001d81c4), and the blood particle effects read it too (0x003a9168,0x003d5058), so it is a "no blood" switch the retail game never sets (inferred). The dual pass draws the blood texture with the model's second texture-coordinate set; at runtime it is the GS context-2 pass that follows each character draw (PCSX2 GS dumps, confirmed (runtime)).No build-up. The texture and the blend are worked out from the current health at every draw; the human keeps no blood state of its own, so the blood never accumulates and goes away as soon as health rises again (a heal, or
Human_ResetStats, Characters, giving a new or respawned human its class health). At a checkpoint it shows whatever health the checkpoint leaves. Confirmed (runtime), PCSX2 2.9.94,level99checkpoint 1 (scenarioblood_textures, hookhuman-bloodat0x0017450c): the player standing still, its health written to 95, 89, 61, 59, 31 and 29 % of 900 twenty updates apart, the draw picked slot+0x74at 100, 95, 89 and 61 %,+0x78at 59 and 31 % and+0x7cat 29 %, each on the first draw after the write; the other humans stayed at 100 % and+0x74. 3. Colour. The material colour is the instance's colour (+0x28, R in the low byte, set byHuColor) with R, G and B ×f.ffollows the brain's "hidden in shadow" byte (+0x2d4, Lighting): witht= min(time since the brain's stamp+0x10, 250 ms) × 0.002,f = 0.5 + twhen not hidden (0.5 up to 1.0) andf = 1 − twhen hidden (1.0 down to 0.5). 4. Alpha. The colour's alpha ×(drawDistance − d) / fadeLengthonce the human's camera distanced(+0x334) is past the fade start, and ×(now − start) / 1000during the first second after the instance's stamp+0x30. Values read at runtime (confirmed (runtime),level99): draw distance0x0050cc60= 70 m (clamped to [0x0050cc5c= 30,0x0050cc58= 70] byHumanRender_SetDrawDistance,0x00174200), fade length0x0050cc68= 10 m, fade start0x0050cc64= draw distance − fade length = 60 m. So a human fades out from 60 to 70 m. 5. Skeleton and lights:CharacterInstance_UpdateSkeleton(inst, 1)(0x00177240), then the lights (Lighting); then the atomic's saved render callback (0x0050cc54, RenderWare's default0x00469ef8), which runs the character pipelines (The streamed world). 6. Shadow: the blob (Lighting).
The bone matrices (CharacterInstance_UpdateSkeleton(inst, full), 0x00177240), confirmed (code). This is the
matrix palette the skin is drawn with; HumanRender_Draw passes full = 1.
- The source is the human's bone cache (
Human_GetBoneCache,0x0023bca0:0x006b6880+ human index ×0x470, Combat: the bone cache): 34 entries of 32 bytes, entrybholding a position (vec4) at+b × 0x20and a rotation quaternion(x, y, z, w)at+b × 0x20 + 0x10, in the model's z-up frame and already composed through the parent table (model space, not parent-relative). - For each pose bone
bfrom 2 to 33 (withfull= 0, bone 2 only):L_b= the rotation as a matrix (Quat_ToMatrix) with the position × the human's scale (Human_GetScale) as its translation. R= the human's world transform: the rotation and position of the object transform table0x00714b00at the human's index (+0x92), 32 bytes each, the rotation a quaternion at+0x10(confirmed (code)) (Characters).- Each skin matrix
i = b − 2(the matrix array of the first atomic's hierarchy, 64 bytes each, flags word+0x0c= 3) =Mat_MulSwapYZ(R, L_b)(0x00336940, confirmed (code)): the row-vector productL_b × R(bone first, then the human's placement), re-expressed in RenderWare's y-up axes: a game vector(x, y, z)becomes(x, z, −y), so the product's rows become(r0),(r2),(−r1)with each row's components reordered the same way. - When the human's scale is not 1, each matrix is also scaled by
(s, s, s)before it (RwMatrixScale,0x0047ce40, pre-concat; confirmed (code)), so with the scaled translations the whole skinned body is scaled bysabout the human's origin. - Then the hierarchy's frame is marked dirty for RenderWare (
0x00483bf8).
So skin matrix i belongs to HAnim node i (pose bone i + 2, Characters), and
a vertex is placed, inferred from RenderWare's skinning and the disc's inverse bind matrices, as
Σ weight_k × (v × inverseBind_k × skin_k) over its up to four weights. Whether the VU1 microcode renormalises the
weights was not read.
Drawing a car¶
Cars are drawn twice in the world pass (The streamed world): CarInstance_Render(car, opaque)
(0x00172c70) over the resource manager's car list +0xc98-+0xc9c, confirmed (code):
| Pass | When | opaque |
Render states | Order |
|---|---|---|---|---|
| Opaque | after the humans, before the other objects | 1 | culling off, Z test and Z write on, fog on | list order |
| Glass | after the water and the type-0x20 objects |
0 | culling off, Z write off, Z test and fog on | from the end of the list |
Each pass stores opaque at the instance's +0x41, selects the objects' lights for the clump's bounding sphere
(flags 1, no glow) and runs CarPart_RenderCallback (0x00172940) on every atomic. The glass pass then clears the
26 "seen" bits +0x11fc that the visibility callback (0x00172d60) sets. Per atomic a (part p = a, or
a − 25 for the damaged form), confirmed (code):
- Skip it unless bit
pof+0x11fcis set. Skip it in the opaque pass whenpis in the glass mask0x2a80c0(parts 6, 7, 15, 17, 19, 21) and in the glass pass when it is not. - Skip it when the part is removed (
+0x11f0), when it is the undamaged form and the part's damage is 0.5 or more (wheels 22-25 excepted), or when it is the damaged form and the damage is below 0.5 (Cars). - Upload the selected lights; give the first material the texture at
+0x10of the car's first texture resource. - Colour: the car's tint (
+0x24) for the parts in the paint mask0x157c31(parts 0, 4, 5, 10-14, 16, 18, 20), else the white at0x005fd268. Alpha ×(now − start) / 1000during the first second after+0x2c. - MatFX (
0x004653d8reads the material's effect): an environment map (2) gets the texture at+0x10of the car's second texture resource and the frame at*(0x0070ad18) + 4(0x00465538,0x004655e0); a dual material (4) gets that second texture as its dual texture (0x00465810). That0x0070ad18is RenderWare's current camera, so that the reflection follows the view, is inferred. - Draw it with the saved render callback (
0x005fd030), then set the material's texture back to none.
The throw-aim arc¶
While a player aims a throw (L1, Objects: throws) a textured ribbon shows the flight from his
hand. One arc record per player (game state +0x5740 + 0x30 × p, code). Confirmed (code) at the
addresses cited unless marked.
The model (ThrowArc_Create, 0x0018f268):
- One clump, one atomic, one geometry made with
RpGeometryCreate(36, 34, 0x0d): 36 vertices (18 pairs) and 34 triangles (two per pair of pairs, 17 quads), flags triangle strip, textured and prelit; no normals and no lighting flag, so the arc is unlit: its colour is the vertex colour times the texture. The bounding sphere is radius 100 at the origin, so it is never culled. - One material, surface properties (1, 1, 1), texture
aimthrow_tex: the WAD file named the decimal CRC-32 of that name,646320484(entry 3,276 ofWARRIORS.DIR, name hash0x3f103fcbof./ee_files/646320484, 1,552 bytes), loaded once and kept (0x005971b0). It is a texture dictionary of one PS2 texture,aimthrow, 32 × 64, 4-bit palettised, linear filtering, wrap in u and v. Its palette uses three colours: light grey (150, 150, 150) at alpha0x0e(about 11 % of the PS2's0x80= opaque), the same grey fully opaque, and transparent black (0, 0, 0, alpha 0). Of the 2,048 texels 640 (31 %) are transparent, 128 (6 %) the faint grey and 1,280 (62 %) opaque (confirmed (disc data): Coney's loader converts the texture andrepo:tests/platform/disc_throw_arc_test.cppcounts the classes; this page's first count, 62 % transparent, 31 % faint and 6 % opaque, was wrong). The texels' layout is in the PS2's swizzled order and was not unswizzled, so the pattern's shape is not described. - Texture coordinates, fixed: pair i has u = 0 on its left vertex and 1 on its right, and v = 15 − i (15 at the hand down to −2 at the end), so the texture repeats once per segment along the arc (wrapping).
Each aiming frame Human_TraceThrowAim (0x0018fee0) traces the arc (Objects: throws) and
calls ThrowArc_BuildStrip(arc, local points, world points, count, hit) (0x0018fa68):
- Points. The 18 kept points (one every sixth 1/60 s step), in the thrower's frame relative to the release point
(
Mat_TransformPointby the inverse of his facing and that point).countis 18 when the trace met nothing; when it ended at a contact in segment k it is k, with point k moved to the contact. - Pairs. Pair 0 at point 0, pairs 1 to count − 1 at their points, pairs count to 16 all at point count − 1 (degenerate triangles), pair 17 at point count (the end or the contact). So the ribbon always has 18 pairs and ends where the trace ended.
- Width. Each pair is the point ± 0.08 m along the thrower's right axis (his frame's x): a flat ribbon
0.16 m wide, level across, in the vertical plane of his facing. The model's axes are stored as (x, z, −y) and
ThrowArc_PlaceAtHand(0x0018f628) builds the clump's frame from his facing and the release point (hand+0x610+ facing × (0.3009, −0.6354, 1.5865)) with the matching swap, so the ribbon lands on the traced points (the swap's cancelling is inferred from the two functions' matrices). - Colour (RenderWare's RGBA bytes): (127, 127, 127) when
hitis 0 and (170, 43, 43) when it is set.hitis the object the trace ended on: the human target, or the first body (world object, car) it met; it is null for the level mesh or nothing. That is the object whose task becomes+0x634, so the arc is red exactly whileHuIsAimingAtwould be true of something (inferred: every task has the type bit+0x634needs). - Alpha by pair: pair 0 (at the hand) 0; every other pair i: 127 + trunc(16 × min(i ÷ count, 1)), so the collapsed pairs and the end pair of a cut arc get 143. With count 18 the pairs 0-17 get 0, 127, 128, 129, 130, 131, 132, 133, 134, 135, 135, 136, 137, 138, 139, 140, 141, 142: the ribbon fades in over its first segment only and then rises slightly toward the end.
Drawing (ThrowArc_Render, 0x00190588, from ThrowArcs_RenderAll 0x00419da0 in the world pass): only
while the human's move function is Human_MoveThrowAim (+0x1c0, with no move id, +0x1be = −1) and he holds a
world object; for each camera view: culling off (RwRenderStateSet(CULLMODE, NONE)), then the clump is rendered.
No other state is set, so Z test, Z write and vertex-alpha blending are the world pass's defaults (inferred).
On screen (inferred): the PS2's GS treats a vertex colour or alpha of 0x80 as 1.0, and the palette's opaque
alpha is 0x80 (confirmed from the disc), so the vertex grey 127 shows the texture's own light grey (150) almost
unchanged, the red (170, 43, 43) tints it to about (199, 50, 50), and alphas 127-143 leave the texture's alpha as it
is (opaque texels solid, the faint texels a light haze). Whether RenderWare's PS2 pipeline passes prelit colours to
the GS unscaled was not read. The measurement below supersedes this prediction: the arc shows far darker (grey about
50, red about 68) and half transparent.
On screen, measured (confirmed (runtime), PCSX2 2.9.94 at its native resolution, window captures of 1575 × 930;
level80 checkpoint 3 reached by the checkpoint jump from slot 6, the hold-up command given, the thrower on the roof
at (−244.3, 268.0, 5.7) with the lesson's bottle, L1 pressed, the aim moved with the left stick at 0.3 in 2-update
steps; the camera is the lesson's fixed ThrowCam, so frames with the arc in different places share a background):
- Not hit (pitch 0.154 rad, heading 79°, the arc crossing a dark wall and a lit window): against the same pixels without the arc, the arc raised a background of about 20 by +13 and lowered one of about 90 by −17.5, the same in the three channels apart from the background's own tint. Fitting out = α·C + (1 − α)·bg to the two gives C ≈ 50 and α ≈ 0.44 (averaged over the band's pixels, edges included); the arc crosses over (neither lighter nor darker) where the background is about 45-50. So the grey arc reads as a dim, half-transparent band.
- Hit (the aim on the
dyn_w_missionmarker, pitch 0.154 rad, heading 58.6°,+0x634the marker; the hint became "Press X to throw"): over a background of about (16-20, 16-20, 18-22) the band rose by +21 to +30 in red and 0 to +4 in green and blue from pixel to pixel. With the same α that is C ≈ 66-87 in red and 18-27 in green and blue; the low end of the rise (about +22 red, +0 to +1 green and blue) gives C ≈ (67-69, 17-20, 17-20). - Both fit one linear rule: on-screen colour ≈ 0.39 × the vertex colour per channel (127 → 50; 170 → 67, 43 → 17), at this distance (the band 10-20 pixels wide, the texture's pattern not resolved). For the red band the rule fits the low end of the measured range; its brighter pixels (up to about 87 red) sit above it.
The colour scale is settled: 255 is 1.0 (the vertex colour and alpha as RenderWare's byte values; confirmed (disc data) for the texture, inferred for the pipeline). The filtered texel's colour is the plain mean of the palette's colours, because the transparent entry is black: (1,280 + 128) ÷ 2,048 × 150 = 103, so a vertex grey of 127 gives 127 ÷ 255 × 103 = 51 (measured 50), the red 170 gives 69 (measured 67-69) and the green and blue 43 give 17 (measured 17-20). That is the 0.39-0.40 rule above, from the texture alone. A 128 = 1.0 reading would show about 102, 137 and 35, twice what was measured. The alpha agrees in kind: the mean texel alpha is 0.63 (the opaque 62 % and the faint 6 % at 0.11), times the vertex alpha 127-143 ÷ 255 (0.50-0.56), about 0.32-0.35; the measured 0.44 is higher, and a Coney render of the same ribbon fits 0.28 to 0.47 by the same two-frame method depending on how far the ribbon is (the texture's minification), so the difference is the filtering of a thin band, not a different scale. Not settled: the exact alpha rule of the GS (the measured 0.44 came from one distance).
Coney's implementation (graphics::throwArcStrip(), repo:src/graphics/throw_arc.h; platform::ThrowArcRenderer;
PlayLevelMode::throwArcRibbon()), tested by repo:tests/graphics/throw_arc_test.cpp, the throw lesson's disc test
(repo:tests/platform/disc_level80_test.cpp) and repo:tests/platform/disc_throw_arc_test.cpp:
- Always 18 pairs (36 vertices, 34 triangles): pair 0 on the hand, pairs 1 to count − 1 on their points, the rest on
point count − 1 and the last pair on the end point (the contact). The trace's contact is point
arcPoints − 1, socountis that, or 18 when the trace met nothing (ThrowAimState::arcCut()tells a contact in the last segment from none). Coney's reading: with no contact the last pair sits on the last point, point 17 (the page has the last pair at "pointcount", which an 18-point arc has no entry for). - Colour (127, 127, 127), or (170, 43, 43) while the trace ends on the human target or a body (
+0x634set), none for the level mesh; alpha 0 at the hand, then 127 + trunc(16 × min(pair ÷ count, 1)). The renderer hands the bytes to librw as they are (255 is 1.0). - Texture
aimthrowfrom WAD entry./ee_files/646320484, wrapping, linearly filtered, u 0 → 1 across and v = 15 − pair along (one repeat per segment); untextured only when the file is missing. - Width ± 0.08 m along the thrower's right axis (his frame's x, from the aim's heading), level across the arc.
- Culling off; drawn only while the aiming state is on, in
PlayLevelMode::render's world pass after the cars' glass. Coney's stand-ins: Z tested but not written, and no fog (the page leaves these to the world pass's defaults, inferred), the draw's place in the frame (ThrowArcs_RenderAllnot placed inWorldManager_Render's order), and the ribbon's heading is the aim's current one (a render between two steps blends the arc's points, not the heading). - A render between two steps draws the arc between their points (Update and render).
What a ribbon laid level looks like: from a camera at the thrower's head height it is edge-on, a line a few pixels thick; from above it is a band 0.16 m wide.
Distance: fades and level of detail¶
What changes with distance, from the draw paths read (confirmed (code) for each item; that there is nothing else is inferred from those paths):
| What | Rule | Where |
|---|---|---|
| World, everything | the far clip is the draw distance: 115 m for the player camera, shrinking when the frame rate drops or scenery is missing | The streamed world |
| Fog | linear from draw distance × fog start (57.5 m) to the draw distance (115 m) | The streamed world |
| Humans | fade out from 60 to 70 m (draw distance 70 m, fade 10 m); beyond, not queued | Drawing a human |
| World objects | by screen size: radius / squared distance under 0.0004 not drawn, faded between 0.0004 and 0.0005 (some types exempt) | World objects |
| Cars | none: drawn while their parts are in view | Drawing a car |
| Textures | the mip level by distance, log2(distance) + K |
Texture dictionaries |
| Skyline | its own near and far clip (39 to 560 m) | Level loading |
No geometry level of detail: a world sector, a human, a car part and an object each have one model, drawn whole
or not at all. The s and d worlds are streamed by distance (The streamed world), which
changes what is loaded, not how detailed it is.
The first screen¶
After the three start-up movies, main pushes modes 8, 6 and 5, so mode 5 runs first
(Boot). Mode 5 is the legal screen, confirmed (code):
Enter(0x00159a58) records the start time, sets a 5,000 ms timeout and callsStartupScreen_Draw(0x00159c08).StartupScreen_Drawpicks a resource name by language (W_GameState + 0x120: 0 English, 1 Spanish, 2 French, 3 Italian, 4 German) and aspect:legal_screen,legal_screen_sp,_fr,_it,_ge, with a_wvariant of each for 16:9 (legal_screen_w,legal_screen_w_sp, ...); with flag0x02set and English it useslegal_screen_euro. The WAD file name is the decimal CRC-32 of that resource name ("%u",0x0054efc8).- It loads that resource through the resource manager and waits for it, servicing the file manager. For each of the two display buffers it clears the overlay camera to opaque black, draws the page's first sprite (placement below), and shows the raster. Then it releases the resource.
Update(0x00159ae0) draws nothing and presents nothing: it ticks the timer, reads the pads and leaves once 5,000 ms have passed. The display keeps showing the last flipped buffer.
The scale factors (horizontal, vertical) by mode flags, confirmed (code): interlaced 4:3 (1.55, 1.35), interlaced 16:9 (1.9, 1.45), progressive 4:3 (1.35, 1.19), progressive 16:9 (1.6, 1.25), neither flag (1.0, 1.0) or with 16:9 (1.45, 1.19).
Placement. The sprite record (0x0015a0b8-0x0015a154, confirmed (code)) has position (0, 0, -d), where d
is the overlay camera wrapper's slot +0xa8 getter (its near clip, 0.5, inferred from the getters' order; the
value cancels out below), size (fx × d × (u1 - u0), fy × d × (v1 - v0)) with (fx, fy) the factors above and
(u0, v0, u1, v1) the page's first rectangle, and colour (255, 255, 255, 255) (four 0xff bytes). A batch sprite
is centred on its position and its size is the full width and height, so the picture covers, as a fraction of the
screen,
width = fx × (u1 - u0) / (2 × viewWindowX) # 1.55 × 0.99902 / 1.45 = 1.068 in interlaced 4:3
height = fy × (v1 - v0) / (2 × viewWindowY) # 1.35 × 0.74902 / 1.0 = 1.011
centred: about 683 × 453 logical pixels on the 640 × 448 screen, so the picture slightly overfills it. About
3.2 % of the image width (16 texels of 512) is cut off on the left and on the right and 0.5 % (2 texels of 384) at
the top and the bottom, and the 4:3 image is shown about 5.6 % wider than its own shape. The factors are matched to
the overlay camera's per-mode aspect (0x0050b208): 1.55 / 1.45 here.
Evidence: confirmed (runtime), PCSX2 2.9.94. The clear colour was patched to blue (0x00159e40, mov.s f14, f15)
and the two factors halved (0x0015a03c, 0x0015a048) before mode 5 ran, so the picture's edges show: PCSX2's
screenshot has the picture centred, covering 0.534 of the width and 0.5075 of the height, against 0.5340 and 0.5056
from the formula with half factors (within the screenshot's one-pixel resolution). Unpatched, the picture fills the
screen to its edges with the image's margins cut as above (the text block scales by 2.0 about the centre between
the two captures). The device flags read 1 (interlaced 4:3), the overlay camera's view window 0.725 × 0.5.
Disc check (corroboration): all eleven names exist in WARRIORS.DIR under their decimal CRC. legal_screen
(CRC-32 863681355) is entry 3,480: 263,680 bytes, one resource of two chunks, a 0x2A texture dictionary holding one
512 × 512 8-bit palettised texture and a 0x4C particle page with one rectangle (the top 512 × 384). So the faithful
first screen is that image, scaled to slightly overfill a black 640 × 448 screen (Placement), held for
five seconds; then mode 6 (memory card checks) and mode 8 take over.
This naming scheme names the whole first block of the WAD: see WAD contents.
Code index¶
Every function of the game's own Graphics/ code (0x0016e388-0x0019dfb0, a range inferred from where its functions
lie, Source map) that the sections above do not already walk through, file by file in address
order, with what it does. Lighting has its own index (Lighting). Names are ours, as in the
local Ghidra project, where each function also carries a plate comment pointing here. A row that only links is described
on the linked page.
Resource manager: animations and cars¶
The resource manager (0x0050cd4c, Level loading, Chunk system) keeps one
balanced-tree map per resource type, keyed by name hash: car models (type 2) at +0x20, texture dictionaries
(type 4) at +0x30, character data (type 5) at +0x40, named animation packs (type 7) at +0x50. A node's
value (+0x14) starts with the real-time stamp of its last use, then a reference count (+0x04). Each type has
the same methods: get (blocking: request it if absent, run the file manager until it is loaded, add a
reference), request (non-blocking; refreshes the stamp when present), free (only at zero references:
destroy, unlink, count − 1; called by the evictions 0x00187d28 and 0x00188aa8), is resident (present, and
referenced when asked) and fits (would a resource of that size fit, evicting with 0x00187d28 when allowed).
Names are ours. Confirmed (code) at each address.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x0016e388 |
CharDataRes_Construct |
holder of one character's animation data (vtable 0x00538988): stamp = now + 3,000 ms, the data, its heap, the stream |
confirmed (code) |
0x0016e420 |
CharDataRes_Destruct |
frees each of the 722 animation slots that differs from the resource manager's default set (+0x70), the slot table, the heap |
confirmed (code) |
0x0016e6a8 |
AnimPackRes_Construct |
holder of one named animation pack (vtable 0x00538970), same layout |
confirmed (code) |
0x0016e740 |
AnimPackRes_Destruct |
frees the pack, its heap and stream | confirmed (code) |
0x0016e8f0 |
ResourceMgr_GetCharData |
get, type 5 (file "%u" of the id, Characters) |
confirmed (code) |
0x0016ea20 |
ResourceMgr_RequestCharData |
request, type 5 | confirmed (code) |
0x0016eb10 |
ResourceMgr_FreeCharData |
free, type 5 | confirmed (code) |
0x0016f128 |
ResourceMgr_GetAnimPack |
get, type 7, keyed by the CRC-32 of the pack's name (0x0010bc38 loads named packs through it) |
confirmed (code) |
0x0016f260 |
ResourceMgr_RequestAnimPack |
request, type 7 (the file is named "%s" of the name) |
confirmed (code) |
0x0016f368 |
ResourceMgr_FreeAnimPack |
free, type 7 | confirmed (code) |
0x0016f980 |
ResourceMgr_HasAnimPack(rm, name, needRef) |
is resident, type 7 | confirmed (code) |
0x0016fa00 |
ResourceMgr_AnimPackFits(rm, name, size, evict) |
fits, type 7: a trial allocation of the size on the resource heap | confirmed (code) |
0x00173068 |
ResourceMgr_HasCar(rm, desc, needRef) |
is resident for a car: its model and its one or two texture dictionaries | confirmed (code) |
0x00173170 |
ResourceMgr_CarFits |
fits, for a car's model | confirmed (code) |
0x001736a0 |
ResourceMgr_RequestCar |
request a car's model and texture dictionaries | confirmed (code) |
0x00173718 |
ResourceMgr_CreateCarInstance |
gets the model and textures and makes a CarInstance (0x44 bytes, 0x001721c8) |
confirmed (code) |
0x00173878 |
ResourceMgr_ReleaseCarInstance |
one reference less, stamp, delete the instance | confirmed (code) |
0x001739b0 |
ResourceMgr_RequestCarModel |
request, type 2 | confirmed (code) |
0x00173aa0 |
ResourceMgr_GetCarModel |
get, type 2 | confirmed (code) |
0x00173bd0 |
ResourceMgr_FreeCarModel |
free, type 2 (CarModel.cpp) |
confirmed (code) |
Litter and ground fog¶
CameraGarbage.cpp is the blowing litter (object 0x005971a0, 0x20 bytes, 64 pieces of
0x70 bytes); CameraGroundFog.cpp the ground fog (object 0x005971a4, 0x0c bytes, 36 patches of 0x60 bytes on a
6 × 6 grid 6 m apart round the player camera, each put on the ground by a ray and kept 0.2 m above it, slowly
turning). The ground fog is switched only by EnableGroundFog, which no script calls
(effects bindings), so it is never seen in the shipped game. Confirmed (code).
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x0016fcc8 |
Garbage_Create |
allocates the litter object at start (Game_InitializeSubsystems) |
confirmed (code) |
0x0016fd90 |
Garbage_SpawnPiece(piece, rect, rectRange, sizeMin, sizeMax) |
places a piece in its grid cell round the camera and gives it its random looks, wind threshold and life (litter) | confirmed (code) |
0x0016ffd8 |
Garbage_LayFlat(piece, normal) |
the turn that lays a piece on a surface, slerped over up to 8 updates; marks it grounded (flag 8) | confirmed (code) |
0x00170300 |
Garbage_Construct |
zeroes the object | confirmed (code) |
0x00170330 |
Garbage_Start(kind) |
StartGarbage |
confirmed (code) |
0x00170528 |
Garbage_End |
EndGarbage: clears the active flag |
confirmed (code) |
0x00170538 |
Garbage_SetPaused(on) |
sets +0x04 while active; the update skips while it is set (called from 0x002195e0) |
confirmed (code) |
0x00170550 |
Garbage_Release |
frees the pieces and ends (level unload, 0x001607b8) |
confirmed (code) |
0x00170600 |
Garbage_ApplyWind(piece) |
the wind step | confirmed (code) |
0x00170b28 |
Garbage_Throw(pos, dir, count) |
extra pieces from breakables | confirmed (code) |
0x00170c88 |
Garbage_Update |
steps every piece at 30 Hz, from Humans_Update |
confirmed (code) |
0x001712c0 |
Garbage_Render |
draws the pieces as textured cards from the world render (0x0040f548), not during scenes |
confirmed (code) |
0x00171540 |
GroundFog_Create |
allocates the ground-fog object at start | confirmed (code) |
0x00171608 |
GroundFog_PlacePatch(patch) |
puts a patch in its grid cell round the player camera, on the ground below (no ground flags it 0x10 for a retry) |
confirmed (code) |
0x00171908 |
GroundFog_Construct |
zeroes the object | confirmed (code) |
0x00171920 |
GroundFog_Start |
allocates the 36 patches (TGroundFogData, 0xd80 bytes) and places them |
confirmed (code) |
0x00171a50 |
GroundFog_Stop |
clears the on flags | confirmed (code) |
0x00171a68 |
GroundFog_Release |
frees the patches and stops | confirmed (code) |
0x00171b18 |
GroundFog_NewDrift(patch) |
a random spin (±0.1, from the wind's strength over the patch's threshold) and a 30-60-update slerp | confirmed (code) |
0x00171d38 |
GroundFog_Update |
per update, from Humans_Update: re-places flagged patches, ages and re-drifts them |
confirmed (code) |
0x00171f58 |
GroundFog_Render |
draws the patches from the world render (0x0040f554) |
confirmed (code) |
Cars' render objects¶
What Cars says is drawn, as code. Confirmed (code).
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x001721c8 |
CarInstance_Construct(model, tex, tex2) |
render instance of a car (vtable 0x00538a10, base 0x0017f650), white tint +0x24, no damage record yet |
confirmed (code) |
0x00172258 |
CarInstance_Destruct |
restores the vtable, base destructor | confirmed (code) |
0x00172280 |
Matrix_RwToGame |
copies a matrix into the game's axes: each row (x, y, z) becomes (x, −z, y) | confirmed (code) |
0x00172388, 0x00172440 |
CarInstance_SetMatrix, CarInstance_SetPartMatrix |
write a matrix into the clump's frame (or a part's) and mark it dirty | confirmed (code) |
0x001724f8, 0x001726f0 |
CarAtomic_CopyLtmCb, CarAtomic_CopyMatrixCb |
atomic callbacks: copy the world matrix / the frame matrix of the atomic whose part number matches | confirmed (code) |
0x001725d8, 0x001727c8 |
CarInstance_GetPartLtm, CarInstance_GetPartMatrix(inst, out, part, toGame) |
a part's world / local matrix, converted by 0x00172280 when asked |
confirmed (code) |
0x001728e0 |
CarInstance_PartColour(inst, part) |
the car's tint for the painted parts (mask 0x157c31), white for the others |
confirmed (code) |
0x00172940 |
CarPart_RenderCallback |
draws one atomic by its part's damage and the pass (Drawing a car) | confirmed (code) |
0x00172c70 |
CarInstance_Render(inst, opaque) |
Drawing a car | confirmed (code) |
0x00172d60 |
CarPart_VisibilityCallback |
per atomic in view: queues the car on the resource manager's car list (+0xc94) when one of its parts draws |
confirmed (code) |
0x00172f00 |
CarInstance_MarkPartDrawn(part) |
sets bit part of +0x11fc |
confirmed (code) |
0x00172f28 |
CarModel_SetupAtomic |
per atomic of a new car model: render callback 0x00172d60, the game pipelines, the bounding sphere's y and z swapped |
confirmed (code) |
0x00172ff0 |
CarInstance_Init |
base set-up, then every atomic's plugin owner (+0x0c) set to the instance and +0x08 to 0 (0x001925d0) |
confirmed (code) |
0x00173928 |
CarModel_Construct |
car model (vtable 0x00538a38, base 0x0017f380), each atomic set up |
confirmed (code) |
0x00173988 |
CarModel_Destruct |
restores the vtable, base destructor | confirmed (code) |
Characters: instance, task stack and model¶
Character.cpp and CharacterModel.cpp are the render side of a human: the CharacterInstance (0x330 bytes,
vtable 0x00538a78; Characters, its task stack) and the character
model and its resource-manager methods (type 3 at map +0x00). Confirmed (code) at each address. Instance fields
used here:
| Offset | Meaning |
|---|---|
+0x04 |
the render instance base (0x0017f650) |
+0x20 |
the clump; +0x50 / +0x60 two quaternions, +0x70 the default-pose pointer (0x00598420), +0x80 the pose |
+0x2a0 / +0x2a4 |
head-look yaw and pitch (radians); +0x2a8 the head-look weight 0-1 |
+0x2ac / +0x2ae |
task-stack count / queue count; stack at +0x2bc (18), queue at +0x304 (10) |
+0x2b8 |
the human |
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00174200 |
HumanRender_SetDrawDistance(d) |
the humans' draw distance 0x0050cc60 = d clamped to [0x0050cc5c, 0x0050cc58] (and 0x0050cc64 = that − 0x0050cc68), from the world render |
confirmed (code) |
0x00174238 |
CharacterInstance_ReturnFalse |
returns 0 (a vtable slot) | confirmed (code) |
0x00174248 |
HumanAtomic_VisibilityCallback |
per atomic in view and not occluded (0x0017a680): queues the human on the resource manager's list +0xc74 when its squared camera distance (+0x334) is under the draw distance |
confirmed (code) |
0x00174320 |
HumanRender_Draw |
Drawing a human, Lighting | confirmed (code) |
0x00174af8 |
CharacterInstance_SetModel(inst, model, tex, data) |
base set-up, each atomic's plugin owner set (0x001925d0), copies the model's 34 bind offsets, then runs the queued tasks |
confirmed (code) |
0x00174c18 |
CharacterModel_SetupAtomic |
per atomic of a new character model: render callback 0x00174248 (first one kept in 0x0050cc54), the game pipelines, the bounding sphere's y and z swapped |
confirmed (code) |
0x00174d00 |
CharacterInstance_Construct(model, data, tex) |
full instance: default pose 0x00598420, reference pose (0x00100a80), empty stack and queue |
confirmed (code) |
0x00174e70 |
CharacterInstance_ConstructPoseOnly(data) |
the same without a model (0x00177b80 when no model is wanted) |
confirmed (code) |
0x00174f40 |
CharacterInstance_Destruct |
frees every stacked and queued task | confirmed (code) |
0x00175080 |
CharacterInstance_GetAnim(id) |
Characters | confirmed (code) |
0x00175120 |
CharacterInstance_ReleaseClip(clip) |
one user of a clip less (+0x43), telling the human first when the clip has flag 4 |
confirmed (code) |
0x00175178, 0x00175180 |
TaskStack_Count, TaskStack_SetCount |
+0x2ac |
confirmed (code) |
0x00175188, 0x001751c0, 0x00175208 |
TaskStack_TopRaw, TaskStack_FromTop(n), TaskStack_Bottom |
the top task, the n-th below it, task 0 | confirmed (code) |
0x00175210 |
TaskStack_Top |
task stack | confirmed (code) |
0x00175258 |
TaskStack_TopAnimId |
the top task's anim id (slot +0xf8), or the bottom one's when the top has none |
confirmed (code) |
0x001752c8 |
CharacterInstance_RefreshAnimId |
task stack | confirmed (code) |
0x00175380 |
TaskStack_Append |
puts a task on top without the fade check | confirmed (code) |
0x001753d8, 0x001754e8 |
TaskStack_Push, TaskStack_InsertBottom |
task stack | confirmed (code) |
0x00175610 |
CharacterInstance_RunQueuedTasks |
Animation | confirmed (code) |
0x00175840, 0x00175848 |
TaskQueue_Count, TaskQueue_SetCount |
+0x2ae |
confirmed (code) |
0x00175850, 0x00175888, 0x001758d0 |
TaskQueue_Last, TaskQueue_FromLast(n), TaskQueue_LastNotFade |
the queue's newest task, the n-th before it, the newest unless it is a fade (type 9; then the first) | confirmed (code) |
0x00175928 |
TaskQueue_Append |
adds a task at the end | confirmed (code) |
0x00175980, 0x00175a68 |
TaskQueue_AppendTrim, TaskQueue_InsertFirstTrim |
add at the end / the front; with more than 2 queued, first finish the queued fade with the most time left (0x00177fa8) |
confirmed (code) |
0x00175b78, 0x00175be8 |
TaskStack_TopAllowsHeadLook, TaskStack_TopAllowsCamera |
the top task's clip exists and lacks descriptor flag 0x4000 / 0x8000 |
confirmed (code) |
0x00175c58 |
CharacterInstance_SetHeadLook(yaw, pitch) |
stores the head-look angles and builds the head and neck turns from them (pitch clamped to −30°..22.5°) | confirmed (code) |
0x00176290 |
CharacterInstance_ApplyHeadLook(inst, pose) |
while the top clip and the human allow it, raises the weight +0x2a8 (faster for small angles) and turns the pose's head and neck by weight × angles; otherwise lowers it |
confirmed (code) |
0x001768e8 |
CharacterInstance_ApplyTorsoTwist(inst, pose) |
turns the spine bones by the human's twist angle (human +0x29c), split over the bones when above 3° |
confirmed (code) |
0x00176d60 |
CharacterInstance_Sample |
Animation | confirmed (code) |
0x00177038 |
TaskStack_ForEach(slot +0xc8) |
calls each stacked task's slot +0xc8 with an argument |
confirmed (code) |
0x001770b8 |
CharacterInstance_SampleThunk |
calls 0x00176d60 |
confirmed (code) |
0x001770d8 |
TaskStack_TopClipProgress |
for a clip task (type 3) with flags 0x48000: current time / length, else 0 |
confirmed (code) |
0x001771a0 |
TaskStack_TopCallEvent(arg) |
when the top task has flag 0x80 and slot +0x110 agrees, calls its slot +0xb8 |
confirmed (code) |
0x00177210 |
Clump_GetFirstAtomicCb |
atomic callback: keeps the atomic's geometry | confirmed (code) |
0x00177240 |
CharacterInstance_UpdateSkeleton(inst, full) |
the bone matrices | confirmed (code) |
0x001774d0 |
ResourceMgr_HasCharacter(rm, desc, dataOnly, needRef) |
is resident for a character: data, and unless dataOnly texture dictionary and model (Characters) |
confirmed (code) |
0x001775d8 |
ResourceMgr_CharacterFits |
fits, for a character's model, data and textures in turn | confirmed (code) |
0x00177b00 |
ResourceMgr_RequestCharacter(rm, desc, dataOnly) |
request its model, data and textures | confirmed (code) |
0x00177b80 |
ResourceMgr_CreateCharacterInstance(rm, desc, dataOnly) |
gets them and makes a CharacterInstance (0x00174d00, or 0x00174e70 without a model) |
confirmed (code) |
0x00177d50 |
ResourceMgr_SetInstanceModel(rm, inst, desc) |
gets a model and textures and gives them to an instance (0x00174af8) with the default data |
confirmed (code) |
0x00177e00 |
ResourceMgr_ReleaseCharacterInstance |
one reference less, stamp, delete | confirmed (code) |
0x00177eb8 |
TaskStack_NewestFade |
the newest fade task on the stack (for the push's early finish) | confirmed (code) |
0x00177fa8 |
TaskQueue_LongestFade |
the queued fade (type 16) with the most time left | confirmed (code) |
0x00178098 |
CharacterList_OnLoaded |
chunk 0x44 (Chunk system) |
confirmed (code) |
0x001780d0, 0x001780d8 |
CharacterList_Count, CharacterList_Record(i) |
the list's count and 32-byte record i | confirmed (code) |
0x001780e8 |
CharacterList_Find(name) |
record index for the CRC-32 of a model name, or −1 (Cfg_SetCharacterClass) |
confirmed (code) |
0x00178178 |
CharacterModel_Construct |
character model (vtable 0x00538b28, base 0x0017f380), atomics set up by 0x001983a8 and 0x00174c18 |
confirmed (code) |
0x00178208 |
CharacterModel_Destruct |
frees the model's bind offsets, base destructor | confirmed (code) |
0x001782e8, 0x001783d0 |
ResourceMgr_RequestCharModel, ResourceMgr_GetCharModel |
request / get, type 3 | confirmed (code) |
0x001784f0 |
ResourceMgr_FreeCharModel |
free, type 3 | confirmed (code) |
0x00178b30 |
DependencyList_OnLoaded |
chunk 0x4F, then marks the always group needed |
confirmed (code) |
0x00178bc8 |
DependencyList_SetGroup(hash, state) |
sets the state word of every 12-byte record {group, resource, state, loaded} whose group or resource is the hash |
confirmed (code) |
0x00178c48 |
DependencyList_IsNeeded(hash) |
binary search; true when a record of that resource is wanted | confirmed (code) |
0x00178d48 |
DependencyList_NextToLoad(out) |
the next wanted, unloaded resource as a load request ("%u" name and size from the object list) |
confirmed (code) |
0x00178ee8 |
AnimList_OnLoaded |
chunk 0x4E: {hash, size} records of the animation packs |
confirmed (code) |
0x00178f38, 0x00178f40 |
AnimList_Count, AnimList_Size(name) |
the count; a pack's size by name (else the WAD file's size) | confirmed (code) |
Embers, fonts, the device base, occluders and colours¶
FallingEmbers.cpp and the unnamed file before the stub 0x0017b1c0 (fonts, the device's base class, the level's
occluders, colour helpers; inferred from position). Confirmed (code) at each address.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00178fd8 |
Embers_Create |
makes the embers object (0x005971ac, 0x50 bytes) on first use |
confirmed (code) |
0x001790a8 |
Embers_Destroy |
deletes it (TermFallingEmbers, level unload) |
confirmed (code) |
0x00179138 |
Embers_Construct |
a free list of 150 embers of 0x30 bytes | confirmed (code) |
0x00179250 |
Embers_Destruct |
frees the list | confirmed (code) |
0x00179310 |
Embers_Setup(emb, pos, minRate, maxRate) |
the centre (the object's position, taken once), the spread vectors (5 m), the per-update counts (rates / 30) (InitFallingEmbers) |
confirmed (code) |
0x00179378 |
Embers_Update |
spawns a random count per update round the centre with random speed and size, sprite 0x10008 + 0-3 (batch 1 part_page1, rectangles 8-11), life 60-90 updates; moves and retires the live ones |
confirmed (code) |
0x001795f8 |
Embers_Render |
draws the live embers from the world render (0x0040f56c) |
confirmed (code) |
0x00179758 |
Embers_Init |
InitFallingEmbers |
confirmed (code) |
0x001797e8 |
Embers_Term |
TermFallingEmbers |
confirmed (code) |
0x00179808 |
Font_Size(scale) |
GUI | confirmed (code) |
0x001798f8 |
Font_GetDefault(out) |
the default font's size record with spacing 3.0 (GetDefaultFont) |
confirmed (code) |
0x00179958, 0x00179c30 |
Font_Measure, Font_Draw |
GUI | confirmed (code) |
0x0017a1b0 |
Font_NoOp |
empty; called after a text widget's draw (0x001cd288) |
confirmed (code) |
0x0017a1c8 |
GraphicsDevice_Construct |
the device base class (vtable 0x00538b40) |
confirmed (code) |
0x0017a1e0 |
GraphicsDevice_Open |
Start-up | confirmed (code) |
0x0017a258 |
Colour_LerpPacked(t, a, b) |
blends two packed RGBA colours by t (VU0) |
confirmed (code) |
0x0017a3c0 |
Colour_LerpFloat(t, out, a, b) |
the same for float colours | confirmed (code) |
0x0017a4f8 |
Device_GetScreenSize |
screen width and height (device slots +0xa8, +0xb0; GetScreenDimensions) |
confirmed (code) |
0x0017a560 |
Occluders_Set |
Level loading | confirmed (code) |
0x0017a610 |
Occluders_SetUpViewport |
Level loading | confirmed (code) |
0x0017a680 |
Occluders_HidesSphere(occ, sphere) |
true when a bounding sphere (radius at least 0x0050ccc4) lies wholly behind the planes of an active occluder (records of 0x70 bytes, active byte +0x6f) |
confirmed (code) |
0x0017a738 |
Occluders_HidesBox |
Level loading | confirmed (code) |
0x0017a7f0 |
Plane_FromPoints(out, a, b, c) |
the plane through three points (normal, −distance) | confirmed (code) |
0x0017a8a8 |
Plane_Distance(point, plane) |
signed distance | confirmed (code) |
0x0017a8d8 |
Occluder_SetUpForViewport |
Level loading | confirmed (code) |
0x0017ab78 |
Occluder_HidesSphere(occ, sphere) |
the sphere is at least its radius behind each of the occluder's 3 planes | confirmed (code) |
0x0017abe0 |
Occluder_HidesBox |
Level loading | confirmed (code) |
0x0017aca8 |
Colour_PackFloat |
float RGBA × 255 to bytes (Cars, World objects) | confirmed (code) |
0x0017ad30 |
Colour_PackFloatRounded |
the same with rounding (0x0042c718) |
confirmed (code) |
0x0017adb0 |
Colour_Unpack |
bytes to float RGBA (÷ 255) | confirmed (code) |
0x0017ae38 |
ColourTable_Init |
GUI | confirmed (code) |
0x0017b1c0 |
Graphics_StaticInit |
calls 0x0017ae38: not needed (compiler stub) |
confirmed (code) |
Models, render instances and world objects¶
Model.cpp (the model base: a RenderWare clump with its stamp and heap, vtable 0x00538cc8), the render-instance
base every car, character and object instance derives from (vtable 0x00538ce0, 0x40 bytes), Object.cpp and
ObjectModel.cpp (world objects, type 1 at map +0x10; World objects). Confirmed (code) at
each address. Render-instance fields: +0x0c model, +0x10-+0x18 texture dictionaries, +0x1c the clump copy,
+0x20 the character data, +0x24 tint, +0x2c the time it was made, +0x30/+0x34 a state and the time it
changed, +0x38 in the collision world.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x0017f278 |
Model_SetupAtomicCb |
per atomic of a new model: sets the geometry's flag 0x40 and clears its first material's texture filter flag (0x0046d010) |
confirmed (code) |
0x0017f2c0 |
ClumpChunk_Read |
reads a RenderWare clump from a chunk (0x09, 0x25, 0x47, Chunk system) and pushes it as 0x0A (one atomic), 0x41 (one atomic, a single frame) or 0x50 (several) |
confirmed (code) |
0x0017f380 |
Model_Construct(model, clump, heap, stream) |
stamp now + 3,000 ms, the clump, the atomics set up | confirmed (code) |
0x0017f4a0 |
Model_Destruct |
destroys the clump's atomics and the clump, frees the heap | confirmed (code) |
0x0017f608 |
Model_IsInUse |
true while any of the clump's atomics is still in use (0x00198358, 0x00198220); asked by MakeRoom (0x00187d28) |
confirmed (code) |
0x0017f650 |
RenderInstance_Construct(inst, data) |
base instance (stamp now + 3,000 ms), cleared (0x0017f6d0) |
confirmed (code) |
0x0017f6d0 |
RenderInstance_Clear |
zero fields, white tint | confirmed (code) |
0x0017f708 |
RenderInstance_Destruct |
releases the model and textures (0x0017f7c8) and the character data |
confirmed (code) |
0x0017f7a0 |
RenderInstance_TexDict(inst, i) |
texture dictionary i | confirmed (code) |
0x0017f7b0, 0x0017f7b8, 0x0017f7c0 |
RenderInstance_Model, RenderInstance_Clump, RenderInstance_CharData |
+0x0c, +0x1c, +0x20 |
confirmed (code) |
0x0017f7c8 |
RenderInstance_Release |
takes the clump out of the world and destroys it; one reference less (with a stamp) on the model and each dictionary | confirmed (code) |
0x0017f8f8 |
RenderInstance_Setup(inst, model, tex0, tex1, tex2) |
keeps them, clones the model's clump, adds it to the collision world, stamps the time (0 while the game clock is paused) | confirmed (code) |
0x0017f998 |
RenderInstance_SetState(inst, s) |
a new state and the real time of the change | confirmed (code) |
0x0017f9e8 |
RenderInstance_SetInWorld(inst, on) |
adds or removes the clump from the solid world's collision set | confirmed (code) |
0x0017fa80 |
ObjectRender_ApplyFadeDistance |
World objects | confirmed (code) |
0x0017faf8 |
ObjectInstance_Construct |
instance of a world object (vtable 0x00538d08), each atomic's plugin owner set (0x001925d0) |
confirmed (code) |
0x0017fbc8 |
ObjectInstance_Destruct |
base destructor | confirmed (code) |
0x0017fbf0 |
ObjectAtomic_VisibilityCallback |
per atomic in view and not occluded: queues the object for drawing unless it is hidden (flag 0x10), dead or flagged 1 << 26, by its distance (+0x134) |
confirmed (code) |
0x0017fd78 |
ObjectRender_Draw |
World objects, Lighting | confirmed (code) |
0x00180790 |
ObjectModel_SetupAtomic |
per atomic of an object model: callback 0x0017fbf0 (first kept at 0x005fde88), the game pipelines |
confirmed (code) |
0x001807e0 |
ObjectInstance_SetMatrix(inst, m) |
writes a game matrix (y and z swapped) into the clump's frame | confirmed (code) |
0x00180858 |
ObjectInstance_BoundingSphere |
the clump's first atomic's world bounding sphere | confirmed (code) |
0x001808b8, 0x001809c0 |
ObjectModel_IsLoaded, ObjectModel_Request |
World objects | confirmed (code) |
0x00180ee0 |
ResourceMgr_RequestObject(rm, rec) |
request an object record's model and dictionaries | confirmed (code) |
0x00180f58 |
ObjectModel_MakeInstance |
World objects | confirmed (code) |
0x001810c0 |
ResourceMgr_ReleaseObjectInstance |
one reference less, stamp, delete | confirmed (code) |
0x00181170 |
ObjectList_OnLoaded |
chunk 0x46 (WAD contents) |
confirmed (code) |
0x001811a8, 0x00181228 |
ObjectList_Count, ObjectList_Record(i) |
count, record i (0x24 bytes) | confirmed (code) |
0x001811b0 |
ObjectList_FindByHash |
World objects | confirmed (code) |
0x00181248 |
ObjectModel_Construct |
object model (vtable 0x00538d30), atomics set up by 0x00180790 |
confirmed (code) |
0x001812a8 |
ObjectModel_Destruct |
base destructor | confirmed (code) |
0x001812d0, 0x00181400 |
ResourceMgr_GetObjectModel, ResourceMgr_RequestObjectModel |
get / request, type 1 | confirmed (code) |
0x001814f0 |
ResourceMgr_FreeObjectModel |
free, type 1 (ObjectModel.cpp) |
confirmed (code) |
Sprite sheets, sprite batches and rain splashes¶
ParticlePage.cpp (sprite sheets: type 6 at map +0x60, GUI), the sprite-batch
("resource instance") methods that follow it (GUI) and RainDrops.cpp. Confirmed (code)
at each address.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00181b68 |
Sheet_Construct(sheet, page, heap, stream) |
sheet holder (vtable 0x00538d48): the page (+0x0c), its texture (+0x14), the texture's height / width (+0x1c, used by the sprite draw, GUI) |
confirmed (code) |
0x00181ca0 |
Sheet_Destruct |
destroys the dictionary's textures and the dictionary, frees the page and the heap | confirmed (code) |
0x00181e18, 0x00181e28 |
Sheet_TextureWidth, Sheet_TextureHeight |
GUI | confirmed (code) |
0x00181e38 |
Page_Rect |
GUI | confirmed (code) |
0x00181e50 |
ResourceMgr_SheetSize |
GUI | confirmed (code) |
0x00181f40 |
ResourceMgr_HasSheet(rm, hash, needRef) |
is resident, type 6 | confirmed (code) |
0x00181fb0 |
ResourceMgr_GetSheet |
get, type 6 (file "%u" of the hash) |
confirmed (code) |
0x001820e0 |
ResourceMgr_RequestSheet |
request, type 6 | confirmed (code) |
0x001821d0 |
ResourceMgr_ReleaseSheet |
one reference less; at none, free, type 6 | confirmed (code) |
0x00182820 |
ChunkLoaded_ParticlePageHeader |
GUI | confirmed (code) |
0x001828b0, 0x001828c0 |
SheetTable_Count, ResourceMgr_SheetRecord |
GUI | confirmed (code) |
0x001828e0 |
Instance_Init |
GUI | confirmed (code) |
0x00182a50 |
Instance_Destroy |
if in use: unload (0x001972b0), switch off, destroy the PTank when no longer drawn |
confirmed (code) |
0x00182ac8 |
Instance_Clear |
zeroes a slot (in use, not resident, no sprites) | confirmed (code) |
0x00182b20, 0x00182b28, 0x00182b30, 0x00182b38 |
Instance_Capacity, Instance_Depth, Instance_Format, Instance_Field24 |
+0x18, +0x20, +0x38, +0x24 |
confirmed (code) |
0x00182b40 |
Instance_PageTexture |
the page's texture (page + 0x14) |
confirmed (code) |
0x00182b50 |
Instance_RequestByDistance |
asks the resource manager (0x0018a830) to stream the sheet by the instance's nearest camera distance less its depth squared |
confirmed (code) |
0x00182bd8 |
Instance_SetInUse(inst, on) |
switching off also unloads a resident one | confirmed (code) |
0x00182c38, 0x00182ca0 |
Instance_MakeRoomIfLoaded, Instance_MakeRoom |
evict (0x00187d28) enough for the sheet's size |
confirmed (code) |
0x00182d08 |
Instance_RequestSheet |
request its sheet with its size | confirmed (code) |
0x00182d60, 0x00182d70 |
Instance_Page, Instance_IsVisible |
+0x0c, byte +0x13 |
confirmed (code) |
0x00182d80, 0x00182d88, 0x00182d90, 0x00182d98, 0x00182da0, 0x00182da8, 0x00182db0, 0x00182db8, 0x00182dc0, 0x00182dc8, 0x00182dd0, 0x00182dd8 |
Instance_PosArray, Instance_PosStride, Instance_ColourArray, Instance_ColourStride, Instance_UvArray, Instance_UvStride, Instance_SizeArray, Instance_SizeStride, Instance_Array64, Instance_Stride68, Instance_Array5C, Instance_Stride60 |
the PTank's arrays and strides: +0x44/+0x48 position, +0x4c/+0x50 colour, +0x54/+0x58 texture rectangle, +0x6c/+0x70 size, +0x64/+0x68 and +0x5c/+0x60 the rotation and matrix arrays (which is which not traced) |
confirmed (code) |
0x00182de0 |
Instance_AddSprite |
GUI | confirmed (code) |
0x00183038 |
Instance_AddSpriteFormat |
the same into whichever arrays the format has (glass panes, World objects) | confirmed (code) |
0x001831c0 |
Instance_DrawOneSpriteIm3D |
World objects | confirmed (code) |
0x00183510 |
Raindrops_Create |
makes the splash object (0x005971a8, 0x6430 bytes) at start |
confirmed (code) |
0x001835d8 |
Raindrops_Construct |
off; defaults: alpha 0.6, colour 0xffffff40 |
confirmed (code) |
0x00183618, 0x00183628 |
Raindrops_Enable, Raindrops_Disable |
on with the rain layer (StartRain), off with it and at level unload |
confirmed (code) |
0x00183630 |
Raindrops_Configure |
CfgRaindrops: count per update, sprite, size |
confirmed (code) |
0x00183640 |
Raindrops_ClassifyTriangle |
which side of the view's splash volume each corner of a ground triangle is on | confirmed (code) |
0x00183760, 0x001837c8 |
Raindrops_AddTriangle, Raindrops_AddClippedTriangle |
store a ground triangle (64 bytes) in the view's list of up to 100 | confirmed (code) |
0x00183838 |
Raindrops_ClipTriangle |
cuts a triangle on the volume's plane and stores the pieces | confirmed (code) |
0x00183b88 |
Raindrops_CollectTriangle |
classify, then keep, clip or drop | confirmed (code) |
0x00183c58 |
Raindrops_BuildVolume |
the splash volume in front of the camera (10 m and 20 m spans) | confirmed (code) |
0x00183f48 |
Raindrops_GatherGround(rd, camera) |
walks the world sectors under the volume and collects their upward-facing triangles | confirmed (code) |
0x00184568 |
Raindrops_Update |
per update, from Humans_Update, while on and the game runs: for each view, gathers the ground and spawns the count of splash particles at random points of random triangles |
confirmed (code) |
0x00184890 |
ResourceMgr_CompareOverlayKeys |
GUI | confirmed (code) |
0x001848d0 |
ResourceMgr_RoundSize(size) |
rounds a size up to its power of two, at least 32 KB (the heap's block) | confirmed (code) |
The resource manager¶
ResourceMgr.cpp: the ResourceManager (0x0050cd4c, 0x904c bytes), which owns the per-type maps
(above), the sprite batches, the streaming of resources by distance and the pending file
requests. How a level uses it: Level loading; a grouped container's load:
Chunk system. Fields used here, confirmed (code) at the functions
cited:
| Offset | Meaning |
|---|---|
+0x00-+0x60 |
the type maps: character models, object models (+0x10), car models (+0x20), textures (+0x30), character data (+0x40), animation packs (+0x50), sheets (+0x60) |
+0x70 / +0x74 |
the default animation set and the default character data |
+0x80-+0xb0 |
the Character List, Object List, Anim List and Dependency List chunks |
+0xb4 |
16 {class, instance} pairs: character instances kept resident by SetCharacterModel (+0x1b4 changed) |
+0x1b8 |
the dynamic animation slots, 0x28 bytes each (SetDynamicAnimation) |
+0xbbc-+0xbfc |
per kind (characters +0xbc0, objects +0xbd0, sprite batches +0xbe0, cars +0xbf0): the farthest resident one and its squared distance, and the nearest missing one and its distance, gathered each frame |
+0xc00-+0xc14 |
the same for gang spawners |
+0xc18-+0xc3c |
a deque of deferred requests {size, callback, arg} (12 bytes) waiting for room |
+0xc44 / +0xc54 |
the glass panes queued far / near this frame (World objects) |
+0xc64 / +0xc6c |
the list of pending file requests and their count |
+0xc74 / +0xc94 |
the humans and cars in view this frame |
+0x189c |
255 sprite batches of 0x78 bytes (GUI) |
+0x9028-+0x9030 |
a free list of request records (name, type, size, callback) |
+0x9034 / +0x9038 |
the 2D and 3D overlay worlds |
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00184918 |
ResourceManager_Create |
allocates and constructs it, loads global.pak (blocking) and makes the 13 start-up sprite batches (GUI) |
confirmed (code) |
0x00184eb0 |
ResourceMgr_LoadGenericHeader |
loads the resource named generic_header into its own heap and keeps its character data as the default set (+0x70, +0x74) |
confirmed (code) |
0x001852d8 |
ResourceManager_Construct |
empty maps and lists, cleared distance records, the request free list, the overlay worlds | confirmed (code) |
0x00185990 |
ResourceMgr_HasPendingRequests |
+0xc6c ≠ 0 |
confirmed (code) |
0x001859a0 |
ResourceMgr_IsRequested(rm, name) |
a pending request of that file name | confirmed (code) |
0x00185ab0 |
ResourceMgr_Instance(rm, i) |
sprite batch i, or null when it was never made (byte +0x40) |
confirmed (code) |
0x00185ae0 |
ResourceMgr_DestroyLevelInstances |
destroys batches 13 and up (level unload) | confirmed (code) |
0x00185b38 |
ResourceMgr_Render3DSprites |
World objects | confirmed (code) |
0x00185cc8 |
ResourceMgr_EmptyInstances |
GUI | confirmed (code) |
0x00185d20 |
ResourceMgr_RenderOverlay |
GUI | confirmed (code) |
0x00186068 |
ResourceMgr_UpdateInstances |
per update: each batch in use asks to be streamed by distance (0x00182b50); one no longer in use is destroyed |
confirmed (code) |
0x00186110 |
ResourceMgr_FindForGroup |
Chunk system | confirmed (code) |
0x001866e8 |
ResourceMgr_OnFileLoadedCb |
file-manager callback: 0x00186710 with the request |
confirmed (code) |
0x00186710 |
ResourceMgr_OnFileLoaded(rm, req, file) |
a heap named after the file, the chunk system over the data, the resource object (0x00187098); the request leaves the pending list and returns to the free list |
confirmed (code) |
0x00186e88, 0x00186ff0 |
ResourceMgr_BeginGroup, ResourceMgr_EndGroup |
Chunk system | confirmed (code) |
0x00187098 |
ResourceMgr_CreateResource(rm, req, heap, stream, group) |
by type: object model (chunk 0x41), car model (0x50), character model (0x28 bind offsets and 0x0A), textures, character data, animation pack, sheet; puts it in its map and marks its dependency group |
confirmed (code) |
0x00187960 |
ResourceMgr_AllocSize(rm, type, size, size2) |
the heap to ask for a type: size + 0x2000 for some types, + 0x70 for others, or the second size |
confirmed (code) |
0x001879b8 |
ResourceMgr_Heap |
the world's sector pool (the heap resources live in) | confirmed (code) |
0x001879e0, 0x00187a48 |
ResourceName_ToNumber, ResourceName_ToHash |
a file name's decimal part as a number / its CRC-32, after the last / |
confirmed (code) |
0x00187aa8 |
ResourceMgr_Request(rm, type, name, size, size2) |
takes a request record, makes room (0x00187d28, forced), asks the file manager for the file with callback 0x001866e8 and adds it to the pending list |
confirmed (code) |
0x00187c38 |
ResourceManager_LoadPack |
Level loading | confirmed (code) |
0x00187d28 |
ResourceManager_MakeRoom |
Level loading | confirmed (code) |
0x001886d8 |
ResourceMgr_QueueWhenRoom(rm, size, callback, arg) |
runs the callback at once if the size fits, else adds it to the deferred deque (Scenes) | confirmed (code) |
0x001887b8 |
ResourceMgr_ServiceDeferred |
for each deferred request whose size now fits: run it and drop it; true when the deque is empty | confirmed (code) |
0x00188878 |
ResourceMgr_ClearDeferred |
empties the deque | confirmed (code) |
0x001888f0 |
ResourceMgr_WaitForRoom |
Chunk system: queues, then streams for up to 200 ms | confirmed (code) |
0x001889d0 |
ResourceMgr_LoadAllBlocking |
until the deque is empty: stream, update the world, service the file manager; then drain the file queue (level load) | confirmed (code) |
0x00188aa8 |
ResourceMgr_FreeUnused |
flushes the renderer, releases the dynamic animations and characters, then frees every resource with no references in every map (level unload) | confirmed (code) |
0x001894f8 |
Sector_ClearFadeInCb |
world-sector callback: the sector's fade-in end (sector plugin +0x10) = 0 |
confirmed (code) |
0x00189528 |
ResourceMgr_ResetFadeIns |
clears the fade-in stamps so everything shows at once: every sector of both solid worlds (0x001894f8), each object task's instance (+0x2c) and 60 more instances' +0x30 |
confirmed (code) |
0x00189628, 0x001896c0 |
ResourceMgr_ResetNearest, ResourceMgr_ResetNearestGangs |
clear the per-kind distance records (+0xbbc, +0xc00) |
confirmed (code) |
0x00189718 |
ResourceMgr_ResetStreaming |
both resets and the dynamic animations released (level unload) | confirmed (code) |
0x00189750 |
ResourceMgr_NearestWanted |
the next dependency to load, or the nearest missing kind's distance (The streamed world) | confirmed (code) |
0x001897a8 |
ResourceManager_LoadNearestModel |
World objects, Characters | confirmed (code) |
0x00189ed8 |
ResourceMgr_StreamGangs |
gang spawners: loads the nearest missing one's character, unloads the farthest when room is short | confirmed (code) |
0x0018a2b0 |
ResourceMgr_EvictFarthest |
drops the farthest resident character, object or car instance when it is no longer used | confirmed (code) |
0x0018a430 |
ResourceMgr_NoteHuman(dist, rm, human) |
records a human's squared distance (farthest loaded / nearest missing) and puts its clump in or out of the collision world by the camera's range | confirmed (code) |
0x0018a5f0, 0x0018a720, 0x0018a830, 0x0018a900 |
ResourceMgr_NoteObject, ResourceMgr_NoteCar, ResourceMgr_NoteInstance, ResourceMgr_NoteGang |
the same for an object, a car, a sprite batch and a gang spawner | confirmed (code) |
0x0018a980 |
ResourceMgr_Stream |
each frame: object spawns, then the humans, objects and cars' distance notes, then the loads and evictions | confirmed (code) |
0x0018ab20 |
ResourceMgr_ReleaseDynamic |
releases the dynamic animation slots and the kept character instances | confirmed (code) |
0x0018abd0 |
ResourceManager_SetDynamicAnimation(rm, name, on) |
keeps a named pack in a free or least-used slot, or releases it | confirmed (code) |
0x0018ad40 |
ResourceManager_SetCharacterModel(rm, cfg, on) |
keeps (or drops) a character instance of that class's model resident | confirmed (code) |
0x0018aec0 |
ResourceMgr_CreateInstance |
GUI | confirmed (code) |
0x0018af88 |
ResourceMgr_DestroyInstance(rm, i) |
destroys sprite batch i | confirmed (code) |
0x0018afb8, 0x0018b008 |
ResourceMgr_QueuePaneFar, ResourceMgr_QueuePaneNear |
World objects | confirmed (code) |
0x0018b058 |
ResourceMgr_DrawPanes |
draws the far panes, then the near ones, and empties the lists | confirmed (code) |
Screen effects manager¶
ScreenEffectsManager.cpp and its script entry points; the manager's layout and behaviour are in
Screen effects. Two managers, 0x005fdeb8 (view 0) and 0x005fdebc (view 1); a script call
acts on both. Times are on the real-time clock in seconds. Confirmed (code) at each address.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x0018b168 |
ScreenFx_Queue(type, seconds) |
ScreenQueueEffect on both managers (0x0018d450) |
confirmed (code) |
0x0018b1e0 |
ScreenFx_Construct(mgr, view) |
a free list of 16 effect records, the 13 looks' defaults, base and current look 8 | confirmed (code) |
0x0018b460 |
ScreenFx_SetBaseLook(mgr, look) |
Looks | confirmed (code) |
0x0018b4a8, 0x0018b648 |
ScreenFx_ConfigLook, ScreenFx_ConfigBlurPulse |
CfgScrFx's colour and numeric forms (Screen effects) |
confirmed (code) |
0x0018b7d0, 0x0018b950 |
ScreenFx_StartLook, ScreenFx_EndLook |
Looks | confirmed (code) |
0x0018ba10 |
ScreenFx_Create |
makes the two managers at start | confirmed (code) |
0x0018bae0, 0x0018bd48 |
ScreenFx_StartLayer, ScreenFx_EndLayer |
Room smoke | confirmed (code) |
0x0018bdd0 |
ScreenFx_SetCameraIndoors(mgr, on) |
from the camera's ground probe: tells the rain layer (its slot +0x40) and switches the layers that only show outdoors (+0x16c) or indoors (+0x18c) |
confirmed (code) |
0x0018be58 |
ScreenFx_AddRainPlane(p, a, b) |
SpawnRainPlane: stores a plane (a point and two vectors) in one of 25 slots at 0x005fdec0 (count 0x005fdf24) for the rain layer |
confirmed (code) |
0x0018bf68 |
ScreenFx_UpdateView |
Room smoke | confirmed (code) |
0x0018c058 |
ScreenFx_ResetAll |
at level start: ends the current look and every layer, clears the blends, base and current look 8 | confirmed (code) |
0x0018c310 |
ScreenFx_DrawBlurPass(a, b, mgr) |
device slot +0x108 on the manager's viewport camera with the viewport rectangle |
confirmed (code) |
0x0018c408 |
ScreenFx_DrawMotionBlur(mgr) |
blends the motion-blur strength from +0x1a4 to +0x1a8 over its time and draws it (not in game mode 100's state, nor when +0x214 is set) |
confirmed (code) |
0x0018c8c8 |
ScreenFx_BlendBlurTo(seconds, mgr, alpha) |
starts that blend (instant when 0 s) | confirmed (code) |
0x0018c988 |
ScreenFx_BlendTintTo(seconds, mgr, colour) |
starts the tint blend from the current tint (instant when 0 s); Tint blend | confirmed (code) |
0x0018ca50 |
ScreenFx_DrawTint(mgr) |
the tint's current colour along its blend, filled over the viewport (0x0018cc20) |
confirmed (code) |
0x0018cb78 |
ScreenFx_FinishTintBlend |
jumps the tint to its target once the time is past | confirmed (code) |
0x0018cc20 |
ScreenFx_FillViewport(mgr, colour) |
device slot +0x130 for the manager's viewport |
confirmed (code) |
0x0018cc60, 0x0018ce58 |
ScreenFx_StartFade, ScreenFx_DrawFade |
Fade, Front end | confirmed (code) |
0x0018d020 |
ScreenFx_IsFadeActive |
true while a fade runs or its level is not 0 | confirmed (code) |
0x0018d058 |
ScreenFx_StartBlurPulse(seconds, mgr, reverse, delay) |
Blur pulse | confirmed (code) |
0x0018d1d0 |
ScreenFx_DrawBlurPulse(dt, mgr) |
Blur pulse | confirmed (code) |
0x0018d450 |
ScreenFx_QueueEffect |
Screen effects | confirmed (code) |
0x0018d5f8, 0x0018d868, 0x0018d910 |
ScreenFx_DrawLetterbox, ScreenFx_StartLetterbox, ScreenFx_StepLetterbox |
Screen effects, HUD | confirmed (code) |
0x0018da80 |
ScreenFx_CopyFadeAndLetterbox(dst, src) |
view 1 follows view 0's fade and letterbox | confirmed (code) |
0x0018dac0 |
ScreenFx_Render |
device slot +0x128: each view's blur pulse, tint, fade, letterbox and motion blur, in that order |
confirmed (code) |
0x0018dcc0 |
ScreenFx_EnableHeat(on) |
EnableHeat: look 7 on or off |
confirmed (code) |
0x0018dd38 |
ScreenFx_SpawnRainPlane |
SpawnRainPlane |
confirmed (code) |
0x0018dd58 |
ScreenFx_SetRaindrops |
CfgRaindrops |
confirmed (code) |
0x0018dda0, 0x0018deb0 |
ScreenFx_StartRain, ScreenFx_EndRain |
StartRain / EndRain (layer 0, effects bindings) |
confirmed (code) |
0x0018df00, 0x0018dfa8 |
ScreenFx_StartFog, ScreenFx_EndFog |
StartFog / EndFog (layer 1) |
confirmed (code) |
0x0018dff8, 0x0018e048 |
ScreenFx_StartFilmGrain_Stub, ScreenFx_EndFilmGrain |
StartFilmGrain does nothing; EndFilmGrain ends layer 2 |
confirmed (code) |
0x0018e070, 0x0018e0f8 |
ScreenFx_StartRoomSmoke, ScreenFx_EndRoomSmoke |
Room smoke | confirmed (code) |
0x0018e148, 0x0018e2f0, 0x0018e3c8 |
Fog3D_Start, Fog3D_SetMaxParticles, Fog3D_End |
Drifting fog | confirmed (code) |
0x0018e470, 0x0018e530 |
ScreenFx_CfgLook, ScreenFx_CfgBlurPulse |
CfgScrFx on both managers |
confirmed (code) |
0x0018e5f0 |
ScreenFx_SetColourOverlay |
SetLevelColour: look 9's tint (× 255) and base look 9 |
confirmed (code) |
0x0018e6b8, 0x0018e780 |
ScreenFx_EnterStore, ScreenFx_ExitStore |
EnterStore: look 10's tint and base look 10; ExitStore (and leaving a store screen): base look 9 |
confirmed (code) |
0x0018e7d0 |
Gamma_SetRamp_Stub |
SetGammaRamp does nothing |
confirmed (code) |
Texture dictionaries and the throw-aim arc¶
Texture.cpp (texture dictionaries, type 4 at map +0x30, above) and, after it, an unnamed file
that draws the arc a held object will fly along while the player aims a throw (its file is inferred from position).
Confirmed (code) at each address unless a row says otherwise.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x0018e7e8 |
TexDict_Construct(holder, page, heap, stream) |
dictionary holder (vtable 0x00538d60): stamp now + 3,000 ms, the dictionary, its heap and stream |
confirmed (code) |
0x0018e898 |
TexDict_Destruct |
destroys the dictionary's textures, the dictionary and its heap | confirmed (code) |
0x0018e9d8 |
ResourceMgr_TexturesFit_Stub |
the type-4 "fits" test of MakeRoom: always 0 |
confirmed (code) |
0x0018e9e0 |
ResourceMgr_GetTextures(rm, hash, size, size2, noLoad) |
get, type 4 (file "%u" of the hash): a resident one gains a reference; else, unless told not to, requested and loaded at once |
confirmed (code) |
0x0018eb10 |
ResourceMgr_RequestTextures |
request, type 4; a resident one only has its stamp renewed | confirmed (code) |
0x0018ec00 |
ResourceMgr_FreeTextures |
free, type 4: a holder with no references is destroyed and leaves the map | confirmed (code) |
0x0018f230 |
ThrowArc_Clear |
a cleared arc record (0x30 bytes: clump, atomic, material, human, start and end points); one per player in the game state at +0x5740 |
confirmed (code) |
0x0018f268 |
ThrowArc_Create |
the arc's model: texture aimthrow_tex (loaded once, 0x005971b0), 36 vertices (18 pairs) and 34 triangles, prelit and unlit, in one material (The throw-aim arc) |
confirmed (code) |
0x0018f628 |
ThrowArc_PlaceAtHand |
puts the arc's frame at the human's hand (position +0x610 plus its facing +0x620 turned by a fixed offset) |
confirmed (code) |
0x0018f7b0 |
ThrowArc_SetHuman |
+0x0c (from PlayerList_Add) |
confirmed (code) |
0x0018f7b8 |
ThrowArc_TargetFilter |
the sweep's callback: for a player, a human counts unless it is an ally (0x00222a90) or lies on the other side from the one the left stick pushes (dead zone 26) |
confirmed (code); the side test inferred |
0x0018fa68 |
ThrowArc_BuildStrip(arc, points, count, hit) |
writes the arc's points as a strip 0.16 m wide: alpha 0 at the hand, then 127-143; (127, 127, 127) or, when hit is set, (170, 43, 43) (The throw-aim arc) |
confirmed (code) |
0x0018fee0 |
Human_TraceThrowAim |
each frame of the aiming state (0x00244770): steps the throw from the hand at 1/60 s with gravity (z speed − 0.2613 a step) for 108 steps, keeps every sixth point (18), sweeps a sphere of half the object's size along them, stops at the first non-friendly AI human (human +0x638) unless a body with THROWNWEAPONTARGET or the level mesh comes first (that body is +0x634; Objects: throws), and rebuilds the strip |
confirmed (code) |
0x00190588 |
ThrowArc_Render |
from WorldManager_Render, while its human is in the aiming state: draws the clump in each view with back-face culling off |
confirmed (code) |
0x001906e8, 0x00190770 |
ChunkReader_TextureDictionaryTid, ChunkReader_RenderwareTextureDic |
Chunk system | confirmed (code) |
The water surface and the atomic plugin¶
WaterEffect.cpp: the level's single water surface, made and moved by
SetPositionOfWater (world manager +0x5c). It is the world
manager's third streamer (Choosing what to stream): its file (a texture dictionary) is read
only when it is near enough. After it come the atomic plugin 0x3F0's methods (The streamed world).
What Water_Set (0x0040ca18) passes, confirmed (code): 2 columns and 8 rows, so a grid of 3 vertices across
the width by 9 along the length; texture repeats 4 across and 16 along; the names water_tex and ocean_tex;
+0x58 = 0. The grid is the unit square (x = i / 2, y = j / 8, z = 0) in the water's frame, which is scaled by
(width, length, 1) (0x001915a0), so the wave height is in metres. The file is the WAD entry named the decimal CRC-32
of water_tex ("725908093", entry 3,354): a texture dictionary of one texture, water, 256 × 256, 8-bit, and the
material takes the dictionary's texture (+0x10 of it; confirmed (disc) for the file). ocean_tex is hashed into
+0x3c, but +0x3c is then overwritten by the file size looked up for +0x38, so its file (entry 1,089, one
texture ocean, 128 × 128) is never loaded. The geometry is built with texture coordinates 2 × 4 × i / 2 and
2 × 4 × j / 8 and prelit 0x7f7f, but the first update (below) replaces them, since the last update time starts at 0.
Water fields, confirmed (code) at 0x00191230 and 0x00190810:
| Offset | Meaning |
|---|---|
+0x00 / +0x04 |
the clump and its atomic (null until loaded) |
+0x10 / +0x20 |
the position and the rotation (a quaternion, normalised; all zeros means none) |
+0x30 / +0x34 |
streaming allowed (1) / loaded |
+0x38 / +0x3c |
the texture's hash; the file's size (looked up in the file table by the hash) |
+0x40 / +0x44 |
width and length in metres |
+0x48 / +0x4c |
grid vertices across and along (columns + 1, rows + 1) |
+0x50 / +0x54 |
texture repeats across and along |
+0x60 |
the time of the last wave update |
+0x64 |
colour, 4 bytes RGBA |
+0x68 / +0x6c |
wave height and wave speed |
+0x70 |
the 8 corners of a unit box, for the visibility and distance tests |
+0xf0 / +0xf4 |
the heap and the texture-dictionary holder |
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00190810 |
WaterEffect_Load |
once its file is read: a heap named Water, the dictionary from it (chunk 0x0B), then the grid geometry (prelit 0x7f7f, texture coordinates by the repeats, two triangles a cell) in a new clump |
confirmed (code) |
0x00191158 |
WaterEffect_Unload |
destroys the clump and the dictionary; not loaded (also when the world manager evicts it) | confirmed (code) |
0x00191230 |
WaterEffect_Init |
SetPositionOfWater: the fields above from its arguments |
confirmed (code) |
0x00191530 |
WaterEffect_Destruct |
unloads, then frees itself when asked | confirmed (code) |
0x00191580 |
WaterEffect_HeapSize |
the room its load needs: vertices × 64 + the file size + 0x1064 |
confirmed (code) |
0x001915a0 |
WaterEffect_SetFrame(water, posRot) |
position and rotation, then (when loaded) the clump's frame from them, scaled by width and length | confirmed (code) |
0x00191728, 0x00191750 |
WaterEffect_SetWidth, WaterEffect_SetLength |
then re-places the frame | confirmed (code) |
0x00191778 |
WaterEffect_SetColour |
+0x64 |
confirmed (code) |
0x00191798 |
WaterEffect_BoxCorners(water, out) |
the unit box's 8 corners through the water's frame | confirmed (code) |
0x00191908 |
WaterEffect_IsVisible |
false when every corner is outside one of player 1's camera planes | confirmed (code) |
0x00191a60 |
WaterEffect_CameraDistance |
the nearest face of the box to player 1's camera, for streaming | confirmed (code) |
0x00191dd8 |
WaterEffect_Update(time, water) |
from WorldManager_Render, when loaded and visible: at most every 0x0050cd84 s rewrites each column's height (sin(time × speed + column) × wave height), alpha and the texture's scroll along x, then draws the clump |
confirmed (code) |
0x00192588, 0x001925b0 |
WaterEffect_OnFileLoaded, WaterEffect_LoadThunk |
the file-read callback the world manager gives it | confirmed (code) |
0x001925d0 |
AtomicPlugin_SetOwnerCb |
per atomic of a new instance: owner (+0x0c) and word +0x08 from the record passed |
confirmed (code) |
0x00192688, 0x00192740 |
AtomicPlugin_Read, AtomicPlugin_Write |
Atomic plugin 0x3F0 |
confirmed (code) |
0x001927d8 |
AtomicPlugin_Register |
registers the 16 bytes and the stream methods | confirmed (code) |
0x00192858, 0x00192870 |
AtomicPlugin_SetOwner, AtomicPlugin_Owner |
+0x0c |
confirmed (code) |
0x00192888 |
AtomicPlugin_SetWord8 |
+0x08 |
confirmed (code) |
0x001928a8, 0x001928c0 |
AtomicPlugin_SetPosScale, AtomicPlugin_PosScale |
+0x00 |
confirmed (code) |
0x001928d8, 0x001928f0 |
AtomicPlugin_SetUvScale, AtomicPlugin_UvScale |
+0x04 |
confirmed (code) |
Drawing the water (WaterEffect_Update(phase, water), 0x00191dd8), confirmed (code); constants read from
.data. WorldManager_Render passes a phase that grows by 0.16 each frame (The streamed world,
step 9) and draws with culling off; Z test, Z write and fog stay as the world pass left them (on).
- Nothing while the water is not loaded or not in view (
0x00191908). - When the phase has moved more than 0.3 (
0x0050cd84, confirmed (code), as are the constants below) since the last update (+0x60), so every second frame, witht = phase × speed(+0x6c):- the texture scroll
u0 = fmod(t × 0.3, repeatsAcross)(0x0050cd90= 0.3, confirmed (code)); - for each column
ibut the last:s = sin(i + t); every vertex of the column gets heights × waveHeight(+0x68, the vertex's z) and the water's colour (+0x64) with alpha225 + 5 s(0x0050cd8c= 225,0x0050cd88= 5, confirmed (code); RenderWare's 0-255 scale, so about 88 % opaque); - texture coordinates
u = u0 + repeatsAcross × i / (columns − 1)andv = repeatsAlong × j / (rows − 1); - the last row and the last column copy the first one's heights and colours, so the surface tiles.
- with
Water_Set's grid that leaves two distinct columns: the two edges atsin t, the middle atsin(1 + t); the surface rocks across its width and is flat along its length.
- the texture scroll
- Unlock the geometry and render the clump (
0x0046a100).
The texture's file, inferred (disc check): the WAD file 725908093, the decimal CRC-32 of water_tex, is a
container holding one texture dictionary; so is 2169582214, of ocean_tex. Coney (effects::Water,
repo:src/effects/water.h; platform::WaterRenderer) loads it when the water is first placed, and draws the grid
after the d world.
The RenderWare device's helpers¶
DevRWGeneric.cpp's functions that are not in the device or camera tables
above: RenderWare's memory hooks, the immediate-mode drawer (vtable 0x00538f08), the sprite batches' PTank methods
(GUI), the custom stream and render-state helpers, and the sector plugin 0x3F1
(The streamed world). Confirmed (code) at each address.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00192a08 |
RwMemory_Free |
RenderWare's free hook: the Filter Pool when the block is inside it, else the current heap (Memory) |
confirmed (code) |
0x00192ac8 |
RwMemory_Realloc |
the realloc hook (a null block allocates) | confirmed (code) |
0x00192b98 |
RwMemory_Calloc |
the calloc hook: allocates Renderware Calloc and zeroes it |
confirmed (code) |
0x00193c20 |
ScreenQuad_DrawOffset |
one textured quad with a shift, for the blur pass | confirmed (code) |
0x00194fb8 |
RwDevice_InitRasterCall |
from Init: a RenderWare call (0x00486120) with 0x85 and the main raster's format bits |
confirmed (code); what the call is not traced |
0x001968c0 |
Im2DDrawer_DrawIndexed(…, raster, matrix, verts, count) |
the drawer's slot +0x48: an RwIm3D indexed list in one viewport only (none in split screen), nearest filtering, Z write off, alpha blending |
confirmed (code) |
0x00196ac8 |
Im2DDrawer_Project(…, out, point) |
slot +0x50: a world point through the main camera (0x00198548), scaled by 1/1024 and 1/512 |
confirmed (code) |
0x00196cd0, 0x00196cf8 |
Im2DDrawer_StaticInit, Im2DDrawer_StaticInitThunk |
static initialiser: the drawer at 0x005ff770 gets vtable 0x00538f08 |
confirmed (code) |
0x00196d18 |
Instance_Construct |
a cleared batch with its own frame | confirmed (code) |
0x00196d60 |
Instance_CanUnload |
not resident, or its PTank no longer in use (0x00198220) |
confirmed (code) |
0x00196d90 |
Instance_Unload |
takes the PTank out of its overlay world, destroys it in the resource heap and releases the sheet | confirmed (code) |
0x00196ee0 |
Instance_SetDepth(depth, inst) |
+0x20 and the PTank's bounding-sphere radius |
confirmed (code) |
0x00196f48, 0x00196f90 |
Instance_Position, Instance_SetPosition |
the frame's position, swapping the y and z axes (y negated) | confirmed (code) |
0x00197108 |
Instance_Restart |
when resident and in use: unlocks, empties the PTank and locks it again (loading screens, ResourceMgr_EmptyInstances) |
confirmed (code) |
0x001976e8 |
Instance_MemoryNeeded |
bytes per sprite by the format's arrays × capacity (twice for format 2) + 0x4000, plus the sheet's size when it is not resident |
confirmed (code) |
0x001977c8 |
Instance_Lock |
locks the PTank arrays its format has (position 0x01, colour 0x02, texture rectangle 0x80, matrix 0x08, rotation 0x20, size 0x04) |
confirmed (code) |
0x00197900 |
Instance_Unlock |
unlocks them and clears the six array pointers | confirmed (code) |
0x00197970 |
Instance_ClearSprites(inst, from, to) |
zeroes those sprites' arrays | confirmed (code) |
0x00197b30 |
ChunkReader_SectorBspData |
chunk 0x15 (Chunk system): a world through device slot +0x170, pushed as 0x42 |
confirmed (code) |
0x00197b78 |
World_RenderSectorCb |
the sector render callback ReadWorld installs: calls the one it replaced (0x005ff77c) |
confirmed (code) |
0x00197cd0 |
World_RemoveObjectCb |
DestroyWorld's callback: takes each clump (and light) out of the world first |
confirmed (code); the light half inferred |
0x00197d60, 0x00197d98, 0x00197dc8 |
RwStreamCustom_Read, RwStreamCustom_Write, RwStreamCustom_Skip |
the custom stream's callbacks over a game stream (0x00197df0) |
confirmed (code) |
0x00197e28, 0x00197ec0 |
RenderState_Save, RenderState_Restore |
six render states (vertex alpha, both blends, culling, Z test, fog) into and out of a record (fills, letterbox) | confirmed (code) |
0x001983a8 |
Atomic_SetPluginWord |
per atomic of a character model: one RenderWare plugin word (0x0052e684) |
confirmed (code) |
0x001983d0, 0x00198428 |
Frame_FindObjectCb, Clump_GameData |
the first object attached in the clump's frame tree (0x00198428's caller reads its word +4, Chunk system) |
confirmed (code); inferred for what the object is |
0x00198458 |
RwCam_Camera |
the wrapper's RwCamera (+0x00) |
confirmed (code) |
0x00198548, 0x001985b0 |
Camera_ProjectThrough, Matrix_Copy |
a point through a camera's view matrix and 0x00198460; a 4 × 4 copy |
confirmed (code) |
0x00198658 |
Matrix_SetIdentity |
identity, flags 0x20003 |
confirmed (code) |
0x001986e8, 0x00198738 |
RwCam_Destruct, RwCam_Clear |
the wrapper's destructor; its cleared fields | confirmed (code) |
0x00198ed0, 0x00198f70 |
SectorPlugin_Write, SectorPlugin_Register |
the 20 streamed bytes; registration (32 bytes, 0x0050ced0) from Init |
confirmed (code) |
0x00198ff0 |
SectorPlugin_SetAtomic |
+0x00 |
confirmed (code) |
0x00199008, 0x00199020, 0x00199038, 0x00199050 |
SectorPlugin_SetIndex, SectorPlugin_SetState, SectorPlugin_SetPart, SectorPlugin_SetFadeEnd |
+0x04, +0x08, +0x0c, +0x10 |
confirmed (code) |
0x00199068 |
SectorPlugin_SetOrigin |
+0x14-+0x1c: where the sector's atomic is placed |
confirmed (code) |
Overlay effects: film grain, fog and rain¶
OverlayEffects/: the screen-effect layers (Screen effects), each an OverlayEffect with a list
of OE_Particle sprite widgets (+0x04-+0x0c) and its settings from +0x98. They share the base's colour slots
(+0x28 set every widget's colour, +0x30 widget 0's colour) and its 30 Hz tick (Room smoke, which
also covers OE_RoomSmoke.cpp and the base). Vtables: film grain 0x00539050, fog 0x005390a0, rain 0x005390f0,
room smoke 0x00539140; slot +0x10 destroys, +0x18 updates, +0x20 takes new settings. Confirmed (code) at each
address.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x001990b8 |
OE_FilmGrain_Construct |
one widget of the settings' sprite and colour, depth 11,000 (layer 2; StartFilmGrain never starts it) |
confirmed (code) |
0x00199208, 0x00199230 |
OE_FilmGrain_Destruct, OE_FilmGrain_Configure |
settings: sprite word +0x9c, colour +0xa0, repeats +0xa4/+0xa5 |
confirmed (code) |
0x00199258 |
OE_FilmGrain_Update |
width × 1/0.7, the colour, and a new random texture offset each tick over the repeats: the grain jumps every update | confirmed (code) |
0x00199380 |
OE_Fog_Construct |
one widget (depth 10,000), random start offsets, the first two drifts | confirmed (code) |
0x001995c0, 0x001995e8 |
OE_Fog_Destruct, OE_Fog_Configure |
the 0x34 settings bytes (StartFog) |
confirmed (code) |
0x00199658 |
OE_Fog_FollowCamera |
adds the camera's heading change to the sideways scroll and half its pitch change to the vertical one | confirmed (code) |
0x001996b0 |
OE_Fog_PickDrift |
a sideways and a vertical scroll (random sign), a width (1.43 to p5 / 0.7), a height (1 to p6), a blend time (p8-p9 ms) and an alpha (between the colour's fourth and fifth values) |
confirmed (code) |
0x001997d8 |
OE_Fog_BlendDrift |
the same linear blend as the room smoke's, then the next drift | confirmed (code) |
0x00199a50 |
OE_Fog_Update |
blend, follow the camera, size, colour, then scroll the texture rectangle (u, -v)-(u + 1, 1 - v), wrapping at 1 |
confirmed (code) |
0x00199ba8 |
OE_Rain_Construct |
two screen sheets (depth 10,000) and, for each rain plane, two more on the plane | confirmed (code) |
0x00199e08 |
OE_Rain_Destruct |
frees the plane batches first | confirmed (code) |
0x00199e50 |
OE_Rain_Configure |
StartRain on a running layer: new layers, new sprites where they changed |
confirmed (code) |
0x0019a048, 0x0019a118 |
OE_Rain_CreatePlaneBatches, OE_Rain_FreePlaneBatches |
one sprite batch per layer (25 sprites, depth 8,000) for the planes | confirmed (code) |
0x0019a1b8 |
OE_Rain_AddSheet(rain, layer, plane, batch) |
a screen sheet, or a sheet on a plane: placed by the plane's point and two edges, each at least 6 m | confirmed (code) |
0x0019a648 |
OE_Rain_Tilt |
eases a vector toward 0x006f31a0 by 0.004 a frame and tilts the sheets by its first part, clamped to ±10° |
confirmed (code); what the vector is not traced |
0x0019a780 |
OE_Rain_PlaneFacesCamera |
outdoors only: the plane's normal points away from the camera | confirmed (code) |
0x0019a8e0 |
OE_Rain_UpdateSheet |
a screen sheet: size 1.7 (2.0 in 16:9, × 1.15 in an armies level), tilt, colour, scroll; a plane sheet copies its layer's colour and rectangle (half the vertical repeats when the plane is 12 m or more) | confirmed (code) |
0x0019ac48 |
OE_Rain_Update |
indoors (+0x90) no tilt and no screen sheets; each plane's sheets only while it faces the camera |
confirmed (code) |
0x0019afd0 |
OE_RoomSmoke_Destruct |
Room smoke | confirmed (code) |
0x0019b8c0 |
OverlayFx_TakePitchDelta |
the camera's pitch mapped from −50°-10° to 0-1, less last time's | confirmed (code) |
0x0019bbb0, 0x0019bbf8 |
OverlayEffect_AddWidget, OverlayEffect_GetWidget |
the widget list | confirmed (code) |
0x0019be28, 0x0019bea0 |
OverlayEffect_SetColourAll, OverlayEffect_Colour |
slots +0x28 and +0x30 |
confirmed (code) |
Heat distortion¶
DistortionEffectManager.cpp and the effects after it: shimmering air over fires and the ring a car explosion sends
out. The manager (0x005ff780) holds three HeatDistortionEffect slots ({effect, used} at +0x00-+0x14, 0x3310
bytes each, vtable 0x005391e0) and one HeatWaveEffect (+0x18/+0x1c, 0x3320 bytes, vtable 0x00539228). An
effect is a grid of (cols + 1) × (rows + 1) vertices, cell metres apart, kept facing the camera at a world point;
each vertex samples the frame buffer at its own place on the screen plus a small moving offset, so what is behind it
wavers. It is drawn by device slot +0x118 after the world (not in an armies level). Confirmed (code) at each
address.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x0019bed0, 0x0019c598, 0x0019c5c8 |
DistortionMgr_Construct, DistortionMgr_StaticInit, DistortionMgr_StaticInitThunk |
the static manager (its constructor does nothing) | confirmed (code) |
0x0019bee0 |
DistortionMgr_Reset |
at level start (InitLevel): destroys every effect and clears the slots |
confirmed (code) |
0x0019bf50 |
DistortionMgr_DestroyAll |
also at level unload | confirmed (code) |
0x0019c030 |
DistortionMgr_Fits(mgr, size) |
the current heap has more than size free |
confirmed (code) |
0x0019c070 |
DistortionMgr_StartHeat(cell, a, b, mgr, pos, cols, rows) |
a heat distortion in a free slot (−1 when none or no room): fires (SubFadeFlame_Init, FireParticle_SetUpByName: cell = size × 0.1, size + 5 × size + 8 cells) and 0x0021adf8 |
confirmed (code) |
0x0019c1d0 |
DistortionMgr_StartWave |
the heat wave, if its slot is free: Car_DoExplode (cell 0.4, 16 × 10 cells, 0.7 m above the car) |
confirmed (code) |
0x0019c328, 0x0019c398 |
DistortionMgr_StopHeat(mgr, i), DistortionMgr_StopWave |
destroy one (fires going out) | confirmed (code) |
0x0019c3f0 |
DistortionMgr_SetHeatPosition(mgr, pos, i) |
moves heat i | confirmed (code) |
0x0019c438 |
DistortionMgr_Render |
device slot +0x120: each live effect updates and draws; finished ones are destroyed |
confirmed (code) |
0x0019c5e8, 0x0019c608 |
HeatDistortion_Construct, HeatDistortion_Destruct |
the vtable, not started and not finished | confirmed (code) |
0x0019c638 |
HeatDistortion_Init |
slot +0x10: position, cell size, grid size, the weights; offsets are clamped to [1/2048, 0.6255] |
confirmed (code) |
0x0019c740 |
HeatGrid_BuildIndices |
two triangles a cell into 0x005ff7a0; returns the index count |
confirmed (code) |
0x0019c7f0 |
HeatDistortion_BuildWeights |
slot +0x20: each vertex's strength from squared sines across and along the grid (0x0050cf1c = 0.8): strongest in the middle, fading to the edges |
confirmed (code); the shape inferred from the formula |
0x0019cac0 |
HeatGrid_PlaceVertices |
slot +0x28: the vertices in the camera-facing plane, centred on the position |
confirmed (code) |
0x0019cde0 |
HeatDistortion_SetPosition |
the position (axes converted) | confirmed (code) |
0x0019ce08 |
HeatGrid_ProjectCorners |
the grid's corners on the screen | confirmed (code) |
0x0019cf50, 0x0019d5a0 |
HeatGrid_SetBaseUv, HeatGrid_Uv |
a vertex's resting and current texture coordinates | confirmed (code) |
0x0019cf90 |
HeatGrid_MapToScreen |
slot +0x30: resting coordinates spread over the projected rectangle (screen 1,024 × 512; 512 across when flag 0x02 is set) |
confirmed (code) |
0x0019d1b0 |
HeatGrid_ClampUv |
to [+0x32fc, +0x3300] |
confirmed (code) |
0x0019d1e0 |
HeatGrid_Shimmer(amount, fx) |
slot +0x38: a shared phase grows by 0.5 a frame; each vertex moves by sin and cos of it × amount × its weight × a random ±0.9 (0x0019d318) |
confirmed (code) |
0x0019d318 |
HeatGrid_RandomSign |
±0.9 (0x0050cf3c) for one vertex |
confirmed (code) |
0x0019d390 |
HeatDistortion_Update |
slot +0x18: faces the camera, rebuilds the grid and shimmers by 0.0025 |
confirmed (code) |
0x0019d5c8 |
HeatGrid_Draw |
the coordinates into the device's distortion mesh and one indexed triangle list | confirmed (code) |
0x0019d6e8, 0x0019d728 |
HeatWave_Construct, HeatWave_Destruct |
the wave's vtable over the heat's | confirmed (code) |
0x0019d750 |
HeatWave_Init |
as above with 0.1 m, the ring's centre cell (cols / 2, given row) |
confirmed (code) |
0x0019d9d0 |
HeatWave_Update |
faces the camera and rebuilds; while 0x0050cf4c is set, steps the ring |
confirmed (code) |
0x0019dbe8 |
HeatWave_InRing(radius, fx, col, row) |
the cell is within the radius of the centre | confirmed (code) |
0x0019dc50 |
HeatWave_StepRing |
the radius grows 0.3 cells a frame; a cell it reaches gets strength 5, which falls 0.3 a frame while the cell shimmers (0.005); once the radius passes cols + rows the wave is finished |
confirmed (code) |
Game-state functions¶
Functions of the game-state module (0x00417af0-0x00424e50, a range inferred from where its functions lie: inventory,
statistics, flags, configuration workers) that belong to this page, by address.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00419da0 |
ThrowArcs_RenderAll |
from WorldManager_Render: draws each player's throw arc (game state +0x5740 + 0x30 × p) |
confirmed (code) |
0x0041d728 |
Cfg_SetOutdoorMode |
CfgSetOutdoorMode: +0x3e4 |
confirmed (code) |
Coney's implementation¶
First pixels (2026-10-04), in src/platform/ and src/graphics/:
RenderEngine(src/platform/render_engine.h) starts librw, built for its GL3 platform with SDL3. librw's GL3 device creates the window and an OpenGL 3.3 core context itself, so the engine owns the window and the event loop gets a non-owningWindow. A run-time NULL backend (--headless) installs librw's NULL device instead, for CI and tests: no window, no GPU. It implementsgraphics::RenderDevice(begin a frame cleared to a colour, present), which game modes call as the original's modes call the device (from theirrender(), see below); the idle mode clears and presents every frame. There are no device cameras yet: one camera covers the window.- The frame rate: a decoupled loop, Coney's choice. The original runs one fixed 1/30 s step and one present per
frame, held to 30 frames a second by its flip every second vertical blank (the frame rate). Coney
keeps the step and frees the display rate: the simulation always advances in steps of exactly
0x960000ticks, and rendering runs once per real frame, blended between the last two steps (Update and render). The platform's frame pacer (src/platform/frame_pacer.h) measures each frame with SDL's nanosecond clock and holds frames to--fps-capwith precise sleeps; core'sFrameClock(src/core/frame_clock.h) adds the real time to an exact integer accumulator (millionths of a tick, so 10 s at any rate are exactly 300 steps) and says how many steps to run and the alpha to render with.--vsync on|offpicks librw's present flag:FLIPWAITVSYNCH(swap interval 1) or 0. The default is no cap with vsync on: as many frames as the display shows, each blended, the game at its fixed speed.--fps-cap 30is the original's rhythm: lockstep, one step and one frame, nothing blended.- Slow frames: a frame is credited with at most four steps (133 ms) of real time, and the rest is dropped, so below 7.5 frames a second the game slows down, as the original slows down when a frame runs long, rather than running ever more steps to catch up; a long gap (a dragged window, a debugger) moves the game on by four steps, not by the gap. The original's level clock catches up by up to 40 ms a frame (Boot); Coney's fixed step needs whole steps, and four keeps full speed at any usable rate.
- What it costs: a blended frame shows the world up to one step (33 ms) behind the newest simulated state, and input is read once per step, so it is quantised to 30 a second as in the original.
- Test mode runs lockstep with no clock (
--headless,--frames,--input-script,--screenshot), so the screenshots and runs of the tests are the same as before the loop was split, pixel for pixel.
- The logical screen (
src/graphics/screen.h): every 2D position is in the 640 × 448 pixels of the video mode. Coney shows that screen at the television's shape, 4:3 (16:9 later, with the widescreen option), as the largest such rectangle centred in the window (fitLogicalScreen), and fills the rest of the window black: letterboxing or pillarboxing is Coney's choice for a window that is not 4:3; the original has a television picture and no border. The 3D view itself fills the whole window at the window's shape, and a window wider than 4:3 is taken as the 16:9 mode (RenderEngine::widescreen); passes over the whole picture (the line blend, the blurs, the screen washes, the room smoke) cover the whole view (RenderEngine::viewRect), not the logical screen's 4:3 place. The window opens at 960 × 720.beginFrame(colour)clears the window to black and the logical screen to the colour (with a flat quad, since librw's clear covers the whole frame buffer). - 2D quads (
RenderDevice::drawQuads): textured or flat rectangles in logical pixels with a colour and texture coordinates, drawn with Z test and Z write off, no culling, no fog, vertex alpha on and source alpha / inverse source alpha blending (2D drawing), with the texture's own filter mode and clamped addressing. Game code passes agraphics::Texture, which the platform implements over a librw texture. - GUI coordinates and the overlay camera (
src/graphics/overlay_camera.h,OverlayCamera): device slots+0x90and+0x98as 2D drawing gives them (guiToOverlay,guiWidthToOverlay), and the overlay camera's perspective projection with the default mode's view window, 0.725 × 0.5 (aspect 1.45) × the view scale, which takes a point or a size of overlay-camera space to logical pixels (project,projectSize;unprojectfor Coney's tools). GUI 0 and 1 land at 5.2 % and 94.8 % across and 4.5 % and 95.5 % down, and the main menu's button glyph where PCSX2 shows it; tests pin both. Coney computes the projection itself rather than rendering through a librw camera, and draws the result as 2D quads. The 16:9 aspect (1.6667, scale 1.1) is there for the legal screen's_wpictures; the progressive modes are not offered. TextureDictionary(src/platform/texture_dictionary.h) reads a dictionary with librw aftergraphics::inspectTexDictionaryhas checked the stream, and converts it to RGBA images (any backend) or to OpenGL textures (Raster::convertTexToCurrentPlatform). The chunk readers for0x0Band0x2Apush it as0x0B(Chunk system); sector atomics files and world streams are read at their fixed offsets byloadTextureDictionaries. Dictionaries are not kept per resource yet.- Texture lookup by name (
src/platform/texture_lookup.h): every dictionary of a loaded streamed world and its parts is registered, newest first, and librw's find callback searches them all, first match winning, with no file fallback (Finding textures by name). librw itself searches only the current dictionary and makes a dictionary of its own current at start-up, so Coney clears the current dictionary, as the game never leaves one set. The newest-first order is still RenderWare's inferred one (open question below). - Alpha: librw's PS2 reader scales alpha by 255/128 when it converts a raster to an image (palette entries and 32-bit texels alike), so 128 becomes 255; that answers the open question below, and a unit test pins it.
- librw and the game's data: librw recomputes each PS2 texture's GS layout and asserts that the stream agrees;
some of the game's textures disagree (the palette's place), so librw is built without its asserts and keeps the
stream's values. Its reader also copies as many bytes as the stream's header says into buffers it sized itself; for
five textures on the disc (tiny 4-bit ones, such as a 2 × 2) the stream's sizes are larger and the copy would
overrun the heap, so Coney leaves those textures out of their dictionaries (
skippedTextures()). coney --disc <disc> --view-txd <entry>draws an entry's textures in a grid with RwIm2D quads, nearest filtering, Z off and source-alpha blending, as the device's screen quads do (2D drawing);--screenshotsaves the last frame. The legal screen (--view-txd 863681355) shows correctly.- The first screen is mode 5 (Front end):
coney --disc <disc>loadslegal_screenthrough the chunk system (its0x2Adictionary and its0x4Csprite sheet, GUI) and draws the sheet's first rectangle, the top 512 × 384 of the texture, on black for 5,000 ms, sized as Placement works out (legalScreenQuad, from the mode's factors and the overlay camera): 1.068 × 1.011 of the logical screen, centred, so the picture slightly overfills it as the original's does. A test pins the 1.068 × 1.011. On the NTSC-U disc it shows correctly and gives way to the memory-card check after frame 150.
Disc check (NTSC-U, 2026-10-04, counts only): coney_tests "[disc]" with CONEY_DISC set reads 20,314
dictionaries (from 3,016 chunk containers, 1,911 sector atomics files and 159 world streams) holding 42,211 textures;
42,206 are read and converted to RGBA images, the 5 above are left out, no entry fails. By format: 32,333 PAL8,
9,132 PAL4, 209 PAL8 with mipmaps and 532 PAL4 with mipmaps, all with 32-bit palettes and raster layout version
2.
What is still to do:
- A device object that owns librw's engine start-up (with the HAnim, Skin, MatFX and world plugins; PTank for sprites) and the cameras of the device object: main, per-viewport, overlay and effects, sharing one frame and Z buffer.
- Clear to the background colour (white until a level sets one; the legal screen clears to black) with Z.
- Texture dictionaries read with librw from the chunk stream (
0x0B,0x2A, world streams, sector atomics) and kept per resource (the lookup by name exists, above; the chunk readers' dictionaries are not registered in it yet). - PS2 native textures: 4- and 8-bit palettised, 32-bit palettes, power-of-two sizes up to 512; check alpha scaling.
- Drawing the streamed world: the readers for
0x3F0and0x3F1and the decoder of the world atomics' PS2 geometry exist, and the world viewer streams and draws thesanddworlds (The streamed world); the level world, PVS and occluders do not. - The 16:9 option: a 16:9 logical screen shape, the overlay view-window scale 1.1 and the
_wlegal screens.
The throw-aim arc is drawn while player 1 aims a throw with L1, with the page's model, colours, alpha and texture (Coney's implementation); its stand-ins are the Z write, fog and place in the frame.
The implementation TODOs from the runtime answers (2026-10-04) are done: the overlay camera's view window is 0.725 × 0.5 and the legal screen overfills the screen as above; its colour was already white.
Still for research:
- Names for device slots
+0x90and+0x98: the@origtags call themRwDevice::GuiToOverlay(0x00195238) andRwDevice::GuiWidthToOverlay(0x00195330) until the research database names them.
Open questions¶
- The throw-aim arc's render state (
ThrowArc_Render0x00190588,ThrowArcs_RenderAll0x00419da0): whether the world pass writes Z and fogs it (Coney does neither), where inWorldManager_Render's order the arcs fall (Coney: after the cars' glass), and the GS's exact alpha rule (the measured 0.44 is one band at one distance; the colour scale is settled). Coney's stand-ins: Coney's implementation. - Exact legal-screen placement (answered): it overfills the screen by 6.8 % across and 1.1 % down, centred (Placement).
- Mode flag
0x02. Read as PAL from the 24.5 frames-a-second threshold and the_euroscreen; nothing that sets it has been found (speculative). - What the game plugins hold (answered for
0x3F1): see The streamed world. For0x3F0the first float scales the packed vertex positions; what the second scales is open (The streamed world). - The overlay world (
ResourceManager + 0x9034) rendered before the sprites. The sort order of the queued PTanks is answered on GUI: ascending key; that the 2D key is the creation depth is inferred. - librw and PS2 alpha: does librw's PS2 native texture reader scale palette alpha from 0-128 to 0-255?
- How the blur slot
+0x108draws (answered): see Motion blur. The previous frame is the other frame buffer, not a capture (answered, confirmed (runtime)). - Texture dictionary list order for name lookups (newest first is RenderWare's usual behaviour; not read here).
- The remaining slots:
+0x148(0x004dee48),+0x180(0x004e3820), and the byte+0x448. - Runtime confirmation of the two-vertical-blank flip (partly answered): PCSX2 shows 30 frames for 60 vertical
blanks (Presenting); a watch on
0x005970f9and0x0059708bthrough a slow frame (to see the 20-a-second and the tearing cases) is still to do. - Display brightness. (The output set-up is now read in full: The PS2 render driver;
it does not darken.) In PCSX2's screenshots the white of the legal image and of
big_fonttext both come out at about 178 of 255 (and the menu's grey and red text at the same 70 % of their vertex colours), so the whole picture is about 70 % bright. NotPMODE(partly answered, 2026-10-04): the game writes the GS output registers from a display-buffer record in memory (0x004aa748, called at the vertical blank by0x0048bdb0), and at the front end that record holdsPMODE=0x8067: both read circuits on, mixed with the fixed alpha0x80(MMOD= 1),SMODE2= 1, the two circuits' frame buffers one address step apart (the usual two-circuit anti-flicker set-up; inferred). A half-and-half mix of two copies of the picture keeps its brightness, so the 70 % is in the drawn colours or in PCSX2's capture. confirmed (runtime) for the values, read from EE memory over PINE at0x0070f610. Answered for text (confirmed (runtime), PCSX2 GS dumps oflevel99): the HUD text sprites reach the GS with vertex colour0x59(89), RenderWare's halving of the game's grey 178 (0x005fd310, GUI), and the GS's texture modulate gives 89 × 255 / 128 = 177 on white texels, the 178 measured. So the dimness is the game's own colours and the capture is faithful: a Faithful grade needs no brightness curve, only the line averaging above. That the legal image is drawn the same way (a 178 grey vertex colour) is inferred.