Skip to content

The PS2 render driver

Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4), static reading in Ghidra (2026-10-07), and the game running under PCSX2 2.9.94 (EE memory read over PINE, in level99 from a copy of the user's quick-save slot 1).

Purpose

How RenderWare's PS2 driver, linked into the game, turns the game's drawing into Graphics Synthesizer (GS) state: the video mode and how the picture is sent to the TV, how GS memory is split, and what each RenderWare render state becomes in GS registers. Graphics covers the game's device object, the cameras, the frame and the flip; The streamed world covers the game's own four PS2 pipelines; the GS state measured per pass of a frame is Rendering. Coney draws through librw on a PC GPU, so most of this is background. What changes the picture is listed under What a reimplementation must keep.

Original structure

The driver is part of the RenderWare 3.7 library block (Source map); it has no path strings. RenderWare's standard ids and render-state ids are those of the public RenderWare 3.7 headers, and the GS register names are Sony's; matching the code's numbers to those names is inferred, the numbers themselves are confirmed (code). Names are ours.

Entry points

Address Name Role Evidence
0x0052f618 the driver's RwDevice record gamma 1.0, system handler, Z near 65,535.0 and far 0.0, render-state set and get; the immediate-mode slots are 0 confirmed (code)
0x00497468 Sky_System the device system handler: open (0x004959e0), start (0x00496758), stop, register, 90 video modes (0x0052ec38), use-mode (0x004958a8), the standard functions, texture memory size, finalise start (0x00495cd0), maximum texture size 1024 confirmed (code)
0x0052f538 standard-function table 28 {id, function} pairs, copied into RenderWare's table by request 11 (below) confirmed (code)
0x00496758 Sky_Start resets the GS for the chosen mode, builds the display records and the frame, Z and texture registers, sets up the texture cache confirmed (code)
0x004aa180 Gs_ResetGraph(mode, interlace, video, field) GS reset and SetGsCrt; keeps the three values at 0x0052fa10 confirmed (code)
0x004aa748 Gs_PutDisplayRecord(record) writes PMODE, SMODE2, DISPFB, DISPLAY and BGCOLOR (or the circuit-1 set) from a 0x28-byte record confirmed (code)
0x0048e250 / 0x0048ece8 Sky_RenderStateSet / _Get RenderWare render states to GS registers (Render states) confirmed (code)
0x0048cf68 Sky_OpenGifPacket(tag, qwords) reserves room in the DMA packet for a GIF tag and n register writes (A+D); every state change goes through it confirmed (code); role inferred
0x004a3600 TexCache_Init(start, size) the texture cache over the GS memory left after the frame and Z buffers confirmed (code)
0x004a3b48, 0x004a4690 TexCache_Find, TexCache_Upload look up and upload a texture's raster before a draw; called by render state 1 and by the game's pipelines confirmed (code) for the callers; roles inferred

Standard functions (RenderWare 3.7 ids), all confirmed (code) at the table:

Id Function Id Function
1 camera begin update 0x0048f2f0 15 raster lock 0x004943f0
2 RGB to pixel 0x0049f428 16 raster unlock 0x00494c60
3 pixel to RGB 0x0049f520 17 raster render 0x00499e38
4 raster create 0x00491750 18 raster render scaled 0x0049b778
5 raster destroy 0x004937c0 19 raster render fast 0x0049ce08
6 image get raster 0x0049e038 20 show raster 0x00498890 (the flip)
7 raster set image 0x0049f608 21 camera clear 0x0048fd00
8 texture set raster 0x00498928 22 hint render front to back 0x00498be0
9 image find raster format 0x0049deb0 23 / 24 lock / unlock palette 0x00498938 / 0x004989f0
10 camera end update 0x004987d8 25 native texture size 0x00498b50
11 set raster context 0x0049de78 26 / 27 native texture read / write 0x004954f0 / 0x00495400
12 sub-raster 0x00493418 28 raster mip levels 0x00498af0
13 / 14 clear rectangle / clear 0x00499350 / 0x0049de30

Ids missing from the table get the stub 0x00498be8. The frame flip (0x0048bdb0, 0x0048b708, 0x0048c508, 0x0048c8f0) is on Graphics.

Data

Video modes

The 90 modes are 24-byte records {width, height, depth, flags, refresh, raster format} at 0x0052ec38 (Graphics for how the game picks one). The game uses mode 82: 640 × 448, 32-bit, flags 0x203, 60 Hz, format 0x500 (current mode 0x00597130 = 0x52, confirmed (runtime)). Its neighbours differ only in flags: 72 is 0x101, 77 is 0x3; 87 is the progressive 640 × 480. The 90 records use five flag sets: 0x1 (22 modes), 0x3 (20), 0x203 (20), 0x5 (20, all 224 or 256 lines high) and 0x101 (8, all 448 or 512 high). Sky_Start (0x00496758) resets the GS (0x004aa180(0, interlace, video mode, frame mode)) by flag: 0x2 (without 0x4) interlaced in field mode, (0, 1, mode, 0); 0x4 and 0x100 interlaced in frame mode, (0, 1, mode, 1); neither, non-interlaced, (0, 0, mode, 1). 0x100 also halves two display factors (bytes 0x00710080 / 0x00710081 = 1 instead of 2) and takes the display width from 0x00596e50. 0x200 adds the second read circuit (below). Confirmed (code) for the branches; reading the reset's arguments as interlace and field/frame is inferred from the GS's SMODE2. So 0x4 is a half-height buffer shown in both fields, and 0x100 a full-height buffer read a line in two per field: neither has the anti-flicker blend of mode 82.

Display records

Two records of five GS registers, one per frame buffer, at 0x0070f610 and 0x0070f638, a copy of both at +0x350 for the other field, and the circuit-1 originals at 0x0070f940. The vertical-blank handler writes the record of the displayed buffer for the current field (0x004aa748, picked by the field bit of CSR). Read in a level, confirmed (runtime); the field layout is the GS's:

Register Value Meaning
PMODE 0x8067 both read circuits on, blended with the fixed alpha ALP = 0x80
SMODE2 0x1 interlaced, FFMD = 0: field mode
DISPFB2 0x00000800_00001400 (buffer 1: FBP 0x08c) 640 wide (FBW 10), 32-bit, starting one line down (DBY = 1)
DISPLAY2 0x001be9ff_0183227c DX 636, DY 50, MAGH 3 (×4), DW 2560, DH 446 (447 lines)
DISPFB1 / DISPLAY1 (originals) DBY = 0, DH 447 (448 lines) the same frame buffer, from its first line

Gs_ResetGraph's record 0x0052fa10 holds interlace 1, video mode 2 (NTSC) and field 0, confirmed (runtime).

GS register shadows

The driver keeps the GS drawing registers it changes in memory and sends the whole register on each change. Confirmed (code) for the roles; the values were read mid-frame in a level, confirmed (runtime):

Address Register Value read
0x00596dd8 ZBUF_1 0x00000001_00000118 Z at block 0x118, write masked at that moment (format: Z)
0x00596de0 FRAME_1 0x000a008c the back buffer, 640 wide, 32-bit
0x00596de8 TEST_1 0x0005140a see Alpha test and Z
0x00596df8 FOGCOL 0x00050c0c fog colour (12, 12, 5)
0x00596e00 CLAMP_1 0 wrap in both directions
0x00596e08 TEX1_1 0x0000000f_c0000060 linear filtering; K = −4.0 for that texture
0x00596e10 ALPHA_1 0x44 (Cs − Cd) × As + Cd
0x00596e20 (primitive bits) 0x10 textured, 0x40 alpha blend, 0x20 fog, 0x08 Gouraud

Behaviour

The picture on the TV

Sky_Start resets the GS with interlace on and field mode (Gs_ResetGraph(0, 1, 0, 0); the NTSC value is filled in by the reset). Because mode 82 has flag 0x200, it turns on both read circuits over the same frame buffer: one from its first line, the other from its second (DBY = 1, one line shorter), mixed half and half by PMODE. Confirmed (code) at 0x00496758 and confirmed (runtime) by the records above.

In field mode the GS shows every other line of the 448-line frame buffer in each field (the GS's documented behaviour; inferred). With the second circuit one line lower, every line on the TV is the average of two adjacent lines of the frame buffer: a vertical two-tap filter, the usual PS2 anti-flicker. The picture is slightly soft vertically and thin horizontal lines do not flicker between fields. It does not change the brightness: the two copies are the same picture. The 70 % brightness seen in PCSX2 is the game's own vertex colours (Graphics, confirmed (runtime) for text). The same registers read from a frame's GS dump are in Rendering: Output.

GS memory

The GS has 4 MB (0x100000 32-bit words). Sky_Start places the two 640 × 448 32-bit frame buffers at blocks 0 and 0x08c and the Z buffer (24-bit Z in 32-bit words) at 0x118, and hands the rest to the texture cache: from word 0xd2000, 753,664 bytes, through TexCache_Init (0x004a3600). The driver reports (0x100000 − end) × 4 as the texture memory size (request 12). Confirmed (code); 0x00597144 = 0xd2000 confirmed (runtime). So textures do not stay in GS memory: the driver uploads each one when a draw needs it and evicts others to make room (TexCache_Find and TexCache_Upload, called before each material's draw).

Eviction (TexCache_Allocate, 0x004a30c8, the cache's placement function when it is on; confirmed (code)): the cache is a ring. A table of {raster, GS address, size} entries (sizes rounded up to 2,048 words) records what is resident, from the oldest (0x0052f948) to the newest (0x0052f944). A raster that is not resident is placed right after the newest one, or back at the start of the cache when it does not fit before the end, and every older entry whose range it overlaps is dropped (its raster loses its cache link). So eviction is first in, first out, by address, with no use counts. Two entries are protected: the rasters bound to context 1 and context 2 for the current draw (0x0052f96c, 0x0052f970); when the new raster would overwrite one of them, it is not placed. A raster flagged locked (raster extension +0x17) is never placed by it. The upload itself (TexCache_UploadRaster, 0x004a2910) sends every mip level and the palette, or a raster's pre-built packets.

The cache does not hold a frame. Confirmed (runtime), quick-save slot 1 in level99's street (scenario render_texture_uploads, hook texture-upload on every placement): standing still, every frame (one per two ticks, 30 per second) placed and uploaded 376-377 rasters, 300 of them different, about 1.08 million words (4.3 MB), nearly six times the cache; with the camera turning (right stick 60 %), 190 to 436 a frame. So nearly every texture is sent to the GS again in every frame, some twice. Nothing of this shows on screen; a PC renderer keeps its textures.

Render states

Sky_RenderStateSet (0x0048e250) handles these RenderWare 3.7 states; anything else returns 0 (not supported). Each change updates the register's shadow and sends it in a two-quadword GIF packet. Confirmed (code):

State Values handled GS effect
1 texture raster a raster or none textured on or off; makes sure the raster is in the cache; alpha test on when the raster's format has alpha (any but 888), else off
2 / 3 / 4 texture address (both, U, V) 1 wrap, 3 clamp CLAMP_1 WMS / WMT repeat or clamp; 2 mirror is refused
5 texture perspective on only clears the FST bit (perspective-correct coordinates); off is refused
6 Z test off, on TEST_1: ZTST always or greater-or-equal (Z)
7 shade mode 1 flat, 2 Gouraud the primitive's IIP bit
8 Z write off, on ZBUF_1 ZMSK; turning it on with Z test off sets ZTST always
9 texture filter 1-6 TEX1_1 (Textures)
10 / 11 source / destination blend 1, 2, 5, 6, 7, 8 ALPHA_1 from a table (Blending)
12 vertex alpha on, off alpha blending (ABE) on when vertex alpha is on or the texture has alpha
14 fog on, off the primitive's FGE bit
15 fog colour RwRGBA FOGCOL (red and blue swapped into the GS order)
16 fog type 1 linear only
20 cull mode 1 none, else kept for the pipelines, which cull in VU1 code
29 / 30 alpha test function / reference 1-8 / 0-255 TEST_1 ATST / AREF = reference × 128/255 (0x005969b8 = 0.50196)

Alpha test

TEST_1 starts as 0x140b (0x00496758 sets 0x0070f6e0 |= 0x140b): test on, pass when alpha ≥ 0x40, and AFAIL = 1, frame buffer only: a pixel that fails is still drawn and blended, but does not write Z. Nothing calls render states 29 or 30: render states are set through the engine's pointer 0x0070ad38, and the 31 functions that read it (game and RenderWare code alike) set only states 1, 2, 6-12, 14-17 and 20. The driver's other TEST_1 writers do not change it either: 0x0048f0f8 replaces the low 16 bits in its case 3, but its four callers (0x0040d0a8, 0x00195400) only pass case 4, and the material upload 0x00477858 (through the table at 0x0051d8d4) sets or clears ATE alone. So this stays for the whole game. Confirmed (code), by decompiling every reader of the pointer and every writer of 0x00596de8. What changes per draw is only the enable bit (ATE), and the rule for it depends on who draws, by the raster format nibble at raster +0x23 (RenderWare's 1 = 1555, 5 = 8888, 6 = 888, and the others): render state 1 and the dual-texture and environment-map uploads (0x004275d0, 0x00428410) turn it on for any format but 888; the plain world upload and the second character upload (0x004290d8, 0x00426430) only for 1555 and 8888; the first character upload (0x004252c8, the character materials' pipeline 0x30081) always. Confirmed (code).

AREF 0x40 on the GS's alpha scale, where 0x80 is 1.0, is 0.5. So for a cut-out texture (foliage, fences, hair), the opaque half writes Z and hides what is drawn after it, while the transparent half is blended over what is already there without occluding later draws. Which draws come later decides how the edges look (the pass order is on The streamed world). The frame dumps agree: every first pass uses context 1 with this test, and the second passes, sprites and text use context 2's own TEST_2, off or NOTEQUAL 0 (Rendering, confirmed (runtime)).

Blending

ALPHA_1 comes from a 6 × 6 table at 0x0052f4a8, indexed [destination][source] over the factors ZERO, ONE, SRCALPHA, INVSRCALPHA, DESTALPHA and INVDESTALPHA (RenderWare's 1, 2, 5, 6, 7, 8; the colour factors 3 and 4 are not handled). Pairs the GS cannot express are −1 and the state change is refused. Confirmed (code):

Destination \ source ZERO ONE SRCALPHA INVSRCALPHA DESTALPHA INVDESTALPHA
ZERO 0 Cs Cs·As Cs − Cs·As Cs·Ad Cs − Cs·Ad
ONE Cd Cs + Cd (FIX = 0x80) Cs·As + Cd refused Cs·Ad + Cd refused
SRCALPHA Cd·As Cd·As + Cs refused (Cd − Cs)·As + Cs refused refused
INVSRCALPHA Cd − Cd·As refused (Cs − Cd)·As + Cd refused refused refused
DESTALPHA Cd·Ad Cd·Ad + Cs refused refused refused (Cd − Cs)·Ad + Cs
INVDESTALPHA Cd − Cd·Ad refused refused refused (Cs − Cd)·Ad + Cd refused

20 of the 36 pairs exist. The usual game pair, SRCALPHA / INVSRCALPHA, is 0x44, the value read in a level. Additive (ONE / ONE) and its alpha-weighted form (SRCALPHA / ONE) are exact; the GS clamps or wraps the sum by COLCLAMP (not read here).

Z

The Z buffer is 24-bit (PSMZ24, 0x31), or PSMZ16S (0x3a) when the Z depth 0x00596e4c is 16: Sky_Start builds ZBUF_1 at 0x0070f680 that way and copies it to the shadow. Confirmed (code) at 0x00496758, and confirmed (runtime) by a GS dump reading ZBUF_1 = 0x31000118, with every Z below 2^24 (the sky near 16,775,500, the HUD at 8,388,607). The driver's RwDevice record gives Z near 65,535 and far 0, so the near value must be raised to the 24-bit range at start (inferred; the writer is not found). Either way larger Z is nearer and "Z test on" is ZTST greater-or-equal. "Z test off" keeps the test enabled (ZTE = 1, which the GS requires) with ZTST always. Z write is the ZMSK bit of ZBUF_1. Confirmed (code) at 0x0048e250 and 0x0052f618; TEST_1 = 0x5140a read in a level (Z test on, greater-or-equal).

Textures

Filtering (render state 9) sets TEX1_1's MMAG / MMIN, confirmed (code):

RenderWare filter MMAG MMIN
1 nearest nearest nearest
2 linear linear linear
3 mip nearest nearest nearest, nearest mip
4 mip linear linear linear, nearest mip
5 linear mip nearest nearest nearest, linear between mips
6 linear mip linear linear linear, linear between mips

Read against RenderWare's names, 4 and 5 look swapped (inferred; the effect is small). The mip bias K and L in TEX1_1 come from each texture's raster (the PS2 sky mipmap extension 0x110, Graphics); K = −4.0 was read for one texture. Addressing is wrap or clamp per axis; mirrored textures cannot be drawn.

Textures arrive as RenderWare PS2 native textures (read by standard 26, 0x004954f0; the formats are on Graphics), already laid out for the GS, so the driver uploads them as they are.

Fog

Fog is per vertex: the pipelines' microcode writes the fog value and the primitive's FGE bit turns it on (render state 14). The colour is FOGCOL (render state 15, the device's background colour, Graphics slot +0x48), and only linear fog exists (state 16). Confirmed (code) for the state handling.

The curve: when the device sets up the cameras (RwDevice_SetUpCameras, 0x00195400) it gives the main camera a fog distance of far × the fog start (Graphics +0x444, 0.5 unless a level script calls SetFogDistance), confirmed (code) at 0x001954f0. The vertex fog value is then linear in the camera depth w: 255 up to that distance, 0 at the far clip, 255 × (far − w) / (far − far × start) between. Confirmed (runtime) twice from GS dumps, with a least-squares fit of the value against w (= 1 / Q): level99 (far 115, ends 57.3 m and 115.1 m, The streamed world) and the title screen (far 150, ends 74.7 m and 151.2 m over 1,998 partly fogged vertices of the world and the Wonder Wheel, residuals about 1, The front end). The world's own pipelines and the objects' share it: an object's atomics get the same game pipelines as the streamed world (ObjectModel_SetupAtomic, 0x001807cc → Atomic_AssignGamePipelines), confirmed (code). How the microcode computes the value was not read.

Packets

Every state change and draw goes into the current DMA packet (0x0059707c, the write pointer) as a GIF tag in A+D mode followed by {value, register} pairs; 0x0048cf68 makes room and flushes when the buffer (0x1c0000 bytes, Graphics) is full. The game's pipelines write their own VIF and GIF data into the same packet (The streamed world). Confirmed (code) for the A+D writes; the flushing was not read.

Per-pipeline states

The game's four PS2 pipelines (The streamed world) change state themselves in their upload callbacks: the alpha-test enable per material (Alpha test), CLAMP_1 clamp from the material's texture addressing (its U and V nibbles, 3 = clamp), the texture through the cache, and the material colour scaled to the GS's range (0.5/255 when textured, The streamed world). They leave blending, Z and fog to the render states set around them. Confirmed (code) at the four upload callbacks.

What a reimplementation must keep

  • The alpha test: pass at alpha ≥ 0.5, and a failing pixel blends its colour but writes no Z. A PC renderer that discards below 0.5 (or writes Z for every drawn pixel) draws cut-out edges and their sorting differently.
  • The vertical anti-flicker (optional, a look): each output line is the average of two adjacent rendered lines.
  • Blend modes: only the six factors above exist, so a reimplementation needs nothing more.
  • Not visible: the texture cache, the packets, GS memory, the display records.

Coney's implementation

Coney draws with librw on the PC; none of this is implemented yet. librw's own alpha-test default and its Z handling of failing pixels have not been compared with the above.

Open questions

None left for the driver; the per-pass measurements are on Rendering.