12 KiB
Handoff: Dungeon Object Emulator Preview
Date: 2025-11-26 Status: Root Cause Identified - Emulator Mode Requires Redesign Priority: Medium
Summary
Implemented a dual-mode object preview system for the dungeon editor. The Static mode (ObjectDrawer-based) works and renders objects. The Emulator mode has been significantly improved with proper game state initialization based on expert analysis of ALTTP's drawing handlers.
CRITICAL DISCOVERY: Handler Execution Root Cause (Session 2, Final)
The emulator mode cannot work with cold-start execution.
Test Suite Investigation
A comprehensive test suite was created at test/integration/emulator_object_preview_test.cc to trace handler execution. The TraceObject00Handler test revealed the root cause:
[TEST] Object 0x00 handler: $8B89
[TRACE] Starting execution trace from $01:8B89
[ 0] $01:8B89: 20 -> $00:8000 (A=$0000 X=$00D8 Y=$0020)
[ 1] $00:8000: 78 [CPU_AUDIO] === ENTERED BANK $00 at PC=$8000 ===
Finding: Object 0x00's handler at $8B89 immediately executes JSR $8000, which is the game's RESET vector. This runs full game initialization including:
- Hardware register setup
- APU initialization (the $00:8891 handshake loop)
- WRAM clearing
- NMI/interrupt setup
Why Handlers Cannot Run in Isolation
ALTTP's object handlers are designed to run within an already-running game context:
- Shared Subroutines: Handlers call common routines that assume game state is initialized
- Bank Switching: Code frequently jumps between banks, requiring proper stack/return state
- Zero-Page Dependencies: Dozens of zero-page variables must be pre-set by the game
- Interrupt Context: Some operations depend on NMI/HDMA being active
Implications
The current approach of "cold start emulation" (reset SNES → jump to handler) is fundamentally flawed for ALTTP. Object handlers are not self-contained functions - they're subroutines within a complex runtime environment.
Recommended Future Approaches
- Save State Injection: Load a save state from a running game, modify WRAM to set up object parameters, then execute handler
- Full Game Boot: Run the game to a known "drawing ready" state (room loaded), then call handlers
- Static Mode: Continue using ObjectDrawer for reliable rendering (current default)
- Hybrid Tracing: Use emulator for debugging/analysis only, not rendering
Recent Improvements (2025-11-26)
CRITICAL FIX: SNES-to-PC Address Conversion (Session 2)
- Issue: All ROM addresses were SNES addresses (e.g.,
$01:8000) but code used them as PC file offsets - Root Cause: ALTTP uses LoROM mapping where SNES addresses must be converted to PC offsets
- Fix: Added
SnesToPc()helper function and converted all ROM address accesses - Conversion Formula:
PC = (bank & 0x7F) * 0x8000 + (addr - 0x8000) - Examples:
$01:8000→ PC$8000(handler table)$01:8200→ PC$8200(handler routine table)$0D:D308→ PC$6D308(sprite aux palette)
- Result: Correct handler addresses from ROM
Tilemap Pointer Fix (Session 2, Update 2)
- Issue: Tilemap pointers read from ROM were garbage - they're NOT stored in ROM
- Root Cause: Game initializes these pointers dynamically at runtime, not from ROM data
- Fix: Manually initialize tilemap pointers to point to WRAM buffer rows
- Pointers:
$BF,$C2,$C5, ... →$7E2000,$7E2080,$7E2100, ... (each row +$80) - Result: Valid WRAM pointers for indirect long addressing (
STA [$BF],Y)
APU Mock Fix (Session 2, Update 3)
- Issue: APU handshake at
$00:8891still hanging despite writing mock values - Root Cause: APU I/O ports have separate read/write latches:
Write($2140)goes toin_ports_[](CPU→SPC direction)Read($2140)returns fromout_ports_[](SPC→CPU direction)
- Fix: Set
out_ports_[]directly instead of using Write():apu.out_ports_[0] = 0xAA; // CPU reads $AA from $2140 apu.out_ports_[1] = 0xBB; // CPU reads $BB from $2141 - Result: APU handshake check passes, handler execution continues
Palette Fix (Both Modes)
- Issue: Tiles specifying palette indices 6-7 showed magenta (out-of-bounds)
- Fix: Now loads sprite auxiliary palettes from ROM
$0D:D308(PC:$6D308) into indices 90-119 - Result: Full 120-color palette support (palettes 0-7)
Emulator Mode Fixes (Session 1)
Based on analysis from zelda3-hacking-expert and snes-emulator-expert agents:
- Zero-Page Tilemap Pointers - Initialized $BF-$DD from
RoomData_TilemapPointersat$01:86F8 - APU Mock - Set
$2140-$2143to "ready" values ($AA,$BB) to prevent infinite APU handshake loop at$00:8891 - Two-Table Handler Lookup - Now uses both data offset table and handler address table
- Object Parameters - Properly initializes zero-page variables ($04, $08, $B2, $B4, etc.)
- CPU State - Correct register setup (X=data_offset, Y=tilemap_pos, PB=$01, DB=$7E)
- STP Trap - Uses STP opcode at
$01:FF00for reliable return detection
What Was Built
DungeonObjectEmulatorPreview Widget
Location: src/app/gui/widgets/dungeon_object_emulator_preview.cc
A preview tool that renders individual dungeon objects using two methods:
-
Static Mode (Default, Working)
- Uses
zelda3::ObjectDrawerto render objects - Same rendering path as the main dungeon canvas
- Reliable and fast
- Now supports full 120-color palette (palettes 0-7)
- Uses
-
Emulator Mode (Enhanced)
- Runs game's native drawing handlers via CPU emulation
- Full room context initialization
- Proper WRAM state setup
- APU mock to prevent infinite loops
Key Features
- Object ID input with hex display and name lookup
- Quick-select presets for common objects
- Object browser with all Type 1/2/3 objects
- Position (X/Y) and size controls
- Room ID for graphics/palette context
- Render mode toggle (Static vs Emulator)
Technical Details
Palette Handling (Updated)
- Dungeon main palette: 6 sub-palettes × 15 colors = 90 colors (indices 0-89)
- Sprite auxiliary palette: 2 sub-palettes × 15 colors = 30 colors (indices 90-119)
- Total: 120 colors (palettes 0-7)
- Source: Main from palette group, Aux from ROM
$0D:D308
Emulator State Initialization
1. Reset SNES, load room context
2. Load full 120-color palette into CGRAM
3. Convert 8BPP graphics to 4BPP planar, load to VRAM
4. Clear tilemap buffers ($7E:2000, $7E:4000)
5. Initialize zero-page tilemap pointers from $01:86F8
6. Mock APU I/O ($2140-$2143 = $AA/$BB)
7. Set object parameters in zero-page
8. Two-table handler lookup (data offset + handler address)
9. Setup CPU: X=data_offset, Y=tilemap_pos, PB=$01, DB=$7E
10. Push STP trap address, jump to handler
11. Execute until STP or timeout
12. Copy WRAM buffers to VRAM, render PPU
Files Modified
src/app/gui/widgets/dungeon_object_emulator_preview.h- Static rendering memberssrc/app/gui/widgets/dungeon_object_emulator_preview.cc- All emulator fixessrc/app/editor/ui/right_panel_manager.cc- Fixed deprecated ImGui flags
Tests
- BPP Conversion Tests: 12/12 PASS
- Dungeon Object Rendering Tests: 8/8 PASS
- Emulator State Injection Tests:
test/integration/emulator_object_preview_test.cc- LoROM Conversion Tests: Validates
SnesToPc()formula - APU Mock Tests: Verifies
out_ports_[]read behavior - Tilemap Pointer Setup Tests: Confirms WRAM pointer initialization
- Handler Table Reading Tests: Validates two-table lookup
- Handler Execution Trace Tests: Traces handler execution flow
- LoROM Conversion Tests: Validates
ROM Addresses Reference
IMPORTANT: ALTTP uses LoROM mapping. Always use SnesToPc() to convert SNES addresses to PC file offsets!
| SNES Address | PC Offset | Purpose |
|---|---|---|
$01:8000 |
$8000 |
Type 1 data offset table |
$01:8200 |
$8200 |
Type 1 handler routine table |
$01:8370 |
$8370 |
Type 2 data offset table |
$01:8470 |
$8470 |
Type 2 handler routine table |
$01:84F0 |
$84F0 |
Type 3 data offset table |
$01:85F0 |
$85F0 |
Type 3 handler routine table |
$00:9B52 |
$1B52 |
RoomDrawObjectData (tile definitions) |
$0D:D734 |
$6D734 |
Dungeon main palettes (0-5) |
$0D:D308 |
$6D308 |
Sprite auxiliary palettes (6-7) |
$7E:2000 |
(WRAM) | BG1 tilemap buffer (8KB) |
$7E:4000 |
(WRAM) | BG2 tilemap buffer (8KB) |
Note: Tilemap pointers at $BF-$DD are NOT in ROM - they're initialized dynamically to $7E2000+ at runtime.
Known Issues
1. SNES-to-PC Address Conversion (FIXED - Session 2)
ROM addresses were SNES addresses but used as PC offsets - Fixed with corrected SnesToPc() helper.
- Formula:
PC = (bank & 0x7F) * 0x8000 + (addr - 0x8000)
2. Tilemap Pointers from ROM (FIXED - Session 2)
Tried to read tilemap pointers from ROM at $01:86F8 - Pointers are NOT stored in ROM.
- Fixed by manually initializing pointers to WRAM buffer rows ($7E2000, $7E2080, etc.)
3. Emulator Mode Fundamentally Broken (ROOT CAUSE IDENTIFIED)
Root Cause: Object handlers are NOT self-contained. Test tracing revealed that handler $8B89 (object 0x00) immediately calls JSR $8000 - the game's RESET vector. This means:
- Handlers expect to run within a fully initialized game
- Cold-start emulation will always hit APU initialization at
$00:8891 - The handler code shares subroutines with game initialization
Test Evidence:
[ 0] $01:8B89: 20 -> $00:8000 (JSR to RESET vector)
[ 1] $00:8000: 78 (SEI - start of game init)
Current Status: Emulator mode requires architectural redesign. See "Recommended Future Approaches" at top of document.
Workaround: Use static mode for reliable rendering (default).
4. BG Layer Transparency
The compositing uses 0xFF as transparent marker, but edge cases with palette index 0 may exist.
How to Test
GUI Testing
# Build
cmake --build build --target yaze -j8
# Run with dungeon editor
./build/bin/Debug/yaze.app/Contents/MacOS/yaze --rom_file=zelda3.sfc --editor=Dungeon
# Open Emulator Preview from View menu or right panel
# Test both Static and Emulator modes
# Try objects: Wall (0x01), Floor (0x80), Chest (0xF8)
Emulator State Injection Tests
# Build tests
cmake --build build --target yaze_test_rom_dependent -j8
# Run with ROM path
YAZE_TEST_ROM_PATH=/path/to/zelda3.sfc ./build/bin/Debug/yaze_test_rom_dependent \
--gtest_filter="*EmulatorObjectPreviewTest*"
# Run specific test suites
YAZE_TEST_ROM_PATH=/path/to/zelda3.sfc ./build/bin/Debug/yaze_test_rom_dependent \
--gtest_filter="*EmulatorStateInjectionTest*"
YAZE_TEST_ROM_PATH=/path/to/zelda3.sfc ./build/bin/Debug/yaze_test_rom_dependent \
--gtest_filter="*HandlerExecutionTraceTest*"
Test Coverage
The test suite validates:
- LoROM Conversion -
SnesToPc()formula correctness - APU Mock - Proper
out_ports_[]vsin_ports_[]behavior - Handler Tables - Two-table lookup for all object types
- Tilemap Pointers - WRAM pointer initialization
- Execution Tracing - Handler flow analysis (reveals root cause)
Related Files
src/zelda3/dungeon/object_drawer.h- ObjectDrawer classsrc/app/gfx/render/background_buffer.h- BackgroundBuffer for tile storagesrc/zelda3/dungeon/room_object.h- RoomObject data structuredocs/internal/architecture/dungeon-object-rendering-plan.md- Overall rendering architectureassets/asm/usdasm/bank_01.asm- Handler disassembly referencetest/integration/emulator_object_preview_test.cc- Emulator state injection test suite