Skip to content

Commit 78aa032

Browse files
trekawekclaude
andcommitted
Let the STAT line dip between mode-0 and the next line's LYC term
Ken Griffey Jr.'s Slugfest (issue #68) paces a per-band palette engine with chained STAT interrupts: an LYC interrupt at the band line arms a mode-0 interrupt, whose handler streams the next 8-line band's palettes during HBlank, reprograms LYC and re-arms. The game keeps both sources enabled across the frame start, so the chain relies on the LYC interrupt of the band line firing while the previous line's mode-0 term is still enabled. Our STAT interrupt line never dipped there: the mode-0 term stays high through the last tick of a line and the coincidence term went high at tick 0 of the next line, so the line stayed high across the boundary and the rising edge - and with it the LYC interrupt - was swallowed. The game's engine then aborted, leaving an interrupt storm (a mode-0 int on all 143 other lines) and only the static VBlank palette load, which painted the perspective-warped field with the wrong per-band colours - the "garbled crowd". On hardware the comparator output feeding the interrupt line settles one machine cycle after the line-start latch, so the line dips for 4 ticks and the LYC edge fires. The readable STAT.2 flag keeps the existing timing (valid at tick 0, pinned by lcdon_timing-GS); only the interrupt-line term gets the 4-tick settling delay. Lines 0 and 153 are excluded, preserving the single LYC=0 interrupt per frame across the 153->0 transition (Altered Space) and the short LYC=153 window. Verified: the palette engine now runs on every frame (408 mid-frame BGPD writes, matching SameBoy exactly); the Home Run Derby field renders pixel-clean. Full battery (unit, mooneye 98, dmg-acid2, blargg 46+8, mealybug 24, controller 12) green; Bug's Life, Altered Space and Zerd no Densetsu re-verified by screenshot. Fixes #68 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J1wLWscyUGS7CFwc3CjJR8
1 parent b3d42c7 commit 78aa032

1 file changed

Lines changed: 23 additions & 3 deletions

File tree

core/src/main/java/eu/rekawek/coffeegb/core/gpu/StatRegister.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ public class StatRegister implements AddressSpace, Originator<StatRegister> {
3838

3939
private boolean coincidence;
4040

41+
// the coincidence term feeding the interrupt line; unlike the readable flag it stays
42+
// low for the first 4 ticks of a line while the comparator output settles
43+
private boolean intCoincidence;
44+
4145
private boolean intLine;
4246

4347
public StatRegister(InterruptManager interruptManager) {
@@ -67,6 +71,21 @@ public void tick() {
6771
// interrupt fires only once per frame there
6872
coincidence = false;
6973
}
74+
intCoincidence = coincidence;
75+
if (ticksInLine < 4 && gpu.getLine() != 0 && gpu.getLine() != 153) {
76+
// the coincidence term feeding the interrupt line settles one machine
77+
// cycle after the line-start latch (the readable flag is already valid
78+
// at tick 0, lcdon_timing-GS). This guarantees the STAT interrupt line
79+
// dips between the previous line's mode-0 term (high until the line
80+
// ends) and the new line's LY=LYC term, so an enabled mode-0 interrupt
81+
// cannot swallow the LYC interrupt of the following line (Ken Griffey's
82+
// Slugfest chains LYC and HBlank interrupts for its per-band palette
83+
// engine, issue #68). Lines 0 and 153 keep the existing model: LY=0 is
84+
// registered at tick 8 of line 153 and stays valid into line 0 (single
85+
// LYC=0 interrupt per frame, Altered Space), and LYC=153 has only the
86+
// tl=0..3 window.
87+
intCoincidence = false;
88+
}
7089

7190
if (gpu.getLine() == 144 && ticksInLine == 0) {
7291
interruptManager.requestInterrupt(InterruptType.VBlank);
@@ -81,7 +100,7 @@ public void onLcdEnabled() {
81100
}
82101

83102
private boolean computeIntLine(int enable) {
84-
boolean line = (enable & 0b01000000) != 0 && coincidence;
103+
boolean line = (enable & 0b01000000) != 0 && intCoincidence;
85104
if (gpu.isLcdEnabled()) {
86105
line |= (enable & 0b00001000) != 0 && gpu.isMode0IntWindow();
87106
line |= (enable & 0b00010000) != 0 && gpu.getVisibleStatMode() == 1;
@@ -119,7 +138,7 @@ public int getByte(int address) {
119138

120139
@Override
121140
public Memento<StatRegister> saveToMemento() {
122-
return new StatRegisterMemento(enableBits, registeredLy, coincidence, intLine);
141+
return new StatRegisterMemento(enableBits, registeredLy, coincidence, intCoincidence, intLine);
123142
}
124143

125144
@Override
@@ -130,10 +149,11 @@ public void restoreFromMemento(Memento<StatRegister> memento) {
130149
this.enableBits = mem.enableBits;
131150
this.registeredLy = mem.registeredLy;
132151
this.coincidence = mem.coincidence;
152+
this.intCoincidence = mem.intCoincidence;
133153
this.intLine = mem.intLine;
134154
}
135155

136156
private record StatRegisterMemento(int enableBits, int registeredLy, boolean coincidence,
137-
boolean intLine) implements Memento<StatRegister> {
157+
boolean intCoincidence, boolean intLine) implements Memento<StatRegister> {
138158
}
139159
}

0 commit comments

Comments
 (0)