This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
PPU Fix: LCD Verification Enabled
Summary
Fixed a critical bug in the PPU: the PPU advanced even when the LCD was off (LCDC bit 7 = 0). According to Pan Docs, when the LCD is off, the PPU should stop and LY should be held at 0. Added a check to the start of the `step()` method to check if the LCD is on before advancing timing. Additionally, the trace limit was increased from 100 to 1000 instructions and an informative log was added when V-Blank is activated for diagnostics.
Hardware Concept
On the Game Boy, the LCDC register (LCD Control, 0xFF40) controls the state of the LCD. Bit 7 (LCDC bit 7) is the "LCD Enable": when it is 0, the LCD is off; when it is 1, the LCD is on.
Critical behavior:When the LCD is off (LCDC bit 7 = 0), the PPU stops completely:
- LY (Current Line) stays at 0
- PPU timing does not advance
- No V-Blank interrupts are generated
- The STAT register (LCD Status) reflects the current mode (usually Mode 0 or Mode 1)
When the LCD is on (LCDC bit 7 = 1), the PPU advances normally:
- LY increases from 0 to 153 (one full frame)
- Timing advances according to clock cycles
- V-Blank interrupts are generated when LY reaches 144
- The STAT register reflects the current mode of the PPU (Mode 0, 1, 2 or 3)
This behavior is critical because many games turn on the LCD and then wait for V-Blank to configure the graphics. If the PPU advances when the LCD is off, the timing goes out of sync and the game never detects V-Blank.
Fountain:Pan Docs - LCD Control Register, LCD Timing, V-Blank Interrupt
Implementation
Added a check to the start of the PPU `step()` method to check if the LCD is on before advancing the timing. If the LCD is off, the method returns immediately without processing cycles.
Modified components
src/gpu/ppu.py: Added LCD enabled check at the start of `step()`. If the LCD is off, the PPU does not advance.src/gpu/ppu.py: Added informative log when V-Blank is activated for diagnostics.src/viboy.py: Increased the trace limit from 100 to 1000 instructions to capture more information.
Design decisions
Early verification:The LCD check is done at the beginning of the `step()` method, before any loop is processed. This ensures that the PPU does not step when the LCD is off, even if `step()` is called with cycles.
Immediate return:If the LCD is off, the method returns immediately without processing cycles or updating the state. This is more efficient than processing loops and then ignoring them.
Informative log:Added an informative log (INFO level, not DEBUG) when V-Blank is activated so that it is always visible in the console. This helps diagnose if the PPU is reaching LY=144.
Increased trace limit:Increased the trace limit from 100 to 1000 instructions to capture more information. Although it is still not enough to reach LY=144 (~5,472 instructions), it allows you to see more of the polling loop.
Affected Files
src/gpu/ppu.py- Added verification of LCD enabled and V-Blank informative logsrc/viboy.py- Increased trace limit from 100 to 1000 instructions
Tests and Verification
State:Run with test ROM (pkmn.gb).
Command executed: python main.py pkmn.gb
Around:Windows, Python 3.13.5
Observed result:The trace with 1000 instructions shows that:
- LY advances correctly (from 0 to 23 in the trace)
- Polling loop is still active (repeating pattern: 0xF0, 0xFE, 0x20)
- IF is always 0x00 (the V-Blank flag is never set)
- The log does not appear
🎯 PPU: V-Blank started(PPU does not reach LY=144)
Analysis:The trace shows that the PPU is advancing correctly when the LCD is on (LY increases from 0 to 23), but the polling loop consumes cycles very slowly. In 1000 instructions, we only advance from LY=0 to LY=23. To reach LY=144 approximately 5,472 instructions are needed, which means that the trace ends before reaching V-Blank.
Identified problem:The polling loop is consuming cycles very slowly. Each iteration of the loop consumes ~8 M-Cycles (3+2+3), which is fine, but the problem is that the game is waiting for V-Blank and never detects it because the trace ends before reaching LY=144.
Hypothesis:The PPU is probably working correctly and will reach LY=144 eventually, but the polling loop consumes so many cycles that we need to wait a long time. The real problem may be that the game gives up and turns off the LCD before reaching V-Blank, or that there is some other timing problem.
State: draft- The fix seems to work (LY advances), but we need to check if it really reaches LY=144 or if the game turns off the LCD before.
Sources consulted
- Bread Docs:LCD Control Register
- Bread Docs:LCD Timing
- Bread Docs:V-Blank Interrupt
Educational Integrity
What I Understand Now
- LCD Enable and PPU:The PPU only advances when the LCD is on (LCDC bit 7 = 1). When the LCD is off, the PPU stops and LY remains at 0. This is critical for correct display timing.
- Polling loop:Games that disable interrupts (IE=00) must manually poll the IF register to detect V-Blank. The typical loop reads IF, compares to 0x01 (V-Blank bit), and if not active, goes back.
- V-Blank Timing:To reach LY=144 (V-Blank start), approximately 5,472 instructions are needed (144 lines × ~38 instructions/line). A 100 instruction trace only captures ~1 line, which is insufficient to see if it reaches V-Blank.
What remains to be confirmed
- Verification of correctness:I need to check if the fix solves the problem. The previous trace showed that LY was advancing, but we did not see if it reached LY=144. With the trace limit increased to 1000, we should see more information.
- LCD Timing:I need to check if there is any problem with the LCD timing. For example, does the LCD turn off before reaching V-Blank? Is there a problem with the synchronization between the CPU and PPU?
- V-Blank Log:I need to check if the log
🎯 PPU: V-Blank startedappears when the PPU reaches LY=144. If it doesn't appear, there may be another problem.
Hypotheses and Assumptions
Main hypothesis:The problem was that the PPU advanced even when the LCD was off, which desynchronized the timing. With the fix, the PPU will only advance when the LCD is on, which should allow it to reach LY=144 and activate the V-Blank flag.
Assumption:I assume that the LCD stays on (LCDC=0x80) for as long as it takes to reach V-Blank. If the LCD turns off before then the problem will persist.
Next Steps
- [ ] Run the emulator with the trace limit increased to 1000 instructions
- [ ] Check if the log appears
🎯 PPU: V-Blank startedwhen the PPU reaches LY=144 - [ ] Check if the V-Blank flag is activated in IF when LY reaches 144
- [ ] Check if the game detects V-Blank and exits the polling loop
- [ ] If the problem persists, investigate other possible problems (timing, synchronization, etc.)