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.
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.
DHGR Page 1 main (also holds SYS stub before DHGRInit)
$4000–$5FFF
8,192
DHGR Page 2 main
$6000–$9C2D
15,406
Game code + tables + data (see breakdown below)
$9C2E–$AFF5
5,064
TileAtlas runtime slot (DUM label, 211 tiles × 24 bytes) — filled at boot by decompressing TileAtlasCmp (LZSA2)
$AFF6–$AFFF
10
FREE (gap between TileAtlasEnd and TileMap1)
$B000–$BEFF
3,840
Runtime buffer: TileMap1/2/3 + CollMap1/2/3 (6×640), decompressed at boot from MapDataCmp (LZSA2)
$BF00–$BFFF
256
ProDOS global page (RESERVED)
$C000–$C0FF
256
I/O soft switches
$D000–$D0F8
249
Main LC bank 2: guard_lc_d0 (copied from SYS at init)
$D0F9–$D0FF
7
FREE (small gap before Bitsy Bye)
$D100–$D3FF
768
Main LC: Bitsy Bye (ProDOS utility) — skipped, not written
$D400–$DFA7
2,984
Main LC bank 2: render_lc + guard_lc (copied from SYS at init)
$DFA8–$DFFF
88
FREE (tail of LCBlock)
$E000–$FFFF
8,192
Main 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.
Aux-RAM base low-byte for each sprite (49 sprites)
$8A0F–$8A3F
49
SprAuxAddrH
Aux-RAM base high-byte for each sprite
$8A40–$8A46
7
ShiftOfsL
Byte offset per shift phase, low byte (7 phases)
$8A47–$8A4D
7
ShiftOfsH
Byte offset per shift phase, high byte
$8A4E–$8A7E
49
CmpMaskAddrL
Compressed mask address per sprite, low byte
$8A7F–$8AAF
49
CmpMaskAddrH
Compressed mask address per sprite, high byte
$8AB0–$8ABD
14
(ancillary)
Minor data between CmpMaskAddrH and MskShiftOfsL
$8ABE–$8AC4
7
MskShiftOfsL
Mask-row byte offset per shift phase, low byte
$8AC5–$8ACB
7
MskShiftOfsH
Mask-row byte offset per shift phase, high byte
$8ACC–$9BCB
4,352
MaskExpTbl
Mask expansion lookup (34 sub-tables × 128 entries) — largest single table
6,830
Subtotal (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.
Aux LC bank 2: aux_read_aux2 (ReadAux2 shadow so DrawSpriteSolid's JSR works from aux-LC callers like ShowSpriteCatalog)
$D0F9–$D0FF
7
FREE (aux mirror of main LC2 gap)
$D100–$DAA4
2,469
Aux LC bank 2: AuxCold block (aux_cold_wrappers, font, fonttest, title, intermission, hud_render_aux, leaderboard_aux) — reached via AuxLCCall trampoline
$DAA5–$E25F
1,979
FREE (largest aux-LC free pool; sits between AuxCold and Font)
$E260–$E65F
1,024
Aux LC bank 2: font data (64 glyphs × 16 bytes, FontBase = $E260)
$E660–$E9D2
883
Aux LC bank 2: Electric Duet music player + 5 songs (accessed via ALTZP trampoline)
$E9D3–$E9FF
45
FREE (small gap between music and logo)
$EA00–$F9ED
4,078
Aux LC bank 2: title screen LZSA2 blob (LogoAux $EA00–$F1CC, LogoMain $F1CD–$F9ED)
$F9EE–$FFFF
1,554
FREE (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
Target
Switches
Accessed 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 page
Always main RAM (unless ALTZP set for LC window)
Direct
Free Memory Summary
Region
Address
Bytes
Notes
Main ZP
$1C, $27–$28, $DF
4
Always-free ZP slots
Main ZP (between LZSA2 calls)
$F8–$FF
8
Free except during decompression
Main (page 3)
$0377–$037B
5
Small gap between ElevOldP1/LivesCnt and GuardOldP0
Main (page 3)
$03AD–$03CF
35
After attract/demo state block, before ProDOS vectors
Main
$0400–$07FF
1,024
Text page 1 (960 usable; 64 bytes of screen holes)
Main
$15DA–$1FFF
2,598
Tail of Low1800 block — largest contiguous low-main pool
Main
$AFF6–$AFFF
10
Between TileAtlasEnd and TileMap1
Main LC bank 2
$D0F9–$D0FF
7
Between LC2 and Bitsy Bye
Main LC bank 2
$DFA8–$DFFF
88
Tail of LCBlock
Main subtotal (excluding transient ZP)
3,767
+ 4 ZP always free
Aux
$0200–$07FF
1,536
Low aux (reach via RAMRD/RAMWRT)
Aux
$1FA0–$1FFF
96
Tail slack after sprites 0–8
Aux
$B760–$BBDF
1,152
Between shifted sprites and compressed masks
Aux
$BE2C–$BEFF
212
After compressed masks, before aux ProDOS window
Aux subtotal
2,996
Aux LC
$D0F9–$D0FF
7
Aux mirror of main LC2 gap
Aux LC
$DAA5–$E25F
1,979
Largest aux-LC free pool (between AuxCold and Font)
Aux LC
$E9D3–$E9FF
45
Between music and logo
Aux LC
$F9EE–$FFFF
1,554
After logo (second-largest aux-LC pool)
Aux LC subtotal
3,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
Target
Tool
Input
Output
make art
gen_tiles.py
play1/2/3.dhr + collision1/2/3.png
screendata.s (TileAddr tables + SlopeProfiles + SlopeList1/2/3) and tileatlas.bin (tile graphics, decompressed into runtime slot)
lzsa.exe -f 2 -r
maps.bin
maps.lzsa2 (compressed tilemaps/collision)
bin2hex.py
maps.lzsa2
mapdata_cmp.s (MapDataCmp, decompresses to $B000)
lzsa.exe + bin2hex.py
tileatlas.bin
tileatlas.lzsa2 → tileatlas_cmp.s (TileAtlasCmp, decompresses to $9C2E runtime slot)
spriteatlas.lzsa2 → spriteatlas_cmp.s (SpriteAtlasCmp, decompresses to $1600)
gen_font.py
namco.dhr
fontdata.s (64 glyphs, copied to aux LC at init)
make build
Merlin32
bagman.s (root with PUT list) + all .s files
BAGMAN.SYSTEM (37,088 bytes); build_info.s regenerated with timestamp each build
make image
Cadius
base.po + BAGMAN.SYSTEM
bagman.po (ProDOS disk image)
make kegs
KEGS
bagman.po
Launches 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.