266 lines
12 KiB
Markdown
266 lines
12 KiB
Markdown
# 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.
|
||
|
||
### Recommended Future Approaches
|
||
|
||
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():
|
||
```cpp
|
||
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
|
||
```bash
|
||
# 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
|
||
```bash
|
||
# 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)
|
||
|
||
## Related Files
|
||
|
||
- `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
|