Skip to content

Commit 66876db

Browse files
trekawekclaude
andcommitted
Model the DMG window LCD desync (mealybug wewx 521->83)
When a window's WX==position+7 activation dot is masked by a mid-line LCDC.5-off pulse, the window still activates as the pulse re-enables it, rendered as if it had triggered at the match dot. This is SameBoy's WX==position+6 + lcd_x-- horizontal desync, generalised: our LCDC-pulse commit lands a fixed number of dots off the pixel position axis, so a literal +6 comparison misfires. Instead we record the +7 match position reached while the window is disabled and, on re-enable, activate and rewind the output back to it when it is within DESYNC_MAX_GAP dots. That reproduces hardware's "activate iff the match is at or just before the re-enable" across every affected line, so m3_lcdc_win_en_change_multiple_wx drops from 521 to 83 diff pixels with no change to the other window tests. Adds WX-just-written tracking to GpuRegisterValues (the desync is suppressed for one machine cycle after a WX write, SameBoy wx_just_changed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J1wLWscyUGS7CFwc3CjJR8
1 parent 9493a84 commit 66876db

3 files changed

Lines changed: 67 additions & 4 deletions

File tree

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ public class GpuRegisterValues implements AddressSpace, Serializable, Originator
2727

2828
private final int[] pendingMixValues = new int[GpuRegister.values().length];
2929

30+
// ticks remaining in which WX counts as "just written": the DMG's WX==position+6
31+
// window desync is suppressed for one machine cycle after a WX write (SameBoy
32+
// wx_just_changed)
33+
private int wxJustChangedTicks;
34+
35+
private static final int WX_CHANGE_TICKS = 2;
36+
3037
private boolean gbc;
3138

3239
public void setGbc(boolean gbc) {
@@ -59,6 +66,13 @@ void tickConflicts() {
5966
mixValues[reg.ordinal()] = pendingMixValues[reg.ordinal()];
6067
pendingMixValues[reg.ordinal()] = -1;
6168
}
69+
if (wxJustChangedTicks > 0) {
70+
wxJustChangedTicks--;
71+
}
72+
}
73+
74+
public boolean isWxJustChanged() {
75+
return wxJustChangedTicks > 0;
6276
}
6377

6478
private static final GpuRegister[] PALETTE_REGISTERS =
@@ -85,6 +99,9 @@ public void setByte(int address, int value) {
8599
if (!gbc && (reg == GpuRegister.BGP || reg == GpuRegister.OBP0 || reg == GpuRegister.OBP1)) {
86100
pendingMixValues[reg.ordinal()] = values[reg.ordinal()] | value;
87101
}
102+
if (reg == GpuRegister.WX) {
103+
wxJustChangedTicks = WX_CHANGE_TICKS;
104+
}
88105
values[reg.ordinal()] = value;
89106
}
90107
}
@@ -110,7 +127,7 @@ private static GpuRegister fromAddress(int address) {
110127

111128
@Override
112129
public Memento<GpuRegisterValues> saveToMemento() {
113-
return new GpuRegisterValuesMemento(values.clone(), mixValues.clone(), pendingMixValues.clone());
130+
return new GpuRegisterValuesMemento(values.clone(), mixValues.clone(), pendingMixValues.clone(), wxJustChangedTicks);
114131
}
115132

116133
@Override
@@ -129,9 +146,11 @@ public void restoreFromMemento(Memento<GpuRegisterValues> memento) {
129146
java.util.Arrays.fill(this.mixValues, -1);
130147
java.util.Arrays.fill(this.pendingMixValues, -1);
131148
}
149+
this.wxJustChangedTicks = mem.wxJustChangedTicks;
132150
}
133151

134-
private record GpuRegisterValuesMemento(int[] values, int[] mixValues, int[] pendingMixValues)
152+
private record GpuRegisterValuesMemento(int[] values, int[] mixValues, int[] pendingMixValues,
153+
int wxJustChangedTicks)
135154
implements Memento<GpuRegisterValues> {
136155
}
137156
}

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ public class PixelTransfer implements GpuPhase, Serializable, Originator<PixelTr
7474
// rolled back at commit so the activation behaves as if it happened at the match
7575
private int windowPendingPos;
7676

77+
// position of a WX==position+7 match that landed while the window was disabled by a
78+
// mid-line pulse, awaiting the window re-enable to activate (DMG desync); -1 = none
79+
private int windowCatchUpPos = -1;
80+
7781
// whether the window already activated on this line: a RE-activation (after a
7882
// mid-line window drop, e.g. an LCDC.5 toggle) restarts its fetch one T-cycle
7983
// later than the line's first activation (m3_lcdc_win_en_change_multiple)
@@ -158,6 +162,7 @@ public PixelTransfer start(int scyLatch, int extraEntryDelay) {
158162
machineActive = true;
159163
position = -16;
160164
windowActivatedThisLine = false;
165+
windowCatchUpPos = -1;
161166
machineStall = 0;
162167
fifo.startLine();
163168
window = false;
@@ -305,6 +310,19 @@ public boolean tick() {
305310
windowBeingFetched = false;
306311
}
307312

313+
// record a WX==position+7 match that lands while the window is disabled by a
314+
// mid-line pulse: the window will "catch up" when it is re-enabled (see the
315+
// desync branch below). Only for regular WX>=7 positions so the discard-phase
316+
// WX=0..6 activation is untouched.
317+
if (!gbc && !window && !lcdc.isWindowDisplay() && !windowActivatedThisLine
318+
&& r.get(LY) >= r.get(WY)) {
319+
int wxNow = r.get(WX);
320+
if (wxNow >= 7 && wxNow < 166 && !r.isWxJustChanged()
321+
&& wxNow == ((position + 7) & 0xff)) {
322+
windowCatchUpPos = position;
323+
}
324+
}
325+
308326
// window activation check (SameBoy model): the comparison wraps in 8 bits, so
309327
// WX values 0..6 can trigger during the discard phase; WX=0 has its own window,
310328
// and the CGB also accepts WX=166. The window line counter increments at the
@@ -323,7 +341,28 @@ public boolean tick() {
323341
} else {
324342
activate = false;
325343
}
326-
if (activate && windowPendingTicks == 0) {
344+
if (!gbc && windowCatchUpPos >= 0 && windowPendingTicks == 0
345+
&& !windowActivatedThisLine
346+
&& position - windowCatchUpPos <= DESYNC_MAX_GAP) {
347+
// DMG LCD-PPU horizontal desync: the WX==position+7 match dot passed while
348+
// the window was disabled (a mid-line LCDC.5-off pulse); the window catches
349+
// up on re-enable, rendered as if it had triggered at the match dot. This is
350+
// SameBoy's WX==position+6 + lcd_x-- generalised to our pulse/position phase
351+
// (mealybug m3_lcdc_win_en_change_multiple_wx). DESYNC_MAX_GAP encodes the
352+
// phase between our LCDC-pulse commit and the pixel position axis.
353+
window = true;
354+
windowBeingFetched = true;
355+
windowLineCounter++;
356+
for (int p = windowCatchUpPos; p < position; p++) {
357+
fifo.rewindOnePixel();
358+
}
359+
position = windowCatchUpPos;
360+
windowCatchUpPos = -1;
361+
fifo.clear();
362+
fetcher.startWindow();
363+
fetcher.advance(position, true, windowLineCounter, false);
364+
windowActivatedThisLine = true;
365+
} else if (activate && windowPendingTicks == 0) {
327366
windowPendingTicks = 1;
328367
windowPendingWx = wx;
329368
windowPendingPos = position;
@@ -384,6 +423,11 @@ public boolean tick() {
384423
}
385424

386425

426+
// gap (in dots) between our LCDC-pulse re-enable commit and the pixel position axis:
427+
// a window whose WX match was masked by a pulse catches up on re-enable only if the
428+
// match is within this many dots of the re-enable position (mealybug wewx)
429+
private static final int DESYNC_MAX_GAP = 3;
430+
387431
private void objTick() {
388432
if (objStep <= 1) {
389433
fetcher.advance(position, window, windowLineCounter, true);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public class MealybugRomTest {
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),
45-
Map.entry("m3_lcdc_win_en_change_multiple_wx.gb", 521),
45+
Map.entry("m3_lcdc_win_en_change_multiple_wx.gb", 83),
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", 0),

0 commit comments

Comments
 (0)