1006 lines
44 KiB
Markdown
1006 lines
44 KiB
Markdown
# 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 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\<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.
|