This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Window Implementation and Instrumentation Fix
Summary
This step implements two critical improvements: (1) Moving the diagnostic monitors (VBLANK-ENTRY, RESET-WATCH, POLLING-WATCH) to the start of CPU::step() to prevent the early return of interrupts from hiding them, and (2) Full implementation of the Window rendering logic in PPU::render_scanline() considering the WY (0xFF4A) and WX (0xFF4B) registers. The window is rendered correctly above the Background but below the Sprites, respecting bit 5 of LCDC (Window Enable) and using the same tile addressing system as the Background according to bit 4 of LCDC.
Hardware Concept
Window: The Game Boy has a rendering layer called "Window" which is an opaque surface that is drawn above the Background but below the Sprites. Unlike the Background, the Window has no scroll: it always starts from the tile (0,0) of the selected tilemap. The position of the Window is controlled by two registers:
- WY (0xFF4A): Y coordinate of the start of the Window. The Window is only drawn when LY >= WY.
- WX (0xFF4B): X coordinate of the start of the Window with an offset of 7 pixels. WX=7 means that the Window starts at X=0 on the screen. The Window is only drawn if WX<= 166.
LCDC Bit 5 (Window Enable): Controls whether Window is enabled. If this bit is disabled, the Window is not rendered, regardless of the WY and WX values.
LCDC Bit 6 (Window Tilemap): Select which tilemap to use for the Window (0x9800 or 0x9C00), independent of the Background tilemap.
LCDC Bit 4 (Tile Data Addressing): Controls tile routing for both Background and Window. Bit 4=1 uses unsigned addressing (0x8000, tile IDs 0-255), Bit 4=0 uses signed addressing (0x8800, tile IDs -128 to 127).
Render Order: Background → Window → Sprites. The Window overwrites the Background in the areas where it is drawn.
Fountain:Pan Docs - "Window", "LCDC Register", "Tile Data Addressing"
Implementation
Three main changes were made:
1. Movement of Diagnostic Monitors
The VBLANK-ENTRY, RESET-WATCH and POLLING-WATCH monitors were located after the instruction switch, which caused them to not be executed when there were interrupts that caused early return. They were moved to the start of CPU::step(), just after capturing the original PC but before handle_interrupts().
Critical change:The original PC is now captured at the start of step() and reused throughout the method, ensuring that monitors run even when there are interruptions.
2. Complete implementation of render_window()
Completed the render_window() function in PPU.cpp with complete rendering logic:
- Checking LCDC bit 5 (Window Enable) and bit 7 (LCD Enable)
- Validation of WY conditions<= LY y WX <= 166
- Tilemap selection according to LCDC bit 6
- Using the same tile addressing system as Background (LCDC bit 4)
- Pixel-by-pixel rendering from window_x_start to SCREEN_WIDTH
- Applying BGP palette to Window pixels
3. Integration in render_scanline()
Added the call to render_window() in render_scanline() after the Background is rendered but before the Sprites, respecting the correct layer order.
Design decisions
- Reuse of original_pc:It is captured once at startup and reused throughout step() to avoid inconsistencies.
- Addressing consistency:Both Background and Window use the same tile addressing logic according to LCDC bit 4, guaranteeing consistency.
- Early validation:All conditions (LCD Enable, Window Enable, WY, WX) are checked before starting rendering to optimize performance.
Affected Files
src/core/cpp/CPU.cpp- Move diagnostic monitors to start of step()src/core/cpp/PPU.cpp- Complete render_window() implementation and integration into render_scanline()
Tests and Verification
The implementation was validated by:
- Successful build:The C++ module compiled without errors using
python setup.py build_ext --inplace - Compiled C++ module validation:viboy_core module was successfully generated as .pyd
- Logic check:Manual code review to ensure that:
- Monitors run before early return of interrupts
- Window is rendered only when LCDC bit 5 is active
- Tile routing is consistent between Background and Window
- The Window respects the WY conditions<= LY y WX <= 166
// Example of checking conditions in render_window()
if ((lcdc & 0x80) == 0 || (lcdc & 0x20) == 0) {
return; // LCD disabled or Window disabled
}
if (ly_< wy || wx >166) {
return; // Position conditions not met
}
Sources consulted
- Pan Docs: "Window" - Description of the window system
- Pan Docs: "LCDC Register" - LCD Control Bits
- Pan Docs: "Tile Data Addressing" - Tile addressing system
- Pan Docs: "Background and Window" - Layer Render Order
Educational Integrity
What I Understand Now
- Window vs Background:The Window is an independent layer without scrolling that always starts from (0,0) of the tilemap. The Background has scrolling via SCX/SCY.
- WX Offset:WX has a 7 pixel offset because the Game Boy hardware has this quirk. WX=7 means position X=0 on screen.
- Shared addressing:Both Background and Window share the same tile addressing system (LCDC bit 4), but can use different tilemaps (LCDC bits 3 and 6).
- Diagnostic Monitors:For monitors to capture critical events (as input to V-Blank), they must be executed before any early returns, especially before handle_interrupts().
What remains to be confirmed
- Behavior with WX< 7:Need validation on real hardware if Window partially renders when WX< 7.
- Performance:Verify that the Window rendering does not significantly affect performance at 60 FPS.
Hypotheses and Assumptions
It is assumed that when WX< 7, la Window simplemente no se renderiza (window_x_start se ajusta a 0). Esto necesita validación con hardware real o ROMs de test.
Next Steps
- [ ] Validate Window rendering with test ROMs that use windows
- [ ] Verify that diagnostic monitors correctly capture interrupt events
- [ ] Optimize render_window() if necessary to maintain 60 FPS
- [ ] Implement specific unit tests for Window rendering