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

Interrupt Decoupled Rendering

Date:2025-12-18 StepID:0054 State: draft

Summary

Decoupled rendering from interrupts to ensure that each frame is drawn to the screen when the PPU reaches V-Blank (LY=144), regardless of the IME (Interrupt Master Enable) state or whether the game uses manual IF polling. Added a `frame_ready` flag in the PPU that fires when LY goes from 143 to 144, and an `is_frame_ready()` method that allows the main loop to check and render without relying on interrupts. This fixes the blue/black screen issue when the game has IME=False and expects V-Blank via polling.

Hardware Concept

On the real Game Boy, pixel rendering and interrupt generation are processesindependentalthough related. The PPU generates a V-Blank event every time LY reaches 144, and this event:

  • Alwaysupdates the IF register (Interrupt Flag) to 0xFF0F, setting bit 0.
  • Optionallytriggers an automatic interrupt if IME=True and IE has bit 0 set.

Games can use two strategies to detect V-Blank:

  1. Automatic interruptions: Enable IME and configure IE so that the CPU automatically jumps to the V-Blank routine.
  2. Manual polling: Disable IME and periodically read the IF register to detect when bit 0 is active.

In both cases, rendering should occur when LY=144, regardless of how the game detects the event. If the emulator only renders when an interrupt is processed, games that use polling will never see updates on the screen.

Fountain:Pan Docs - V-Blank Interrupt, Interrupt Flag (IF), Interrupt Master Enable (IME)

Implementation

A flag system was implemented in the PPU that indicates when a frame is ready to render, completely decoupling the rendering from the interrupt system.

Components created/modified

  • PPU (`src/gpu/ppu.py`): Added the `frame_ready` flag that is activated when LY goes from 143 to 144, and the `is_frame_ready()` method that returns the flag and resets it automatically.
  • Viboy (`src/viboy.py`): Changed main loop to use `ppu.is_frame_ready()` instead of `_prev_vblank` based logic. Removed `_prev_vblank` variable which is no longer needed.

Design decisions

Flag with automatic reset:The `is_frame_ready()` method resets the flag to `False` after reading it. This ensures that each frame is only rendered once, avoiding duplicate renderings if the main loop calls the method multiple times in the same loop.

Activation at the exact moment:The flag is set when LY goes from 143 to 144, which is the exact moment V-Blank starts. This ensures that rendering occurs at the correct time in the PPU cycle.

IME Independence:The flag is activated regardless of the state of IME or IE. This allows rendering to work for both games that use interrupts and those that use polling.

Affected Files

  • src/gpu/ppu.py- Added `frame_ready` flag and `is_frame_ready()` method
  • src/viboy.py- Modified main loop to use `is_frame_ready()` instead of `_prev_vblank`

Tests and Verification

This modification requires verification by running ROMs, as it affects the visual behavior of the emulator. The existing PPU unit tests still pass, but do not cover the new flag.

Next check:Run test ROMs (e.g. pkmn.gb, tetris_dx.gbc) and verify that:

  • The screen refreshes correctly when LY reaches 144, even with IME=False.
  • There are no duplicate renders (each frame is rendered exactly once).
  • Performance is not degraded by checking the flag on each iteration of the loop.

Note: Verification with ROMs will be carried out in the next testing step.

Sources consulted

Implementation based on analysis of the execution trace that showed that the game expects V-Blank through IF polling, and on the principle that rendering should be independent of the interrupt system.

Educational Integrity

What I Understand Now

  • Rendering vs Interrupts:The frame rendering and the interrupt system are independent processes. The PPU generates V-Blank events that always update IF, but rendering must occur regardless of whether an interrupt is processed.
  • Polling vs Interruptions:Games can use two strategies to detect V-Blank: automatic interrupts (IME=True) or manual polling (IME=False, reading IF). The emulator must support both strategies correctly.
  • Render timing:The exact time to render is when LY goes from 143 to 144, which is when V-Blank starts. This is the time when the PPU has finished drawing all visible lines.

What remains to be confirmed

  • Performance:Verify that checking `is_frame_ready()` at each iteration of the loop does not introduce significant overhead.
  • Behavior with multiple frames:Confirm that there are no race conditions or lost renders when the PPU advances too fast.
  • Compatibility with all games:Verify that this implementation works correctly with games that use interruptions and with games that use polling.

Hypotheses and Assumptions

Main hypothesis:The blue/black screen issue is because rendering only occurred when an interrupt was processed, and games that use polling never triggered rendering. With this modification, rendering should occur whenever the PPU reaches V-Blank, regardless of the interrupt system.

This hypothesis is based on analysis of the execution trace which showed that the game is in a polling loop waiting for IF to have a specific value, and that LY is advancing correctly (12→23 in the trace), indicating that the PPU is working but the rendering is not triggered.

Next Steps

  • [ ] Run test ROMs (pkmn.gb, tetris_dx.gbc) and verify that the screen updates correctly
  • [ ] Verify that there are no duplicate renders or lost frames
  • [ ] If the screen is still blue/black, investigate other possible problems (LCDC, BGP, tile rendering, etc.)
  • [ ] Add unit tests for the `frame_ready` flag if necessary