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

266 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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