Game Boy STAT and LYC Interrupts: Building Reliable Scanline Effects
Model the Game Boy's combined STAT interrupt line and LY-to-LYC coincidence correctly, then debug raster effects across scanlines and hardware revisions.
The Game Boy LCD controller exposes its current scanline through LY and continuously compares that value with the programmable LYC register. A match sets the coincidence flag in STAT; software can enable a STAT interrupt for that condition and use the interrupt to change display registers during a frame. This is a compact raster-effect mechanism, but it is not a timer that independently emits one interrupt for every enabled cause. Correct software and emulator behavior depend on the level and edges of the combined STAT signal.
This distinction explains a familiar emulator defect: a title’s status bar jitters, a split-screen effect fires on the wrong row, or enabling a second STAT source appears to lose an interrupt. A design that calls an interrupt handler separately for every matching condition can produce extra requests that physical hardware would not produce. Conversely, comparing LY and LYC only when software writes LYC misses matches that arise as the PPU advances through a frame.
The registers describe state, not an event queue
LY is the LCD controller’s current line counter. LYC is a software-programmable compare value. While the LCD is running, equality controls STAT bit 2, the coincidence flag. STAT bit 6 enables the coincidence source as an interrupt cause. Other STAT enable bits select Mode 0, Mode 1, and Mode 2 conditions. The interrupt request is therefore derived from changing PPU state, the enables, and the interrupt-line rules; it is not equivalent to storing a future callback when LYC is written.
An emulator should update the comparison at the hardware-relevant point as LY changes and when software changes LYC or the STAT enables. The public comparison flag and interrupt request are related but not identical: a disabled source can still have a visible coincidence flag, while it does not assert its interrupt contribution. Preserve that distinction in the register model.
Combine causes before detecting an edge
The useful implementation model is one STAT interrupt signal formed from the enabled mode and coincidence conditions. The interrupt controller requests STAT when that combined signal makes the documented inactive-to-active transition. If one enabled cause keeps the line active while another cause begins, there is no new low-to-high transition to generate a second request. This is why independent “if mode, request; if coincidence, request” checks are unsafe.
Conceptually, keep the previous line state and calculate the next one whenever an input to the line changes:
next_stat_line = lcd_enabled AND (
(mode_0 AND stat_enable_mode_0) OR
(mode_1 AND stat_enable_mode_1) OR
(mode_2 AND stat_enable_mode_2) OR
(ly_equals_lyc AND stat_enable_lyc)
)
if previous_stat_line == 0 AND next_stat_line == 1:
request_interrupt(STAT)
previous_stat_line = next_stat_line
This is an explanatory model, not a substitute for a model-specific implementation. LCD-off behavior, the precise update points, and known silicon quirks need to follow the target revision’s documented behavior and test results. Do not copy the pseudocode as a universal statement about every DMG, MGB, CGB, or later compatible device.
Why writing LYC is not the whole story
Raster code commonly waits for a compare interrupt, performs a small register update, then writes a new LYC value for a later line. The time spent in the handler matters. A handler that changes the background scroll register may be safe at one point in the PPU’s drawing sequence and visibly late at another. Intervening interrupts also matter: an interrupt can delay the handler long enough for the PPU to move into a different mode before the write occurs.
Do not assume that an interrupt requested for the start of a visible line means the CPU begins executing the handler at the first pixel of that line. Interrupt recognition and entry consume CPU time, and the PPU keeps running. Use the timing guide and measurements for the exact effect being built. When pixel-precise placement is required, document the expected interrupt entry and register-write window, then test it on hardware or against a conformance ROM rather than relying on source-code instruction counts alone.
Separate coincidence, interrupt request, and handler work
Keep three questions separate while debugging:
- Does LY currently equal LYC, and is STAT bit 2 correct?
- Is the coincidence source enabled, and did the combined STAT line make a new active transition?
- Did the CPU accept the requested interrupt at the expected instruction boundary, and did the handler finish before the target PPU event?
This decomposition localizes errors. A wrong coincidence flag points toward register/PPU timing. A correct flag with no request may be an enable or edge-state bug. A correctly requested interrupt with a late visual effect points toward interrupt latency, handler duration, or the chosen scanline strategy.
For emulator traces, record the frame, dot or clock position, LY, LYC, STAT mode, STAT enables, coincidence flag, prior and current combined STAT-line values, IF, and IE. Logging only the handler entry line is insufficient: it hides whether the request was late or the handler simply arrived after the PPU had advanced.
Design raster handlers to be bounded
A scanline handler should do only the work that has a strict deadline. Keep large calculations, memory copies, game logic, and unbounded loops outside it. Prepare per-line values in advance when possible, and have the handler perform a short register update plus the next compare scheduling operation. If a scheme depends on multiple interrupts close together, account for the CPU budget and the interaction of all enabled STAT sources.
The following is a scheduling sketch, not a complete assembly routine:
on_stat_interrupt:
acknowledge_or_mask_sources_as_required
write_precomputed_scroll_or_palette_value
write_lyc_for_next_target_line
return_from_interrupt
Whether a source is acknowledged explicitly depends on the console’s interrupt registers and the software design; the conceptual steps above must not be mistaken for register-level Game Boy code. The handler must also preserve registers and obey the selected assembler’s calling conventions.
Emulator conformance matrix
Test more than one compare line. Include an equality that is true on a single visible scanline, equality near the end of VBlank, a compare value changed while the LCD is active, and multiple enabled STAT causes that overlap or meet without the combined line dropping. Check LCD disable and re-enable transitions separately. Run tests on representative DMG and CGB modes where available, because a result from one model does not establish another model’s behavior.
Use established test ROMs and hardware captures for known edge cases. Record the exact ROM revision, console or emulator version, rendering mode, and expected trace. If a behavior is marked as revision-specific, preserve it as such in the emulator rather than generalizing from one device. A stable screenshot alone does not prove that the interrupt occurred at the correct time.
A practical visual-debug workflow
Start with a minimal scene containing a solid background and a single horizontal split. Disable unrelated sprite animation, audio scheduling, and game logic. Add a trace marker in emulator debugging output for every STAT-line transition, then compare the line counter and mode with the expected target. Next add the real handler’s register writes one at a time. If the split shifts, measure handler latency and PPU phase before changing the compare value; an arbitrary one-line offset may conceal rather than fix a timing error.
For software development, use the public LY/LYC and STAT documentation as the register contract, then consult the dedicated LYC timing guide for raster-effect timing considerations. Treat Pan Docs as a maintained community technical reference: it is highly useful, but model quirks should be cross-checked against test ROMs, hardware observations, and emulator implementations where the reference marks uncertainty.
Related:
- Game Boy OAM DMA: Copy Timing, Bus Conflicts, and Emulator Accuracy
- Sprite Limits and Scanlines: Why Accurate Emulators Preserve Flicker and Dropout
Sources: