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

44 KiB
Raw Blame History

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 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.