Files
yaze/docs/internal/debug/emulator-regressions-2025-11.md

11 KiB

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

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:

// 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:

// 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):

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:

// 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):

case 512: {
  if (!in_vblank_ && memory_.v_pos() > 0)
    ppu_.RunLine(memory_.v_pos());  // Everything at H=512
}

After PPU JIT:

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:

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:

    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

# 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):

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:

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():

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:

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):

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
    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)