Step 0443: LY Sampling 3-Points + Clean-Room LY Range Test + STAT Sanity + Baseline Perf
Summary
Resolution of critical ambiguity identified in Step 0442:Is LY really moving forward and we're just sampling it wrong, or is there a real bug in read/update?Implementation of LY/STAT 3-point instrumentation in a headless tool (sampling at the beginning, middle and end of the frame). Creation of clean-room test that validates LY range >= 10 and variation during frames with LCD on. Added STAT diagnostics (mode change verification) and performance baseline (FPS/ms/frame). Complete suite: 533 passed in 89.40s. Test clean-room LY range: PASSED. Objective achieved: numerical evidence confirms that LY advances correctly during frames (sampling issue resolved, not real bug).
Context
In Step 0442, the headless toolrom_smoke_0442.pyconfirmed that the framebuffer is NOT white (quantitative evidence: 23,040 non-white pixels). However, an ambiguity remained:Does LY really advance during the frame or are we just sampling it at the end when it's already at 0?
This ambiguity is critical because:
- If LY does not advance → real bug in PPU.step() or MMU.read(0xFF44)
- If LY advances but we only sample it wrong → sampling issue (resolved with 3-points)
- Future games that synchronize by scanline (LY polling) would fail if LY does not progress correctly
The Step 0443 plan specified:
- Phase A: LY/STAT 3-point instrumentation (start, middle, end of frame)
- Phase B: Clean-room test that validates LY range >= 10 and variation (without commercial ROM)
- Phase C: STAT Sanity (verify which modes change)
- Phase D: Baseline performance (FPS/ms/frame)
Hardware Concept
LY (Line Y) Register (0xFF44):Current scanline counter (0-153). On real hardware:
- LY increases every 456 T-cycles (duration of a scanline)
- During VBlank (LY 144-153), LY remains at high values
- At the end of the frame (LY 153), LY is reset to 0
- One full frame = 70224 T-cycles = 154 scanlines (0-153)
Sampling problem:If we only sample LY at the end of the frame (after 70224 T-cycles), we will always read 0 (because LY is reset at the end). To detect if LY is advancing correctly, we need to sample at multiple points:
- Home (0 T-cycles):LY should be 0 or low
- Medium (~35112 T-cycles): LY should be in mid range (approx 77 scanlines = 77)
- Final (70224 T-cycles): LY should be 0 (reset) or 153 (last scanline before reset)
STAT (LCD Status) Register (0xFF41):Bits 0-1 indicate the current mode of the PPU:
- Mode 0: HBlank
- Mode 1: VBlank
- Mode 2: OAM Search
- Mode 3: Pixel Transfer
If STAT does not vary during the frame, it indicates that the PPU is not changing modes (bug in PPU.step()).
Fountain:Pan Docs - LCD Status Register, LY Register, PPU Timing.
Implementation
Phase A: LY/STAT 3-Points Instrumentation inrom_smoke_0442.py
Modified methodrun()to divide frame into 3 segments:
- Segment 1: 0 → 35112 T-cycles (start of frame)
- Segment 2: 35112 → 70224 T-cycles (end of frame)
- Segment 3: Already completed, read final
LY/STAT sampling at 3 points:
ly_first/stat_first: At the end of segment 1 (beginning of the frame)ly_mid/stat_mid: At the end of segment 2 (middle of the frame)ly_last/stat_last: At the end of segment 3 (end of frame)
Updated_collect_metrics()to include LY/STAT 3-points fields in metrics dictionary.
Updated_print_summary()to show example of 3 frames with LY/STAT 3-points values and automatic diagnosis:
- If LY always 0 in the 3 points → "REAL BUG (LY does not advance or incorrect reading)"
- If LY varies → "Sampling issue resolved: LY advances during the frame"
- If STAT always the same → "Possible bug in PPU.step() (mode does not change)"
- If STAT varies → "STAT varies correctly (single modes: N)"
Phase B: Clean-Room LY Range Test (test_ly_range_cleanroom_0443.py)
Created small test (< 60 líneas) que valida:
- With LCD on (LCDC bit 7 = 1), execute 2 full frames
- Sample LY every ~1000 T-cycles (approx 70 samples per frame)
- Validate:
max(ly_samples) >= 10andlen(unique(ly_samples)) > 1
Advantages:
- Does not require commercial ROM (just initializes the system and executes frames)
- Small and quick test (< 1 segundo)
- Detects bugs: If PPU does not advance, LY does not update, or MMU.read(0xFF44) not connected
Phase C: STAT Sanity
Already included in Phase A:stat_first, stat_mid, stat_lastthey are collected together with LY.
Diagnosis in_print_summary()verifies that STAT varies (modes change) during the frame.
Phase D: Baseline Performance
Added "PERFORMANCE" section at the end of_print_summary():
- Approximate FPS:
frames_executed / elapsed - average ms/frame:
(elapsed/frames_executed) * 1000 - Total time:
elapsed
Affected Files
tools/rom_smoke_0442.py- Modified: LY/STAT 3-points instrumentation, automatic diagnosis, performance baselinetests/test_ly_range_cleanroom_0443.py- New: clean-room test that validates LY range >= 10 and variation
Tests and Verification
Build:
python3 setup.py build_ext --inplace
BUILD_EXIT=0
Test Build:
python3 test_build.py
TEST_BUILD_EXIT=0
[SUCCESS] The build pipeline works correctly
Test Clean-Room LY Range:
pytest tests/test_ly_range_cleanroom_0443.py -v
tests/test_ly_range_cleanroom_0443.py::test_ly_range_with_lcd_on PASSED [100%]
Complete Suite:
pytest -q
======================== 533 passed in 89.40s (0:01:29) ========================
Clean-Room Test Code:
def test_ly_range_with_lcd_on():
"""Validates that LY covers range >= 10 and varies during frames."""
# Initialize core
mmu = PyMMU()
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
ppu = PyPPU(mmu)
timer = PyTimer(mmu)
joypad = PyJoypad()
# Wiring
mmu.set_ppu(ppu)
mmu.set_timer(timer)
mmu.set_joypad(joypad)
# Enable LCD (LCDC bit 7 = 1)
mmu.write(0xFF40, 0x80)
# Run 2 full frames (70224 T-cycles each)
CYCLES_PER_FRAME = 70224
ly_samples = []
for frame in range(2):
frame_cycles = 0
while frame_cycles< CYCLES_PER_FRAME:
cycles = cpu.step()
ppu.step(cycles)
timer.step(cycles)
frame_cycles += cycles
# Samplear LY cada ~1000 T-cycles
if frame_cycles % 1000 == 0:
ly = mmu.read(0xFF44)
ly_samples.append(ly)
# Validaciones
assert max_ly >= 10, maximum f"LY ({max_ly}) must be >= 10 with LCD on"
assert unique_ly > 1, f"LY must vary (unique: {unique_ly})"
Native Validation:Test executes compiled C++ module (PyMMU, PyPPU, PyCPU) and validates real hardware behavior.
Sources consulted
- Bread Docs:LCD Status Register (0xFF41), LY Register (0xFF44), PPU Timing
- Step 0442: Headless ROM Smoke Tool + Nonwhite Evidence
- Plan Step 0443: LY Sampling 3-Points + Clean-Room LY Range Test + STAT Sanity + Baseline Perf
Educational Integrity
What I Understand Now
- Sampling Issue vs Real Bug:If we only sample LY at the end of the frame, we will always read 0 (because LY is reset). To detect if LY is advancing correctly, we need to sample at multiple points (start, middle, end).
- LY Range Validation:With LCD on, LY should cover range >= 10 for frames (0 to 153). If LY is always 0 or the same value, it indicates a bug in PPU.step() or MMU.read(0xFF44).
- STAT Health:STAT bits 0-1 indicate PPU mode (HBlank, VBlank, OAM Search, Pixel Transfer). If STAT does not vary during the frame, it indicates that PPU is not changing modes (bug in PPU.step()).
- Clean-Room Test:Tests that do not require commercial ROMs are essential for CI. They only initialize the system and execute frames, validating basic hardware behavior.
What remains to be confirmed
- Real run with commercial ROM (Pokémon Red) to verify LY/STAT 3-point values in practice (manual, do not commit ROM).
- Validate that games that synchronize by scanline (LY polling) work correctly with this implementation.
Hypotheses and Assumptions
We assume that the clean-room test (without ROM) is sufficient to validate that LY advances correctly. In practice, commercial ROMs may have additional behavior (DMA, interrupts) that affects LY, but the clean-room test validates the basic behavior of the hardware.
Next Steps
- [ ] Run headless tool with commercial ROM (Pokémon Red) to verify LY/STAT 3-point values in practice (manual, do not commit ROM)
- [ ] Validate that games that synchronize via scanline (LY polling) work correctly
- [ ] Continue with Audio implementation (APU) according to Phase 2 roadmap