Rendering: the anatomy of a frame¶
Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4), running under PCSX2 2.9.94:
single-frame GS dumps of the reference views, parsed register by register.
Purpose¶
What the Graphics Synthesizer is told to do in one in-game frame, pass by pass, in the order it happens: the primitives, the blend equation, the alpha test, the Z test and write, fog, texture filtering and the frame-buffer formats. It is the "how it looks" companion to Graphics, which says which code issues each pass, and to the RenderWare PS2 driver, which turns RenderWare render states into GS registers (PS2 render driver). A reimplementation that matches the tables below draws the same picture; the differences found so far in Coney are tracked outside the repository and fixed against these sections.
Every value below is confirmed (runtime) from the GS dumps of the reference views unless a line says otherwise; "code" points to the page where the issuing function is documented.
How the frames were captured¶
PCSX2's single-frame GS dump (the GSDumpSingleFrame hotkey, Shift+F8, posted to the window by handle) records the
GS state at the start of the frame, the 4 MB of GS memory and every GIF packet until two frames have been drawn
(one into each frame buffer). A scratch parser replays the packets, tracks every register write and groups the
primitives into runs that share one state. The two frames of each dump agree pass for pass.
A view at any chosen camera, such as a scripted shot played without the script, is drawn by the game itself: over
PINE, player 1's follow camera object (0x005d9158) gets the locked camera's vtable (0x005362d0), the shot's
position at +0x1e0 and orientation at +0x1f0, its field of view, near and far, and is made current
(0x005d9150); the locked update (Scripted camera angles) then places it each frame
and the world streams round it. The people and cars stay wherever the save state had them.
Reference views¶
Camera values are read from the current camera of player 1 (0x005d9150, Camera:
position +0x10, orientation quaternion +0x20 as (x, y, z, w), field of view +0x48, near / far +0x50 /
+0x54). The orientation is the identity when the camera looks along +y with +z up.
| View | Where | Camera position | Orientation (x, y, z, w) | Field of view, near, far |
|---|---|---|---|---|
| street | level99 checkpoint 3, the street at night, Rembrandt walking away from the camera |
(80.319, 41.122, 2.815) | (−0.07495, −0.07494, 0.70306, 0.70319) | 65°, 0.1, 115 |
| store | level99, inside the gift shop in front of the glass cabinets |
(50.929, 51.816, 2.819) | (−0.13968, 0.00042, −0.00294, 0.99019) | 65°, 0.1, 115 |
| car | level99, beside a parked car with Ash, steam from a grate |
(57.090, 44.868, 2.815) | (0.00802, −0.10177, 0.99170, −0.07816) | 65°, 0.1, 115 |
| fence | level99, a fence ahead, three pedestrians |
(48.434, 48.591, 2.703) | (−0.00023, −0.10529, 0.99444, 0.00222) | 65°, 0.1, 115 |
| arena | level99, the initiation's last fight: lamps, the objective marker, the crowd behind the fence |
(−284.326, 121.271, 2.792) | (−0.07781, −0.09395, 0.76440, 0.63309) | 65°, 0.1, 115 |
| l80_letsgo | level80 checkpoint 1, the "Let's Go" tutorial's first locked camera (StartCam1), the Warriors in a hall with a pink lamp and vent steam |
(−194.746, 130.353, 2.699) | (0.00498, −0.03360, 0.98860, −0.14667) | 65°, 0.1, 150 |
| title, menu | the front end (level100): the title screen and the main menu over the Wonder Wheel |
(462.602, −122.348, −187.930) | (0.00012, 0, −0.29090, 0.95675) | 54.43°, 0.5, 150 |
State shared by every pass¶
| Register | Value | Meaning |
|---|---|---|
FRAME_1, FRAME_2 |
PSMCT32, FBW 10, FBP 140 or 0 |
32-bit colour, 640 pixels wide; two buffers at block 140 and block 0 (× 2,048 words), drawn alternately, both contexts the same |
ZBUF_1, ZBUF_2 |
0x31000118 |
24-bit Z (PSMZ24) at block 0x118; Z test enabled in every pass (ZTE 1); a larger Z is nearer |
SCISSOR |
(0, 639, 0, 447) | the whole 640 × 448 frame (the clears use 2047 × 2047) |
DTHE |
0 | no dithering |
COLCLAMP |
1 | colours are clamped to 0-255, not wrapped |
PABE, FBA |
0, 0 | blending is per pixel by the source alpha; the alpha written is the source alpha unchanged |
TEXA |
TA0 0, TA1 0x80, AEM 0 |
only matters for 16- and 24-bit textures; the game's are palettised (T8, T4) |
PRMODECONT |
AC 1 |
primitive attributes come from PRIM |
TEX0 |
TFX MODULATE, TCC 1 |
every texture is multiplied by the vertex colour, alpha included |
TEX1 |
MMAG linear, LCM 0, L 0 |
bilinear magnification everywhere; minification per pass (below) |
PRIM |
IIP 1, FST 0 |
Gouraud shading; texture coordinates as STQ (perspective-correct) |
Colours are in GS units, where 0x80 is 1.0. MODULATE gives texture × vertex colour / 128 for each channel and
for alpha, so a vertex colour of 0x80 shows the texture as it is, and values above it brighten up to twice. The
prelit night world uses vertex colours of about 0x1d-0x50 (the street's darkest walls are 0x1d1d1d, about 23%
of the texture), and every opaque vertex's alpha is 0x80.
The blend is the same almost everywhere: ALPHA 0x44, Cv = (Cs − Cd) × As / 128 + Cd, the ordinary "source
over" mix by the source alpha (texture alpha × vertex alpha / 128). Only the character overlay pass uses another
value (Characters).
Fog is the GS's per-vertex fog: each vertex carries a fog value F (255 = no fog, 0 = all fog), interpolated across
the primitive, and the pixel becomes (F × colour + (255 − F) × FOGCOL) / 255 after texturing and before blending.
FOGCOL was (12, 12, 5) in every level99 view and (17, 12, 15) in level80's (it is the level's fog colour,
confirmed (runtime)); how F is computed from the distance is RenderWare's linear fog (PS2 render
driver).
The two GS contexts are used deliberately: context 1 for the first pass of everything (alpha test on:
ATST GEQUAL, AREF 0x40, AFAIL FB_ONLY), context 2 for second passes, sprites and text (alpha test off,
or ATST NOTEQUAL with AREF 0). With AFAIL FB_ONLY, a pixel whose alpha is below 0x40 is still blended into
the colour but does not write Z.
The passes, in order¶
The order is that of a frame; the numbers are the street view's (frame A), the other views differ only in counts.
| # | Pass | Primitives | Blend | Alpha test | Z test / write | Fog | Texture filter | Code |
|---|---|---|---|---|---|---|---|---|
| 1 | Clear | 40 untextured sprites over 640 × 448 | none | off | always / write Z = 0 | no | none | a frame, device slot +0x38 |
| 2 | Sky and clouds | 2 strips, T8 256 × 512 and 512 × 256 |
0x44 |
GEQUAL 0x40 |
GEQUAL / no write | no | bilinear, no mipmaps | the background |
| 3 | Skyline | 1 batch of 1,837 strip vertices, T8 32 × 16 |
0x44 |
GEQUAL 0x40 |
GEQUAL / write | no | bilinear | the background |
| 4 | Z clear | 40 untextured sprites, FBMSK 0xffffffff (colour masked) |
none | off | always / write Z = 0 | no | none | the background |
| 5 | The world | strips, T8 / T4 16-256 texels |
0x44 |
GEQUAL 0x40 |
GEQUAL / write | yes | trilinear (MMIN 5), K per texture |
the streamed world |
| 6 | Second layers | the same strips again, a second texture | 0x44 |
off | GEQUAL / write | yes | as the first layer | world pipelines (MatFX dual) |
| 7 | Objects and props | strips | 0x44 |
GEQUAL 0x40 |
GEQUAL / write | yes | bilinear (MMIN 1) |
object drawing |
| 8 | Characters | strips, two passes per mesh | 0x44, then the overlay's |
GEQUAL 0x40, then off |
GEQUAL / write | yes | bilinear | character drawing |
| 9 | Translucent objects, objective markers | strips | 0x44 |
GEQUAL 0x40 |
GEQUAL / no write | yes | bilinear | the streamed world, step 10 |
| 10 | 3D sprites | sprites and strips, T8 512 × 256 particle pages |
0x44 |
NOTEQUAL 0 | GEQUAL / no write | yes | bilinear | 3D sprite batches |
| 11 | HUD | radar fans (T4 256 × 256, clamped), text sprites (T8 512 × 256 font page) |
0x44 |
radar GEQUAL 0x40, text NOTEQUAL 0 |
always / no write | no | bilinear | HUD |
| 12 | Level tint | 1 untextured fan over the whole screen | 0x44 |
off | GEQUAL / no write | no | none | screen effects |
| 13 | Motion blur | 1 fan over the whole screen textured with the previous frame | 0x44 |
GEQUAL 0x40 |
always / no write | no | nearest | screen effects |
No pass in any of the four views draws a shadow: the characters stand on the ground with nothing under them.
Clear¶
The colour clear writes the level's background colour with alpha 0x80, the same value as FOGCOL in every
level99 view (12, 12, 5) and in level80 (17, 12, 15), so the far edge of the fogged world meets the background
without a seam. Z is cleared
to 0 (the farthest value under GEQUAL).
Sky and clouds¶
Drawn with Z test GEQUAL against the cleared Z (always passes) and no Z write, no fog, bilinear without mipmaps; the sky box's Z values are near the top of the range (about 1,456,800 to 16,775,539), the camera's translation being zeroed for it (the background). The skyline that follows writes Z, then a second clear resets Z (colour masked) so the world, drawn with its own near and far planes, covers it.
The background's models (sky box, cloud box, skyline, glows) carry no light flag and no normals, yet reach the GS
lit by the world ambient: level99's sky box (prelight 60), cloud box (70) and the skyline's prelight-0 vertices
come out at 59, 64 and 29 of 128, level100's sky box (70) at 64, prelight + 0.227 each. Inferred (Coney's models
against the GS dumps, Lighting); the VU1 background path that ignores the flag
has not been found in code. Raising the world ambient brightens the sky on screen about as much as the walls
(confirmed (runtime), Lighting).
The world¶
The streamed world's sectors are the only geometry minified with mipmaps: MMIN 5 (linear mipmap linear) with L
0 and a per-texture K from −7.6 to −2.0 (most −4 to −5); most textures have MXL 0 (no mip levels, so the mode
reduces to bilinear), a few 4-bit ones MXL 3. Objects, props, characters, the sky and everything 2D use MMIN 1
(bilinear). Repeat addressing (CLAMP 0) everywhere except the radar.
Vertex alpha is 0x80 for opaque geometry; a few world and prop strips carry lower values (0x20, 0x64 in the
street view, 0x55 on a distant prop in the car view), which the 0x44 blend turns into translucency while Z is still
written.
Second layers¶
Materials with the MatFX dual effect (world pipelines) draw their geometry twice back to back: first
the base texture in context 1, then, at once, the same strip with a second texture (128 × 128 T8 in these views)
in context 2: blend 0x44 (the ALPHA_2 value also carries FIX 0x80, unused by this equation), no alpha
test, Z test GEQUAL with Z write, fog on, the same vertex colours and fog values as the first pass. The second
texture's alpha therefore decides where it shows (decals, dirt and grime on walls).
Characters¶
Every human is drawn mesh by mesh, each mesh twice in a row (the code: Drawing a human):
- Context 1: its 256 × 256
T8texture, blend0x44, alpha test GEQUAL0x40, Z write, fog, bilinear. - Context 2: the same vertices (same colours, a second set of texture coordinates) with a 64 × 64
T8texture, alpha test off, Z write, fog. The blend isALPHA_2= A 0, B 1, C 0, D 1 (0x44, the texture shows by its alpha) for Rembrandt in every view, and A 1, B 1, C 0, D 1 (0x45: (Cd − Cd) × As + Cd = Cd, the pass changes nothing) for Ash and every pedestrian seen.
The second pass is the human material's MatFX dual texture, a shared blood texture chosen by the human's
health h in percent: 60 and above charblood_d1, 30-60 charblood_d2, below 30 charblood_d3 (resource manager
+0x74 / +0x78 / +0x7c, set with RpMatFXMaterialSetDualTexture, 0x00465810). The blend word of the material's
effect is 0x44 when h is below 90 and W_GameState + 0x454 is 0, otherwise 0x45, so a human at 90% health or
more shows no blood. Confirmed (code) at HumanRender_Draw (0x00174320, read by the graphics engineer; the texture
names confirmed (runtime) in level99); +0x454 is only ever written as 0, so it is a no-blood switch the retail
game never sets (inferred). In the store view Rembrandt, under 90%, shows blood on his arms. Characters are lit per
vertex (prelit plus the lights, Lighting); the vertex colours of both passes are equal.
3D sprites¶
Particles (the steam from the grates, Steam vents) are SPRITE primitives and short strips
textured from a 512 × 256 particle page, in context 2: alpha test NOTEQUAL 0 (only fully transparent texels are
dropped), blend 0x44, Z test GEQUAL, no Z write, fog on with the vertex fog value 254. Their vertex alphas are low
(about 0x02-0x38), so each puff adds a little. level80's vent steam at the top of the frame is the same class of
batch (17 sprites, grey 0x7f, alpha 3-38, the same particle page, context 2, NOTEQUAL 0, fog, no Z write), confirmed
(runtime).
The fixed fog value means a sprite takes 1/255 of the fog colour at any distance, never the world's distance fog.
Coney (repo:src/platform/particle_renderer.cpp) draws its 3D sprites (particles, glints, fog wisps) with fog off,
rounding that 1/255 to nothing.
Health rings and blood¶
In a fight (level99, the Bumper Bash yard, a GS dump with Rembrandt hit and an enemy targeted; confirmed (runtime))
the ring on the ground under the target (The health rings) is one list of 64 triangles
(TRI, 192 vertices) textured from the same 512 × 256 particle page as the steam, in context 1 (alpha test
GEQUAL 0x40, AFAIL FB_ONLY), blend 0x44, Z test GEQUAL with Z write, no fog, drawn after the
particles and before the tint. Its vertex colours are (38, 61, 13) green for the health it shows and (30, 30, 30)
grey for the rest, both alpha 0x7f. The blood is a batch of SPRITEs from the same page in context 2 (alpha test
NOTEQUAL 0, no Z write, fog value 254), vertex colours (76, 10, 10) and (60, 0, 10) at alphas 0x6b-0x6f among
grey dust puffs at 0x16-0x23. The frame also carries the motion-blur fan of Motion blur
(vertex alpha 7).
HUD¶
The radar is two triangle fans of a 256 × 256 T4 texture with clamp addressing (the only clamped texture
seen), alpha test GEQUAL 0x40, Z test always, no Z write, no fog, at Z 0x7fffff; the vertex alpha 0x78 makes
the disc 94% opaque. The objective text and its box are sprites from a 512 × 256 T8 font page in context 2 (alpha test
NOTEQUAL 0, Z always), the box's vertex alpha 0x3e-0x40 (half transparent), the text 0x80.
Level tint¶
The last primitive of each frame is a full-screen untextured fan of colour (7, 20, 30) with alpha 0x15 (21 / 128,
16%) in level99, blend 0x44, Z test GEQUAL at Z 0xfffffe (always passes), no Z write: a blue-green wash over
everything, including the radar and the HUD text, which were drawn before it. It is the screen-effects tint
(Screen effects, the SetLevelColour look), filled at once rather than queued. In
level80 the colour is the same but the alpha is 0x19 (25 / 128), so the alpha (and so the whole tint) is per
level, from the level script's SetLevelColour, not a constant; confirmed (runtime).
GameMode_DrawOverlays (0x00156658) normally runs the screen effects after the HUD (0x001b1688) and its overlay
flush and before the captions, which stay on top of the tint; it runs them before the HUD instead while
W_GameState + 0x268 is above 0 or a blur pulse runs in either view. Confirmed (code) at 0x00156658 (read by the
graphics engineer) and confirmed (runtime) for the normal order; no caption was on screen in these views.
Per level (confirmed (runtime), a GS dump of each level in play, PCSX2 2026-10-07; the script values read from
the compiled level scripts). The fog colour and the clear colour are the level script's SetFogColor(r, g, b)
× 255, truncated (Lighting); the tint is SetLevelColour(LightData.overlay), called by global.lua's
SetupCharacterLights with the level's lighting table, and differs in colour as well as alpha:
| Level | SetFogColor |
FOGCOL = clear |
Tint (r, g, b), alpha / 128 |
|---|---|---|---|
level99 |
0.05, 0.05, 0.02 | (12, 12, 5) | (7, 20, 30), 21 |
level80 |
0.07, 0.05, 0.06 | (17, 12, 15) | (7, 20, 30), 25 |
level87 |
0.012, 0.018, 0.035 | (3, 4, 8) | (0, 25, 45), 15 |
level2 |
0.05, 0.05, 0.02 | (12, 12, 5) | (38, 45, 51), 28 |
level3 |
0.02, 0.02, 0.02 | (5, 5, 5) | (0, 0, 5), 12 |
level5 |
0.04, 0.025, 0.057 | (10, 6, 14) | (56, 61, 51), 28 |
level34 |
0.02, 0.02, 0.02 | (5, 5, 5) | none, in the intro scene and in play (the subway tunnel at the start) |
level95 |
0.05, 0.05, 0.02 | (12, 12, 5) | (19, 19, 38), 31 |
How the fill is drawn (ScreenFx_DrawTint 0x0018ca50 → ScreenFx_FillViewport 0x0018cc20 → device slot
+0x130, RwDevice_FillViewport 0x001959a8), confirmed (code) at those addresses:
- The colour.
SetLevelColour(0x0018e5f0) andEnterStore(0x0018e6b8) take the script's four floats 0-1 and store each × 255, truncated (Float_ToUInt), as the look's packed tint, bytes red, green, blue, alpha (look 9 and look 10). The tint blend (ScreenFx_BlendTintTo) lerps the packed colour from+0x1bcto the target+0x1c4by the time left (Looks); a tint whose alpha byte is 0 is not drawn, nor any tint while the current look is 2 with+0x1b80. - The fill. Four RwIm2D vertices over the viewport, each vertex colour the tint's four bytes as floats
0-255, with the raster unset (
rwRENDERSTATETEXTURERASTER0), culling off (state 20 = 1), vertex alpha on (state 12 = 1), fog off (state 14 = 0) and Z write off (state 8 = 0, put back on after), drawn as a triangle strip. The source and destination blends are not set, so the current ones apply: the driver defaultSRCALPHA/INVSRCALPHA. - What reaches the GS (confirmed (runtime), the dumps above):
ALPHA=0x44, that is A = Cs, B = Cd, C = As, D = Cd, so Cd' = (Cs − Cd) × As / 128 + Cd per channel, clamped (COLCLAMP1);FIXis not used. RGB is passed 0-255 as stored; the alpha is scaled to 0-128: As = the alpha byte × 128 / 255, truncated. Every level matches:level2's overlay (0.15, 0.18, 0.2, 0.22) is stored (38, 45, 51, 56) and drawn (38, 45, 51) with As 28;level87's (0, 0.1, 0.18, 0.12) is (0, 25, 45, 30), drawn with As 15; the table's (0.03, 0.08, 0.12, 0.2) is (7, 20, 30, 51), drawn with As 25 (level80). Below 255, × 128/255 truncated and ÷ 2 truncated give the same As; at 255 the scale gives 128, which no dump shows (inferred from the driver's colour conversion elsewhere,0x0049ea80, which maps alpha by ×0x808081>> 24: 255 → 128).
So in 0-1 terms the wash is out = dst + (round_down(255 × rgb) / 255 − dst) × As / 128: the opacity is about the
script's alpha (0.12 → 15/128 = 0.117), not twice it, and the colour is not halved. The game-over tint
0xd0000014 (Camera) is red 20, green 0, blue 0 at As 104 (81%).
Motion blur¶
In the arena view (not in the street, store, car or fence views) one more full-screen fan follows the tint
(Motion blur): its
texture is the other frame buffer, the previous frame (TEX0 PSMCT32, 1024 × 512 with TBW 10, TBP = that
buffer's FBP × 32), TCC 0 so the alpha comes from the vertex: colour 0x80 with alpha 0x07, the old frame laid
over the new at 7/128 (5.5%) with nearest filtering, Z test always, no Z write. It is the screen effects'
motion blur (Screen effects), whose strength is the current look's.
The front end¶
The title screen and the main menu draw through the same passes as a level: a black clear, a small sky (T4 16 × 16
strips, no Z write), a Z clear, the world (trilinear, K down to −13.2) and the Wonder Wheel's objects, then the
menu text sprites and the tint. The same tint as in level99 ((7, 20, 30), alpha 21) is drawn last, so the black
background shows as a very dark blue-grey.
Fog and colour on the title screen (the fe_title dump, confirmed (runtime)): FOGCOL is black (0, 0, 0)
and the fog is the usual linear curve (Fog) for the scene camera's far clip of 150 and the
default fog start 0.5: 255 up to 75 m, 0 at 150 m (fit 74.7 m and 151.2 m). The wheel stands 54-100 m from the
camera, so its vertex fog values run only from 255 down to 168: its far side darkens by at most a third, it never
fades out. The values that reach 0 are those of the 16 × 16 T4 world strips beyond 90 m. What keeps the wheel's
frame dark is its vertex colour (on the GS's scale, 128 = 1.0), and through it the object tint.
The batches that span the wheel's outline (bilinear, K −4) match the scene's objects by texture size, vertex count
and colour (inferred: matched against Coney's draw of the same models):
| Batch | Vertex colour | Objects | Why that colour |
|---|---|---|---|
32 × 32 T8, 2,561 vertices |
68 on every vertex | the 24 carts dyn_s_wwcart_simple_* (dyn_wwcart_simple) |
prelit 255, tint 0x888888ff: 136/255 × 128 = 68 |
128 × 128 T8, 749 vertices |
5-12 | the wheel dyn_s_wwheel_a |
no prelight, lit by the object lights, tint 0x474542ff |
32 × 32 T8, 338 vertices |
128 | the neon signs dyn_s_neon_* (dyn_neon) |
prelit 255, no tint |
The nearby world strips have 29: the lattice girders round the hub are world geometry (metl140_blk,
metlgirdr13_blk, propcony11_blk), prelight 0, lit by the world ambient 0.227 (inferred, the same match).
No sprite, particle or corona is drawn on the wheel: the frame has no batch between the world and the menu text
(confirmed (runtime)). The lights that look like bulbs on the rim are the carts, geometry dimmed by their tint.
The Rumble screens (Game Mode, Choose Gangs, Front end) draw no world and no tint: a black clear,
then one 512 × 512 T4 sprite, the grey gang-logo collage, modulated by a per-screen vertex colour, (24, 49, 50)
alpha 0x7f on Game Mode (teal) and (25, 61, 38) alpha 0x80 on Choose Gangs (green), then the T8 font sprites
(text 0x59 grey, shadows 0x28, half-alpha boxes). Choose Gangs' two 3D fighters are drawn exactly like characters
in a level (Characters: each mesh twice, the second pass 0x45 at full health), with Z test GEQUAL
and fog value 255 (no fog). The sprites carry fog value 254 with FOGCOL black. Confirmed (runtime), PCSX2 GS dumps
of both screens.
Drawing between steps¶
The original draws one frame per simulation step, at 30 a second (the step is 1/30 s), so nothing it draws is ever
between two steps. Coney steps at the same rate at any display rate, and a frame that falls between two steps draws
what moves blended alpha of the way from the step before to the newest
(Update and render). This is Coney's rendering choice, not gameplay:
the simulation never reads it, and at alpha 1 (test mode, --fps-cap 30) every helper returns the newest step's value
bit for bit, so lockstep draws exactly what the original would.
Blended: the camera (follow, scene and free), every human's feet, heading and pose (player, targets, AI fighters,
scene puppets), the draw distance, and the world objects. The world objects (world_objects::DrawnPoses,
repo:src/world_objects/drawn_poses.h) keep the pose each one was drawn at on the last two steps, by the id it is
drawn under, and are drawn with the position lerped and the rotation slerped between them: the objects the object
tasks draw (thrown and dropped objects in flight, objective markers' discs and columns), the door leaves and barriers,
and the spinning icons over humans. The object in a hand and the hats follow the blended human that holds or wears
them. An object with no pose on the step before (just spawned, just let go of, back in reach) is drawn where it is,
and so is a jump: a script's move or a drop counts on its spawn record (SpawnRecord::jumps) and is not blended
across, nor is a move of more than 10 m in one step (nothing in the game moves at 300 m/s).
Not blended yet, and drawn as the newest step has them: the particles and debris sprites (each would need its position on the step before kept from the step it is made in), and the cars and their stereos, which stand still in Coney.
Output¶
The frame buffer is presented as an interlaced field-mode picture (SMODE2 1, PMODE 0x8067): both read circuits
show the same 640-wide buffer, one from line 0 (DISPLAY1 height 448) and one from line 1 (DISPLAY2 height 447,
DISPFB2 DBY 1), mixed half and half (ALP 0x80), so every output line is the average of two neighbouring frame
lines: a slight vertical blur that softens the image and hides the jagged edges of thin lines. The driver side is
The picture on the TV.
Coney offers this as an optional PS2 look, off by default (--line-blend on). The blend existed to hide interlace
flicker on a TV; Coney draws whole frames on a full-resolution monitor, where there is nothing to hide, and the
one-line shift (about 2.4 pixels at 1080p, vertical only) reads as a smear on faces and thin lines rather than the
soft TV picture players remember. This is a quirk of the console's video output, not of the game, so turning it off
leaves the game's behaviour as the original's. When on, Coney (RenderEngine::setLineBlend,
repo:src/platform/render_engine.h) does the same after the game's
2D layers and before the debug menus: the frame is copied and laid back over itself at half strength, shifted up by
one of the original's 448 lines at the window's size, the last line clamped. It covers the whole 3D view
(RenderEngine::viewRect), so in a window wider than 4:3 the view's sides are softened too: the original's output
blends its whole picture, and a blend of only the logical screen's 4:3 place left a visible edge 12.5 % in from each
side. --line-blend off shows the frame as drawn (Frame rate); reference renders are
never blended.
Open questions¶
- The views not captured yet: rain, interiors with lights, water, glass breaking, split screen.
level80's pink lamp has one 254-vertex strip with a 64 × 32T8texture in context 1 (alpha test GEQUAL0x40, no Z write) drawn after the world: which object draws it (a lamp shade, or a corona-like glow, Lighting) is not known.- The front end's background reads a uniform (2, 6, 7) in the original's capture, including where no sky draws,
against the (1, 3, 5) the screen tint gives over black (The front end). The output merge
is a guess (
PMODE0x8067mixes both read circuits), but it averages neighbouring lines and so cannot brighten a uniform area; level 100's sky texture is near black. Speculative: the capture's own processing, or a draw not yet matched.