⚠️ Clean-Room / Educational

This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.

Minimal and Correct Fix (Close API Bug)

Date:2026-01-04 StepID:0468 State: VERIFIED

Summary

Correction of the API bug identified in Step 0467:get_framebuffer_indices()does not automatically "present" asget_framebuffer(), causing tests to read the wrong buffer (clean front before swap instead of the presented frame). A new getter was implementedget_presented_framebuffer_indices_ptr()which guarantees automatic present if there is a swap pending (same asget_framebuffer_ptr()). All 0464 tests were updated to use the new getter and now pass correctly. ✅ API bug closed.

Hardware Concept

API Bug Identified in Step 0467: The problem was NOT that BG was not rendering (bg_pixels_written=23040), nor that the bytes read were incorrect (last_tile_bytes=[85, 51]). The problem was thatget_framebuffer_indices_ptr()It did NOT make "automatic present" likeget_framebuffer_ptr().

Automatic Present: When a frame completes (LY=144), the rendered content is in the back buffer. The swap to the front buffer occurs when callingget_frame_ready_and_reset(). However, so that tests can read the most recent frame without having to explicitly call reset, framebuffer getters must do "auto present": if a swap is pending (framebuffer_swap_pending_), do the swap before returning the pointer.

Problematic Design:

  • get_framebuffer_ptr(): Automatically present if there is a pending swap (line 1389)
  • get_framebuffer_indices_ptr(): DOES NOT do automatic present, it only returnsframebuffer_front_.data()(line 1420)

Problem: Having two getters with different contracts about when the frame is "presented" is a source of bugs. Tests read the wrong buffer (clean front before swap instead of the presented frame).

Solution: Create a new getterget_presented_framebuffer_indices_ptr()that guarantees present. This method is NOTconstbecause it can swap (mutate state), just likeget_framebuffer_ptr().

Reference: Step 0364 - Double Buffering. Step 0428 - Automatic Present inget_framebuffer_ptr(). Step 0457 - Debug API for tests. Step 0467 - Bug diagnosis.

Implementation

Implemented a new "presented" getter that guarantees automatic present, without breaking compatibility (the old getter still exists).

Phase A: Getter "Presented Indices" Explicit

was addedget_presented_framebuffer_indices_ptr()in C++:

// In PPU.hpp:
/**
 * Step 0468: Getter "presented" for framebuffer indices.
 * 
 * Guarantees that it returns the last frame presented (makes present automatic
 * if swap is pending, same as get_framebuffer_ptr()).
 * 
 * Contract: Always returns the most recent frame rendered and presented.
 * 
 * @return Pointer to the presented index framebuffer (23040 bytes)
 */
const uint8_t* get_presented_framebuffer_indices_ptr();

// In PPU.cpp:
const uint8_t* PPU::get_presented_framebuffer_indices_ptr() {
    // --- Step 0468: Automatic present if swap is pending ---
    if (framebuffer_swap_pending_) {
        swap_framebuffers();
        framebuffer_swap_pending_ = false;
    }
    
    // Return the front buffer (stable, updated with the latest content)
    return framebuffer_front_.data();
}

Note: This method is NOTconstbecause it can swap (mutate state). This is consistent withget_framebuffer_ptr()which is notconst.

Phase B: Exposure to Python

The new method was exposed to Python inppu.pxdandppu.pyx:

// In ppu.pxd:
const uint8_t* get_presented_framebuffer_indices_ptr() # Step 0468

// In ppu.pyx:
def get_presented_framebuffer_indices(self):
    """
    Step 0468: Get the presented index framebuffer.
    
    Guarantees to return the last frame presented (makes present automatic
    if swap is pending, same as get_framebuffer()).
    
    Returns:
        bytes of 23040 bytes (160*144), values 0..3 of the presented frame
    """
    if self._ppu == NULL:
        return None
    
    cdef const uint8_t* indices_ptr = self._ppu.get_presented_framebuffer_indices_ptr()
    if indices_ptr == NULL:
        return None
    
    # Create bytes from pointer (23040 bytes = 160*144)
    return(indices_ptr)

Phase C: Test Update 0464

All 0464 tests were updated to useget_presented_framebuffer_indices()ratherget_framebuffer_indices():

  • test_tilemap_base_select_9800(): Removed pre/post reset experiment (no longer needed), use getter "presented"
  • test_tilemap_base_select_9C00(): Use getter "presented"
  • test_scx_pixel_scroll_0_to_7(): Use getter "presented"

Example of change:

# Before (problematic):
indices = self.ppu.get_framebuffer_indices() # Can read clean front

# After (correct):
indices = self.ppu.get_presented_framebuffer_indices() # Ensure present

Affected Files

  • src/core/cpp/PPU.hpp- Added methodget_presented_framebuffer_indices_ptr()
  • src/core/cpp/PPU.cpp- Implementedget_presented_framebuffer_indices_ptr()with automatic present
  • src/core/cython/ppu.pxd- Added declarationget_presented_framebuffer_indices_ptr()
  • src/core/cython/ppu.pyx- Added Python wrapperget_presented_framebuffer_indices()
  • tests/test_bg_tilemap_base_and_scroll_0464.py- Updated all tests to useget_presented_framebuffer_indices(), removed pre/post reset experiment

Tests and Verification

Command executed:

pytest -q tests/test_bg_tilemap_base_and_scroll_0464.py

Result: ✅ 3 passed in 1.19s

Tests that pass:

  • test_tilemap_base_select_9800()- Check base tilemap selection 0x9800
  • test_tilemap_base_select_9C00()- Verify base tilemap selection 0x9C00
  • test_scx_pixel_scroll_0_to_7()- Check horizontal scroll SCX 0-7

Test Code:

def test_tilemap_base_select_9800(self):
    # ... setup tiles and tilemaps ...
    
    # Run frame
    self.run_one_frame()
    
    # Use "presented" getter that guarantees automatic present
    indices = self.ppu.get_presented_framebuffer_indices()
    assert indices is not None
    assert len(indices) == 23040
    
    # Check expected pattern
    row0_start = 0 * 160
    expected_p0 = [0, 1, 2, 3, 0, 1, 2, 3]
    for i in range(8):
        current_idx = indices[row0_start + i] & 0x03
        expected_idx = expected_p0[i]
        assert actual_idx == expected_idx

Native Validation:✅ Compilation successful. C++ module compiled successfully. Methodget_presented_framebuffer_indices_ptr()correctly exposed to Python.

Real Validation (ROMs): Run rom_smoke for tetris.gb, pkmn.gb, tetris_dx.gbc, mario.gbc (240 frames each) and grid UI. They all completed without crashes. Logs show PPU-TILEMAP-DIAG diagnostic when gated.

Fuentes Consultadas

  • Step 0364: Double Buffering in PPU
  • Step 0428: Automatic Present inget_framebuffer_ptr()
  • Step 0457: Debug API for tests -get_framebuffer_indices_ptr()
  • Step 0467: Bug diagnosis - Evidence collected (nz_pre=0, nz_post=17280)

Educational Integrity

What I Understand Now

  • API consistency: All framebuffer getters must have the same contract about when the frame is "presented." Having getters with different contracts is a source of bugs.
  • Automatic Present: Getters that return the framebuffer should do automatic present if swap is pending. This ensures that Python always sees the latest content without having to explicitly call reset.
  • Non-Const Methods: Methods that make present automatic CANNOT beconstbecause they mutate the state (they swap). This is consistent with the design ofget_framebuffer_ptr().
  • Backwards Compatibility: New getters can be added without breaking compatibility. The ancient getterget_framebuffer_indices_ptr()still exists (it can be useful for cases where you want to read without presenting).

What remains to be confirmed

  • Impact on other tests: Check if there are other tests that useget_framebuffer_indices()and must be updated toget_presented_framebuffer_indices().
  • Using the old getter: If there are cases where you want to read without doing present, the old getter can be useful. For now, all tests use the "presented" getter.

Hypotheses and Assumptions

Confirmed hypothesis:The problem was purely API/presentation sync.get_framebuffer_indices_ptr()did not do automatic present likeget_framebuffer_ptr(). With the new getter "presented", all tests pass correctly.

Design decision: Chose to create a new getter instead of modifying the existing one to maintain backward compatibility. The old getter can be useful for cases where you want to read without doing present (although there are no known cases for now).

Next Steps

  • [x] Implementget_presented_framebuffer_indices_ptr()
  • [x] Expose Python asget_presented_framebuffer_indices()
  • [x] Update tests 0464 to use the new getter
  • [x] Verify that all tests pass
  • [ ] Check if there are other tests that need to be updated
  • [ ] Continue with real emulation bugs (not test infrastructure)