fs2port/port/docs/scenery_opcodes.md
2026-05-13 21:32:05 -05:00

1006 lines
44 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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... <terminator>`)
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<addr>` placeholder labels for branch
targets. We've added semantic names for the scenery-interpreter family
(L<addr> 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 X1X2 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 3D2D 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\<addr\> 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.