Skip to content

Commit 9493a84

Browse files
trekawekclaude
andcommitted
Read the object's high tile byte one dot after the fetch
The mode-3 object fetch read both tile-data bytes during its fixed 6-cycle window. On hardware the high byte is read one dot after the fetch's last cycle (SameBoy reads it with during_object_fetch already cleared), so a mid-mode-3 write to LCDC.2 (sprite height) or the tile that lands on that dot affects the high byte alone. Deferring the high read by one dot halves the residual of m3_lcdc_obj_size_change (45->30) and m3_lcdc_obj_size_change_scx (90->30); baselines tightened. The object fetch still occupies the same six cycles and advances the pixel machine identically, so the sprite-stall timing pinned by intr_2_mode0_timing_sprites is unchanged (mooneye still green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J1wLWscyUGS7CFwc3CjJR8
1 parent 0a9d4e4 commit 9493a84

2 files changed

Lines changed: 46 additions & 36 deletions

File tree

core/src/main/java/eu/rekawek/coffeegb/core/gpu/phase/PixelTransfer.java

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ public class PixelTransfer implements GpuPhase, Serializable, Originator<PixelTr
110110

111111
private int objTileLine;
112112

113+
private boolean objData1Pending;
114+
115+
// an object match is waiting for the background fetcher (counts as an object fetch
116+
// for the DMG LCDC.1 write conflict)
117+
private boolean objWaiting;
118+
113119
public PixelTransfer(
114120
Display display,
115121
AddressSpace videoRam0,
@@ -157,6 +163,8 @@ public PixelTransfer start(int scyLatch, int extraEntryDelay) {
157163
window = false;
158164
windowBeingFetched = false;
159165
objStep = -1;
166+
objData1Pending = false;
167+
objWaiting = false;
160168

161169
spriteCount = 0;
162170
for (int i = 0; i < sprites.length; i++) {
@@ -203,7 +211,7 @@ public int getPosition() {
203211
}
204212

205213
public boolean isObjectFetchInProgress() {
206-
return objStep >= 0;
214+
return objStep >= 0 || objWaiting;
207215
}
208216

209217
public boolean isWindowBeingFetched() {
@@ -235,6 +243,20 @@ public boolean tick() {
235243
return true;
236244
}
237245

246+
if (objData1Pending) {
247+
objData1Pending = false;
248+
// hardware reads the object's high tile byte one dot after the fetch's last
249+
// cycle, so a mid-mode-3 LCDC.2 (sprite height) or tile write during that dot
250+
// affects the high byte only (mealybug m3_lcdc_obj_size_change[_scx])
251+
int objData1 = fetcher.readSpriteData(objTileId, objTileLine, 1, objAttributes);
252+
fifo.setOverlay(
253+
fetcher.zip(objData0, objData1, objAttributes.isXflip()),
254+
0,
255+
objAttributes,
256+
spriteOrder[spriteHead]);
257+
spriteHead++;
258+
}
259+
238260
// commit or drop a pending window activation (see windowPendingTicks)
239261
if (windowPendingTicks > 0) {
240262
windowPendingTicks--;
@@ -343,14 +365,18 @@ public boolean tick() {
343365
&& sprites[spriteOrder[spriteHead]].getX() == match
344366
&& (lcdc.isObjDisplayEffective() || gbc)) {
345367
if (fetcher.getState() < Fetcher.GET_TILE_DATA_HIGH_T2 || fifo.getLength() == 0) {
346-
// the object fetch waits for the background fetcher's tile data
368+
// the object fetch waits for the background fetcher's tile data; the wait
369+
// already counts as "during object fetch" for the LCDC.1 conflict special
370+
objWaiting = true;
347371
fetcher.advance(position, window, windowLineCounter, true);
348372
return true;
349373
}
374+
objWaiting = false;
350375
objStep = 0;
351376
objTick();
352377
return true;
353378
}
379+
objWaiting = false;
354380

355381
renderPixelIfPossible();
356382
fetcher.advance(position, window, windowLineCounter, false);
@@ -359,38 +385,22 @@ public boolean tick() {
359385

360386

361387
private void objTick() {
362-
switch (objStep) {
363-
case 0:
364-
fetcher.advance(position, window, windowLineCounter, true);
365-
break;
366-
367-
case 1: {
368-
fetcher.advance(position, window, windowLineCounter, true);
369-
SpritePosition sprite = sprites[spriteOrder[spriteHead]];
370-
objTileId = fetcher.readSpriteTileId(sprite);
371-
objAttributes = fetcher.readSpriteAttributes(sprite);
372-
objTileLine = r.get(LY) + 16 - sprite.getY();
373-
break;
374-
}
375-
376-
case 3:
377-
objData0 = fetcher.readSpriteData(objTileId, objTileLine, 0, objAttributes);
378-
break;
379-
380-
case 5: {
381-
int objData1 = fetcher.readSpriteData(objTileId, objTileLine, 1, objAttributes);
382-
fifo.setOverlay(
383-
fetcher.zip(objData0, objData1, objAttributes.isXflip()),
384-
0,
385-
objAttributes,
386-
spriteOrder[spriteHead]);
387-
spriteHead++;
388-
objStep = -1;
389-
return;
390-
}
391-
392-
default:
393-
break;
388+
if (objStep <= 1) {
389+
fetcher.advance(position, window, windowLineCounter, true);
390+
}
391+
if (objStep == 1) {
392+
SpritePosition sprite = sprites[spriteOrder[spriteHead]];
393+
objTileId = fetcher.readSpriteTileId(sprite);
394+
objAttributes = fetcher.readSpriteAttributes(sprite);
395+
objTileLine = r.get(LY) + 16 - sprite.getY();
396+
}
397+
if (objStep == 5) {
398+
// low tile byte on the fetch's last cycle; the high byte is read one dot
399+
// later (objData1Pending, handled at the top of the next tick)
400+
objData0 = fetcher.readSpriteData(objTileId, objTileLine, 0, objAttributes);
401+
objData1Pending = true;
402+
objStep = -1;
403+
return;
394404
}
395405
objStep++;
396406
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ public class MealybugRomTest {
3737
Map.entry("m3_lcdc_bg_map_change.gb", 0),
3838
Map.entry("m3_lcdc_obj_en_change.gb", 2),
3939
Map.entry("m3_lcdc_obj_en_change_variant.gb", 50),
40-
Map.entry("m3_lcdc_obj_size_change.gb", 45),
41-
Map.entry("m3_lcdc_obj_size_change_scx.gb", 90),
40+
Map.entry("m3_lcdc_obj_size_change.gb", 30),
41+
Map.entry("m3_lcdc_obj_size_change_scx.gb", 30),
4242
Map.entry("m3_lcdc_tile_sel_change.gb", 0),
4343
Map.entry("m3_lcdc_tile_sel_win_change.gb", 192),
4444
Map.entry("m3_lcdc_win_en_change_multiple.gb", 0),

0 commit comments

Comments
 (0)