Skip to content

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 (PAL8 or PAL4) 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 is lerp(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 +0x1c0 and filled over the viewport when its alpha byte is not 0. Nothing is drawn while the current look is 2 and +0x1b8 is 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:

  1. Nothing when +0x214 is 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.
  2. 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.
  3. State 0 with level 0: nothing.
  4. 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.
  5. 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 global 0x00510174 is 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):

  1. 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.
  2. 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).
  3. Up: the view's camera draws raster +0x43c over 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). With ONE / ZERO it 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 to 0x50001933, 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_BlendBlurTo 0x0018c8c8, confirmed (code)) from its colour to 0x96ffffff, 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)):

  1. 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 +0x1b8 is 0, but only ScreenFx_Construct (0x0018b1e0) and ScreenFx_ResetAll (0x0018c058, the store at 0x0018c260) 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: +0x1b8 was 1 throughout).
  2. 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 grid 0x00193298; 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):

  1. Nothing in game mode 100's state or when +0x214 is set. The alpha blends from +0x1a4 to +0x1a8 over [+0x1ac, +0x1b0] (0x0017a258), never below the floor +0x1b4; at 0 nothing is drawn (except in look 5).
  2. 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.
  3. 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 the level99 arena (re-frame): the blur is one full-screen fan drawn after the tint, textured from the other frame buffer (CT32 1024 × 512, TBW 10, TBP = that buffer's FBP × 32, TCC 0, nearest filtering), vertex colour 0x80808080 with alpha 7 (the old frame at 7/128, about 5.5 %), blend 0x44, Z always, no Z write. A port with one back buffer must keep the last presented frame itself.
  4. 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, with d = |column − 4| + |row − 4| (0-8) and the 9-byte table at 0x0050ce30 (10, 50, 180, 225, 230, 240, 240, 250, 252; the mult and andi 0xff at 0x00193a58), flat 200 in look 4: so by d 0, 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 at 0x005fe030 + i × 0x40 + 0x2c during 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.
  5. 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 is 0x834dd0f0, the CRC-32 of room_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 word 0x1102). The wrap matters: the texture coordinates below run past 1. (Record 18 is fog_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 at v 0.5, under 10 by 0.62) to 0 in every texel from row 191 (v 0.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); one OE_Particle sprite widget (BaseWidget) set up with size 1.0, depth 10,000, GUI position (0.5, 0, 0.2, 1) (centred across, y 0.2), the tint, visible, sprite word 0x130000, 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 (0x0050b8b8 slot +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 are from × (1 - t) + to × t (straight lines, no easing; the alpha rounded to a byte and written into the sprite's colour). When t reaches 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 GUI y (+0xf8). Pitch -50° puts the sprite's centre at y -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).
  • The sprite (OE_RoomSmoke_Update, 0x0019b4c0, the effect's slot +0x18): size (0x001a2120, without the device's width conversion) width = width scale × *0x0050cefc and 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: overlay X 0, 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 flag 0x02 or 0x20 and the other, store 1.3 (0x3fa66666) in 0x0050cefc; only the first also stores 1.0 in 0x0050cf00, whose static value is already 1.0, and nothing else in the program writes either global (their only references are these). The other path computes flags & 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; centre Y 0.24-0.75 from GUI y -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 v 1 to v 0, 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 v 0.746 and the sprite stops at v 0.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 update 0x0018bf68 for each layer that is on, while the view's player camera exists): nothing while the game clock is paused (0x0050b734 slot +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, unless 0x00512c44 is 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: StartRoomSmoke on 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. EndRoomSmoke deletes 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.

  1. RwDevice_Create(0, 0) (0x00194488): allocate 0x460 bytes, construct (0x00194d58: base constructor, vtable, the ten camera wrappers), store the device in 0x0050cdb4 and 0x0050cdb8, set progressive mode off (slot +0xc8), then RwDevice_SetWidescreen(dev, 0): 4:3, field-of-view constants 1.3333 and 1.45 handed to the camera system (0x00122ca0, 0x00122d38), 0x0050b20c = 1.0.
  2. GraphicsDevice_Open (0x0017a1e0) calls Init (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):

  1. Screen 640 × 448, flags 0x01, scale 1.0; in progressive mode 640 × 480 and flags 0x20; 16:9 adds 0x04.
  2. Print the largest free block ("... Before Initializing RenderWare"), set RenderWare's free-list sizes, and RwEngineInit (0x00483530) with a 256 KB resource arena.
  3. Attach the plugins (above).
  4. RwEngineOpen (0x004833e8), choose the video mode (above), RwEngineSetVideoMode.
  5. Two vertical blanks per displayed frame (0x0048df98(2), the value from 0x0050cdb0), see Presenting and the frame rate.
  6. RwEngineStart (0x00483258); on failure close and terminate RenderWare.
  7. Install a callback the driver calls while it waits for the GPU (0x0048df90(0x00154ee8)).
  8. 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.
  9. 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):

  1. Cameras (0x001562c8): device slot +0x28 with 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's RwCamera at 0x00a5bea0 holds view window 0.725 × 0.5 at +0x68, near 0.5 at +0x80, far 10,000,000 at +0x84). Then slot +0x38 clears the frame buffer and Z to the background colour.
  2. Simulation runs; then slot +0x18 flushes the render queue before the world update (why there: inferred, so that streaming does not free data the GPU still reads).
  3. Each viewport (0x00156408): slot +0x88 positions viewport i's cameras on their part of the screen (columns × rows from 0x0050b1a8/0x0050b1aa; one viewport covers the whole screen); lights and the level's sky, clouds and skyline (Level loading); the world (0x0040e8d8: several RwCameraBeginUpdate/EndUpdate passes: 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: the part_page1 batches, then lighting, GUI); the ground rings (0x0017b2e0, HUD); slot +0x118 (heat distortion).
  4. Overlays (0x00156658): the HUD (0x001b1688) and the overlay pass 0x00185d20 that 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 while W_GameState + 0x268 is 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.
  5. 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 at 0x0052f538). That queues a flip token into the DMA chain (0x0048c8f0; tokens 0x40 and 0x41 alternate by frame).
  • The DMA handler (0x0048b708, installed for channels 1 and 2 by 0x0048d9f0) stops at a flip token and marks a flip as pending.
  • The vertical-blank interrupt handler (0x0048bdb0, installed with AddIntcHandler(2, ...) by 0x0048d9f0) 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 (GS DISPFB1 and DISPLAY1), 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:

  1. 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 +0x10 of the instance's texture resource (RpMaterialSetTexture, 0x0046d010). After the draw it is set back to none.
  2. 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), with i = min(trunc(h / 30), 2):

    h i Slot Texture (confirmed (runtime), PCSX2 2.9.94, level99)
    60 and above 2 resource manager +0x74 charblood_d1
    30 to 60 1 +0x78 charblood_d2
    below 30 0 +0x7c charblood_d3

    (RpMatFXMaterialSetDualTexture, 0x00465810; the slot is +0x74 + ((2 - i) mod 3) × 4, the texture its object's +0x10.) The dual pass's GS ALPHA register, the 64-bit word at +0x28 of the material's first MatFX effect, gets the low byte 0x44 while h < 90 and the half-word W_GameState + 0x454 is 0, and 0x45 otherwise. 0x44 is (Cs − Cd) × As + Cd, an ordinary alpha blend of the blood texture over the first pass; 0x45 is (Cd − Cd) × As + Cd, which leaves the frame unchanged. So blood shows below 90 % health, and heavier textures replace it at 60 % and 30 % (that d1-d3 grow heavier is inferred from the names). W_GameState + 0x454 is 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, level99 checkpoint 1 (scenario blood_textures, hook human-blood at 0x0017450c): the player standing still, its health written to 95, 89, 61, 59, 31 and 29 % of 900 twenty updates apart, the draw picked slot +0x74 at 100, 95, 89 and 61 %, +0x78 at 59 and 31 % and +0x7c at 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 by HuColor) with R, G and B × f. f follows the brain's "hidden in shadow" byte (+0x2d4, Lighting): with t = min(time since the brain's stamp +0x10, 250 ms) × 0.002, f = 0.5 + t when not hidden (0.5 up to 1.0) and f = 1 − t when hidden (1.0 down to 0.5). 4. Alpha. The colour's alpha × (drawDistance − d) / fadeLength once the human's camera distance d (+0x334) is past the fade start, and × (now − start) / 1000 during the first second after the instance's stamp +0x30. Values read at runtime (confirmed (runtime), level99): draw distance 0x0050cc60 = 70 m (clamped to [0x0050cc5c = 30, 0x0050cc58 = 70] by HumanRender_SetDrawDistance, 0x00174200), fade length 0x0050cc68 = 10 m, fade start 0x0050cc64 = 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 default 0x00469ef8), 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.

  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, entry b holding a position (vec4) at +b × 0x20 and 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).
  2. For each pose bone b from 2 to 33 (with full = 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.
  3. R = the human's world transform: the rotation and position of the object transform table 0x00714b00 at the human's index (+0x92), 32 bytes each, the rotation a quaternion at +0x10 (confirmed (code)) (Characters).
  4. 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 product L_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.
  5. 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 by s about the human's origin.
  6. 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):

  1. Skip it unless bit p of +0x11fc is set. Skip it in the opaque pass when p is in the glass mask 0x2a80c0 (parts 6, 7, 15, 17, 19, 21) and in the glass pass when it is not.
  2. 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).
  3. Upload the selected lights; give the first material the texture at +0x10 of the car's first texture resource.
  4. Colour: the car's tint (+0x24) for the parts in the paint mask 0x157c31 (parts 0, 4, 5, 10-14, 16, 18, 20), else the white at 0x005fd268. Alpha × (now − start) / 1000 during the first second after +0x2c.
  5. MatFX (0x004653d8 reads the material's effect): an environment map (2) gets the texture at +0x10 of 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). That 0x0070ad18 is RenderWare's current camera, so that the reflection follows the view, is inferred.
  6. 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 of WARRIORS.DIR, name hash 0x3f103fcb of ./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 alpha 0x0e (about 11 % of the PS2's 0x80 = 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 and repo:tests/platform/disc_throw_arc_test.cpp counts 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_TransformPoint by the inverse of his facing and that point). count is 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 hit is 0 and (170, 43, 43) when it is set. hit is 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 while HuIsAimingAt would be true of something (inferred: every task has the type bit +0x634 needs).
  • 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_mission marker, pitch 0.154 rad, heading 58.6°, +0x634 the 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:

  1. 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, so count is 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 "point count", which an 18-point arc has no entry for).
  2. Colour (127, 127, 127), or (170, 43, 43) while the trace ends on the human target or a body (+0x634 set), 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).
  3. Texture aimthrow from 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.
  4. Width ± 0.08 m along the thrower's right axis (his frame's x, from the aim's heading), level across the arc.
  5. 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_RenderAll not placed in WorldManager_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).
  6. 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):

  1. Enter (0x00159a58) records the start time, sets a 5,000 ms timeout and calls StartupScreen_Draw (0x00159c08).
  2. StartupScreen_Draw picks 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 _w variant of each for 16:9 (legal_screen_w, legal_screen_w_sp, ...); with flag 0x02 set and English it uses legal_screen_euro. The WAD file name is the decimal CRC-32 of that resource name ("%u", 0x0054efc8).
  3. 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.
  4. 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).

  1. Nothing while the water is not loaded or not in view (0x00191908).
  2. 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, with t = phase × speed (+0x6c):
    • the texture scroll u0 = fmod(t × 0.3, repeatsAcross) (0x0050cd90 = 0.3, confirmed (code));
    • for each column i but the last: s = sin(i + t); every vertex of the column gets height s × waveHeight (+0x68, the vertex's z) and the water's colour (+0x64) with alpha 225 + 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) and v = 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 at sin t, the middle at sin(1 + t); the surface rocks across its width and is flat along its length.
  3. 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-owning Window. A run-time NULL backend (--headless) installs librw's NULL device instead, for CI and tests: no window, no GPU. It implements graphics::RenderDevice (begin a frame cleared to a colour, present), which game modes call as the original's modes call the device (from their render(), 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 0x960000 ticks, 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-cap with precise sleeps; core's FrameClock (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|off picks 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 30 is 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 a graphics::Texture, which the platform implements over a librw texture.
  • GUI coordinates and the overlay camera (src/graphics/overlay_camera.h, OverlayCamera): device slots +0x90 and +0x98 as 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; unproject for 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 _w pictures; the progressive modes are not offered.
  • TextureDictionary (src/platform/texture_dictionary.h) reads a dictionary with librw after graphics::inspectTexDictionary has checked the stream, and converts it to RGBA images (any backend) or to OpenGL textures (Raster::convertTexToCurrentPlatform). The chunk readers for 0x0B and 0x2A push it as 0x0B (Chunk system); sector atomics files and world streams are read at their fixed offsets by loadTextureDictionaries. 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); --screenshot saves the last frame. The legal screen (--view-txd 863681355) shows correctly.
  • The first screen is mode 5 (Front end): coney --disc <disc> loads legal_screen through the chunk system (its 0x2A dictionary and its 0x4C sprite 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 0x3F0 and 0x3F1 and the decoder of the world atomics' PS2 geometry exist, and the world viewer streams and draws the s and d worlds (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 _w legal 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 +0x90 and +0x98: the @orig tags call them RwDevice::GuiToOverlay (0x00195238) and RwDevice::GuiWidthToOverlay (0x00195330) until the research database names them.

Open questions

  • The throw-aim arc's render state (ThrowArc_Render 0x00190588, ThrowArcs_RenderAll 0x00419da0): whether the world pass writes Z and fogs it (Coney does neither), where in WorldManager_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 _euro screen; nothing that sets it has been found (speculative).
  • What the game plugins hold (answered for 0x3F1): see The streamed world. For 0x3F0 the 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 +0x108 draws (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 0x005970f9 and 0x0059708b through 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_font text 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. Not PMODE (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 by 0x0048bdb0), and at the front end that record holds PMODE = 0x8067: both read circuits on, mixed with the fixed alpha 0x80 (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 at 0x0070f610. Answered for text (confirmed (runtime), PCSX2 GS dumps of level99): the HUD text sprites reach the GS with vertex colour 0x59 (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.