Step 0443: LY Sampling 3-Points + Clean-Room LY Range Test + STAT Sanity + Baseline Perf

Date:2026-01-02 |StepID:0443 |State: VERIFIED

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 baseline
  • tests/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

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