This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Step 0395 - Visual Diagnostics: Verify Framebuffer Correspondence vs VRAM Metrics
Summary
This step implements a complete visual diagnostic system to verify the correspondence between the VRAM metrics (which report correct values since Step 0394) and the actual contents of the C++ framebuffer. 5 verification functions are implemented that capture snapshots of the framebuffer, validate the tilemap→framebuffer correspondence, verify scroll/wrap-around, validate the BGP palette application, and verify the C++→Python pipeline. The results reveal critical discrepancies: Frame 676 shows completely white framebuffer (0=23040) while VRAM metrics report TileData 14.2%, and Frame 742 shows BGP=0x00 causing incorrect color mapping.
Hardware Concept
The Game Boy rendering pipeline follows this flow (according to Pan Docs):
- Tilemap → Tile ID: The PPU reads the tilemap (0x9800-0x9FFF) using coordinates (map_x, map_y) calculated with SCX/SCY, obtaining a tile_id (0-255).
- Tile ID → Tile Address: Depending on the addressing mode (unsigned: 0x8000 + tile_id*16, signed: 0x9000 + (tile_id-128)*16), the address of the tile in VRAM is calculated.
- 2bpp decoding: Each line of the tile (8 pixels) occupies 2 consecutive bytes. It is decoded:
color_index = (bit_high<< 1) | bit_low(values 0-3). - BGP Palette Application: The BGP record (0xFF47) maps each color_index to a final index:
final_color = (BGP >> (color_index * 2)) & 0x03. - Writing to Framebuffer: The final value (0-3) is written to
framebuffer_back_[ly * 160 + x]. - Buffer Swap: Upon completing the frame (LY=144), they exchange
framebuffer_back_↔framebuffer_front_. - Python Pipeline: Cython reads
framebuffer_front_→ NumPy → Pygame applies RGB colors according to palette.
Identified problem: VRAM metrics (Step 0394) report correct values (TileData 14.2%, TileMap 100%), but visually Tetris DX shows fragmented text and Zelda DX reported "sees nothing" (Step 0390). This suggests a disconnect between the contents of VRAM and what is rendered in the framebuffer.
Implementation
5 diagnostic functions were implemented in C++ and a verification in Python:
1. C++ Framebuffer Snapshot
Functiondump_framebuffer_snapshot()which captures the distribution of values (0, 1, 2, 3) in key frames (1, 676, 742, 1080). Analyze by region (top/mid/bottom) and count lines with data vs completely white lines.
2. Tilemap → Framebuffer verification
Functionverify_tilemap_to_framebuffer()which validates line by line (LY=0, 72, 143) that the tiles referenced by the tilemap are rendered correctly. Reads bytes of the tile from VRAM, manually decodes the first 4 pixels, and compares with the values written to the framebuffer.
3. Scroll and Wrap-around Verification
Functionverify_scroll_wraparound()which checks SCX/SCY and the tilemap wrap-around at frames 676 and 742. Displays map_x, map_y, tilemap_addr, tile_id, and wrap-around flags.
4. BGP Palette Verification
Functionverify_palette_bgp()which confirms that the palette correctly maps color indices. Compares the decoded color_index, BGP read, calculated final_color, and value in framebuffer.
5. Pipeline C++ → Python Verification
Functionget_framebuffer_snapshot()in Cython that returns a NumPy array of the entire framebuffer. In Python, the distribution of values in frames 676 and 742 is checked and compared with the C++ snapshot.
Components created/modified
src/core/cpp/PPU.cpp: 5 diagnostic functions addedsrc/core/cpp/PPU.hpp: Added function declarationssrc/core/cython/ppu.pyx: Functionget_framebuffer_snapshot()addedsrc/viboy.py: Python pipeline check added
Affected Files
src/core/cpp/PPU.cpp- Diagnostic features addedsrc/core/cpp/PPU.hpp- Added function declarationssrc/core/cython/ppu.pyx- get_framebuffer_snapshot() function addedsrc/viboy.py- Python pipeline check added
Tests and Verification
Tests were executed with specific ROMs and logs were analyzed:
- test ROMs: Tetris DX (30 seconds), Zelda DX (30 seconds)
- Logs generated:
logs/step0395_tetris_dx.log,logs/step0395_zelda_dx.log - Analysis: grep commands with limits to avoid context saturation
Key Results
| Frame | ROM | Framebuffer Distribution | Observation |
|---|---|---|---|
| 1 | Tetris DX | 0=11520, 3=11520 | Correct checkerboard (empty VRAM) |
| 676 | Tetris DX | 0=23040 (all white) | ⚠️ PROBLEM: Framebuffer empty even though VRAM has 14.2% TileData |
| 742 | Tetris DX | 0=22560, 1=360, 3=120 | Some data but very little (3 lines with data) |
| 1080 | Tetris DX | 0=130, 1=12295, 2=3262, 3=7353 | ✅ Complete data (144 lines with data) |
Critical Findings
- Frame 676: Framebuffer completely white (0=23040) while VRAM metrics report TileData 14.2%. This confirms the disconnect between VRAM and rendering.
- Frame 742: BGP=0x00 detected, causing all colors to be mapped to 0 (white). This explains the visual fragmentation.
- Tilemap → Framebuffer: Discrepancies detected - empty tiles (0x00) but framebuffer has different values (checkerboard active).
- Python Pipeline: The distribution in Python matches C++, confirming that the problem is in the C++ rendering, not in the pipeline.
Sources consulted
Educational Integrity
What I Understand Now
- Rendering Pipeline: The complete flow from tilemap to framebuffer, including 2bpp decoding and BGP palette application.
- Double Buffering: The buffer swap system (front/back) prevents race conditions but requires content verification after the swap.
- Visual Diagnosis: The importance of capturing snapshots in key frames to identify where the correspondence between VRAM and visualization is lost.
What remains to be confirmed
- BGP=0x00 on Frame 742: Why the BGP register is at 0x00 when it should have a valid value. Is it an initialization problem or is the game writing it?
- Empty Framebuffer on Frame 676: If VRAM has 14.2% TileData, why is the framebuffer completely white? Does the tilemap point to empty tiles or is there a problem in the rendering?
- Visual fragmentation: If the framebuffer has correct data in Frame 1080, why does it look fragmented? Is it a timing or palette application problem?
Hypotheses and Assumptions
Main hypothesis: The problem is in the application of the BGP palette or in the moment when BGP is read. If BGP=0x00, all color_index are mapped to 0 (white), which would explain why the framebuffer is empty even though VRAM has data.
Next Steps
- [ ] Investigate why BGP=0x00 on Frame 742 - is the game writing it or is it an initialization issue?
- [ ] Check if the tilemap points to empty tiles in Frame 676 even though VRAM has data
- [ ] Implement fix to ensure BGP has a valid value during rendering
- [ ] Check BGP read timing - is it read before or after the game updates it?