# FS2 Scenery Database Opcode Reference Complete reference for the chunk5 scenery interpreter opcodes, derived from the source disassembly at `/home/scott/claude/flight/src/chunk5.s`. Each record in the scenery DB starts with a 1-byte opcode followed by a fixed or variable number of parameter bytes. The interpreter (`SceneryInterpreterStep` at chunk5.s:1264) reads one byte from the cursor `($8B/$8C)`, dispatches via `SceneryOpcodeTable` (= 70 2-byte addresses at chunk5.s:1165). Opcodes with bit 7 set OR codes in `[$46..$7F]` BOTH trigger `SceneryStreamEnd` (= terminator) -- the dispatch predicate is `cmp #$46 / bmi SceneryDispatch`, so $46..$7F fall through to the same end-of-stream handler as $80..$FF. Total record length includes the opcode byte itself. --- ## "source" vs "MAME-patched" This document distinguishes two views of the chunk5 binary: - **"source"** — the original 48K Apple ][+ image, as it appears in the disassembly at `/home/scott/claude/flight/src/chunk5.s`. This is what shipped on disk. - **"MAME-patched"** — the post-boot binary as actually observed in `port/sceneryRam_FS2.1.bin` (a 64K RAM snapshot from a running MAME `apple2gs` after FS2 boot). The reference screenshot `port/screenshots/mame_meigs.png` also comes from that same MAME instance, so to match the reference the port must replicate the runtime behavior — not just the source-faithful behavior. **Where the divergences come from (revised).** An earlier version of this doc attributed the source-vs-RAM-dump differences to `Apply64KPatchTable`. That turned out to be wrong: Apply64KPatchTable patches ~30 sites in the `$6000..$AE00` range — instrument hooks, ADF lookup, course plotter, magneto / engine model, day phase, ATIS dispatch, the chunk4 scenery-loader thunks, etc. — but it does **not** touch the scenery-opcode dispatch table, the polygon/clipper kernel, the matrix at `$78..$89`, the camera ZP `$66..$6B`, the section-base accumulator, `EmitClippedLine`, or any of the `TransformVertex*` routines. Those addresses are unchanged between the source image and the runtime image. So why does the captured RAM look different at `$4A..$52` vs `$2A..$2F`, at `$2C` vs `$29`, at `$35/$36` vs `$38/$39`? The MAME RAM snapshot was taken **mid-flight**, after the scenery dispatcher had already walked into `$A800` and executed a sequence of opcodes. Scenery bytecode itself can mutate chunk5's code area via `$1A SceneryOpWriteWord` and `$25 SceneryOpStoreImmWord`. These two ops accept any 16-bit destination, including absolute addresses inside chunk5. The runtime image therefore differs from the source not because of one-time patching at boot, but because of **continuous scenery-driven self-modification** as the dispatcher walks sections. This has practical consequences for the port: - The port can faithfully implement the SOURCE semantics of every opcode and still produce different rendering output from MAME if the scenery files it loads contain `$1A`/`$25` writes that target ZP slots used by the source code paths. The port may need to apply the same writes when it sees those opcodes, but only as a side effect — not as a baked-in alternate layout. - Reading the RAM snapshot as "the right" view is misleading. The RAM is the state AFTER scenery has executed `N` opcodes. A clean source-faithful port that walks the same scenery files should converge on the same observable state. - The differences listed in older sections of this doc (`$4A..$52` vs `$2A..$2F`, etc.) are real OBSERVATIONS of the RAM but unreliable as STATEMENTS about the engine. The engine reads from the source-defined locations. Wherever the runtime appears to write to a different location, that's a scenery `$1A`/`$25` artifact and needs to be traced through the scenery byte stream, not modeled as an alternate engine layout. The older "source vs MAME-patched" divergence table below is preserved for historical reference and may still be useful when comparing the port's runtime against a captured RAM image. Read it as "these are the addresses where the RAM dump deviated from what a source-faithful run would produce," not as "the engine implements two layouts." | Aspect | Source (chunk5.s) | RAM-snapshot observation | |---|---|---| | Section base accumulator | `$4A..$52` (3 axes × 24-bit MID/HI/LO) | `$2A..$2F` (3 axes × 16-bit LE) | | L631D scale cache | `$35/$36` | `$38/$39` | | Polygon-mode flag (set by $2F, checked by $29/$41) | `$2C` | `$29` | | Y-axis scratch (in $07/$24 setup) | `$1B/$1C` | `$1A/$1B` | | Z-axis scratch (in $07/$24 setup) | `$1E/$1F` | `$1C/$1D` | | `EmitClippedLine` tail | `JMP SceneryInterpreterStep` | `RTS` (likely from scenery $1A overwriting the JMP opcode) | | Op handler addresses | per source labels (e.g. `$6AAD`) | offset (likely the result of multi-step scenery-driven mutation) | | Chunk3 callbacks ($05 ADF, $0E 64K-call, $1D NAV, $1E COM, $03 rotated transform) | mostly no-ops on 48K | wired up to chunk3 routines via Apply64KPatchTable (this row IS a real Apply64KPatchTable patch) | Where this document says "source" or "RAM-snapshot", that's what's meant. Code and addresses cited as just "chunk5.s:line" refer to the source disassembly. --- ## Vertex emit / line draw family ### $00 SceneryOpEmitV1XformAndPlot (xform-A V1 + single-pixel plot) **Length:** 7 bytes (`$00 X_lo X_hi Y_lo Y_hi Z_lo Z_hi`) Reads 6 stream bytes as a 16-bit X, Y, Z vertex. Transforms via `TransformVertex80C5` into V1 (`$CB..$D0`). If V1's outcode is 0 (= in frustum), projects to screen and PLOTS A SINGLE PIXEL via `PlotColorPixel`. Used for star fields, runway threshold dots, etc. ### $01 SceneryOpEmitV1Xform80C5 (xform-A V1, no draw) **Length:** 7 bytes (`$01 X_lo X_hi Y_lo Y_hi Z_lo Z_hi`) Same as $00 but no pixel plot. Just transforms V1, classifies, and stores shadow at `$07..$10`. Used to set the start endpoint for a subsequent $02 line emit. ### $02 SceneryOpEmitV2Xform80C5 (xform-A V2 + line v1->v2) **Length:** 7 bytes (`$02 X_lo X_hi Y_lo Y_hi Z_lo Z_hi`) Transforms V2 via `TransformVertex80C5`. If polygon-mode flag (`$2C`, MAME-patched: `$29`) is non-zero, append V2 to PrimVerts polygon array. Otherwise tail-jump to `EmitClippedLine` to draw a line from V1 to V2. ### $40 SceneryOpEmitV1Xform7EBC (xform-B V1) **Length:** 5 bytes (`$40 X_lo X_hi Z_lo Z_hi`) Like $01 but uses `TransformVertex7EBC` (= xform-B). Stream encodes only X and Z (16-bit each); Y is implicit from base accumulator at `$2A..$2F` (MAME-patched) or `$4A..$52` (source). Used for ground/runway polygons where vertices share a base Y. ### $41 SceneryOpEmitV2Xform7EBC (xform-B V2 + line v1->v2) **Length:** 5 bytes (`$41 X_lo X_hi Z_lo Z_hi`) Like $02 but xform-B. Same polygon-mode-vs-line-emit branch. After EmitClippedLine, V1 is replaced by V2 (= chained polyline). ### $42 SceneryOpRefreshCachedXform7EBC (cache-update V2 with xform-B) **Length:** 6 bytes (`$42 vertex_idx X_lo X_hi Z_lo Z_hi`) Reads vertex_idx, points `($2D)` at cached vertex slot (`$0140 + idx*8`). Snapshots V2 ($D2..$DA) to $DB shadow, runs xform-B on stream bytes into V2, writes transformed V2 back to the cache slot. Used to refresh dynamic vertices (e.g. moving aircraft hash marks). ### $31 SceneryOpRefreshCachedXform80C5 (cache-update with xform-A) **Length:** 8 bytes (`$31 vertex_idx X_lo X_hi Y_lo Y_hi Z_lo Z_hi`) Same as $42 but xform-A (full XYZ stream). ### $32 SceneryOpVertexCachedV1 (load cached vertex into V1) **Length:** 2 bytes (`$32 vertex_idx`) If `$2C` set (= polygon mode), copies cached vertex via `EmitPrimaryVertex`. Otherwise loads cached 8-byte record into V1 (`$CB..$D2`) and shadow (`$09..$10`). ### $33 SceneryOpVertexCachedV2 (load cached vertex into V2 + line) **Length:** 2 bytes (`$33 vertex_idx`) Like $32 for V2. If polygon mode off, tail-jumps to EmitClippedLine to draw v1->v2. ### $35 SceneryOpVertexCachedDraw (load cached + plot pixel) **Length:** 2 bytes (`$35 vertex_idx`) Loads cached vertex into V1, then if in-frustum plots a single pixel. Same plot path as $00. ### $2B SceneryOpEmitCurve (8-segment cubic curve via subdivision) **Length:** 9 bytes (`$2B v1_X_lo v1_X_hi v1_Z_lo v1_Z_hi v2_X_lo v2_X_hi v2_Z_lo v2_Z_hi`) Reads V1 stream bytes, transforms via xform-B. Backs cursor by 1 (so V2 reads from same offset advance). Reads V2 stream bytes, transforms. Subdivides V2-V1 into 8 equal segments via repeated halving, emits each segment as a clipped line. Used for cockpit gauge needle arcs and curved runway markers. Self-modifies `L6B4F` to RTS during emission. ### $06 SceneryOpDrawLine (raw screen-space line, no transform) **Length:** 5 bytes (`$06 X1 Y1 X2 Y2`) Copies 4 stream bytes directly into `$E9..$EC` and calls `DrawColorLine`. No vertex transform. Used by 2D scenery (radar overlays, instrument panel lines). --- ## Polygon control ### $29 SceneryOpCopyToD2 (polygon fill OR single line emit) **Length:** 1 byte (`$29`) If `$2C` (MAME: `$29`) flag is set: takes the `Op29PolygonFillBranch` which decrements `$B5` (vertex count), and if vertex count > 0 calls `PolygonScanFillSetup` to scan-fill the accumulated PrimVerts polygon via DrawColorSpan. After fill, clears the flag. If flag is 0: takes the `Op29LineEmitBranch` — copies 9 bytes from `$07..` (= V1 shadow) into `$D2..` (= V2 slot), advances cursor by 1, then jumps to `EmitClippedLine` to draw a single line from V1 to V2. ### $2F SceneryOpResetState (start polygon mode) **Length:** 1 byte (`$2F`) Sets `$2C` = `$FF` (= polygon mode ON, so subsequent $02/$41 ops accumulate vertices instead of drawing lines). Clears `$B5` (= polygon vertex count) to 0. In MAME-patched binary the flag is at `$29` instead of `$2C`. ### $1B SceneryOpModeWhite (force WHITE color, restore drawer) **Length:** 1 byte (`$1B`) Self-modifies the line/pixel kernel back to its standard plot opcodes (undoes any night-mode BPL skips from $1C). Calls `SetPixelDrawMode` with A=$03 (= HIRES_WHITE1). ### $1C SceneryOpDayOnly (suppress draws at night) **Length:** 1 byte (`$1C`) If `$083C` bit 0 == 0 (night), patches DrawColorLine kernel with BPL opcodes so subsequent line/pixel emits skip the paint write. Daytime is a no-op. ### $12 SceneryOpSetColor (set draw color from table index) **Length:** 2 bytes (`$12 code`) Reads next byte (0..15) as index into `ToHiresColorTable`, calls `SetPixelDrawMode` with the resulting hires color. Mapping: | code | hires | typical use | |------|-------|------| | $00 | BLACK1 | sky/null | | $01 | GREEN | ground (day) | | $02 | VIOLET | water (day) | | $03 | GREEN | | | $04 | VIOLET | | | $05 | BLACK1 | building shadow | | $06 | VIOLET | runway edge | | $07 | VIOLET | building | | $08 | BLACK1 | | | $09 | WHITE1 | aircraft wing/tail | | $0A | BLACK1 | | | $0B | GREEN | | | $0C | VIOLET | | | $0D | WHITE1 | | | $0E | VIOLET | haze | | $0F | WHITE1 | "city" (Hancock tower, runway centerline) | --- ## Coordinate frame management ### $07 SceneryOpEnterLocalFrame (enter sub-record frame, no stash) **Length:** 14 bytes (`$07 variant 6×16-bit-anchor`) Variant byte selects axis-permutation cascade (0=`FrameVariantRolShift`, 2=`FrameVariantSwap`, 4=`FrameVariantAsl4`, 5+=`FrameVariantPassthrough`, others fall straight through to `FrameSetupEpilogue`). The 6 anchor coords (= 3 axes × 16-bit) encode the section's origin. `ComputeSectionCameraDelta` SBC chain computes camera-vs-section delta into `$66/$68/$6A`. `FrameSetupEpilogue` calls `RecomputeSectionBase` (= former L631D) to refresh the base accumulator. ### $24 SceneryOpPushOriginWithStash (enter sub-record frame, with stash) **Length:** 8 bytes (`$24 variant 3×16-bit-anchor`) Like $07 but only 3 anchor coords (skips the lo-precision SBC for $5A/$5E/$62). Pre-populates `$18/$1B/$1E` from aircraft full position before L6BB0 cascade. Used inside polygon batches where the parent has already set up the frame and only needs scaling. --- ## Subroutines ### $18 SceneryOpSubInvoke (recursive sub-record) **Length:** 3 bytes (`$18 offset_lo offset_hi`) Pushes current cursor + 3 (= return address) on stack as artificial RTS target. Computes sub-stream cursor = current + signed offset. Sets $8B/$8C to sub-stream. Calls `JSR SceneryInterpreterStep`. After return (= when an op RTSes), restores cursor and JMPs to interpreter to continue parent stream. Increments `$08E1` (sub-depth counter). ### $19 SceneryOpReturn (RTS — exit current dispatcher) **Length:** 1 byte (`$19`) RTS immediately. Returns to whoever called `SceneryInterpreterStep`, either the main loop or `$18` SubInvoke handler. The handler's first byte IS `$60` (RTS), nothing else. ### $0B SceneryOpJumpRelative (relative jump within stream) **Length:** 3 bytes (`$0B offset_lo offset_hi`) Reads signed 16-bit offset, sets cursor = cursor + offset, continues interpreting from the new position. ### $0E SceneryOpCall64K (chunk3 callback if 64K, recursion-pop if 48K) **Length:** 0 bytes advance (the opcode itself is 1 byte in the stream but the handler does NOT advance the cursor past it). If `Has64K` flag set, JMP to `SceneryOp64KCallback` (= chunk3 entry that processes more chunk5-style records). On 48K: falls through into `SceneryOpReturn` which is a bare RTS — and that RTS pops one level of dispatcher recursion. If $0E is reached at the TOP level the RTS returns to whoever called `ProcessScenery` (= the main loop). If reached from inside a `$18 SubInvoke` recursion, the RTS returns to the parent section's dispatcher. ### $03 SceneryOpCall64K_2 (chunk3 SceneryRotatedTransform if 64K) **Length:** 6 bytes on 48K (skip), variable on 64K If 64K, JMP to `SceneryRotatedTransform` in chunk3 (= 3D rotated transform helper). On 48K, advance 6 and continue. --- ## Header / data load ### $0D SceneryOpHeader (load section header + trigger demand-load) **Length:** 6 bytes (`$0D byte1 byte2 byte3 byte4 byte5`) Copies 5 inline payload bytes into `$08E5..$08E9` (= section ID, sector count, destination address, cache slot index). If high bit of byte5 (= `$08E9`) is clear, cumulatively advances the saved section-base cursor at `$08E7` by the current dispatcher cursor `$8B`. Masks `$08E9` to a cache slot index (0..3), then calls `SceneryHeaderLoadIfMiss` via the `SceneryHeaderLoadTrampoline` at `L8776`. On a cache miss, that routine invalidates newer slots and DMA-fetches the section bytecode from disk via `SceneryHeaderRunSection` (which sets `L1E03 = $08E7` so the loaded bytes land at the saved cursor's address). On a cache hit, returns immediately. Total record = 6 bytes (1 opcode + 5 payload). ### $11 SceneryOpSkip1 (skip 1 byte) **Length:** 1 byte total (= just the opcode; no payload). Advance cursor by 1. ### $09 / $0A SceneryOpSkip3 (skip 3 bytes) **Length:** 3 bytes each (= opcode + 2 unused payload bytes). Advance cursor by 3. --- ## Memory ops ### $1A SceneryOpWriteWord (`*dst = *src`) **Length:** 5 bytes (`$1A dst_lo dst_hi src_lo src_hi`) Reads 2 bytes from `*src` (= memory at the src pointer) and writes them to `*dst`. Used to copy state between scenery's 16-bit slots (e.g. copying a saved color register). ### $25 SceneryOpStoreImmWord (`*dst = imm16`) **Length:** 5 bytes (`$25 dst_lo dst_hi imm_lo imm_hi`) Writes the immediate 16-bit value into `*dst`. --- ## Conditional jumps / culls ### $13 SceneryOpJumpIfBeyondXY (cull XY box, jump alt path on fail) **Length:** 9 bytes (`$13 jumpOff_lo jumpOff_hi 2×(ptr_lo ptr_hi lo hi)`) Reads jump target and 2 (pointer, low_bound, high_bound) triples — one for each of two axes. Compares the value at each pointer against the bounds. On failure (= camera outside box) jumps to the alternate target via `L6EE6`. Otherwise advance 9 and continue. ### $14 SceneryOpJumpIfBeyondXYZ (cull XYZ box, jump alt on fail) **Length:** 14 bytes (`$14 jumpOff_lo jumpOff_hi 2×(triple) + Z_lo Z_hi`) Like $13 but adds explicit Z comparison against `$60/$61` (or `ZoomLevel+1/+2` in radar view) using the supplied Z bound at `$98/$99`. ### $20 SceneryOpCullIfOutside1 (1-axis bounding cull) **Length:** 9 bytes Calls `TestSceneryRange` once. On failure redirects via L6EE6. ### $21 SceneryOpCullIfOutside2 (2-axis bounding cull) **Length:** 15 bytes Calls `TestSceneryRange` twice. ### $22 SceneryOpCullIfOutside3 (3-axis bounding cull) **Length:** 21 bytes Calls `TestSceneryRange` three times. ### $04 SceneryOpCullByOutcodeList (cull when all listed vertices share outcode bit) **Length:** variable (`$04 jumpOff_lo jumpOff_hi vertex_idx... `) Initializes `$DC = $F8`. For each vertex_idx in the stream (until a byte with bit 7 set), points `($2D)` at cached vertex, ANDs the vertex's outcode high byte into `$DC`. If after all vertices `$DC != 0` (= all share at least one off-screen half-plane), jumps to the alternate target. ### $23 SceneryOpJumpIfBitsClear (mask AND test, jump on no-bits-set) **Length:** 7 bytes (`$23 jumpOff_lo jumpOff_hi ptr_lo ptr_hi mask1 mask2`) If `(mask1 AND *(ptr+0)) == 0` AND `(mask2 AND *(ptr+1)) == 0`, jump. Used to gate scenery on aircraft state flags (landing-gear, lights, etc.). ### $28 SceneryOpJumpIfWordCompare (compare two pointers, jump on match) **Length:** 8 bytes (`$28 mode jumpOff_lo jumpOff_hi ptr1_lo ptr1_hi ptr2_lo ptr2_hi`) Loads `mode` (0=eq, 1=signed-lt, 2=signed-lt-alt). Compares 16-bit `*ptr1` vs `*ptr2`. On match jumps via L6EE6. --- ## Station records (NAV/COM/ADF radio) ### $05 SceneryOpADFRecord (ADF station) **Length:** 9 bytes on 48K (skip), variable on 64K 48K: just advance 9. 64K: patched to JMP `LookupADFStation` in chunk3 which matches against the user's tuned ADF frequency and copies the station descriptor on hit. ### $1D SceneryOpNAVRecord (NAV1/NAV2 station) **Length:** 11 bytes (`$1D freq_lo freq_hi 8×descriptor`) Tries the record's 2-byte frequency against `$08F7` (NAV1 tune) and `$08F5` (NAV2 tune) via `MatchNAVFreq`. On a hit, copies the 8-byte descriptor (X, Y, Z, ranges...) into `$08F9` (NAV1 slot) or `$0901` (NAV2 slot), and ORs the active bit into `$08F4`. ### $1E SceneryOpCOMRecord (COM/airport) **Length:** variable (= byte 1 holds record length) Reads record length from byte 1. If record's COM frequency matches the user's tuned COM (`$089F/$08A0`), copies the airport position into `$0905+`, latches a pointer to the inline airport name string at `$092A`, and triggers the ATIS message renderer. Otherwise skips by the length. --- ## Invalid / terminator opcodes ### $08, $0C, $0F, $10, $15, $16, $17, $1F, $26, $27, $2A, $2C, $2D, $2E, $30, $34, $36-$3F, $43, $44, $45 SceneryOpInvalid When dispatched, falls through to `SceneryStreamEnd`: clears scenery in-progress flags (`$0916`, `$08FF`, `$0E3D`) and RTSes. Effectively terminates the current stream. Any opcode with bit 7 set ($80-$FF) OR opcode >= $46 also triggers `SceneryStreamEnd`. --- ## Quick reference — record lengths | op | name | bytes | side effects | |-----|-------------------|-------|---------------------------------| | $00 | EmitV1+Plot | 7 | xform-A V1, plot pixel | | $01 | EmitV1 xform-A | 7 | xform-A V1, no draw | | $02 | EmitV2 xform-A | 7 | xform-A V2, draw line v1->v2 | | $03 | Call64K_2 | 6 | chunk3 callback (64K only) | | $04 | CullByOutcodeList | var | jump if shared off-screen half | | $05 | ADFRecord | 9 | match tuned ADF freq | | $06 | DrawLine | 5 | raw 2D line at screen coords | | $07 | EnterLocalFrame | 14 | section frame setup, no stash | | $0B | JumpRelative | 3 | cursor += signed16 | | $0D | Header | 6 | section header + demand-load | | $0E | Call64K | 0 | chunk3 callback (64K); 48K = RTS pops recursion | | $11 | Skip1 | 1 | bare opcode, no payload | | $12 | SetColor | 2 | set draw color from table | | $13 | JumpIfBeyondXY | 9 | XY cull, jump alt | | $14 | JumpIfBeyondXYZ | 14 | XYZ cull, jump alt | | $18 | SubInvoke | 3 | recursive sub-stream | | $19 | Return | 0 | RTS — pops dispatcher recursion (no cursor advance) | | $1A | WriteWord | 5 | *dst = *src (word) | | $1B | ModeWhite | 1 | set color WHITE, restore drawer | | $1C | DayOnly | 1 | night → patch drawer to skip | | $1D | NAVRecord | 11 | match tuned NAV1/NAV2 freq | | $1E | COMRecord | var | match tuned COM, ATIS draw | | $20 | CullIfOutside1 | 9 | 1-axis bounding cull | | $21 | CullIfOutside2 | 15 | 2-axis bounding cull | | $22 | CullIfOutside3 | 21 | 3-axis bounding cull | | $23 | JumpIfBitsClear | 7 | mask AND test, jump if zero | | $24 | PushOriginWithStash | 8 | section frame setup, stashed | | $25 | StoreImmWord | 5 | *dst = imm16 | | $28 | JumpIfWordCompare | 8 | cmp *p1 *p2, jump on match | | $29 | CopyToD2 | 1 | polygon fill OR line emit | | $2B | EmitCurve | 9 | 8-segment subdivided curve | | $2F | ResetState | 1 | polygon mode ON, vertex count=0 | | $31 | RefreshCachedXform80C5 | 8 | reload cached vertex xform-A | | $32 | VertexCachedV1 | 2 | load cached V1 | | $33 | VertexCachedV2 | 2 | load cached V2 + line | | $35 | VertexCachedDraw | 2 | load cached + plot | | $40 | EmitV1 xform-B | 5 | xform-B V1, no draw | | $41 | EmitV2 xform-B | 5 | xform-B V2, draw line v1->v2 | | $42 | RefreshCachedXform7EBC | 6 | reload cached vertex xform-B | --- ## Coordinate frames xform-A (`TransformVertex80C5` at chunk5.s:5776) uses a full 6-byte (X, Y, Z) stream and the 16-bit `ZPScale` multiplier for all 9 matrix entries. The routine itself advances the dispatcher cursor `$8B` by 7 (opcode + 6-byte payload), so the surrounding opcodes ($00/$01/$02) declare their record length as 7 bytes. xform-B (`TransformVertex7EBC` at chunk5.s:5425) uses a 4-byte (X, Z) stream — Y is implicit from the section base accumulator at `$4A..$52` (source) or `$2A..$2F` as observed in the RAM snapshot. Uses the 8-bit `op_l1818` multiplier. The routine advances the cursor by 5 (opcode + 4-byte payload), giving $40/$41 a 5-byte record length. The 3x3 rotation matrix lives at `$78..$89`: - `$78/$79` = X-out-from-X-delta - `$7A/$7B` = Y-out-from-X-delta - `$7C/$7D` = Z-out-from-X-delta - `$7E/$7F` = X-out-from-Y-delta (= matrix row 2 used by L631D) - `$80/$81` = Y-out-from-Y-delta - `$82/$83` = Z-out-from-Y-delta - `$84/$85` = X-out-from-Z-delta - `$86/$87` = Y-out-from-Z-delta - `$88/$89` = Z-out-from-Z-delta Camera ZP at `$66..$6B`: - `$66/$67` = camera X delta (post-section-anchor subtract) - `$68/$69` = camera Y delta - `$6A/$6B` = camera Z delta Section base accumulator (= L631D output): - Source layout: `$4A..$52` as 24-bit MID/HI/LO per axis - MAME-patched layout: `$2A..$2F` as 16-bit LE per axis `RecomputeSectionBase` (= former L631D) fires from `FrameSetupEpilogue` (= former L6D28) when `$68/$69` differs from the cache at `$35/$36` (source) or `$38/$39` (MAME-patched). ## Renamed labels (= what we changed in chunk5.s) The disassembly originally used `L` placeholder labels for branch targets. We've added semantic names for the scenery-interpreter family (L kept as `.refto` aliases so existing references still resolve). ### Frame setup ($07/$24 family) | Old | New | What it is | |---|---|---| | L631D | `RecomputeSectionBase` | matrix*scale → base accumulator | | L6363 | `ScaleSignedC2` | signed multiply for one base-axis | | L6379 | `ScaleSignedC2_DoMultiply` | multiply step inside ScaleSignedC2 | | L639C | `ScaleSignedC2_Exit` | RTS | | L6BB0 | `ComputeSectionCameraDelta` | $07/$24 SBC chain | | L6BC3 | `ComputeSectionCameraDelta_X` | X-axis SBC | | L6BEB | `ComputeSectionCameraDelta_Y_Radar` | Y-axis radar branch | | L6BFE | `ComputeSectionCameraDelta_Y_NonRadar` | Y-axis non-radar branch | | L6C10 | `ComputeSectionCameraDelta_Y_NonRadarStash` | Y-axis stash variant | | L6C1E | `ComputeSectionCameraDelta_Z` | Z-axis SBC | | L6C31 | `ComputeSectionCameraDelta_Z_Upper` | Z-axis upper-byte SBC | | L6C48 | `FrameVariantDispatch` | variant lookup + JMP | | L6C53 | `FrameVariantPassthrough` | variant 5+ (= identity) | | L6C6E | `FrameVariantSwap` | variant 2 (= lo/hi swap) | | L6C89 | `FrameVariantAsl4` | variant 4 (= asl×4) | | L6CCE | `FrameVariantRolShift` | variant 0 (= rol-shift) | | L6D28 | `FrameSetupEpilogue` | $07/$24 cache-check + L631D fire | | L6D32 | `FrameSetupEpilogue_CacheCheck` | scale cache check | | L6D40 | `FrameSetupEpilogue_FireRecompute` | call to L631D | | L6D43 | `FrameSetupEpilogue_UpdateCache` | $36 := $69 | | L6B8A | `PushOriginWithStash_NonRadar` | $24 non-radar Y scratch | | L6B92 | `PushOriginWithStash_RunChain` | $24 stashed-mode chain entry | ### Polygon fill kernel ($29 + L6F98) | Old | New | What it is | |---|---|---| | L67CE | `Op29LineEmitBranch_CopyLoop` | shadow→V2 copy loop | | L67DD | `Op29PolygonFillBranch` | $29 with $2C set | | L67E8 | `Op29PolygonFillBranch_ClearAndAdvance` | clear $2C tail | | L6F98 | `PolygonScanFillSetup` | polygon fill kernel entry | | L7095 | `PolygonClipTopPass` | clip pass 2 (top edge) | | L7190 | `PolygonClipRightPass` | clip pass 3 (right edge) | | L728B | `PolygonClipBottomPass` | clip pass 4 (bottom edge) | | L7826 | `PolygonScanFillRow` | per-row scan-line emitter | ### EmitClippedLine + curve | Old | New | What it is | |---|---|---| | L6AF7 | `EmitClippedLine_ClipV2` | snapshot+clip V2 phase | | L6AFD | `EmitClippedLine_SnapshotV2Loop` | V2→shadow copy loop | | L6B0D | `EmitClippedLine_ProjectV1IfNeeded` | conditional V1 project | | L6B15 | `EmitClippedLine_ProjectV2IfNeeded` | conditional V2 project | | L6B1C | `EmitClippedLine_PrepareEndpoints` | load $E9..$EC | | L6B2C | `EmitClippedLine_DoDraw` | jsr DrawColorLine | | L6B2F | `EmitClippedLineCleanup` | V1 restore tail | | L6B35 | `EmitClippedLineCleanup_FromShadow` | restore V1 from $DB | | L6B3F | `EmitClippedLineCleanup_FromV2` | restore V1 from $D2 (= unmodified V2) | | L6B41 | `EmitClippedLineCleanup_FromV2Loop` | copy loop | | L6B48 | `EmitClippedLineCleanup_ClearFlags` | $8A = $08C4 = 0 | | L6B4F | `EmitClippedLineTail` | self-modified JMP/RTS | | L6A03 | `EmitCurve_ReadV2` | $2B V2 read | | L6A0C | `EmitCurve_AlignExponents` | scale-align V1 vs V2 | | L6A20 | `EmitCurve_HalveV2` | V2 halve branch | | L6A2A | `EmitCurve_BuildStep` | (V2-V1)/16 step build | | L6A69 | `EmitCurve_SegmentLoop` | per-segment emit loop | ### Vertex emit / cache ($00/$01/$02/$32/$33/$35/$40/$41/$42) | Old | New | What it is | |---|---|---| | L6839 | `ProcessVertex2_LineMode` | $2C clear → classify+project V2 | | L6843 | `ProcessVertex2_Exit` | shared RTS | | L6874 | `ProcessVertex1_LineMode` | $2C clear → classify+project V1 | | L687E | `SnapshotVertex1ToShadow` | save V1 to $07.. | | L6880 | `SnapshotVertex1ToShadow_Loop` | copy loop | | L6893 | `EmitV1Common` | $01/$40 setup (xform-A vs B select) | | L689C | `ResumeAfterVertexEmit` | dispatcher resume after silent emit | | L68AA | `EmitV2Common` | $02/$41 setup | | L68C7 | `VertexCachedV1_LineMode` | $32 line-mode load | | L68CF | `VertexCachedV1_CopyLoop` | cache → V1 copy | | L68E4 | `VertexCachedV1_Advance` | RTS to dispatcher | | L68FB | `VertexCachedV2_LineMode` | $33 line-mode load | | L6901 | `VertexCachedV2_CopyLoop` | cache → V2 copy | | L6911 | `VertexCachedV2_Emit` | tail-jump to EmitClippedLine | | L6952 | `RefreshCachedXform_Common` | $31/$42 shared body | | L695D | `RefreshCachedXform_SnapshotV2` | save V2 | | L6971 | `RefreshCachedXform_WriteCache` | write transformed V2 to cache | | L6973 | `RefreshCachedXform_WriteLoop` | copy loop | | L697D | `RefreshCachedXform_RestoreV2` | restore V2 from snapshot | | L698A | `SetVertexPointerFromIdx` | cached-vertex slot lookup (idx in A) | | L69B5 | `VertexCachedDraw_LoadLoop` | $35 cache → V1 copy | | L69D5 | `EmitV1AndPlotTail` | $00/$35 frustum-test tail | | L69DC | `EmitV1AndPlotTail_DoPlot` | actually plot the pixel | | L69E3 | `EmitV1AndPlotTail_Resume` | resume dispatcher | ### xform-A (TransformVertex80C5) internals | Old | New | What it is | |---|---|---| | L80D2 | `XformA_SBCChain` | stream→delta SBC chain | | L810E | `XformA_SBCChain_StoreZHi` | store Z high byte | | L8110 | `XformA_AutoScaleLoop` | auto-scale shift loop | | L81D9 | `XformA_OverflowRecoverX` | X overflow path | | L820F | `XformA_OverflowRecoverY` | Y overflow path | | L821E | `XformA_OverflowRecoverZ` | Z overflow path | ### xform-B (TransformVertex7EBC) internals | Old | New | What it is | |---|---|---| | L7EAD | `XformBOverflowRecoverXZ` | X+Z overflow recovery | | L7F1A | `XformBAutoScaleLoop` | left-shift loop | | L7F5C | `XformBOverflowRecoverZ` | Z-only overflow recovery | | L7F64 | `XformBOverflowRecoverX` | X-only overflow recovery | | L7F7F | `XformBHalveBaseAccumulators` | post-overflow halve epilogue | | L7F96 | `XformBMatrixMultiply` | matrix-multiply tail entry | | L8091 | `XformBStoreOutput` | vertex result writeback | | L80B0 | `XformBHalveAccumulators` | post-multiply overflow halver | ### Conditional jumps / culls | Old | New | What it is | |---|---|---| | L6DB9 | `CullByOutcodeList_ResetAccum` | reset $DC AND mask | | L6DBB | `CullByOutcodeList_NextVertex` | next-vertex top | | L6DCF | `CullByOutcodeList_AndOutcode` | AND outcode bits into mask | | L6DD9 | `CullByOutcodeList_End` | end-of-list test + branch | | L6DF0 | `CullSucceedAndContinue` | shared continue | | L6E07 | `JumpIfBeyondXYZ_Z_NonRadar` | non-radar Z bounds test | | L6E0D | `JumpIfBeyondXYZ_Z_TestHi` | high-byte Z test | | L6E14 | `JumpIfBeyondXYZ_Continue` | inside-bounds path | | L6EBF | `TestSceneryRange_TestLow` | low-bound test | | L6ECE | `TestSceneryRange_LowOverflow` | low overflow gate | | L6ED0 | `TestSceneryRange_TestHigh` | high-bound test | | L6EE1 | `TestSceneryRange_HighOverflow` | high overflow gate | | L6EE3 | `TestSceneryRange_Pass` | inside-range RTS | | L6EE4 | `TestSceneryRange_StripAndJump` | strip JSR + take alt jump | | L6EE6 | `JumpToFetchedTarget` | conditional-jump take-branch path | | L6F16 | `JumpIfBitsClear_NoJump` | $23 no-jump path | | L6F55 | `JumpIfWordCompare_Mode1Overflow` | $28 mode 1 V-flag gate | | L6F57 | `JumpIfWordCompare_Mode1Jump` | $28 mode 1 take-jump | | L6F5A | `JumpIfWordCompare_Mode2` | $28 mode 2 entry | | L6F6C | `JumpIfWordCompare_Mode2Overflow` | $28 mode 2 V-flag gate | | L6F6E | `JumpIfWordCompare_Mode2Jump` | $28 mode 2 take-jump | | L6F71 | `JumpIfWordCompare_Mode0` | $28 mode 0 (= equality) | | L6F84 | `Op28NoJump` | $28 word-compare no-match path | ### Misc op handlers | Old | New | What it is | |---|---|---| | L6063 | `MatchNAVFreq_CopyLoop` | NAV descriptor copy | | L6076 | `MatchNAVFreq_SetActiveBit` | OR active flag into $08F4 | | L607C | `MatchNAVFreq_Exit` | RTS | | L609F | `SceneryOpCOMRecord_CopyDescriptorLoop` | COM descriptor copy | | L60BD | `SceneryOpCOMRecord_AdvanceAndContinue` | skip past record | | L60FD | `SceneryOpHeader_RunSetup` | mask + jsr L8776 | | L7AF2 | `SceneryOpDayOnly_Exit` | shared advance-and-continue | ### Setup / view projection | Old | New | What it is | |---|---|---| | L612D | `SetupViewProjection_NotRadar` | non-radar entry | | L6155 | `SetupViewProjection_SideOrForwardView` | ViewDirection 0..n | | L617C | `SetupViewProjection_NegateAngle` | angle negation branch | | L6199 | `SetupViewProjection_BuildXZBasis` | sin/cos build | | L61F0 | `SetupViewProjection_BuildMatrix` | converged matrix builder | | L6210 | `SetupViewProjection_NormaliseAngle` | angle normalisation | ### Polygon clipping passes (new this round) | Old | New | What it is | |---|---|---| | L741C | `SwapVertices_Loop` | V1↔V2 byte swap loop | | L742C | `ClipVertex2ToFrustum_Exit` | empty-outcode RTS | | L742D | `ClipVertex2ToFrustum_TestRight` | bit 6 → ClipRight | | L743D | `ClipVertex2ToFrustum_TestLeft` | bit 5 → ClipLeft | | L744F | `ClipVertex2ToFrustum_TestBottom` | bit 4 → ClipBottom | | L7461 | `ClipVertex2ToFrustum_TestTop` | bit 3 → ClipTop | | L746D | `ClipVertex2ToTop_RetryAfterHalve` | overflow-recovery entry | | L74EB | `ClipVertex2ToBottom_RetryAfterHalve` | "" | | L7573 | `ClipVertex2ToLeft_RetryAfterHalve` | "" | | L75F1 | `ClipVertex2ToRight_RetryAfterHalve` | "" | ### Outcode classifiers (new this round) | Old | New | What it is | |---|---|---| | L738A | `ClassifyVertex1_TestRight` | X+Z half-plane | | L7399 | `ClassifyVertex1_TestLeft` | Z-X half-plane | | L73A8 | `ClassifyVertex1_TestBottom` | Y+Z half-plane | | L73B7 | `ClassifyVertex1_TestTop` | Z-Y half-plane | | L73C8 | `ClassifyVertex1_StoreAndExit` | RTS path | | L73D3 | `ClassifyVertex2_TestRight` | "" for V2 | | L73E2 | `ClassifyVertex2_TestLeft` | "" | | L73F1 | `ClassifyVertex2_TestBottom` | "" | | L7400 | `ClassifyVertex2_TestTop` | "" | | L7411 | `ClassifyVertex2_StoreAndExit` | "" | Outcode bits (= shared by both classifiers): - bit 7: behind viewer (Z < 0) - bit 6: right of right frustum plane (X+Z < 0) - bit 5: left of left frustum plane (Z-X < 0) - bit 4: below bottom frustum plane (Y+Z < 0) - bit 3: above top frustum plane (Z-Y < 0) ### FlipPagesFillViewport + sky/ground fill | Old | New | What it is | |---|---|---| | L63A2 | `FlipPagesFillViewport_RotateRowTable` | rotate HiresTableHi for page swap | | L63BB | `FlipPagesFillViewport_FlipToHires` | STA HISCR | | L63BE | `FlipPagesFillViewport_RunHorizonPoly` | $0AB9 horizon-line entry | | L644A | `FlipPagesFillViewport_StoreGroundColor` | save ground tint | | L6457 | `FlipPagesFillViewport_StoreSkyColor` | save sky tint | | L6467 | `FlipPagesFillViewport_PrepRow` | call SetEvenAndOddColors | | L646D | `FlipPagesFillViewport_DetermineExtents` | min/max screen-Y for horizon | | L6482 | `FlipPagesFillViewport_FillFullViewport` | no-horizon-line path | | L6485 | `TrackHorizonExtent` | min/max helper | | L648B | `TrackHorizonExtent_TestMax` | max-side test | | L6491 | `TrackHorizonExtent_Exit` | RTS | | L6492 | `FlipPagesFillViewport_HorizonEmitted` | horizon-line setup | | L64B9 | `FlipPagesFillViewport_AboveDiag_TestSwap` | sky/ground swap test | | L64BD | `FlipPagesFillViewport_AboveDiag_DoSwap` | "" do | | L64C0 | `FlipPagesFillViewport_AboveDiag_Fill` | fill upper rows | | L64CC | `FlipPagesFillViewport_DiagFill_TestSwap` | "" for diagonal rows | | L64D9 | `FlipPagesFillViewport_DiagFill_TestSwap2` | "" alt path | | L64E3 | `FlipPagesFillViewport_DiagFill_DoSwap` | swap | | L64E6 | `FlipPagesFillViewport_DiagFill_Fill` | call FillMixedViewportRows | | L64FC | `FlipPagesFillViewport_BelowDiag_TestSwap` | below-diag swap test | | L6506 | `FlipPagesFillViewport_BelowDiag_DoSwap` | "" do | | L6509 | `FlipPagesFillViewport_BelowDiag_Fill` | fill lower rows | | L6515 | `FlipPagesFillViewport_FullSky` | end + UpdateArtificialHorizon | | L6555 | `FillViewportRows_SolidStart` | optimized solid-color start | | L6557 | `FillViewportRows_SolidLoop` | unrolled fill loop | | L6567 | `FillViewportRows_ColorFill` | call DrawSkyGroundRowUnrolled | | L656A | `FillViewportRows_Tail` | row decrement + loop | | L6637 | `DrawSkyGroundRow_NoSplit` | edge case at column $27 | | L668B | `DrawSkyGroundRow_FillRightSide` | DrawColorSpan right-of-transition | | L6695 | `DrawSkyGroundRow_TestFillColor` | test before left-side fill | | L669D | `DrawSkyGroundRow_FillLeftSide` | DrawColorSpan left-of-transition | | L66A4 | `DrawSkyGroundRow_Exit` | RTS | ### DrawColorLine + PlotColorPixel + DrawColorSpan paint sites | Old | New | What it is | |---|---|---| | L7921 | `DCSPixelOraOp` | DrawColorSpan ORA self-mod | | L7922 | `DCSPixelOraOperand` | "" operand | | L792D | `DCSPixelAndOp` | DrawColorSpan AND self-mod | | L792E | `DCSPixelAndOperand` | "" operand | | L7976 | `PlotColorPixel_OraOp` | PlotColorPixel ORA self-mod | | L7977 | `PlotColorPixel_OraOperand` | "" operand | | L7982 | `PlotColorPixel_AndOp` | PlotColorPixel AND self-mod | | L7983 | `PlotColorPixel_AndOperand` | "" operand | | L7A1A | `DCLYMajor_OraMaskOp` | DCL Y-major ORA | | L7A1B | `DCLYMajor_OraMaskOperand` | | | L7A26 | `DCLYMajor_AndMaskHiOp` | split-byte hi half AND | | L7A27 | `DCLYMajor_AndMaskHiOperand` | | | L7A2E | `DCLYMajor_AndMaskLoOp` | non-split AND | | L7A2F | `DCLYMajor_AndMaskLoOperand` | | | L7A8E | `DCLXMajor_OraMaskOp` | DCL X-major ORA | | L7A8F | `DCLXMajor_OraMaskOperand` | | | L7A9A | `DCLXMajor_AndMaskOp` | DCL X-major AND | | L7A9B | `DCLXMajor_AndMaskOperand` | | ### DrawColorLine internals (new this round) | Old | New | What it is | |---|---|---| | L79A3 | `DrawColorLine_PostSwap` | after X1≤X2 swap | | L79BF | `DrawColorLine_TestZero` | single-point shortcut | | L79E4 | `DCLPrepYMajor` | Y-major axis setup | | L79FE | `DCLYMajor_DecPixelInByte` | byte-bit transition | | L7A4D | `DCLPrepXMajor_AddCycles` | X-major cycle adjust | | L7A55 | `DCLPrepXMajor_AddOverhead` | "" overhead | | L7A60 | `DCLPrepXMajor_InitError` | "" init error | | L7AF2 | `SceneryOpDayOnly_Exit` | shared advance-and-continue | ### Polygon scan-fill row body (new this round) | Old | New | What it is | |---|---|---| | L7684 | `PolygonScanFill_ProjectVertices` | per-vertex 3D→2D project | | L7699 | `PolygonScanFill_ProjectVertexLoop` | "" loop top | | L76C3 | `PolygonScanFill_TestMaxX` | screen-X max accumulate | | L76C9 | `PolygonScanFill_StoreYHi` | screen-Y store | | L76D3 | `PolygonScanFill_TestMaxY` | screen-Y max accumulate | | L76D9 | `PolygonScanFill_NextVertex` | loop step | | L7724 | `PolygonScanFill_BuildEdgeList` | edge-list builder loop | | L77EA | `PolygonScanFill_HorizEdge` | special-case horizontal edge | | L783A | `PolygonScanFill_SetupRowEmit` | setup per-row scan | | L784A | `PolygonScanFill_RowTop` | per-row scan top | | L785D | `PolygonScanFill_EdgeLoop` | edge walk for current row | | L787B | `PolygonScanFill_EdgeIntercept` | interpolate X intercept | | L789B | `PolygonScanFill_NextEdge` | loop step | | L78B2 | `PolygonScanFill_SortIntercepts` | bubble-sort the X buffer | | L78B5 | `PolygonScanFill_SortInner` | "" inner loop | | L78C6 | `PolygonScanFill_SortInnerStep` | "" step | | L78CE | `PolygonScanFill_EmitSpansLoop` | DrawColorSpan-per-pair loop | | L78E0 | `PolygonScanFill_NextRow` | row advance | | L78EB | `PolygonScanFill_FinalCost` | work cost accumulate | ### Section loader (new this round) | Old | New | What it is | |---|---|---| | L8776 | `SceneryHeaderLoadTrampoline` | $0D Header → demand-load | | LA64C | `SceneryHeaderLoadIfMiss_InvalidateNewerSlots` | cache invalidation | | LA656 | `SceneryHeaderLoadIfMiss_RunLoad` | actually issue load | | LA66A | `SceneryHeaderLoadIfMiss_Hit` | cache hit RTS | | LA6F9 | `SceneryCopyLoadedSection` | $4000-$5FFF → $2000-$3FFF | | LA712 | `SceneryCopyLoadedSection_PageLoop` | "" 256-byte loop | ### What's left Remaining L\ clusters across the chunks are aircraft physics integration, instrument panel update routines (DrawHeading, DrawDME, UpdateAltimeterPose, UpdateMagneticHeading, etc.), demo mode logic, crash detection / handling, joystick / keyboard input handling, magnetic compass / VOR / DME / ATIS internals, AltDrawSkyGroundRow detailed body, ApplyArtificialHorizonMask, and the ZPScale 16-bit multiplier family in chunk4. PerspectiveDivide is named at the entry but its 7-iteration unrolled shift-subtract body has internal labels (L7C3D, L7C9E, etc.) that are best understood as "iteration N of the divide" rather than getting individual semantic names. Each rename requires understanding the surrounding code — speculative renames would mislead future readers. Add new entries here as more routines get reverse-engineered. ## Algorithm understanding (= what reverse-engineering revealed) ### DrawColorLine ($795A) is bit-level Bresenham Apple II hires has 7 color pixels per byte. The high bit (bit 7) selects palette (= violet/green or blue/orange depending on parity); bits 0..6 hold 7 color pixels. DrawColorLine paints ONE color pixel = ONE bit per loop iteration via self-modified ORA/AND mask opcodes (= installed by SetPixelDrawMode). Two inner loops: - **Y-major** (`DCLYMajorTop` at L79F0): for vertical-ish lines. Y steps every iteration; X (column + bit-in-byte) steps when the Bresenham error overflows. - **X-major** (`DCLXMajorTop` at L7A67): for horizontal-ish lines. X steps every iteration; Y conditionally. The "split-byte boundary" handling at bit-3 (`DCLYMajor_DecPixelInByte` at L79FE) is because 7 pixels per byte means transitions at bits 0 and 6 wrap to the previous/next byte cleanly — but bit 3 needs special care for the high-bit palette plane. For our port: `hiresDrawLine` should produce the same bit pattern as DrawColorLine, OR we accept that pixel-pair-level rasterization will look slightly different (= no NTSC color fringing). ### PolygonScanFill ($6F98) uses 3D clipping, not 2D scanline `PolygonScanFillSetup` receives the polygon as 3D-XYZ vertices (= the post-xform output stored in PrimVerts). It runs four 3D clipping passes against the viewing frustum half-planes (Left/Top/Right/Bottom), each pass alternating PrimVerts <-> SecVerts. **Each clipping pass INTRODUCES NEW VERTICES at the frustum-edge intersections.** After clipping, `PolygonScanFill_ProjectVertices` (L7684) projects every clipped vertex (including the new intersections) to screen coords. Then `PolygonScanFill_BuildEdgeList` (L7724) builds the per-edge data (top row, row count, top column, column step per row) and `PolygonScanFillRow` (L7826) iterates rows and emits scan-line spans via DrawColorSpan. **This is why our port's polygon fill produces the wrong shape:** Our port's `rendererFillPolygon` takes pre-projected screen coords from the vertex emit ops and rasterizes via standard 2D scanline edge intersection. **It skips the 3D clipping entirely.** When a polygon's original 3D vertices all map to a small screen Y range (= e.g. all near row 55-56 because they're all at similar Z), our fill produces just 1-2 scan rows. But chunk5's 3D clipping introduces frustum-edge intersection vertices that, when projected, expand the polygon's screen-Y range. A polygon whose 3D vertices all sit at large Z but extend in Y can produce intersection vertices at the near-Z plane (= mapping to row 99 after projection). The resulting clipped polygon spans rows 56..99 — the WEDGE shape that's the runway centerline at boot Meigs. **To fix:** port `PolygonScanFillSetup`'s 4 clipping passes faithfully. The infrastructure is in place (= polyXs/polyYs accumulator) but should be replaced with 3D-XYZ vertex storage and the clipping should run BEFORE projection. Then the existing scanline rasterizer would see the correct vertex set. ### RecomputeSectionBase ($631D) computes 24-bit base from matrix*scale Source layout: `$4A..$52` as 3 axes × 24-bit MID/HI/LO. MAME-patched moves this to `$2A..$2F` as 3 axes × 16-bit LE. For each axis: `base[i] = -((matrix_row_2[i] * scale) >> 15)` where `scale` is `$68/$69` (= camera-vs-section Y delta). The base is then read by xform-B as the Y-axis-implicit base for vertices that don't have explicit Y in stream. This was the source of our long-running base-divergence bug: source chunk5.s describes the $4A..$52 layout, but MAME's running binary uses $2A..$2F because the patcher rewrites the relevant LDA/STA addresses. ### xform-B chained accumulator `TransformVertex7EBC` reads BOTH the persistent base ($2A..$2F) AND the running auto-scale exponent ($2F at $35/$36). After the matrix multiply (= XformBMatrixMultiply at L7F96), output is stored to the caller's destSlot ($CB for V1, $D4 for V2) AND the accumulator slots $18..$1D persist for the next transform. The auto-scale shift loop (XformBAutoScaleLoop at L7F1A) keeps all 5 high bytes ($9F = X delta hi, $A3 = Z delta hi, $19/$1C/$1F = base high bytes for X/Y/Z) within the [$40..$BF] band. Each shift up by 1 increments the exponent at $2F. After the multiply, post-overflow recovery (= L7F7F XformBHalveBaseAccumulators) shifts the accumulator back down to maintain scale alignment.