12 · Media & Data

Rendering Pipeline

The game never calls OpenGL from game logic. Everything draws by recording numeric opcodes into a packet buffer; one render thread replays them as GLES 2.0 calls. This chapter maps the whole path: opcodes, shaders, the 3D world, sprites, and the kanji engine.

12.1Draw packets: the deferred command buffer

Game logic, running on fibers across the update threads, records what to draw. The recording surface is ALDrawPacketBuffer::AddWriteWork (@ 0x39488c); the consumer is ALDrawPacketGLES::ExecPacketMain (@ 0x393c74), a large switch over 16-bit opcodes 0x00–0x41. Even material parameter updates are opcodes: ALShaderImpGLES::SetMaterialParam (@ 0x39018c) never calls glUniform*; it writes work items (0xf alpha, 0x10 UV offset, 0x11/0x12 light colors, 0x13 camera-relative light direction, 0x19 diffuse, 0x1c light arrays…).

OpcodeGL effectOpcodeGL effect
0x00–0x0estate setup (viewport, clears, binds)0x2a–0x2dVBO bind / vertex attrib setup (real VBOs, not client arrays)
0x16compressed texture upload (glCompressedTexImage2D)0x31glVertexAttribPointer (dropped if location −1)
0x1b/0x1cuEyePos / light arrays upload0x32–0x34constant-value attribute fallbacks
0x37/0x38bulk matrix uploads (e.g. uBoneMatrix[])0x3falpha/stencil test state (depth test, stencil invert, color mask)
0x3bend-of-frame marker0x3c/0x3dframe-order tags written by the render thread itself

Submission is two-lane: game logic enqueues; and 3D model draws + 2D sprite draws run on ALJobPool worker threads via JobFuncScreenDraw, each batch tagged with 0x3d (LastPacketOrder) so the render thread can preserve submit order across workers.

The render thread

One thread owns the GL context (the deferred-bind workaround that the port shim reproduces): DrawThreadFunc @ 0x3942f0:

/* DrawThreadFunc @ 0x3942f0 — the render thread's whole loop */
DrawThreadState = 0;
DrawThreadSema->Post();
while (!ThreadTerminate) {
    DrawThreadState = 1;
    ALScreenList::DrawAll(GetScreenList());        // replay screens' packets
    SetReady();
    /* atomic: CurrentDrawOrder++ */
    if (order != 0) {                              // tag frame order (0x3d)
        packet.write(0x3d, order); AddWriteWork(8);
    }
    SwapBuffer();                                  // flip packet buffers
    packet.write(0x3c); AddWriteWork(4);           // 0x3c end tag
    SetReady();
    DrawThreadState = 2;
    DrawWaitSema->Wait();                          // sleep until next frame's work
    DrawThreadSema->Post();
}
Exhibit: mutex + semaphore sync, packet replay until 0x3b, buffer swap, sleep. No double buffering of GL state; the packets themselves are the frame.
Draw packet pipeline from game threads to GLES
Two producer lanes (game fibers + job-pool workers) feed the packet buffer; the single render thread replays opcodes into GLES 2.0 and ends each frame at 0x3b.

12.2The uber-shader system

Shaders ship embedded in the binary as ALLZ-compressed streams (decompressed at init). The default program is an uber-shader with per-feature variant selection: 72 × 48 variant grid, addressed by 576-entry tables; the shader use-table that pairs with it is never read by the active path (authored metadata, safe to skip in a reimplementation). Uniform set: model/view/projection matrices, uFog/uFogColor (fog opcodes 0x11/0x1c/0x1b; the legacy FA/FB fog computation is dead code), uEyePos, light arrays, bone palette (uBoneMatrix[], 0x37/0x38 uploads). The recovered GLSL for skinning: one-bone and two-bone paths selected per vertex attribute.

12.3The 3D world

12.4Sprites, HUD and the kanji engine

2D is the same packet system: ALSpriteDrawGLES::Draw → ALVertexList::AddRect → opcodes. The sprite sort record (120 B), register record (44 B) and command (68 B) layouts are fully mapped. The HUD (speedometer, gauges, timetable) and all menus are sprite sheets; text goes through the kanji glyph-cache engine: glyphs rasterized into an atlas texture on demand, with the menu font drawn from pre-baked sheets for the fixed UI strings.

HUD with speedometer and gauges
The sprite HUD: analog speedometer (a rotating needle over a baked dial), brake pressure bar, notch indicator, timetable clocks, all packet-drawn quads.
Normal mode briefing
Menu screens are sprite sheets with pre-baked kanji text; the live text engine serves dynamic strings (station names, scores).

12.5The dead PS2 layer

A complete PS2-era immediate-mode layer rides along unused: WinAdd* and ScreenEffect* tables, ALTileMap/ALFontCodeConv hierarchies, all unreferenced by the live path. Its presence explains the engine's PS2/PSP-era packet design (the opcode stream is a portable command buffer, originally for a GPU without programmable shaders).

← 11 · Tutorial (Lecture) Mode