3D Render Core & SH Interpreter (GR)¶
The 3D scene pipeline that turns transformed geometry into the 2D rasterizer calls — and the
hand-written threaded-code SH interpreter that executes .SH shape bytecode into it.
0x4CD588–0x4D6C00. This is the layer above the 2D rasterizer: render-core
issues the G_* calls, renderer fills them.
Provenance: Ghidra static analysis of the game executable with FA.SMS symbols applied; recorded in the symbol database and applied to the Ghidra project (the ~120
vector_tableSH-opcode handlers, previously created only byAnalyzeSHDispatch.java, are now captured indb/and materialised on apply). Progress: reconstruction matrix. Markers follow spec-authoring.md: confirmed · inferred · unknown.
The 3D pipeline¶
render_3d (0x4CDCB8) traverses the scene; setup_view_projection builds the per-frame
aspect/head-vector/frustum constants from zoom and screen size; check_flat picks the flat
vs. perspective path from the object matrix. Fixed-point math leaves (sincos, acos,
isqrt16) back the transforms; Sutherland–Hodgman clip_edge_{left,right,top,bottom} clip
polygons to the screen; _Sun computes the lighting dot product; and sort_objs_wrapper
depth-sorts (painter's order, no z-buffer). The public API (GRInit3d, GRRender, GRExec,
GRTo2d, GRSinCos, GRAddBrentObj, GRSetLightSource) is how the rest of the engine
drives it; GRAddBrentObj queues a shape into the render-sort list.
The clip and sort stages are reproduced in fx_render::fa: the screen-edge clip order and
crossing interpolation (#331) and the GRAddBrentObj → sort_objs_wrapper painter's-order
submission list on the centroid+size key (#332), both pinned by tests/render/test_fa.cpp.
The SH interpreter¶
Each queued shape is drawn by the SH bytecode interpreter — a hand-written threaded-code
dispatcher over the vector_table (0x5183A0, ~120 handlers indexed by opcode). Handlers
(do_* / sh_op_*) walk the .SH instruction stream, apply transforms
(do_xformunmask), and synthesize the polygon micro-programs that dispatch into the
G_* 2D rasterizer (do_new_poly for opcode 0xFC faces). Ten unassigned
opcodes share a single sh_op_stub no-op. This is the same interpreter documented, from the
format side, in SH.md (epic #52) — here it is named and mapped as engine
code.
Functions¶
Full record: db/symbols/render-core.csv.
| VA | Symbol | Role |
|---|---|---|
0x4CDCB8 |
render_3d |
scene traversal / per-frame 3D entry |
0x4CDEB4 |
setup_view_projection |
per-frame view/projection/frustum setup |
0x4CE4B4 |
check_flat |
pick the flat vs. perspective render path |
0x4CD588 |
sincos |
table-lookup sine/cosine (transform core) |
0x4CD8B0 |
_Sun |
light dot-product against the world light source |
0x4CD8F0 |
clip_edge_right |
Sutherland–Hodgman screen-edge clip |
0x4CE968 |
sort_objs_wrapper |
painter's-order depth sort |
0x4D3194 |
do_xformunmask |
SH opcode 0xC4: render a sub-stream at a relative transform |
0x4D0C8A |
sh_op_6A |
SH opcode 0x6A handler (render-state/geometry) |
0x4D17E0 |
sh_op_stub |
shared no-op for 10 unassigned SH opcodes |
Open Questions¶
1. Remaining sh_op_* handler semantics¶
The vector_table handlers are named and materialised; the larger handlers' exact
geometry/state effects are the remaining substrate for finishing SH.md's
Unk* opcodes.
Characterized. sh_op_80 (0x4D1FC0) is a shading/colour-setup opcode: it early-outs
on codes_and, and when gouraudOn == 0 calls SetFlatColor(a, b) — i.e. it selects flat vs.
Gouraud shading and stages the primitive colour. sh_op_78 (0x4D3938, the 2085-byte largest
handler) is a bounding-box visibility cull: it reads a center point plus an extent vector,
transforms the extent by the view matrix, and forms the box's 8 corners (center ± (±dx,
±dy, ±dz)); it projects each via code_pnt (a Cohen–Sutherland clip outcode) and range-tests
them, trivially rejecting the guarded geometry when the box falls outside a frustum edge. It
emits no geometry — a cull/LOD gate, not a mesh op. (This matched OpenFA, which likewise treats
0x78 as an opaque fixed-size instruction, and explains why a static codec can skip it safely.)
Also characterized — the fragment-call structure. do_short_eof (0x4D17F4, opcode
0x1E) is a plain ret — the fragment return, not a NOP pad; do_unmask (0x4D2278,
0x12) calls its target sub-stream via the dispatch call-form and resumes after the opcode
when the callee's ShortEOF returns; and sh_op_6C / sh_op_06 / sh_op_0C / sh_op_0E /
sh_op_10 are draw-order selectors: each always renders both of its sub-chains (call one,
tail-continue the other) with an object-field or face-plane dot-product sign only swapping the
order — painter's-algorithm sorting in bytecode. Layouts and the static-walk consequences are in
SH.md → Fragment calls and draw-order selectors.
Status: open — re-static (#262; sh_op_78,
sh_op_80, the ShortEOF/Unmask call structure, and the 0x6C/0x06-family selectors
characterized; the remaining larger handlers' fine state effects continue).
Related¶
- renderer.md — the
G_*2D rasterizer this core drives. - formats/SH.md — the
.SHshape-bytecode format this interpreter runs. - shape-selection.md — whole-model selection feeding
GRAddBrentObj. - terrain.md — terrain geometry enters the same pipeline.