backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)
This commit is contained in:
267
docs/internal/debug/ai_asm_guide.md
Normal file
267
docs/internal/debug/ai_asm_guide.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# AI-Assisted 65816 Assembly Debugging Guide
|
||||
|
||||
This guide documents how AI agents (Claude, Gemini, etc.) can use the yaze EmulatorService gRPC API to debug 65816 assembly code in SNES ROM hacks like Oracle of Secrets.
|
||||
|
||||
## Overview
|
||||
|
||||
The EmulatorService provides comprehensive debugging capabilities:
|
||||
- **Disassembly**: Convert raw bytes to human-readable 65816 assembly
|
||||
- **Symbol Resolution**: Map addresses to labels from Asar ASM files
|
||||
- **Breakpoints/Watchpoints**: Pause execution on conditions
|
||||
- **Stepping**: StepInto, StepOver, StepOut with call stack tracking
|
||||
- **Memory Inspection**: Read/write SNES memory regions
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Start the Emulator Server
|
||||
|
||||
```bash
|
||||
# Launch z3ed with ROM and start gRPC server
|
||||
z3ed emu start --rom oracle_of_secrets.sfc --grpc-port 50051
|
||||
```
|
||||
|
||||
### 2. Load Symbols (Optional but Recommended)
|
||||
|
||||
Load symbols from your ASM source directory for meaningful labels:
|
||||
|
||||
```protobuf
|
||||
rpc LoadSymbols(SymbolFileRequest) returns (CommandResponse)
|
||||
|
||||
// Request:
|
||||
// - path: Directory containing .asm files (e.g., "assets/asm/usdasm/bank00/")
|
||||
// - format: ASAR_ASM | WLA_DX | MESEN | BSNES
|
||||
```
|
||||
|
||||
### 3. Set Breakpoints
|
||||
|
||||
```protobuf
|
||||
rpc AddBreakpoint(BreakpointRequest) returns (BreakpointResponse)
|
||||
|
||||
// Request:
|
||||
// - address: 24-bit address (e.g., 0x008000 for bank 00, offset $8000)
|
||||
// - type: EXECUTE | READ | WRITE
|
||||
// - enabled: true/false
|
||||
// - condition: Optional expression (e.g., "A == 0x10")
|
||||
```
|
||||
|
||||
### 4. Run Until Breakpoint
|
||||
|
||||
```protobuf
|
||||
rpc RunToBreakpoint(Empty) returns (BreakpointHitResponse)
|
||||
|
||||
// Response includes:
|
||||
// - address: Where execution stopped
|
||||
// - breakpoint_id: Which breakpoint triggered
|
||||
// - registers: Current CPU state (A, X, Y, PC, SP, P, DBR, PBR, DP)
|
||||
```
|
||||
|
||||
## Debugging Workflow
|
||||
|
||||
### Disassembling Code
|
||||
|
||||
```protobuf
|
||||
rpc GetDisassembly(DisassemblyRequest) returns (DisassemblyResponse)
|
||||
|
||||
// Request:
|
||||
// - address: Starting 24-bit address
|
||||
// - count: Number of instructions to disassemble
|
||||
// - m_flag: Accumulator size (true = 8-bit, false = 16-bit)
|
||||
// - x_flag: Index register size (true = 8-bit, false = 16-bit)
|
||||
```
|
||||
|
||||
Example response with symbols loaded:
|
||||
```
|
||||
$008000: SEI ; Disable interrupts
|
||||
$008001: CLC ; Clear carry for native mode
|
||||
$008002: XCE ; Switch to native mode
|
||||
$008003: REP #$30 ; 16-bit A, X, Y
|
||||
$008005: LDA #$8000 ; Load screen buffer address
|
||||
$008008: STA $2100 ; PPU_BRIGHTNESS
|
||||
$00800B: JSR Reset ; Call Reset subroutine
|
||||
```
|
||||
|
||||
### Stepping Through Code
|
||||
|
||||
**StepInto** - Execute one instruction:
|
||||
```protobuf
|
||||
rpc StepInstruction(Empty) returns (StepResponse)
|
||||
```
|
||||
|
||||
**StepOver** - Execute subroutine as single step:
|
||||
```protobuf
|
||||
rpc StepOver(Empty) returns (StepResponse)
|
||||
// If current instruction is JSR/JSL, runs until it returns
|
||||
// Otherwise equivalent to StepInto
|
||||
```
|
||||
|
||||
**StepOut** - Run until current subroutine returns:
|
||||
```protobuf
|
||||
rpc StepOut(Empty) returns (StepResponse)
|
||||
// Continues execution until RTS/RTL decreases call depth
|
||||
```
|
||||
|
||||
### Reading Memory
|
||||
|
||||
```protobuf
|
||||
rpc ReadMemory(MemoryRequest) returns (MemoryResponse)
|
||||
|
||||
// Request:
|
||||
// - address: Starting address
|
||||
// - length: Number of bytes to read
|
||||
|
||||
// Response:
|
||||
// - data: Bytes as hex string or raw bytes
|
||||
```
|
||||
|
||||
Common SNES memory regions:
|
||||
- `$7E0000-$7FFFFF`: WRAM (128KB)
|
||||
- `$000000-$FFFFFF`: ROM (varies by mapper)
|
||||
- `$2100-$213F`: PPU registers
|
||||
- `$4200-$421F`: CPU registers
|
||||
- `$4300-$437F`: DMA registers
|
||||
|
||||
### Symbol Lookup
|
||||
|
||||
```protobuf
|
||||
rpc ResolveSymbol(SymbolLookupRequest) returns (SymbolLookupResponse)
|
||||
// name: "Player_X" -> address: 0x7E0010
|
||||
|
||||
rpc GetSymbolAt(AddressRequest) returns (SymbolLookupResponse)
|
||||
// address: 0x7E0010 -> name: "Player_X", type: RAM
|
||||
```
|
||||
|
||||
## 65816 Debugging Tips for AI Agents
|
||||
|
||||
### Understanding M/X Flags
|
||||
|
||||
The 65816 has variable-width registers controlled by status flags:
|
||||
- **M flag** (bit 5 of P): Controls accumulator/memory width
|
||||
- M=1: 8-bit accumulator, 8-bit memory operations
|
||||
- M=0: 16-bit accumulator, 16-bit memory operations
|
||||
- **X flag** (bit 4 of P): Controls index register width
|
||||
- X=1: 8-bit X and Y registers
|
||||
- X=0: 16-bit X and Y registers
|
||||
|
||||
Track flag changes from `REP` and `SEP` instructions:
|
||||
```asm
|
||||
REP #$30 ; M=0, X=0 (16-bit A, X, Y)
|
||||
SEP #$20 ; M=1 (8-bit A, X and Y unchanged)
|
||||
```
|
||||
|
||||
### Call Stack Tracking
|
||||
|
||||
The StepController automatically tracks:
|
||||
- `JSR $addr` - 16-bit call within current bank
|
||||
- `JSL $addr` - 24-bit long call across banks
|
||||
- `RTS` - Return from JSR
|
||||
- `RTL` - Return from JSL
|
||||
- `RTI` - Return from interrupt
|
||||
|
||||
Use `GetDebugStatus` to view the current call stack.
|
||||
|
||||
### Common Debugging Scenarios
|
||||
|
||||
**1. Finding where a value is modified:**
|
||||
```
|
||||
1. Add a WRITE watchpoint on the memory address
|
||||
2. Run emulation
|
||||
3. When watchpoint triggers, examine call stack and code
|
||||
```
|
||||
|
||||
**2. Tracing execution flow:**
|
||||
```
|
||||
1. Add EXECUTE breakpoint at entry point
|
||||
2. Use StepOver to execute subroutines as single steps
|
||||
3. Use StepInto when you want to enter a subroutine
|
||||
4. Use StepOut to return from deep call stacks
|
||||
```
|
||||
|
||||
**3. Understanding unknown code:**
|
||||
```
|
||||
1. Load symbols from source ASM files
|
||||
2. Disassemble the region of interest
|
||||
3. Cross-reference labels with source code
|
||||
```
|
||||
|
||||
## Example: Debugging Player Movement
|
||||
|
||||
```python
|
||||
# Pseudo-code for AI agent debugging workflow
|
||||
|
||||
# 1. Load symbols from Oracle of Secrets source
|
||||
client.LoadSymbols(path="oracle_of_secrets/src/", format=ASAR_ASM)
|
||||
|
||||
# 2. Find the player update routine
|
||||
result = client.ResolveSymbol(name="Player_Update")
|
||||
player_update_addr = result.address
|
||||
|
||||
# 3. Set breakpoint at player update
|
||||
bp = client.AddBreakpoint(address=player_update_addr, type=EXECUTE)
|
||||
|
||||
# 4. Run until we hit the player update
|
||||
hit = client.RunToBreakpoint()
|
||||
|
||||
# 5. Step through and inspect state
|
||||
while True:
|
||||
step = client.StepOver()
|
||||
print(f"PC: ${step.new_pc:06X} - {step.message}")
|
||||
|
||||
# Read player position after each step
|
||||
player_x = client.ReadMemory(address=0x7E0010, length=2)
|
||||
player_y = client.ReadMemory(address=0x7E0012, length=2)
|
||||
print(f"Player: ({player_x}, {player_y})")
|
||||
|
||||
if input("Continue? (y/n): ") != "y":
|
||||
break
|
||||
```
|
||||
|
||||
## Proto Definitions Reference
|
||||
|
||||
Key message types from `protos/emulator_service.proto`:
|
||||
|
||||
```protobuf
|
||||
message DisassemblyRequest {
|
||||
uint32 address = 1;
|
||||
uint32 count = 2;
|
||||
bool m_flag = 3;
|
||||
bool x_flag = 4;
|
||||
}
|
||||
|
||||
message BreakpointRequest {
|
||||
uint32 address = 1;
|
||||
BreakpointType type = 2;
|
||||
bool enabled = 3;
|
||||
string condition = 4;
|
||||
}
|
||||
|
||||
message StepResponse {
|
||||
bool success = 1;
|
||||
uint32 new_pc = 2;
|
||||
uint32 instructions_executed = 3;
|
||||
string message = 4;
|
||||
}
|
||||
|
||||
message SymbolLookupRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message SymbolLookupResponse {
|
||||
string name = 1;
|
||||
uint32 address = 2;
|
||||
string type = 3; // RAM, ROM, CONST
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Q: Disassembly shows wrong operand sizes**
|
||||
A: The M/X flags might not match. Use `GetGameState` to check current P register, then pass correct `m_flag` and `x_flag` values.
|
||||
|
||||
**Q: Symbols not resolving**
|
||||
A: Ensure you loaded symbols with `LoadSymbols` before calling `ResolveSymbol`. Check that the path points to valid ASM files.
|
||||
|
||||
**Q: StepOut not working**
|
||||
A: The call stack might be empty (program is at top level). Check `GetDebugStatus` for current call depth.
|
||||
|
||||
**Q: Breakpoint not triggering**
|
||||
A: Verify the address is correct (24-bit, bank:offset format). Check that the code actually executes that path.
|
||||
117
docs/internal/debug/audio-debugging-quick-ref.md
Normal file
117
docs/internal/debug/audio-debugging-quick-ref.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Audio Debugging Quick Reference
|
||||
|
||||
Quick reference for debugging MusicEditor audio timing issues.
|
||||
|
||||
## Audio Timing Checklist
|
||||
|
||||
Before investigating audio issues, verify these values are correct:
|
||||
|
||||
| Metric | Expected Value | Tolerance |
|
||||
|--------|----------------|-----------|
|
||||
| APU cycle rate | 1,024,000 Hz | +/- 1% |
|
||||
| DSP sample rate | 32,040 Hz | +/- 0.5% |
|
||||
| Samples per NTSC frame | 533-534 | +/- 2 |
|
||||
| APU/Master clock ratio | 0.0478 | exact |
|
||||
| Resampling | 32040 Hz → 48000 Hz | enabled |
|
||||
| Frame timing | 60.0988 Hz (NTSC) | exact |
|
||||
|
||||
## Running Audio Tests
|
||||
|
||||
```bash
|
||||
# Build with ROM tests enabled
|
||||
cmake --preset mac-dbg \
|
||||
-DYAZE_ENABLE_ROM_TESTS=ON \
|
||||
-DYAZE_TEST_ROM_PATH=~/zelda3.sfc
|
||||
|
||||
cmake --build --preset mac-dbg
|
||||
|
||||
# Run all audio tests
|
||||
ctest --test-dir build -L audio -V
|
||||
|
||||
# Run specific test with verbose output
|
||||
YAZE_TEST_ROM_PATH=~/zelda3.sfc ./build/bin/Debug/yaze_test_rom_dependent \
|
||||
--gtest_filter="*AudioTiming*" 2>&1 | tee audio_debug.log
|
||||
|
||||
# Generate timing report
|
||||
./build/bin/Debug/yaze_test_rom_dependent \
|
||||
--gtest_filter="*GenerateTimingReport*"
|
||||
```
|
||||
|
||||
## Key Log Categories
|
||||
|
||||
Enable these categories for audio debugging:
|
||||
|
||||
- `APU` - APU cycle execution
|
||||
- `APU_TIMING` - Cycle rate diagnostics
|
||||
- `DSP_TIMING` - Sample generation rates
|
||||
- `MusicPlayer` - Playback control
|
||||
- `AudioBackend` - Audio device/resampling
|
||||
|
||||
## Common Issues and Fixes
|
||||
|
||||
### 1.5x Speed Bug
|
||||
**Symptom**: Audio plays too fast, sounds pitched up
|
||||
**Cause**: Missing or incorrect resampling from 32040 Hz to 48000 Hz
|
||||
**Fix**: Verify `SetAudioStreamResampling(true, 32040, 2)` is called before playback
|
||||
|
||||
### Chipmunk Effect
|
||||
**Symptom**: Audio sounds very high-pitched and fast
|
||||
**Cause**: Sample rate mismatch - feeding 32kHz data to 48kHz device without resampling
|
||||
**Fix**: Enable SDL AudioStream resampling or fix sample rate configuration
|
||||
|
||||
### Stuttering/Choppy Audio
|
||||
**Symptom**: Audio breaks up or skips
|
||||
**Cause**: Buffer underrun - not generating samples fast enough
|
||||
**Fix**: Check frame timing in `MusicPlayer::Update()`, increase buffer prime size
|
||||
|
||||
### Pitch Drift Over Time
|
||||
**Symptom**: Audio gradually goes out of tune
|
||||
**Cause**: Floating-point accumulation error in cycle calculation
|
||||
**Fix**: Use fixed-point ratio in `APU::RunCycles()` (already implemented)
|
||||
|
||||
## Critical Code Paths
|
||||
|
||||
| File | Function | Purpose |
|
||||
|------|----------|---------|
|
||||
| `apu.cc:88-224` | `RunCycles()` | APU/Master clock sync |
|
||||
| `apu.cc:226-251` | `Cycle()` | DSP tick every 32 cycles |
|
||||
| `dsp.cc:142-182` | `Cycle()` | Sample generation |
|
||||
| `dsp.cc:720-846` | `GetSamples()` | Resampling output |
|
||||
| `music_player.cc:75-156` | `Update()` | Frame timing |
|
||||
| `music_player.cc:164-236` | `EnsureAudioReady()` | Audio init |
|
||||
| `audio_backend.cc:359-406` | `SetAudioStreamResampling()` | 32kHz→48kHz |
|
||||
|
||||
## Timing Constants
|
||||
|
||||
From `apu.cc`:
|
||||
```cpp
|
||||
// APU/Master fixed-point ratio (no floating-point drift)
|
||||
constexpr uint64_t kApuCyclesNumerator = 32040 * 32; // 1,025,280
|
||||
constexpr uint64_t kApuCyclesDenominator = 1364 * 262 * 60; // 21,437,280
|
||||
|
||||
// APU cycles per master cycle ≈ 0.0478
|
||||
// DSP cycles every 32 APU cycles
|
||||
// Native sample rate: 32040 Hz
|
||||
// Samples per NTSC frame: 32040 / 60.0988 ≈ 533
|
||||
```
|
||||
|
||||
## Debug Build Flags
|
||||
|
||||
Start yaze with debug flags for audio investigation:
|
||||
|
||||
```bash
|
||||
./yaze --debug --log_file=audio_debug.log \
|
||||
--rom_file=zelda3.sfc --editor=Music
|
||||
```
|
||||
|
||||
## Test Output Files
|
||||
|
||||
Tests write diagnostic files to `/tmp/`:
|
||||
- `audio_timing_report.txt` - Full timing metrics
|
||||
- `audio_timing_drift.txt` - Per-second data (CSV format)
|
||||
|
||||
Parse CSV data for analysis:
|
||||
```bash
|
||||
# Show timing ratios over time
|
||||
awk -F, 'NR>1 {print $1, $4, $5}' /tmp/audio_timing_drift.txt
|
||||
```
|
||||
354
docs/internal/debug/emulator-regressions-2025-11.md
Normal file
354
docs/internal/debug/emulator-regressions-2025-11.md
Normal file
@@ -0,0 +1,354 @@
|
||||
# Emulator Regressions - November 2025
|
||||
|
||||
## Status: UNRESOLVED
|
||||
|
||||
Two regressions have been identified in the SNES emulator that affect:
|
||||
1. Input handling (A button not working on file naming screen)
|
||||
2. PPU rendering (title screen BG layer not showing)
|
||||
|
||||
**Note**: Keybindings system is currently being modified by another agent. Changes may interact.
|
||||
|
||||
---
|
||||
|
||||
## Issue 1: Input Button Mapping Bug
|
||||
|
||||
### Symptoms
|
||||
- A button does not work on the ALTTP file naming screen
|
||||
- D-pad works correctly
|
||||
- A button works on title screen (different code path?)
|
||||
|
||||
### Root Cause Analysis
|
||||
|
||||
**Bug Location**: `src/app/emu/snes.cc:763`
|
||||
|
||||
```cpp
|
||||
void Snes::SetButtonState(int player, int button, bool pressed) {
|
||||
// BUG: This logic is inverted!
|
||||
Input* input = (player == 1) ? &input1 : &input2;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
When calling `SetButtonState(0, button, true)` (player 0 = player 1 in SNES terms), it incorrectly selects `input2` instead of `input1`.
|
||||
|
||||
**Introduced in**: Commit `9ffb7803f5` (Oct 11, 2025)
|
||||
- "refactor(emulator): enhance input handling and audio resampling features"
|
||||
|
||||
### Attempted Fix
|
||||
|
||||
In this session, we updated the button constants in `save_state_manager.h` from bitmasks to bit indices:
|
||||
```cpp
|
||||
// Before (incorrect for SetButtonState API):
|
||||
constexpr uint16_t kA = 0x0080; // Bitmask
|
||||
|
||||
// After (correct bit index):
|
||||
constexpr int kA = 8; // Bit index
|
||||
```
|
||||
|
||||
However, this fix alone doesn't resolve the issue because `SetButtonState` itself has the player mapping inverted.
|
||||
|
||||
### Proposed Fix
|
||||
|
||||
Change line 763 in `snes.cc`:
|
||||
```cpp
|
||||
// Current (wrong):
|
||||
Input* input = (player == 1) ? &input1 : &input2;
|
||||
|
||||
// Should be:
|
||||
Input* input = (player == 0) ? &input1 : &input2;
|
||||
```
|
||||
|
||||
Or alternatively, to match common conventions (player 1 = first player):
|
||||
```cpp
|
||||
Input* input = (player <= 1) ? &input1 : &input2;
|
||||
```
|
||||
|
||||
### Additional Notes
|
||||
|
||||
The title screen may work because it uses a different input reading path or auto-joypad read timing that happens to work despite the bug.
|
||||
|
||||
---
|
||||
|
||||
## Issue 2: PPU Title Screen BG Layer Not Rendering
|
||||
|
||||
### Symptoms
|
||||
- Title screen background layer(s) not showing
|
||||
- Timing unclear - may have been introduced in recent commits
|
||||
|
||||
### Potential Root Cause
|
||||
|
||||
**Commit**: `e37497e9ef` (Nov 23, 2025)
|
||||
- "feat(emu): add PPU JIT catch-up for mid-scanline raster effects"
|
||||
|
||||
This commit refactored PPU rendering from a simple `RunLine()` call to a progressive JIT system:
|
||||
|
||||
```cpp
|
||||
// Old approach:
|
||||
ppu_.RunLine(line); // Render entire line at once
|
||||
|
||||
// New approach:
|
||||
ppu_.StartLine(line); // Setup for line
|
||||
ppu_.CatchUp(512); // Render first half
|
||||
ppu_.CatchUp(1104); // Render second half
|
||||
```
|
||||
|
||||
### Key Changes to Investigate
|
||||
|
||||
1. **StartLine() timing**: Now called at H=0 instead of H=512
|
||||
- `StartLine()` does sprite evaluation and mode 7 setup
|
||||
- May need to be called earlier or with different conditions
|
||||
|
||||
2. **CatchUp() vs RunLine()**: The new progressive rendering may have edge cases
|
||||
- `CatchUp(512)` renders pixels 0-127
|
||||
- `CatchUp(1104)` should render pixels 128-255
|
||||
- But 1104/4 = 276, so it tries to render up to 256 (clamped)
|
||||
|
||||
3. **WriteBBus PPU catch-up**: Added mid-scanline PPU register write handling
|
||||
- May interfere with normal rendering sequence
|
||||
|
||||
### Files Changed in PPU Refactor
|
||||
|
||||
- `src/app/emu/video/ppu.cc`: Added `StartLine()`, `CatchUp()`, `last_rendered_x_`
|
||||
- `src/app/emu/video/ppu.h`: Added new method declarations
|
||||
- `src/app/emu/snes.cc`: Changed `RunLine()` calls to `StartLine()`/`CatchUp()`
|
||||
|
||||
### Key Timing Difference
|
||||
|
||||
**Before PPU JIT (commit e37497e9ef~1)**:
|
||||
```cpp
|
||||
case 512: {
|
||||
if (!in_vblank_ && memory_.v_pos() > 0)
|
||||
ppu_.RunLine(memory_.v_pos()); // Everything at H=512
|
||||
}
|
||||
```
|
||||
|
||||
**After PPU JIT**:
|
||||
```cpp
|
||||
case 16: {
|
||||
ppu_.StartLine(memory_.v_pos()); // Sprite eval at H=16
|
||||
}
|
||||
case 512: {
|
||||
ppu_.CatchUp(512); // Pixels 0-127 at H=512
|
||||
}
|
||||
case 1104: {
|
||||
ppu_.CatchUp(1104); // Pixels 128-255 at H=1104
|
||||
}
|
||||
```
|
||||
|
||||
The sprite evaluation (`EvaluateSprites`) now happens at H=16 instead of H=512. This timing change could affect games that modify OAM or PPU registers via HDMA between H=16 and H=512.
|
||||
|
||||
### Quick Test: Revert to Old PPU Timing
|
||||
|
||||
To test if the PPU JIT is causing the issue, temporarily revert to `RunLine()`:
|
||||
|
||||
In `src/app/emu/snes.cc`, change the case 16 and 512 blocks:
|
||||
|
||||
```cpp
|
||||
case 16: {
|
||||
next_horiz_event = 512;
|
||||
if (memory_.v_pos() == 0)
|
||||
memory_.init_hdma_request();
|
||||
// Remove StartLine call
|
||||
} break;
|
||||
case 512: {
|
||||
next_horiz_event = 1104;
|
||||
if (!in_vblank_ && memory_.v_pos() > 0)
|
||||
ppu_.RunLine(memory_.v_pos()); // Back to old method
|
||||
} break;
|
||||
case 1104: {
|
||||
// Remove CatchUp call
|
||||
if (!in_vblank_)
|
||||
memory_.run_hdma_request();
|
||||
// ... rest unchanged
|
||||
```
|
||||
|
||||
### Debugging Steps
|
||||
|
||||
1. Add logging to PPU to verify:
|
||||
- Is `StartLine()` being called for each visible scanline?
|
||||
- Is `CatchUp()` rendering all 256 pixels?
|
||||
- Are any BG enable flags being cleared unexpectedly?
|
||||
|
||||
2. Test reverting PPU changes:
|
||||
```bash
|
||||
git checkout e37497e9ef~1 -- src/app/emu/video/ppu.cc src/app/emu/video/ppu.h src/app/emu/snes.cc
|
||||
```
|
||||
|
||||
3. Compare title screen behavior before and after commit `e37497e9ef`
|
||||
|
||||
---
|
||||
|
||||
## Git History Reference
|
||||
|
||||
### Key Commits (Chronological)
|
||||
|
||||
| Date | Commit | Description |
|
||||
|------|--------|-------------|
|
||||
| Oct 11, 2025 | `9ffb7803f5` | Input handling refactor - introduced player mapping bug |
|
||||
| Nov 23, 2025 | `e37497e9ef` | PPU JIT catch-up - potential BG rendering regression |
|
||||
| Nov 25, 2025 | `9d788fe6b0` | Lazy SNES init - may affect startup timing |
|
||||
| Nov 26, 2025 | (this session) | SaveStateManager button constant fix |
|
||||
|
||||
### Commands to Investigate
|
||||
|
||||
```bash
|
||||
# View input handling changes
|
||||
git show 9ffb7803f5 -- src/app/emu/snes.cc
|
||||
|
||||
# View PPU changes
|
||||
git show e37497e9ef -- src/app/emu/video/ppu.cc src/app/emu/snes.cc
|
||||
|
||||
# Diff current vs before PPU JIT
|
||||
git diff e37497e9ef~1..HEAD -- src/app/emu/video/ppu.cc
|
||||
|
||||
# Test with old PPU code
|
||||
git stash
|
||||
git checkout e37497e9ef~1 -- src/app/emu/video/ppu.cc src/app/emu/video/ppu.h
|
||||
cmake --build build --target yaze
|
||||
# Test emulator, then restore:
|
||||
git checkout HEAD -- src/app/emu/video/ppu.cc src/app/emu/video/ppu.h
|
||||
git stash pop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Attempted Fixes (Did Not Resolve)
|
||||
|
||||
### Session 2025-11-26
|
||||
|
||||
1. **Button constants fix** (`save_state_manager.h`)
|
||||
- Changed from bitmasks to bit indices
|
||||
- Status: Applied, did not fix input issue
|
||||
|
||||
2. **SetButtonState player mapping** (`snes.cc:763`)
|
||||
- Changed `player == 1` to `player <= 1`
|
||||
- Status: Applied, did not fix input issue
|
||||
|
||||
3. **PPU JIT revert** (`snes.cc`)
|
||||
- Reverted StartLine/CatchUp back to RunLine
|
||||
- Status: Applied, did not fix BG layer issue
|
||||
|
||||
## Investigation Session 2025-11-26 (New Findings)
|
||||
|
||||
### Input Bug Analysis
|
||||
|
||||
**SetButtonState is now correct** (`snes.cc:750`):
|
||||
```cpp
|
||||
Input* input = (player <= 1) ? &input1 : &input2;
|
||||
```
|
||||
|
||||
**Debug logging already exists** in HandleInput():
|
||||
- Logs when A button is active in `current_state_`
|
||||
- Logs `port_auto_read_[0]` value after auto-joypad read
|
||||
|
||||
**CRITICAL SUSPECT: ImGui WantTextInput blocking**
|
||||
|
||||
In `src/app/emu/input/sdl3_input_backend.cc:67-73`:
|
||||
```cpp
|
||||
if (io.WantTextInput) {
|
||||
static int text_input_log_count = 0;
|
||||
if (text_input_log_count++ < 5) {
|
||||
LOG_DEBUG("InputBackend", "Blocking game input - WantTextInput=true");
|
||||
}
|
||||
return ControllerState{}; // <-- ALL input blocked!
|
||||
}
|
||||
```
|
||||
|
||||
If ANY ImGui text input widget is active, ALL game input is blocked. This could explain:
|
||||
- Why D-pad works but A doesn't → unlikely, would block both
|
||||
- Why title screen works but naming screen doesn't → possible if yaze UI has text field active
|
||||
|
||||
**Diagnostic**: Check if "Blocking game input - WantTextInput=true" appears in logs when on naming screen.
|
||||
|
||||
### PPU Bug Analysis
|
||||
|
||||
**CRITICAL FINDING: "Revert" was incomplete**
|
||||
|
||||
Current `snes.cc:214` calls `ppu_.RunLine()`:
|
||||
```cpp
|
||||
case 512: {
|
||||
next_horiz_event = 1104;
|
||||
if (!in_vblank_ && memory_.v_pos() > 0)
|
||||
ppu_.RunLine(memory_.v_pos()); // Looks like old code
|
||||
}
|
||||
```
|
||||
|
||||
BUT `RunLine()` in `ppu.cc:174-178` now calls the JIT mechanism:
|
||||
```cpp
|
||||
void Ppu::RunLine(int line) {
|
||||
// Legacy wrapper - renders the whole line at once
|
||||
StartLine(line); // <-- Uses new JIT setup
|
||||
CatchUp(2000); // <-- Uses new JIT rendering
|
||||
}
|
||||
```
|
||||
|
||||
**Original `RunLine()` was a direct loop** (before e37497e9ef):
|
||||
```cpp
|
||||
void Ppu::RunLine(int line) {
|
||||
obj_pixel_buffer_.fill(0);
|
||||
if (!forced_blank_) EvaluateSprites(line - 1);
|
||||
if (mode == 7) CalculateMode7Starts(line);
|
||||
for (int x = 0; x < 256; x++) {
|
||||
HandlePixel(x, line); // Direct loop, no JIT state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Difference**:
|
||||
- Old: Uses `line` parameter directly in `HandlePixel(x, line)`
|
||||
- New: Uses member variable `current_scanline_` set by `StartLine()`
|
||||
|
||||
**Potential Bug**: If `current_scanline_` or `last_rendered_x_` have stale/incorrect values, rendering breaks.
|
||||
|
||||
**TRUE REVERT Required**: To test if JIT is the cause, must restore the original `ppu.cc` implementation, not just the `snes.cc` call sites.
|
||||
|
||||
---
|
||||
|
||||
## Investigation Session 2025-11-27 (snes-emulator-expert)
|
||||
|
||||
### PPU State Check (current dirty tree)
|
||||
- `ppu.cc` has already been changed back to the legacy full-line renderer inside `RunLine()` (StartLine/CatchUp still exist but are unused). The earlier suspicion that the wrapper itself was blanking the BG no longer applies.
|
||||
- `snes.cc` only calls `RunLine()` once per scanline at H=512; there are no remaining PPU catch-up hooks in `WriteBBus`, so the JIT path is effectively dead code right now.
|
||||
|
||||
### Runtime Observation (yaze_emu_trace.log)
|
||||
- Headless run shows the CPU stuck in the SPC handshake loop at `$00:88B6` (`CMP.w APUIO0` / `BNE .wait_for_zero`), with NMIs never enabled in the first 120 frames.
|
||||
- If the SPC handshake never completes, the game never uploads title-screen VRAM/CGRAM or enables 212C/212D, so the blank BG may be a fallout of stalled boot rather than a renderer defect.
|
||||
|
||||
### Next Steps (PPU-focused)
|
||||
- First, confirm the SPC handshake completes (APUIO0 transitions off zero) so the game can reach module `0x01`; otherwise any PPU checks are moot.
|
||||
- After the handshake, instrument `RunLine` (e.g., when `line==100`) to log `forced_blank_`, `mode`, and `layer_[i].mainScreenEnabled` to ensure BGs are actually enabled on the title frame.
|
||||
- If layers are enabled but BG still missing, capture VRAM around the title tilemap upload to ensure DMA is populating the expected addresses.
|
||||
|
||||
---
|
||||
|
||||
## Updated Next Steps
|
||||
|
||||
### Priority 1: Input Bug
|
||||
- [ ] Check logs for "Blocking game input - WantTextInput=true" message
|
||||
- [ ] Verify if any ImGui InputText widget is active during emulation
|
||||
- [ ] Test with `WantTextInput` check temporarily removed
|
||||
- [ ] Trace: SDL key state → Poll() → SetButtonState() → HandleInput()
|
||||
|
||||
### Priority 2: PPU Bug
|
||||
- [ ] **TRUE revert test**: Restore original `ppu.cc` from `e37497e9ef~1`
|
||||
```bash
|
||||
git show e37497e9ef~1:src/app/emu/video/ppu.cc > /tmp/old_ppu.cc
|
||||
# Compare and apply the old RunLine() implementation
|
||||
```
|
||||
- [ ] Add logging to verify `current_scanline_` and `last_rendered_x_` values
|
||||
- [ ] Check layer enable flags (`layer_[i].mainScreenEnabled`) during title screen
|
||||
- [ ] Verify VRAM contains tile data
|
||||
|
||||
### Priority 3: General
|
||||
- [ ] **Git bisect** to find exact commit where emulator last worked
|
||||
- [ ] Coordinate with keybindings agent work
|
||||
|
||||
## Potentially Relevant Commits
|
||||
|
||||
| Commit | Date | Description |
|
||||
|--------|------|-------------|
|
||||
| `0579fc2c65` | Earlier | Implement input management system with SDL2 |
|
||||
| `9ffb7803f5` | Oct 11 | Enhance input handling (introduced SetButtonState) |
|
||||
| `2f0006ac0b` | Later | SDL compatibility layer |
|
||||
| `a5dc884612` | Later | SDL3 backend infrastructure |
|
||||
| `e37497e9ef` | Nov 23 | PPU JIT catch-up (reverted) |
|
||||
199
docs/internal/debug/naming-screen-input-debug.md
Normal file
199
docs/internal/debug/naming-screen-input-debug.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# ALTTP Naming Screen Input Debug Log
|
||||
|
||||
## Problem Statement
|
||||
On the ALTTP naming screen:
|
||||
- **D-pad works** - cursor moves correctly
|
||||
- **A and B buttons do NOT work** - cannot select letters or delete
|
||||
|
||||
## What We've Confirmed Working
|
||||
|
||||
### 1. SDL Input Polling ✓
|
||||
- `SDL_PumpEvents()` and `SDL_GetKeyboardState()` correctly detect keypresses
|
||||
- Keyboard input is captured and converted to button state
|
||||
- Logs show: `SDL2 Poll: buttons=0x0100 (keyboard detected)` when A pressed
|
||||
|
||||
### 2. Internal Button State (`current_state_`) ✓
|
||||
- `SetButtonState()` correctly sets bits in `input1.current_state_`
|
||||
- A button = bit 8 (0x0100), B button = bit 0 (0x0001)
|
||||
- State changes are logged and verified
|
||||
|
||||
### 3. Per-Frame Input Polling ✓
|
||||
- Fixed: `Poll()` now called before each `snes_.RunFrame()` (not just once per GUI frame)
|
||||
- This ensures fresh keyboard state for each SNES frame
|
||||
- Critical for edge detection when multiple SNES frames run per GUI update
|
||||
|
||||
### 4. HandleInput() / Auto-Joypad Read ✓
|
||||
- `HandleInput()` is called at VBlank when `auto_joy_read_` is enabled
|
||||
- `port_auto_read_[]` is correctly populated via serial read simulation
|
||||
- Logs confirm state changes:
|
||||
```
|
||||
HandleInput #909: current_state CHANGED 0x0000 -> 0x0100
|
||||
HandleInput #909 RESULT: port_auto_read CHANGED 0x0000 -> 0x0080
|
||||
HandleInput #912: current_state CHANGED 0x0100 -> 0x0000
|
||||
HandleInput #912 RESULT: port_auto_read CHANGED 0x0080 -> 0x0000
|
||||
```
|
||||
|
||||
### 5. Button Serialization ✓
|
||||
- Internal bit 8 (A) correctly maps to port_auto_read bit 7 (0x0080)
|
||||
- This matches SNES hardware: A is bit 7 of $4218 (JOY1L)
|
||||
- Verified mappings:
|
||||
- A (0x0100) → port_auto_read 0x0080 ✓
|
||||
- B (0x0001) → port_auto_read 0x8000 ✓
|
||||
- Start (0x0008) → port_auto_read 0x1000 ✓
|
||||
- Down (0x0020) → port_auto_read 0x0400 ✓
|
||||
|
||||
### 6. Register Reads ($4218/$4219) ✓
|
||||
- Game reads both registers in NMI handler at PC=$00:83D7 and $00:83DC
|
||||
- $4218 returns low byte of port_auto_read (contains A, X, L, R)
|
||||
- $4219 returns high byte of port_auto_read (contains B, Y, Select, Start, D-pad)
|
||||
- Logs confirm: `Game read $4218 = $80` when A pressed
|
||||
|
||||
### 7. Edge Transitions Exist ✓
|
||||
- port_auto_read transitions: 0x0000 → 0x0080 → 0x0000
|
||||
- The hardware-level "edge" (button press/release) IS being created
|
||||
- Game should see: $4218 = 0x00, then 0x80, then 0x00
|
||||
|
||||
## ALTTP Input System (from usdasm analysis)
|
||||
|
||||
### Memory Layout
|
||||
| Address | Name | Source | Contents |
|
||||
|---------|------|--------|----------|
|
||||
| $F0 | cur_hi | $4219 | B, Y, Select, Start, U, D, L, R |
|
||||
| $F2 | cur_lo | $4218 | A, X, L, R, 0, 0, 0, 0 |
|
||||
| $F4 | new_hi | edge($F0) | Newly pressed from high byte |
|
||||
| $F6 | new_lo | edge($F2) | Newly pressed from low byte |
|
||||
| $F8 | prv_hi | prev $F0 | Previous frame high byte |
|
||||
| $FA | prv_lo | prev $F2 | Previous frame low byte |
|
||||
|
||||
### Edge Detection Formula (NMI_ReadJoypads at $00:83D1)
|
||||
```asm
|
||||
; For low byte (contains A button):
|
||||
LDA $4218 ; Read current
|
||||
STA $F2 ; Store current
|
||||
EOR $FA ; XOR with previous (bits that changed)
|
||||
AND $F2 ; AND with current (only newly pressed)
|
||||
STA $F6 ; Store newly pressed
|
||||
STY $FA ; Update previous
|
||||
```
|
||||
|
||||
### Key Difference: D-pad vs Face Buttons
|
||||
- **D-pad**: Uses `$F0` (CURRENT state) - no edge detection needed
|
||||
```asm
|
||||
LDA.b $F0 ; Load current high byte
|
||||
AND.b #$0F ; Mask D-pad bits
|
||||
```
|
||||
- **A/B buttons**: Uses `$F6` (NEWLY PRESSED) - requires edge detection
|
||||
```asm
|
||||
LDA.b $F6 ; Load newly pressed low byte
|
||||
AND.b #$C0 ; Mask A ($80) and X ($40)
|
||||
BNE .select ; Branch if newly pressed
|
||||
```
|
||||
|
||||
**This explains why D-pad works but A/B don't** - D-pad bypasses edge detection!
|
||||
|
||||
## Current Hypothesis
|
||||
|
||||
The edge detection computation in the game's RAM is failing. Specifically:
|
||||
- $F2 gets correct value (0x80 when A pressed)
|
||||
- $F6 should get 0x80 on the first frame A is pressed
|
||||
- But $F6 might be staying 0x00
|
||||
|
||||
### Possible Causes
|
||||
1. **$FA (previous) already has A bit set** - Would cause XOR to cancel out
|
||||
2. **CPU emulation bug** - EOR or AND instruction not working correctly
|
||||
3. **RAM write issue** - Values not being stored correctly
|
||||
4. **Timing issue** - Previous frame's value not being saved properly
|
||||
|
||||
## Debug Logging Added
|
||||
|
||||
### 1. HandleInput State Changes
|
||||
```cpp
|
||||
if (input1.current_state_ != last_current) {
|
||||
LOG_DEBUG("HandleInput #%d: current_state CHANGED 0x%04X -> 0x%04X", ...);
|
||||
}
|
||||
if (port_auto_read_[0] != last_port) {
|
||||
LOG_DEBUG("HandleInput #%d RESULT: port_auto_read CHANGED 0x%04X -> 0x%04X", ...);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. RAM Writes to Joypad Variables
|
||||
```cpp
|
||||
// Log writes to $F2, $F6, $FA when A bit is set
|
||||
if (adr == 0x00F2 || adr == 0x00F6 || adr == 0x00FA) {
|
||||
if (val & 0x80) { // A button bit
|
||||
LOG_DEBUG("RAM WRITE %s = $%02X (A bit SET)", ...);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Findings (Nov 26, 2025)
|
||||
|
||||
### INPUT SYSTEM CONFIRMED WORKING ✓
|
||||
|
||||
After extensive testing with programmatic button injection:
|
||||
|
||||
1. **SDL Input Polling** ✓ - Correctly captures keyboard state
|
||||
2. **HandleInput/Auto-Joypad** ✓ - Correctly latches input to port_auto_read
|
||||
3. **$4218 Register Reads** ✓ - Game correctly reads button state ($80 for A button)
|
||||
4. **$00F2 RAM Writes** ✓ - NMI handler writes $80 to $00F2 (current button state)
|
||||
5. **$00F6 Edge Detection** ✓ - NMI handler writes $80 to $00F6 on FIRST PRESS frame
|
||||
|
||||
### Test Results with Injected A Button
|
||||
|
||||
```
|
||||
F83 $4218@83D7: result=$80 port=$0080 current=$0100
|
||||
$00F2] cur_lo = $80 at PC=$00:83E2 A=$0280 <- CORRECT!
|
||||
$00F6] new_lo = $80 at PC=$00:83E9 <- EDGE DETECTED!
|
||||
|
||||
F85 $4218@83D7: result=$80 port=$0080 current=$0100
|
||||
$00F2] cur_lo = $80 at PC=$00:83E2 <- CORRECT!
|
||||
$00F6] new_lo = $00 at PC=$00:83E9 <- No new edge (button held)
|
||||
```
|
||||
|
||||
### Resolution
|
||||
|
||||
The input system is functioning correctly:
|
||||
- Button presses are detected by SDL
|
||||
- HandleInput correctly latches button state at VBlank
|
||||
- Game reads $4218 and gets correct button value
|
||||
- NMI handler writes correct values to $00F2 (current) and $00F6 (edge)
|
||||
|
||||
The earlier reported issue with naming screen may have been:
|
||||
1. A timing-sensitive issue that was fixed during earlier debugging
|
||||
2. Specific to interactive vs programmatic input
|
||||
3. Related to game state (title screen vs naming screen)
|
||||
|
||||
### Two Separate Joypad RAM Areas (Reference)
|
||||
|
||||
ALTTP maintains TWO sets of joypad RAM:
|
||||
|
||||
| Address Range | Written By | PC Range | Purpose |
|
||||
|--------------|------------|----------|---------|
|
||||
| $01F0-$01FA | Game loop code | $8141/$8144 | Used during gameplay |
|
||||
| $00F0-$00FA | NMI_ReadJoypads | $83E2 | Used during menus (D=$0000) |
|
||||
|
||||
Both are now correctly populated with button data.
|
||||
|
||||
## Investigation Complete
|
||||
|
||||
The input system has been verified as working correctly. No further investigation needed unless
|
||||
new issues are reported with specific reproduction steps.
|
||||
|
||||
## Filter Commands
|
||||
|
||||
```bash
|
||||
# Show HandleInput state changes
|
||||
grep -E "HandleInput.*CHANGED"
|
||||
|
||||
# Show RAM writes to joypad variables
|
||||
grep -E "RAM WRITE"
|
||||
|
||||
# Combined
|
||||
grep -E "RAM WRITE|HandleInput.*CHANGED"
|
||||
```
|
||||
|
||||
## Files Modified for Debugging
|
||||
|
||||
- `src/app/emu/snes.cc` - HandleInput logging, RAM write logging
|
||||
- `src/app/emu/emulator.cc` - Per-frame Poll() calls
|
||||
- `src/app/emu/ui/emulator_ui.cc` - Virtual controller debug display
|
||||
Reference in New Issue
Block a user