This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
PPU C++ Initialization and Visual Debug Fix
Summary
The C++ emulator ran at 60 FPS but showed a black screen. Diagnostics revealed that the PPU registers (LCDC, BGP) were not initialized correctly in the C++ constructor, remaining at 0. Implemented explicit initialization of registers with safe values (LCDC=0x91, BGP=0xE4) and added a red diagnostic pixel in the framebuffer to verify the C++ → Python link. This allows you to confirm that the memory bridge is working correctly before debugging the full render.
Hardware Concept
On the Game Boy, the PPU (Pixel Processing Unit) controls screen rendering through several memory-mapped I/O registers:
- LCDC (0xFF40): LCD control. Bit 7 (LCD Enable) must be active (1) for the PPU to operate. If it is 0, the screen remains white/black. Bit 0 (BG Display Enable) controls whether the background is drawn.
- BGP (0xFF47): Background Palette. Defines the 4 colors of the background (each pair of bits represents a color 0-3). If set to 0x00, all colors are transparent/white, resulting in a white screen.
- SCX/SCY (0xFF43/0xFF42): Scroll X/Y. They control the movement of the bottom.
- OBP0/OBP1 (0xFF48/0xFF49): Object Palettes. Sprite palettes.
Identified problem:In C++, if variables are not explicitly initialized, they can remain at 0 (undefined behavior). If LCDC=0, the PPU does not render. If BGP=0, all pixels are transparent. This caused the black screen even though the emulation loop was working correctly.
Solution:Explicitly initialize the registers in the PPU constructor with safe values that allow something to be seen on the screen, even if the game has not yet configured these registers.
Source: Pan Docs - LCD Control Register (LCDC), Background Palette (BGP)
Implementation
The constructor was modifiedPPUin C++ to initialize critical registers with safe values and add a diagnostic pixel in the framebuffer.
Modified components
- src/core/cpp/PPU.cpp: Updated constructor to initialize logs and add diagnostic pixel
Changes made
1. Initialization of records in the constructor:
// CRITICAL: Initialize PPU registers with safe values
if (mmu_ != nullptr) {
mmu_->write(IO_LCDC, 0x91); // LCD ON, BG ON
mmu_->write(IO_BGP, 0xE4); // Standard palette
mmu_->write(IO_SCX, 0x00); // Initial scroll
mmu_->write(IO_SCY, 0x00);
mmu_->write(IO_OBP0, 0xE4); // Sprite palettes
mmu_->write(IO_OBP1, 0xE4);
}
2. Diagnostic pixel:
// DIAGNOSIS: Paint a RED pixel in the upper left corner
// This will allow us to verify that the C++ -> Python link works
framebuffer_[0] = 0xFFFF0000; // ARGB: Solid red
3. Framebuffer initialized to gray:
framebuffer_(FRAMEBUFFER_SIZE, 0xFF808080) // Gray instead of white
The framebuffer is initialized to gray (0x808080) instead of white to make it easier to see the red diagnostic pixel. If the C++ → Python link works, we should see a gray screen with a red dot in the top left corner.
Design decisions
- Safe defaults:Values were chosen that allow you to see something on the screen even if the game has not initialized the records. LCDC=0x91 activates the LCD and background. BGP=0xE4 is the standard value used by many games.
- Diagnostic pixel:Added a red pixel at position 0 of the framebuffer to verify that the memory link is working. If we see red, we know that Python is correctly reading C++ memory.
- Initialization in constructor:It was decided to initialize the registers in the PPU constructor instead of in viboy.py to ensure that they always have valid values, regardless of the initialization order.
Affected Files
src/core/cpp/PPU.cpp- Modified constructor to initialize registers and add diagnostic pixel
Tests and Verification
Visual validation:
- When running
python main.py roms/tetris.gb, expected to see:- If the link works: Gray screen with a red dot in the upper left corner
- If the rendering works: The game should appear above the gray
- If still black: The framebuffer pointer is not being passed correctly
Compilation:
$ python setup.py build_ext --inplace
running build_ext
building 'viboy_core' extension
...
copying build\lib.win-amd64-cpython-313\viboy_core.cp313-win_amd64.pyd ->
Successful compilation without errors (only minor non-critical warnings).
Next verification steps:
- Run the emulator and check the screen color
- If it appears gray with red dot: Link works, debug rendering
- If it is still black: Check the Cython wrapper and the framebuffer memoryview
Sources consulted
- Bread Docs:LCD Control Register (LCDC)
- Bread Docs:Palettes (BGP, OBP0, OBP1)
- Bread Docs:Scrolling (SCX, SCY)
Educational Integrity
What I Understand Now
- Initialization in C++:Unlike Python, in C++ uninitialized variables can have random values or 0. It is critical to explicitly initialize all registers that control the hardware.
- PPU records:LCDC and BGP are essential for the PPU to render. If they are at 0, the screen remains black/white even if the emulation loop works correctly.
- Visual debug:Adding diagnostic pixels to the framebuffer is an effective technique to verify that the C++ → Python memory link is working correctly.
What remains to be confirmed
- Memory link:Verify that the Cython memoryview correctly points to the C++ memory and that the changes are reflected in Python.
- Full render:Once the link is confirmed, verify that the tile rendering works correctly and that the game appears on the screen.
Hypotheses and Assumptions
Main hypothesis:The problem was the lack of register initialization, not a rendering bug. If the red pixel appears, we confirm that the link works and we can debug the rendering. If it doesn't appear, the problem is with the Cython bridge.
Next Steps
- [ ] Run the emulator and check the screen color
- [ ] If gray with red dot appears: Debug tile rendering (render_bg)
- [ ] If it is still black: Check the Cython wrapper (ppu.pyx) and the framebuffer memoryview
- [ ] Once the link is confirmed, remove the diagnostic pixel and the gray color from the framebuffer