Bagman — Memory Usage Report

Apple //e 128K • DHGR Double-Buffered • Merlin32 • Binary: BAGMAN.SYSTEM — 37,088 bytes ($90E0) • Branch: bigmap • Generated 2026-04-21 from asm/bagman_Output.txt and source

SYS File Layout (Load Order at $2000)

ProDOS loads BAGMAN.SYSTEM at $2000. The binary is a contiguous byte stream: the stub at $2000 copies each subsequent block to its runtime ORG (forward for most, reverse for the $6000 game-code block which overlaps source/dest). ORG directives do not pad between blocks in the file.
BlockRuntime ORGSizeHexContents (PUT order)
Stub$2000–$3C377,224$1C38dp.s (equ), Speed.s, init.s, fontdata.s, spriteinit.s, spriteatlas_cmp.s, mapdata_cmp.s, tileatlas_cmp.s, sprshift.s, fontinit.s, hud_init.s, hiscore_seed.s, build_info.s
Low1800$0800–$15D93,546$0DDAlzsa2, musictramp, aux_cold_call, elevator, guard_1800, dhgr, collision, guard_data, hud_overlay, attract
LC2 (main LC bank 2)$D000–$D0F8249$00F9guard_lc_d0.s
LCBlock (main LC bank 2)$D400–$DFA72,984$0BA8render_lc.s, guard_lc.s
AuxLC2 (aux LC bank 2)$D000–$D0F8249$00F9aux_read_aux2.s (shadow of ReadAux2 for aux-LC callers of DrawSpriteSolid)
AuxCold (aux LC bank 2)$D100–$DAA42,469$09A5aux_cold_wrappers, font, fonttest, title, intermission, hud_render_aux, leaderboard_aux
AuxMusic (aux LC bank 2)$E660–$E9D2883$0373music.s (Electric Duet player), musicdata.s (5 songs)
AuxLogo (aux LC bank 2)$EA00–$F9ED4,078$0FEElogodata.s (LogoAux $EA00–$F1CC / LogoMain $F1CD–$F9ED, LZSA2 compressed)
GameCode$6000–$9C2D15,406$3C2Egame.s, player.s, render.s, tables.s, screendata.s, spritedata.s, leaderboard.s
BAGMAN.SYSTEM total37,088$90E0Headroom to SYS ceiling ($BF00−$2000 = $9F00 = 40,704): 3,616 bytes
SYS file ceiling = 40,704 bytes ($9F00). ProDOS loads SYS files at $2000; the file cannot extend past $BEFF (ProDOS globals begin at $BF00). Every code block above is in the binary; runtime-free pools (e.g. $B000 tilemap buffer) don't expand the file budget. Growing any block moves the file size 1:1 closer to the ceiling.

ProDOS Safety

ProDOS global page ($BF00-$BFFF): SAFE. Tilemap/collision runtime buffer ends at $BEFF. No game writes to $BF00-$BFFF.
Main LC Bank 1 (Bitsy Bye): SAFE. Game never writes to main LC bank 1. Bank 1 holds ProDOS utilities / Bitsy Bye at $D100-$D3FF.
Main LC Bank 2: partially used by game. guard_lc_d0 occupies $D000-$D0F8; render_lc+guard_lc occupy $D400-$DFA7. The $D100-$D3FF window (Bitsy Bye) is never written. Bank 2 enabled via LDA $C083 ✕ 2 at init.
Protected RegionAddressStatus
ProDOS global page$BF00–$BFFFGame runtime buffers end at $BEFF
ProDOS keyboard buffer$0200–$02FFNot overwritten by game
ProDOS/DOS vectors$03D0–$03FFWarm-start vectors preserved
Text screen holes (page 1)$0478/$04F8/.../ $07F88×8 = 64 bytes avoided within $0400–$07FF
Main LC Bank 1 (Bitsy Bye)$D000–$FFFFNo main LC bank 1 writes
Main LC Bank 2 Bitsy window$D100–$D3FFGame code skips this range

Zero Page Map ($00-$FF)

Vars
Coll
RA1
WB
Bufs
Mix
ReadAux
WB2
Plyr
ZPComposite
Game
LZ
RangeBytesContents
$00–$078Pointers: ScreenPtr, TempPtr, SrcPtr, DstPtr
$08–$0F8Tile renderer / reloc reuse: MapPtr/RelocSrc, TilePtr/RelocDst, RelocLen, SubRow, TileCol, CurScan
$10–$134GameState, FrameCnt, PixByte0/1
$14–$174SprX, SprY, SprFrame, SprDir (sprite renderer)
$18–$1B4Temp1–Temp4
$1C1FREE
$1D–$1F3OldMapIdx, PageOfs, DrawPage (double-buffer)
$20–$267Collision: SlopeListPtr, CollRow, CollOff, CollPtr
$27–$282FREE at runtime (DstBuf/ShiftBuf during init/sprshift/font only)
$291MapTransReq (0=none, 1=right, 2=left)
$2A–$329Executable: ReadAuxByte trampoline (read 1 aux byte)
$33–$375Wheelbarrow: WBX, WBY, WBFrame, WBState, WBDirty (init: sprshift SrcBuf/ShiftCnt/GenSprIdx/GenRowIdx/GenSrcOfs)
$38–$3F8SprBuf — sprite row data for composite
$40–$478MskBuf — expanded 8-byte mask
$48–$492GuardDirty, GuardWakeCnt
$4A–$4D4Spr2X/Y/Frame/Dir (test) — reused as OldLootX/Y/Fr at runtime
$4E–$4F2FontRow, BaseScan (font renderer)
$50–$6825Executable: ReadAux trampoline (aux RAM read via ALTZP, mask fetch)
$69–$6F7OldWBX/Y/Fr, WBGrabOfs, WBMapIdx, CarryIdx, LootDirty
$70–$7F16Player: X, Y, Fr, Dir, St, Anim, InputFlags, OldX/Y/Fr, FallCount, LastKey, PlayerDirty, InputTimer, FrameBudget, NewKey
$80–$DE95Executable: ZPComposite (masked sprite blit, RAMRD/RAMWRT switching)
$DF1FREE
$E0–$E89Electric Duet music: MusicPtr, Dur, Loop, Frq1/2, Dty1/2, Spkr
$E9–$EB3CartDirty, StunTimer, RideCartIdx
$EC–$EE3Sprite sleep system: LootWakeCnt, WBWakeCnt, LootDrawOnly
$EF–$F35Elevator: ElevY, ElevDir, ElevTimer, OnElevator, ElevDirty
$F4–$F74GuardIdx, AITemp, EntX, EntY — transiently clobbered by LZSA2 ($F5–$F7 = cmdbuf/nibflg/nibble) during map decompression
$F8–$FF8LZSA2 state (offset, winptr, srcptr, dstptr) — free at runtime between decompress calls
Used~252142 vars + 34 trampolines + 95 ZPComposite (some init-only aliases)
Always free4$1C (1) + $27–$28 (2) + $DF (1)
Free between decompress+8$F8–$FF (LZSA2 working state)

Main RAM Map (Runtime)

ZP/Sys
TxtPg
Low1800
Free
DHGR Pg1
DHGR Pg2
GameCode
TileAtlas
Maps
P
I/O
LC (ProDOS / BitsyBye / game LC code)
DHGR Display
Code
Runtime data buffers
ProDOS
LC region
Free
AddressSizeContents
$0000–$00FF256Zero page (vars + trampolines + ZPComposite — see ZP map)
$0100–$01FF2566502 stack
$0200–$02FF256ProDOS keyboard buffer (reserved)
$0300–$030F16AuxLCCall arg/return pass-through ($0302/$0303 target, $0304/$0305 A/X save, $0306–$030C wrapper args; ReadAuxLC reads aux via ALTZP)
$0310–$032118Player / WB / carried-loot old-pos (PlayerOldP0/P1, WBOldP0/P1, LootOldP0/P1 — 3 bytes each × P0+P1)
$0322–$033318Ground-loot old-pos (GLootOldP0/P1 — 9 bytes each for 3 items)
$0334–$034518Mine-cart old-pos (CartOldP0/P1 — 9 bytes each for 3 carts)
$0346–$035718Preview-cart old-pos (PrvCartOldP0/P1)
$0358–$036918Preview-loot old-pos (PrvLootOldP0/P1)
$036A–$036F6Preview-WB old-pos (PrvWBOldP0/P1 — 3 bytes each)
$0370–$03756Elevator old-pos (ElevOldP0/P1 — 3 bytes each)
$03761LivesCnt (HUD scratch)
$0377–$037B5FREE
$037C–$038D18Guard old-pos (GuardOldP0/P1 — 9 bytes each for 3 guards)
$038E–$03969Preview-guard old-pos page 0 (PrvGuardOldP0)
$0397–$03993Preview-elevator old-pos page 0 (PrvElevOldP0)
$039A–$03A29Preview-guard old-pos page 1 (PrvGuardOldP1)
$03A3–$03A53Preview-elevator old-pos page 1 (PrvElevOldP1)
$03A6–$03AC7Attract/demo state: AttractState, DemoActive, DemoHoldCnt, DemoBtn, DemoIdx, DemoInp, NotesLeft (music early-exit)
$03AD–$03CF35FREE
$03D0–$03FF48ProDOS/DOS vectors, warm start (RESERVED)
$0400–$07FF1,024Text page 1 (unused in DHGR; 64 bytes of screen holes if used)
$0800–$15D93,546Low1800 code block (lzsa2, musictramp, aux_cold_call, elevator, guard_1800, dhgr, collision, guard_data, hud_overlay, attract)
$15DA–$1FFF2,598FREE (largest contiguous low-main pool)
$2000–$3FFF8,192DHGR Page 1 main (also holds SYS stub before DHGRInit)
$4000–$5FFF8,192DHGR Page 2 main
$6000–$9C2D15,406Game code + tables + data (see breakdown below)
$9C2E–$AFF55,064TileAtlas runtime slot (DUM label, 211 tiles × 24 bytes) — filled at boot by decompressing TileAtlasCmp (LZSA2)
$AFF6–$AFFF10FREE (gap between TileAtlasEnd and TileMap1)
$B000–$BEFF3,840Runtime buffer: TileMap1/2/3 + CollMap1/2/3 (6×640), decompressed at boot from MapDataCmp (LZSA2)
$BF00–$BFFF256ProDOS global page (RESERVED)
$C000–$C0FF256I/O soft switches
$D000–$D0F8249Main LC bank 2: guard_lc_d0 (copied from SYS at init)
$D0F9–$D0FF7FREE (small gap before Bitsy Bye)
$D100–$D3FF768Main LC: Bitsy Bye (ProDOS utility) — skipped, not written
$D400–$DFA72,984Main LC bank 2: render_lc + guard_lc (copied from SYS at init)
$DFA8–$DFFF88FREE (tail of LCBlock)
$E000–$FFFF8,192Main LC shared region (ProDOS) — not written

Game Code Segment ($6000-$9C2D)

game
player
render
tables
TA
S
SL
SprT
MaskExpTbl
LB
Executable code
Slope/collision data
Lookup tables
MaskExpTbl
TileAtlas lives outside the GameCode block. The 5,064-byte decompressed tile atlas is reserved with a DUM block at $9C2E–$AFF5 (see Main RAM Map above) so the SYS file only carries the LZSA2-compressed blob in the stub (tileatlas_cmp.s). TileAddrL/H in screendata.s still reference it via TileAtlas+N math.

Code Modules

AddressBytesSourceKey entry points
$6000–$6AC22,755game.sGameInit ($6000), TitleReset, MainLoop ($6048), ResetLevel, DoMapTransition, AddScore, TickBonus, LoseLife, DoDeath, PlayerRespawn
$6AC3–$7CE44,642player.sPlayerInit, ReadInput, UpdatePlayer, DoStand/DoWalk/DoClimb/DoFall/DoRide/DoStun, WB + loot + cart + preview helpers, CartAdjL/R tables
$7CE5–$811D1,081render.sDrawScreenNoClear, DrawScreen, DrawSprite, DrawSpriteSolid, ReadAuxLC (aux-LC pass-through via $0300 buffer)
$9BCC–$9C2D98leaderboard.sMain-side leaderboard helpers (entry into aux-LC HiScoreView / GoToLeaderboard)
8,576Subtotal (executable code)

Lookup Tables and Game Data ($811E–$9BCB)

AddressBytesLabelDescription
$811E–$81DD192ScanAdrLDHGR scanline address low byte (192 lines)
$81DE–$829D192ScanAdrHDHGR scanline address high byte
$829E–$8329140XDiv7Pixel X → tile column
$832A–$83B5140XMod7Pixel X → sub-column shift
$83B6–$8475192YDiv6Scanline Y → tile row
$8476–$8535192YMod6Scanline Y → sub-row
$8536–$855532RowOfs20LTile row → 20-column offset, low byte
$8556–$857532RowOfs20HTile row → 20-column offset, high byte
$8576–$8648211TileAddrLTile graphics pointers, low byte (211 tiles, anchored at TileAtlas = $9C2E)
$8649–$871B211TileAddrHTile graphics pointers, high byte
$871C–$87A0133SlopeProfilesSlope shape profiles
$87A1–$885F191SlopeList1Map 1 slope placements
$8860–$892D206SlopeList2Map 2 slope placements
$892E–$89DD176SlopeList3Map 3 slope placements
$89DE–$8A0E49SprAuxAddrLAux-RAM base low-byte for each sprite (49 sprites)
$8A0F–$8A3F49SprAuxAddrHAux-RAM base high-byte for each sprite
$8A40–$8A467ShiftOfsLByte offset per shift phase, low byte (7 phases)
$8A47–$8A4D7ShiftOfsHByte offset per shift phase, high byte
$8A4E–$8A7E49CmpMaskAddrLCompressed mask address per sprite, low byte
$8A7F–$8AAF49CmpMaskAddrHCompressed mask address per sprite, high byte
$8AB0–$8ABD14(ancillary)Minor data between CmpMaskAddrH and MskShiftOfsL
$8ABE–$8AC47MskShiftOfsLMask-row byte offset per shift phase, low byte
$8AC5–$8ACB7MskShiftOfsHMask-row byte offset per shift phase, high byte
$8ACC–$9BCB4,352MaskExpTblMask expansion lookup (34 sub-tables × 128 entries) — largest single table
6,830Subtotal (tables + data)
Constant: CmpMaskBase = $BBE0 — the aux-RAM base for compressed masks (CmpMaskAddrL/H store pre-computed addresses anchored to this base).
Sprite count: 49 (SpriteCount = 49 in spritedata.s).

Stub Section ($2000-$3C37, overwritten by DHGR Page 1)

The stub holds init-only code and compressed data blobs. It's copied/overwritten during boot and entirely reclaimed as DHGR Page 1 main after DHGRInit. Individual sub-section boundaries aren't labeled; the numbers below are rough order-of-magnitude sizes from source-file lengths, not assembler-emitted spans.
RangeBytesDescription
$2000+dp.s (equ), Speed.s (equ), init.s (boot/copy/decompress driver), BuildInfo ASCII blob + build_info.s
~1,024fontdata.s — 64-glyph font bitmap copied to aux LC $E260 at init
~100spriteinit.s — SpriteAtlas decompression driver (SpriteAtlas = $1600 during init)
~1,100spriteatlas_cmp.s — LZSA2 blob (decompresses to $1600, consumed by GenShiftedSprites)
~1,200mapdata_cmp.s (MapDataCmp) — LZSA2-compressed tilemaps + collision (decompresses to $B000)
~1,100tileatlas_cmp.s — LZSA2-compressed tile atlas (decompresses to $9C2E runtime slot)
sprshift.s (GenShiftedSprites, pre-shift generator), fontinit.s (InstallFont), hud_init.s (HUD lookup builder), hiscore_seed.s (default leaderboard seed)
StubEnd = $3C387,224Total stub (init-only; reclaimed as DHGR Page 1 main after DHGRInit)
Boot sequence: ProDOS loads BAGMAN.SYSTEM at $2000 → init.s decompresses SpriteAtlas to $1600 (LZSA2) → forward-copies Low1800 block ($0800) → copies LC2 ($D000) / LCBlock ($D400) → flips ALTZP to aux and copies AuxLC2 (aux $D000) / AuxCold (aux $D100) / AuxMusic (aux $E660) / AuxLogo (aux $EA00) into the aux language card → reverse-copies GameCode to $6000 (source/dest overlap) → decompresses MapDataCmp to $B000 and TileAtlasCmp to $9C2E (LZSA2) → GameInit → GenShiftedSprites (fills aux RAM) → InstallFont (aux LC $E260) → InstallZPComposite → DHGRInit → AttractSeed → ShowTitle → MainLoop.

Auxiliary RAM Map (Runtime)

ZP
Free
Spr 0-8
DHGR Pg1
DHGR Pg2
Shifted Sprites 9-48
Msk
F
P
I/O
A2
AuxCold
Free
Fnt
Mus
Logo
Free
AddressSizeContents
$0000–$01FF512Aux zero page + stack (used transiently by ALTZPON for aux-LC reads)
$0200–$07FF1,536FREE (aux low memory; $0400–$07FF is aux text page with hardware banking)
$0800–$1F9F6,048Shifted sprites 0–8 (9 × 672 bytes: 7 shift phases × 12 rows × 8 bytes)
$1FA0–$1FFF96FREE (tail slack)
$2000–$3FFF8,192DHGR Page 1 aux
$4000–$5FFF8,192DHGR Page 2 aux
$6000–$B75F22,368Shifted sprites 9–48 (placed by ISprAuxAdL/H in spriteinit.s; 5 slots unused and 2 pairs share data, so occupied bytes < 40×672)
$B760–$BBDF1,152FREE (tail gap before compressed masks)
$BBE0–$BE2B588Compressed sprite masks (49 sprites × 12 bytes; CmpMaskBase = $BBE0)
$BE2C–$BEFF212FREE
$BF00–$BFFF256Aux ProDOS? (caution; leave alone)
$C000–$CFFF4,096I/O (mirrored)
$D000–$D0F8249Aux LC bank 2: aux_read_aux2 (ReadAux2 shadow so DrawSpriteSolid's JSR works from aux-LC callers like ShowSpriteCatalog)
$D0F9–$D0FF7FREE (aux mirror of main LC2 gap)
$D100–$DAA42,469Aux LC bank 2: AuxCold block (aux_cold_wrappers, font, fonttest, title, intermission, hud_render_aux, leaderboard_aux) — reached via AuxLCCall trampoline
$DAA5–$E25F1,979FREE (largest aux-LC free pool; sits between AuxCold and Font)
$E260–$E65F1,024Aux LC bank 2: font data (64 glyphs × 16 bytes, FontBase = $E260)
$E660–$E9D2883Aux LC bank 2: Electric Duet music player + 5 songs (accessed via ALTZP trampoline)
$E9D3–$E9FF45FREE (small gap between music and logo)
$EA00–$F9ED4,078Aux LC bank 2: title screen LZSA2 blob (LogoAux $EA00–$F1CC, LogoMain $F1CD–$F9ED)
$F9EE–$FFFF1,554FREE (second-largest aux-LC free pool)
Sprite data budget: 49 sprites × 672 bytes = 32,928 bytes of shifted data (7 shift phases × 12 rows × 8 bytes per sprite). All sprite data lives in aux main RAM ($0800–$1F9F for sprites 0–8, irregularly placed within $6000–$B6A0 for sprites 9–48). Aux LC no longer hosts any sprites — that region was reclaimed for the AuxCold cold-code block. ISprAuxAdL/H in spriteinit.s contains the exact per-sprite aux base; $00 entries mark slots that share data with another sprite.
Compressed masks: Stored compressed (12 bytes/sprite) at $BBE0 in aux RAM; MaskExpTbl in main RAM expands each 12-byte compressed mask into the 8-byte per-row mask used by ZPComposite.

Access Patterns

TargetSwitchesAccessed via
Aux $0200–$BFFF (sprites, DHGR aux)RAMRD ($C003), RAMWRT ($C005)ReadAux/ReadAuxByte trampolines in ZP $2A/$50
Aux LC $D000–$FFFF (sprites 44–50, font, music, logo)ALTZP ($C009), LC bank 2 read/write ($C083×2)ReadAuxLC in render.s ($803C); main $0300 is the pass-through buffer
Main LC bank 2 $D000/$D400 (render_lc, guard_lc, guard_lc_d0)LC bank 2 read+write ($C083×2)Normal JSR (LC is pre-banked at init)
Zero pageAlways main RAM (unless ALTZP set for LC window)Direct

Free Memory Summary

RegionAddressBytesNotes
Main ZP$1C, $27–$28, $DF4Always-free ZP slots
Main ZP (between LZSA2 calls)$F8–$FF8Free except during decompression
Main (page 3)$0377–$037B5Small gap between ElevOldP1/LivesCnt and GuardOldP0
Main (page 3)$03AD–$03CF35After attract/demo state block, before ProDOS vectors
Main$0400–$07FF1,024Text page 1 (960 usable; 64 bytes of screen holes)
Main$15DA–$1FFF2,598Tail of Low1800 block — largest contiguous low-main pool
Main$AFF6–$AFFF10Between TileAtlasEnd and TileMap1
Main LC bank 2$D0F9–$D0FF7Between LC2 and Bitsy Bye
Main LC bank 2$DFA8–$DFFF88Tail of LCBlock
Main subtotal (excluding transient ZP)3,767+ 4 ZP always free
Aux$0200–$07FF1,536Low aux (reach via RAMRD/RAMWRT)
Aux$1FA0–$1FFF96Tail slack after sprites 0–8
Aux$B760–$BBDF1,152Between shifted sprites and compressed masks
Aux$BE2C–$BEFF212After compressed masks, before aux ProDOS window
Aux subtotal2,996
Aux LC$D0F9–$D0FF7Aux mirror of main LC2 gap
Aux LC$DAA5–$E25F1,979Largest aux-LC free pool (between AuxCold and Font)
Aux LC$E9D3–$E9FF45Between music and logo
Aux LC$F9EE–$FFFF1,554After logo (second-largest aux-LC pool)
Aux LC subtotal3,585
Total free (runtime)10,348+ ~4 ZP always free + ~8 transient ZP
Largest contiguous blocks:
• Main: 2,598 bytes ($15DA–$1FFF) — best for more low-main code or data
• Aux LC: 1,979 bytes ($DAA5–$E25F) — needs ALTZP to access
• Aux LC: 1,554 bytes ($F9EE–$FFFF) — needs ALTZP to access
• Aux: 1,536 bytes ($0200–$07FF) — needs RAMRD/RAMWRT to access
• Aux: 1,152 bytes ($B760–$BBDF) — between sprites and masks
• Main: 1,024 bytes ($0400–$07FF) — text page 1 area (watch screen holes)
SYS file headroom: 3,616 bytes ($2000–$BEFF window minus current 37,088 bytes). Adding to any code block (stub, Low1800, LC2, LCBlock, AuxLC2, AuxCold, AuxMusic, AuxLogo, GameCode) grows the file 1:1 regardless of which runtime free pool the code ends up in. Runtime free pools do not extend the file-size budget.

Build Pipeline

TargetToolInputOutput
make artgen_tiles.pyplay1/2/3.dhr + collision1/2/3.pngscreendata.s (TileAddr tables + SlopeProfiles + SlopeList1/2/3) and tileatlas.bin (tile graphics, decompressed into runtime slot)
lzsa.exe -f 2 -rmaps.binmaps.lzsa2 (compressed tilemaps/collision)
bin2hex.pymaps.lzsa2mapdata_cmp.s (MapDataCmp, decompresses to $B000)
lzsa.exe + bin2hex.pytileatlas.bintileatlas.lzsa2 → tileatlas_cmp.s (TileAtlasCmp, decompresses to $9C2E runtime slot)
gen_shifted_sprites.pysprites.dhrspritedata.s (tables, MaskExpTbl) + spriteinit.s (SprAddrL/H, ISprAuxAdL/H)
lzsa.exe + bin2hex.pyspriteatlas.binspriteatlas.lzsa2 → spriteatlas_cmp.s (SpriteAtlasCmp, decompresses to $1600)
gen_font.pynamco.dhrfontdata.s (64 glyphs, copied to aux LC at init)
make buildMerlin32bagman.s (root with PUT list) + all .s filesBAGMAN.SYSTEM (37,088 bytes); build_info.s regenerated with timestamp each build
make imageCadiusbase.po + BAGMAN.SYSTEMbagman.po (ProDOS disk image)
make kegsKEGSbagman.poLaunches emulator (KEGSMAC on macOS, kegswin.exe on WSL)

Notable Architectural Choices

Compressed data at boot. Both the sprite atlas and the tilemap/collision data ship LZSA2-compressed and decompress into their runtime homes during init: SpriteAtlas to $1600 (transient, consumed by GenShiftedSprites that fills aux RAM), tilemaps to $B000 (persistent runtime buffer). The title-screen image also ships LZSA2-compressed in aux LC and decompresses into the DHGR pages from ShowTitle.
Aux LC holds cold code + art + audio. $D000–$F9ED packs the AuxLC2 shadow ($D000–$D0F8), the AuxCold cold-code block ($D100–$DAA4: title, fonttest, intermission, HUD render, leaderboard, wrappers), the 1 KB font ($E260), the Electric Duet player + 5 songs ($E660), and the compressed title screen ($EA00–$F9ED). Free pools: $DAA5–$E25F (1,979 bytes, largest) and $F9EE–$FFFF (1,554 bytes). Access requires ALTZP and uses the main $0300 buffer as pass-through for main-RAM ↔ aux-LC calls.
Main LC bank 2 layout works around Bitsy Bye. Bitsy Bye lives at $D100–$D3FF in LC bank 2 and is not safe to overwrite. The game fills $D000–$D0F8 (249 bytes, guard_lc_d0) and $D400–$DFA7 (2,984 bytes, render_lc + guard_lc), leaving the Bitsy window intact so ProDOS utilities survive the run.

Potential Optimization Targets

1. MaskExpTbl dominates the GameCode segment (28% of $6000 block)

At 4,352 bytes ($8ACC–$9BCB), MaskExpTbl is the largest single table and the single biggest lever for shrinking the SYS file. It's already trimmed to 34 sub-tables. Further reduction is possible via runtime mask expansion at a cost of per-scanline cycles inside ZPComposite.

2. Aux-LC free pool at $DAA5–$E25F is the largest free aux region

Nearly 2 KB of aux LC sits free between the AuxCold cold-code block and the font at $E260. This is the best target for new aux-LC code (additional title/hiscore/intermission variants) or for offloading data from main RAM via ALTZP access. Growing AuxCold toward $E260 is cheap from a budget standpoint.

3. LC2 gap at $D0F9–$D0FF is only 7 bytes

guard_lc_d0.s occupies $D000–$D0F8 (249 bytes), leaving only 7 free bytes before Bitsy Bye at $D100. Any further growth at main LC $D000 must relocate to another free pool (e.g., the $DFA8 tail of LCBlock or aux LC).

4. Low1800 runway is 2,598 bytes

The $15DA–$1FFF free pool is the largest contiguous main-RAM area available for code that must live in main (e.g., the attract state machine added at boot). Watch this narrow as gameplay code grows.

5. SYS headroom: 3,616 bytes total

The binary is 37,088 bytes; the SYS ceiling is 40,704 ($BF00−$2000). Any block growth consumes this budget 1:1. Compression (LZSA2) of large read-only payloads is the main lever used so far: SpriteAtlas, MapData, TileAtlas, and the title logo all ship compressed and decompress to runtime homes during init.