Skip to content

Commit 0976199

Browse files
trekawekclaude
andcommitted
Read the deferred high tile-data byte on the fetch's own schedule
When the push has to wait (occupied FIFO, or an object fetch pausing the pipeline), the hardware background fetch still reads its high data byte two dots after the low one - the wait defers only the push, not the read. We read the high byte at the actual push tick, so register writes landing during the wait (an LCDC.4 toggle around a sprite fetch, an SCY write) leaked into data the hardware fetch had already read. Diagnosed with wall-clock-aligned read traces against an instrumented SameBoy on m3_lcdc_tile_sel_change line 100: the object fetch at x=8 interposed between our low and high byte reads, so the LCDC.4 pulse hit our high byte one fetch early; hardware completes both reads before the object fetch and catches the pulse on the *next* fetch's low byte instead. With a free FIFO the push and the read still share the HIGH_T2 tick, so the calibrated normal-flow timing is unchanged. m3_lcdc_tile_sel_change 534 -> 0 (pixel-perfect), m3_scy_change 464 -> 152, m3_lcdc_tile_sel_win_change 478 -> 192. Mealybug DMG total 2684 -> 1552, 11 of 24 tests pixel-perfect. Full battery green (mooneye 98, blargg, dmg-acid2, unit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J1wLWscyUGS7CFwc3CjJR8
1 parent 2ace103 commit 0976199

2 files changed

Lines changed: 35 additions & 7 deletions

File tree

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,20 @@ public void disableInsertionGlitch() {
126126
* @param windowY the window line counter
127127
* @param duringObjectFetch whether the advance happens as part of an object fetch
128128
*/
129+
private boolean data2Pending;
130+
131+
private int data2Delay;
132+
133+
private void readData2(boolean window, int windowY) {
134+
tileData2 = getTileData(tileId, effectiveY(window, windowY) & 7, 1,
135+
lcdc.getBgWindowTileData(), lcdc.isBgWindowTileDataSigned(), tileAttributes, 8);
136+
data2Pending = false;
137+
}
138+
129139
public void advance(int position, boolean window, int windowY, boolean duringObjectFetch) {
140+
if (data2Pending && state == PUSH && --data2Delay <= 0) {
141+
readData2(window, windowY);
142+
}
130143
switch (state) {
131144
case GET_TILE_T1: {
132145
// the map offset (SCX/SCY, window position) is sampled at the start of
@@ -200,6 +213,13 @@ public void advance(int position, boolean window, int windowY, boolean duringObj
200213
// itself (mealybug m3_scy_change / m3_lcdc_tile_sel_change)
201214
tileData1 = getTileData(tileId, effectiveY(window, windowY) & 7, 0,
202215
lcdc.getBgWindowTileData(), lcdc.isBgWindowTileDataSigned(), tileAttributes, 8);
216+
data2Pending = true;
217+
// when the push must wait, the hardware fetch still reads the high byte
218+
// on its own schedule two dots later - even while an object fetch pauses
219+
// the pipeline; register writes during the wait must not affect it
220+
// (m3_lcdc_tile_sel_change around a sprite fetch). With a free FIFO the
221+
// push (and with it the read) happens in this same T-cycle.
222+
data2Delay = fifo.getLength() != 0 ? 2 : 0;
203223
state = PUSH;
204224
// falls through: the push happens in the same T-cycle when the FIFO is free
205225

@@ -218,8 +238,10 @@ public void advance(int position, boolean window, int windowY, boolean duringObj
218238
break;
219239
}
220240
}
221-
tileData2 = getTileData(tileId, effectiveY(window, windowY) & 7, 1,
222-
lcdc.getBgWindowTileData(), lcdc.isBgWindowTileDataSigned(), tileAttributes, 8);
241+
if (data2Pending) {
242+
// the push arrived before the scheduled high-byte read
243+
readData2(window, windowY);
244+
}
223245
if (window) {
224246
windowTileX = (windowTileX + 1) & 0x1f;
225247
}
@@ -327,7 +349,9 @@ public Memento<Fetcher> saveToMemento() {
327349
tileData2,
328350
insertionGlitchDisabled,
329351
scyAtLineStart,
330-
firstFetch);
352+
firstFetch,
353+
data2Pending,
354+
data2Delay);
331355
}
332356

333357
@Override
@@ -352,6 +376,8 @@ public void restoreFromMemento(Memento<Fetcher> memento) {
352376
this.insertionGlitchDisabled = mem.insertionGlitchDisabled;
353377
this.scyAtLineStart = mem.scyAtLineStart;
354378
this.firstFetch = mem.firstFetch;
379+
this.data2Pending = mem.data2Pending;
380+
this.data2Delay = mem.data2Delay;
355381
}
356382

357383
private record FetcherMemento(
@@ -366,7 +392,9 @@ private record FetcherMemento(
366392
int tileData2,
367393
boolean insertionGlitchDisabled,
368394
int scyAtLineStart,
369-
boolean firstFetch)
395+
boolean firstFetch,
396+
boolean data2Pending,
397+
int data2Delay)
370398
implements Memento<Fetcher> {
371399
}
372400
}

core/src/test/java/eu/rekawek/coffeegb/core/integration/mealybug/MealybugRomTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ public class MealybugRomTest {
3939
Map.entry("m3_lcdc_obj_en_change_variant.gb", 50),
4040
Map.entry("m3_lcdc_obj_size_change.gb", 45),
4141
Map.entry("m3_lcdc_obj_size_change_scx.gb", 90),
42-
Map.entry("m3_lcdc_tile_sel_change.gb", 534),
43-
Map.entry("m3_lcdc_tile_sel_win_change.gb", 478),
42+
Map.entry("m3_lcdc_tile_sel_change.gb", 0),
43+
Map.entry("m3_lcdc_tile_sel_win_change.gb", 192),
4444
Map.entry("m3_lcdc_win_en_change_multiple.gb", 0),
4545
Map.entry("m3_lcdc_win_en_change_multiple_wx.gb", 521),
4646
Map.entry("m3_lcdc_win_map_change.gb", 92),
4747
Map.entry("m3_obp0_change.gb", 0),
4848
Map.entry("m3_scx_high_5_bits.gb", 84),
4949
Map.entry("m3_scx_low_3_bits.gb", 0),
50-
Map.entry("m3_scy_change.gb", 464),
50+
Map.entry("m3_scy_change.gb", 152),
5151
Map.entry("m3_window_timing.gb", 0),
5252
Map.entry("m3_window_timing_wx_0.gb", 126),
5353
Map.entry("m3_wx_4_change.gb", 0),

0 commit comments

Comments
 (0)