⚠️ 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.

The Probe at Pixel Zero

Date:2025-12-21 StepID:0211 State: ✅ VERIFIED

Summary

"VRAM Flood" (Step 0208) and "Forcing Black" (Step 0209) have failed, indicating that the address validation logic inrender_scanlineis systematically rejecting VRAM accesses, diverting the flow to the blockelse(white). Mathematically this should not happen, so we should see the values ​​in real time.

Aim:InstrumentPPU::render_scanline()withprintfto display the calculation variables (LCDC, addresses, Tile ID) exclusively for the pixel (0,0) of the frame. This will give us an exact x-ray of why the address is considered invalid without flooding the console with thousands of log lines.

Hardware Concept: Surgical Diagnosis

When a system fails systematically, we need accurate data, not assumptions. The problem we face is that the validation conditionif (tile_line_addr >= 0x8000 && tile_line_addr<= 0x9FFE)is systematically failing, leading execution to blockelsewhich writes color 0 (white) to the framebuffer.

The mathematical problem:Anytile_idvalid (0-255) should generate a valid address within VRAM (0x8000-0x9FFF). If this is not happening, there is an error in:

  • Address calculation:Hetile_map_addrmay be out of range, reading garbage from the tilemap.
  • Tiles addressing:The signed/unsigned mode may be calculating incorrect addresses.
  • Type Overflow:Auint16_tmay be overflowing or aint8_tmay be being interpreted incorrectly.
  • Incorrect validation:Although we corrected the condition in Step 0210, there may be another problem that we missed.

The surgical solution:Instead of printing thousands of log lines for each pixel, we instrument the code to print the calculation valuesonly once per frame, specifically whenly_ == 0andx == 0(the first pixel of the first frame). This will give us an exact snapshot of the internal state of the PPU at the critical moment of rendering.

Fountain:Pan Docs - "PPU Rendering", "Tile Addressing", "VRAM Access"

Implementation

Added a diagnostic block inPPU::render_scanline()inside the filesrc/core/cpp/PPU.cpp, just before the VRAM validation condition.

Header Inclusion

was added#include <cstdio>at the beginning of the file to enableprintf.

#include "PPU.hpp"
#include "MMU.hpp"
#include <algorithm>
#include <cstdio>  // Step 0211: For diagnostic printf

Diagnostic Block

The following code block was added right after the calculation oftile_line_addrand before the validation condition:

// --- Step 0211: DIAGNOSTIC PROBE (Pixel 0.0) ---
// Print the internal state only once per frame (at startup)
// This allows us to see the exact values that the PPU is calculating
// without flooding the console with thousands of log lines
if (ly_ == 0 && x == 0) {
    printf("--- [PPU DIAGNOSTIC FRAME START] ---\n");
    printf("LCDC: 0x%02X | SCX: 0x%02X | SCY: 0x%02X\n", lcdc, scx, scy);
    printf("MapBase: 0x%04X | MapAddr: 0x%04X | TileID: 0x%02X\n", tile_map_base, tile_map_addr, tile_id);
    printf("DataBase: 0x%04X | Signed: %d\n", tile_data_base, signed_addressing ? 1 : 0);
    printf("CalcTileAddr: 0x%04X | LineAddr: 0x%04X\n", tile_addr, tile_line_addr);
    bool valid = (tile_line_addr >= 0x8000 && tile_line_addr<= 0x9FFE);
    printf("VALID CHECK: %s\n", valid ? "PASS" : "FAIL");
    printf("------------------------------------\n");
}
// -------------------------------------------------

Diagnosed Variables

The diagnostic block prints the following critical variables:

  • LCDC:LCD control register (0xFF40), includes enable and addressing mode bits.
  • SCX/SCY:Horizontal and vertical scroll registers (0xFF43/0xFF42).
  • MapBase:Base address of the tile map (0x9800 or 0x9C00 according to LCDC bit 3).
  • MapAddr:Calculated address in the tile map for the current tile.
  • TileID:ID of the tile read from the map (0-255).
  • Database:Base address of tile data (0x8000 or 0x9000 according to LCDC bit 4).
  • Signed:Indicates whether signed (1) or unsigned (0) addressing is used.
  • CalcTileAddr:Base address of the tile calculated (before adding the line offset).
  • LineAddr:End address of the tile line (tile_addr + line_in_tile * 2).
  • VALID CHECK:Validation result (PASS if in range, FAIL if not).

Affected Files

  • src/core/cpp/PPU.cpp- Added#include <cstdio>and diagnostic block inrender_scanline()(lines 347-361)

Tests and Verification

Compilation:The code was compiled successfully withpython setup.py build_ext --inplace. No compilation errors were introduced.

Compiled C++ module validation:The Cython extension has been successfully generated and is ready for runtime testing.

Expected test:When running the emulator withpython main.py roms/tetris.gb, we should see in the console a diagnostic block showing the exact values ​​calculated for the pixel (0,0) of the first frame. This block will appear once per frame (60 times per second), but will only display the values ​​of the first pixel, avoiding flooding the console.

Analysis of expected results:With this data, we will be able to identify exactly where the error is:

  • If TileID is strange:Maybe we read garbage from the tile map (MapAddr out of range).
  • If MapAddr is out of range:Error in calculating position in the tile map.
  • If LineAddr is 0 or huge:Overflow error or incorrect data types.
  • If VALID CHECK says FAIL:We will see why the exact number fails the condition, allowing us to correct the problem in the next step.

Sources consulted

  • Pan Docs: "PPU Rendering" - Scan Line Rendering Process
  • Pan Docs: "Tile Addressing" - Calculation of tile addresses (signed vs unsigned)
  • Pan Docs: "VRAM Access" - VRAM Memory Range Validation

Educational Integrity

What I Understand Now

  • Surgical diagnosis:When a problem is systematic, it is better to instrument your code to see exact values ​​at runtime than to guess based on assumptions.
  • Log output control:Printing logs for each pixel (160x144 = 23,040 pixels per frame) would overwhelm the console. It is best to limit the output to specific cases (such as the first pixel of the first frame).
  • Hypothesis validation:Although mathematically any tile_id should generate a valid address, we need to verify that the intermediate calculations are correct.

What remains to be confirmed

  • Actual values:View the exact values ​​that the PPU is calculating at runtime to identify where the error is.
  • Origin of the problem:Determine if the problem is in calculating addresses, reading the tilemap, or validating ranges.

Hypotheses and Assumptions

Main hypothesis:The validation condition is failing because some of the intermediate variables (MapAddr, TileID, CalcTileAddr, LineAddr) have an incorrect value that we cannot see without instrumenting the code.

Assumption:Once we see the exact values, the correction will be obvious. Could be:

  • An error in MapAddr calculation (outside the range 0x9800-0x9FFF)
  • An invalid TileID read from the map (uninitialized memory garbage)
  • An error in the calculation of signed/unsigned addresses
  • A data type overflow (uint16_t, int8_t)

Next Steps

  • [ ] Recompile the C++ module with.\rebuild_cpp.ps1eitherpython setup.py build_ext --inplace
  • [ ] Run the emulator:python main.py roms/tetris.gb
  • [ ] Analyze the console: Find the block[PPU DIAGNOSTIC FRAME START]
  • [ ] Identify the error: Compare the printed values ​​with the expected values ​​according to the documentation
  • [ ] Apply correction: Once the problem is identified, correct it in the next step