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

Debug: Pixel Pipeline Instrumentation in C++

Date:2025-12-20 StepID:0180 State: 🔍 DRAFT

Summary

Milestone reached!The native loop architecture has solved all thedeadlocksand the emulator runs at 60 FPS withL.Y.cycling correctly. However, the screen remains blank because the methodrender_scanline()of the PPU in C++ is generating a framebuffer full of zeros.

This Step implements the pixel rendering pipeline withinPPU::render_scanline()with detailed diagnostic logs to identify why tile data is not being read from VRAM. The "blind renderer" diagnosis suggests that the method executes correctly but fails at some point in the rendering chain (address calculation, memory read, bit decoding).

Hardware Concept: The Pixel Rendering Pipeline

To draw a single pixel on the screen, the PPU performs a complex chain of calculations and memory reads. Each step in this chain is critical and any failure will result in an incorrect or blank pixel:

  1. Calculation of Coordinates in the Tilemap:Calculate the coordinate(map_x, map_y)on the background map of 256x256 pixels, applying the scroll (SCX, SCY).
  2. Search for the Tile on the Tilemap:Use(map_x, map_y)to find the position of the corresponding tile in thetilemap (0x9800either0x9C00).
  3. Reading the Tile ID:Read thetile ID (tile_id) of that tilemap position.
  4. Tile Direction Calculation:Use thetile_idto calculate the base address of the tile data in thetile table (0x8000either0x8800).
  5. Reading Pixel Data:Read the2 bytesthat correspond to the correct line of pixels within that tile.
  6. Color Index Decoding:Decode those 2 bytes to get thecolor index (0-3)of the final pixel.

If any step in this chain fails (an incorrect address calculation, a memory read that returns 0, a failed decode), the end result will be a pixel of color 0 (white). The zero-filled framebuffer we observed suggests that one of these steps is systematically failing.

According toBread Docs, the data format of a tile is:

  • Each tile occupies 16 bytes (8 lines × 2 bytes per line).
  • Each line of the tile occupies 2 consecutive bytes in VRAM.
  • The low byte contains the least significant bit of each pixel (bit 7 = pixel 0, bit 6 = pixel 1, ...).
  • The high byte contains the most significant bit of each pixel (bit 7 = pixel 0, bit 6 = pixel 1, ...).
  • The final color index is calculated as:color_index = (byte_high_bit<< 1) | byte_low_bit.

Implementation

The method was implementedrender_scanline()with detailed debug logs showing the intermediate render pipeline values ​​for the first pixels of the first two lines.

Modification in PPU.cpp

Added#include <cstdio>at the beginning of the file and added debug logs inside the rendering loop:

#include "PPU.hpp"
#include "MMU.hpp"
#include <cstdio>

// ... inside render_scanline() ...

// --- DEBUG LOGS (Step 0180) ---
// Static variable to print logs only once (first two lines, first 8 pixels)
static bool debug_printed = false;

//...rendering code...

// --- DEBUG LOGS (Step 0180) ---
// Print detailed logs for the first pixels of the first two lines
if (!debug_printed && ly_ < 2 && x < 8) {
    // Show tile_id as signed if we are in signed mode, unsigned if we are in unsigned mode
    int display_tile_id = signed_addressing ? static_cast<int8_t>(tile_id) : static_cast<int>(tile_id);
    printf("[PPU DEBUG] ly=%d, x=%d | map_x=%d, map_y=%d | tile_map_addr=0x%04X | tile_id=%d | tile_addr=0x%04X | byte1=0x%02X, byte2=0x%02X | color=%d\n",
        ly_, x, map_x, map_y, tile_map_addr, display_tile_id, tile_addr, byte1, byte2, color_index);
}

// Mark that we already printed the logs (only once, after line 1)
if (ly_ == 1) {
    debug_printed = true;
}

Design Decisions

  • Limited Logs:Logs are only printed for the first 8 pixels of the first 2 lines to avoid cluttering the console. Once printed, they are automatically deactivated.
  • Complete Information:Each log shows all the intermediate values ​​of the pipeline: coordinates, addresses, IDs, bytes read and final color.
  • Signed/Unsigned Mode:Hetile_idis displayed correctly depending on the addressing mode (signed or unsigned) to facilitate diagnosis.

Affected Files

  • src/core/cpp/PPU.cpp- Added#include <cstdio>and instrumentation with debugging logs inrender_scanline()

Tests and Verification

This change requires rebuilding the C++ module and running the emulator to capture debug logs:

  1. Recompiling the C++ Module:
    .\rebuild_cpp.ps1
  2. Running the Emulator:
    python main.py roms/tetris.gb
  3. Output Analysis:

    The console will show a detailed log for the first few pixels. We will be looking for:

    • Yeahbyte1andbyte2are always0x00:The problem is in the calculation oftile_addreithertile_line_addr. We are targeting an empty area of ​​the VRAM.
    • Yeahtile_idis always0:The problem is in the calculation oftile_map_addr. We are not reading the tile map correctly.
    • Yeahbyte1andbyte2They have correct values ​​butcolor_indexis0:The problem is in the final decoding of the bits.

C++ Compiled Module Validation:Logs are generated directly from the compiled C++ code, confirming that the rendering pipeline is running in the native code.

Sources consulted

  • Bread Docs:Section on tile data format and PPU rendering pipeline
  • Blind Renderer Diagnosis:Analysis of zero-filled framebuffer and method behaviorrender_scanline()

Educational Integrity

What I Understand Now

  • Rendering Pipeline:Rendering a pixel is a complex process that involves multiple steps: coordinate calculation, tilemap reading, address calculation, VRAM reading, and bit decoding. Any failure in any step will result in an incorrect pixel.
  • Surgical Instrumentation:Debug logs should be limited and specific to avoid overwhelming the console, but should show enough information to diagnose the problem.
  • Blind Renderer Diagnosis:The framebuffer full of zeros suggests that the method executes but fails at some point in the pipeline. The logs will allow us to identify exactly where.

What remains to be confirmed

  • Pipeline Failure Point:We need to run the emulator and analyze the logs to identify exactly where the rendering pipeline is failing.
  • VRAM Status:We need to confirm if the tile data is actually in VRAM or if the problem is that it has not been copied yet.
  • Address Calculation:We need to check if the calculated addresses (tile_map_addr, tile_addr, tile_line_addr) are correct.

Hypotheses and Assumptions

Main Hypothesis:The rendering pipeline is running, but it fails at some specific step (address calculation, memory read, decoding). The logs will allow us to identify exactly where.

Assumption:We assume that the tile data is in VRAM (the CPU has executed the initialization routines). If the logs show thatbyte1andbyte2are always0x00, this would suggest that we are targeting an empty VRAM area or that the address calculation is incorrect.

Next Steps

  • [ ] Recompile the C++ module with debug instrumentation
  • [ ] Run the emulator and capture the debug logs
  • [ ] Analyze the logs to identify the point of failure in the pipeline
  • [ ] Yeahbyte1andbyte2are0x00: Investigate the calculation of tile addresses
  • [ ] Yeahtile_idis always0: Investigate the calculation of tilemap addresses
  • [ ] If the bytes are correct butcolor_indexis0: Investigate bit decoding
  • [ ] Correct the identified problem and verify that the framebuffer is filled correctly