Files
yaze/docs/internal/archive/handoffs/handoff-dungeon-object-preview.md

12 KiB
Raw Blame History

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:

  1. Shared Subroutines: Handlers call common routines that assume game state is initialized
  2. Bank Switching: Code frequently jumps between banks, requiring proper stack/return state
  3. Zero-Page Dependencies: Dozens of zero-page variables must be pre-set by the game
  4. 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.

  1. Save State Injection: Load a save state from a running game, modify WRAM to set up object parameters, then execute handler
  2. Full Game Boot: Run the game to a known "drawing ready" state (room loaded), then call handlers
  3. Static Mode: Continue using ObjectDrawer for reliable rendering (current default)
  4. 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:8891 still hanging despite writing mock values
  • Root Cause: APU I/O ports have separate read/write latches:
    • Write($2140) goes to in_ports_[] (CPU→SPC direction)
    • Read($2140) returns from out_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:

  1. Zero-Page Tilemap Pointers - Initialized $BF-$DD from RoomData_TilemapPointers at $01:86F8
  2. APU Mock - Set $2140-$2143 to "ready" values ($AA, $BB) to prevent infinite APU handshake loop at $00:8891
  3. Two-Table Handler Lookup - Now uses both data offset table and handler address table
  4. Object Parameters - Properly initializes zero-page variables ($04, $08, $B2, $B4, etc.)
  5. CPU State - Correct register setup (X=data_offset, Y=tilemap_pos, PB=$01, DB=$7E)
  6. STP Trap - Uses STP opcode at $01:FF00 for 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:

  1. Static Mode (Default, Working)

    • Uses zelda3::ObjectDrawer to render objects
    • Same rendering path as the main dungeon canvas
    • Reliable and fast
    • Now supports full 120-color palette (palettes 0-7)
  2. 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 members
  • src/app/gui/widgets/dungeon_object_emulator_preview.cc - All emulator fixes
  • src/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

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:

  1. LoROM Conversion - SnesToPc() formula correctness
  2. APU Mock - Proper out_ports_[] vs in_ports_[] behavior
  3. Handler Tables - Two-table lookup for all object types
  4. Tilemap Pointers - WRAM pointer initialization
  5. Execution Tracing - Handler flow analysis (reveals root cause)
  • src/zelda3/dungeon/object_drawer.h - ObjectDrawer class
  • src/app/gfx/render/background_buffer.h - BackgroundBuffer for tile storage
  • src/zelda3/dungeon/room_object.h - RoomObject data structure
  • docs/internal/architecture/dungeon-object-rendering-plan.md - Overall rendering architecture
  • assets/asm/usdasm/bank_01.asm - Handler disassembly reference
  • test/integration/emulator_object_preview_test.cc - Emulator state injection test suite