9.2 KiB
Emulator Regression Trace
Tracking git history to find root cause of title screen BG being black.
Issue Description
- Symptom: Title screen background is black (after sword comes down)
- Working: Triforce animation, Nintendo logo, cutscene after title screen, file select
- Broken: Title screen BG layer specifically
Commit Analysis
Commit: e37497e9ef - "feat(emu): add PPU JIT catch-up for mid-scanline raster effects"
Date: Sun Nov 23 00:40:58 2025 Author: scawful + Claude
This is the commit that introduced the JIT progressive rendering system.
Changes Made
ppu.h additions:
void StartLine(int line);
void CatchUp(int h_pos);
// New members:
int last_rendered_x_ = 0;
int current_scanline_; // (implicit, used in CatchUp)
ppu.cc changes:
// OLD RunLine - rendered entire line at once:
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);
}
}
// NEW - Split into StartLine + CatchUp:
void Ppu::StartLine(int line) {
current_scanline_ = line;
last_rendered_x_ = 0;
obj_pixel_buffer_.fill(0);
if (!forced_blank_) EvaluateSprites(line - 1);
if (mode == 7) CalculateMode7Starts(line);
}
void Ppu::CatchUp(int h_pos) {
int target_x = h_pos / 4; // 1 pixel = 4 master cycles
if (target_x > 256) target_x = 256;
if (target_x <= last_rendered_x_) return;
for (int x = last_rendered_x_; x < target_x; x++) {
HandlePixel(x, current_scanline_);
}
last_rendered_x_ = target_x;
}
void Ppu::RunLine(int line) {
// Legacy wrapper
StartLine(line);
CatchUp(2000); // Force full line render
}
snes.cc changes:
// Timing calls in RunCycle():
case 16: { // Was: case 0 in some versions
// ... init_hdma_request ...
if (!in_vblank_ && memory_.v_pos() > 0)
ppu_.StartLine(memory_.v_pos()); // NEW: Initialize scanline
}
case 512: {
if (!in_vblank_ && memory_.v_pos() > 0)
ppu_.CatchUp(512); // CHANGED: Was ppu_.RunLine(memory_.v_pos())
}
case 1104: {
if (!in_vblank_ && memory_.v_pos() > 0)
ppu_.CatchUp(1104); // NEW: Finish line
// Then run HDMA...
}
// WriteBBus addition:
void Snes::WriteBBus(uint8_t adr, uint8_t val) {
if (adr < 0x40) {
// NEW: Catch up before PPU register write for mid-scanline effects
if (!in_vblank_ && memory_.v_pos() > 0 && memory_.h_pos() < 1100) {
ppu_.CatchUp(memory_.h_pos());
}
ppu_.Write(adr, val);
return;
}
// ...
}
Potential Issues Identified
-
current_scanline_not declared in ppu.h - The variable is used but may not be properly declared as a member. Need to verify. -
h_pos timing at case 16 vs case 0 - The code shows
case 16:but original may have been different. Need to verify StartLine is called at correct time. -
CatchUp(512) only renders pixels 0-127 - With
target_x = 512/4 = 128, this only renders half the line. The secondCatchUp(1104)renders1104/4 = 276→ clamped to 256, so pixels 128-255. -
WriteBBus CatchUp may cause double-rendering - If HDMA writes to PPU registers, CatchUp is called, but then CatchUp(1104) is also called. This should be fine since
last_rendered_x_prevents re-rendering. -
HDMA runs AFTER CatchUp(1104) - HDMA modifications to PPU registers happen after the scanline is fully rendered. This is correct for HDMA (affects next line), but need to verify.
Commit: 9d788fe6b0 - "perf: Implement lazy SNES emulator initialization..."
Date: Tue Nov 25 14:58:52 2025
Changes to emulator:
- Only changed
Snes::Initsignature fromstd::vector<uint8_t>&toconst std::vector<uint8_t>& - No changes to PPU or rendering logic
Verdict: NOT RELATED to rendering bug.
Commit: a0ab5a5eee - "perf(wasm): optimize emulator performance and audio system"
Date: Tue Nov 25 19:02:21 2025
Changes to emulator:
- emulator.cc: Frame timing and progressive frame skip (no PPU changes)
- wasm_audio.cc: AudioWorklet implementation (no PPU changes)
Verdict: NOT RELATED to rendering bug.
Commits Between Pre-JIT and Now
Only 2 commits modified PPU/snes.cc:
e37497e9ef- JIT introduction (SUSPECT)9d788fe6b0- Init signature change (NOT RELATED)
Hypothesis
The JIT commit e37497e9ef is the root cause. Possible bugs:
Theory 1: current_scanline_ initialization issue
If current_scanline_ is not properly initialized or declared, HandlePixel(x, current_scanline_) could be passing garbage values.
To verify: Check if current_scanline_ is declared in ppu.h
Theory 2: h_pos timing mismatch
The title screen may rely on specific timing that the JIT system breaks. If PPU registers are read/written at different h_pos values than expected, rendering could be affected.
Theory 3: WriteBBus CatchUp interference with HDMA
The title screen uses HDMA for wavy cloud scroll. If the WriteBBus CatchUp is being called for HDMA writes and causing state issues, BG rendering could be affected.
HDMA flow:
- h_pos=1104: CatchUp(1104) renders pixels 128-255
- h_pos=1104: run_hdma_request() executes HDMA
- HDMA writes to scroll registers via WriteBBus
- WriteBBus calls CatchUp(h_pos) - but line is already fully rendered!
This should be harmless since last_rendered_x_ = 256 means CatchUp returns early. But need to verify.
Theory 4: Title screen uses unusual PPU setup
The title screen may have a specific PPU configuration that triggers a bug in the JIT system that other screens don't trigger.
Verification Results
Theory 1: VERIFIED - current_scanline_ is declared
Found at ppu.h:335: int current_scanline_ = 0; - properly declared and initialized.
Theory 2: Timing Analysis
- h_pos=16: StartLine(v_pos) called, resets
last_rendered_x_=0 - h_pos=512: CatchUp(512) renders pixels 0-127
- h_pos=1104: CatchUp(1104) renders pixels 128-255, then HDMA runs
Math check:
- 512/4 = 128 pixels (0-127)
- 1104/4 = 276 → clamped to 256 (128-255)
- Total: 256 pixels ✓
Theory 3: WriteBBus CatchUp Analysis
if (!in_vblank_ && memory_.v_pos() > 0 && memory_.h_pos() < 1100) {
ppu_.CatchUp(memory_.h_pos());
}
- HDMA runs at h_pos=1104, which is > 1100, so NO catchup for HDMA writes ✓
- This is correct - HDMA changes affect next scanline
New Theory 5: HandleFrameStart doesn't reset JIT state
HandleFrameStart() doesn't reset last_rendered_x_ or current_scanline_. These are only reset in StartLine() which is called for scanlines 1-224.
Potential issue: If WriteBBus CatchUp is called during vblank or on scanline 0, current_scanline_ might have stale value from previous frame.
Check: WriteBBus condition is !in_vblank_ && memory_.v_pos() > 0, so this should be safe.
New Theory 6: Title screen specific PPU configuration
The title screen might use a specific PPU configuration that triggers a rendering bug. Need to compare $212C (layer enables), $2105 (mode), $210B-$210C (tile addresses) between title screen and working screens.
Next Steps
- ✅ Verify
current_scanline_is properly declared - DONE, it's fine - Test pre-JIT commit to confirm title screen BG works without JIT
- Add targeted debug logging for title screen (module 0x01) PPU state
- Compare PPU register state between title screen and cutscene
- Check if forced_blank is stuck during title screen BG rendering
Conclusion
Root Cause Commit: e37497e9ef - "feat(emu): add PPU JIT catch-up for mid-scanline raster effects"
This is the ONLY commit that changed PPU rendering logic. The bug must be in this commit.
What the JIT system changed:
- Split
RunLine()intoStartLine()+CatchUp() - Added progressive pixel rendering based on h_pos
- Added WriteBBus CatchUp to handle mid-scanline PPU register writes
Why title screen specifically might be affected: The title screen uses HDMA for the wavy cloud scroll effect on BG1. While basic HDMA timing appears correct (runs after CatchUp(1104)), there may be a subtle timing or state issue that affects only certain screen configurations.
Recommended debugging approach:
- Test pre-JIT to confirm title screen works:
git checkout e37497e9ef~1 -- src/app/emu/video/ppu.cc src/app/emu/video/ppu.h src/app/emu/snes.cc - If confirmed, binary search within the JIT changes to isolate the specific bug
- Add logging to compare PPU state ($212C, $2105, etc.) between title screen and working screens
Testing Commands
# Checkout pre-JIT to test (CONFIRMS if JIT is the culprit)
git checkout e37497e9ef~1 -- src/app/emu/video/ppu.cc src/app/emu/video/ppu.h src/app/emu/snes.cc
# Restore JIT version
git checkout HEAD -- src/app/emu/video/ppu.cc src/app/emu/video/ppu.h src/app/emu/snes.cc
# Test with just WriteBBus CatchUp removed (isolate that change)
# Edit snes.cc WriteBBus to comment out the CatchUp call
# Test with RunLine instead of StartLine/CatchUp (but keep WriteBBus CatchUp)
# Would require manual code changes
Status
- Identified root cause commit:
e37497e9ef - Verified no other commits changed PPU rendering
- Documented JIT system changes
- PENDING: Test pre-JIT to confirm
- PENDING: Isolate specific bug in JIT implementation