Skip to content

Commit f7192e3

Browse files
authored
Fix DMG VBlank STAT timing and require ROM report deltas (#13)
* docs: require ROM report before-after comparisons * ppu: fix dmg vblank stat timing * test: stabilize sameboy tester missing-image case
1 parent c06ad5f commit f7192e3

5 files changed

Lines changed: 101 additions & 7 deletions

File tree

AI/TESTING.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ This checklist should move only when one of the following becomes true:
9494
## External ROM harness policy
9595

9696
- Test ROM execution must be automatable; manual inspection of the LCD is an auxiliary debugging aid, not the primary acceptance path.
97+
- When working on already-known external ROM failures, timing regressions, or exploratory PPU/MMIO fixes, always preserve a baseline snapshot of `/.roms/test/test-report.md` before making the validation run, preserve the final report again after the run, and compare the two before deciding whether the iteration is worth keeping.
98+
- That baseline/final comparison is mandatory for go/no-go decisions on exploratory ROM-driven work. Save the raw markdown report first; also save a rendered image of the report when practical so visual status drift is easy to review later.
99+
- If the current tree is not already a clean baseline, compare against a clean reference such as `main` in a separate branch or worktree rather than hand-waving the previous state from memory.
100+
- Do not summarize a ROM-driven iteration as "no regressions" unless the before/after report comparison has been done explicitly and the changed rows have been named.
97101
- The harness should support at least framebuffer capture and serial / link-port capture when the ROM exposes machine-readable output there.
98102
- When an external suite prints self-validating text through a documented screen-console protocol, the harness should also support a typed text-extraction path for that protocol rather than falling back to a circular framebuffer fixture generated by this project.
99103
- Prefer serial / link-port capture for suites such as Blargg `cpu_instrs` when that path is available, because it avoids treating a scrolling framebuffer as the primary machine-readable result channel.

AI/hardware/PPU.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ For this project, the PPU should be modeled dot-by-dot, where `1 dot = 1 T-cycle
107107
- `stat_mode1_enable && mode == 1`
108108
- `stat_mode2_enable && mode == 2`
109109
- `stat_lyc_enable && ly == lyc`
110+
- In the current DMG-family baseline, the Mode `2` STAT enable should also request LCD STAT at the exact VBlank-entry transition on line `144`, but it should not be treated as a continuously active source for the rest of VBlank.
110111
- LCD STAT interrupt requests should be emitted only on a rising edge of that internal line, not merely because one contributing condition is true.
111112
- STAT blocking should be preserved: if one enabled source keeps the internal line high while another source becomes true, no new LCD STAT interrupt should be requested until the line first drops low and rises again.
112113
- Mode `3` must not be treated as a direct STAT interrupt source.

AI/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
Read the matching file directly; this index is a routing guide plus a summary of document authority boundaries, but detailed behavioral rules still live in the owning file.
44

5+
## Mandatory external-ROM regression workflow
6+
7+
When working on already-known external ROM failures or rerunning curated ROM suites to evaluate a timing-sensitive change, always capture a baseline copy of `/.roms/test/test-report.md` before the work, capture the final report again after the run, and compare the two before deciding whether the change is worth keeping. Treat that before/after report-delta check as mandatory, not optional; see `TESTING.md` for the authoritative workflow details.
8+
59
## Global docs
610

711
- `ARCHITECTURE.md`: project goals, crate layout, subsystem boundaries, and portability rules.

crates/gb-core/src/ppu.rs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1361,6 +1361,11 @@ impl Ppu {
13611361
!= 0
13621362
&& self.ly + 1 < VISIBLE_SCANLINES
13631363
&& self.line_dot + 4 >= DOTS_PER_SCANLINE;
1364+
let dmg_mode2_vblank_entry_source = self.console_model.is_dmg_family()
1365+
&& self.stat_interrupt_enable & STAT_MODE2_INTERRUPT_ENABLE_BIT != 0
1366+
&& self.current_access_mode() == PpuAccessMode::VBlank
1367+
&& self.ly == VISIBLE_SCANLINES
1368+
&& self.line_dot == 0;
13641369
let mode_source = match self.current_access_mode() {
13651370
PpuAccessMode::HBlank => {
13661371
self.stat_interrupt_enable & STAT_MODE0_INTERRUPT_ENABLE_BIT != 0
@@ -1374,7 +1379,10 @@ impl Ppu {
13741379
PpuAccessMode::Drawing => false,
13751380
};
13761381

1377-
coincidence_source || mode_source || mode2_pretrigger_source
1382+
coincidence_source
1383+
|| mode_source
1384+
|| mode2_pretrigger_source
1385+
|| dmg_mode2_vblank_entry_source
13781386
}
13791387

13801388
fn compute_stat_irq_line(&self, quirk_active: bool) -> bool {
@@ -2367,6 +2375,68 @@ mod tests {
23672375
assert!(drain_ppu_interrupts(&mut ppu).is_empty());
23682376
}
23692377

2378+
#[test]
2379+
fn dmg_mode2_enable_requests_lcd_stat_at_vblank_entry_only() {
2380+
let mut ppu = Ppu::new(ConsoleModel::Dmg);
2381+
let oam_bytes = [0; 160];
2382+
2383+
ppu.apply_startup_state(PpuStartupState {
2384+
lcdc: 0x80,
2385+
stat: STAT_MODE2_INTERRUPT_ENABLE_BIT,
2386+
scy: 0x00,
2387+
scx: 0x00,
2388+
ly: 143,
2389+
lyc: 0x00,
2390+
bgp: 0x00,
2391+
wy: 0x00,
2392+
wx: 0x00,
2393+
obj_palette_read_policy: DmgObjPaletteReadPolicy::ReadAsFfUntilWritten,
2394+
});
2395+
ppu.line_dot = DOTS_PER_SCANLINE - 1;
2396+
ppu.refresh_stat_irq_line(false);
2397+
assert!(!ppu.snapshot().stat_irq_line);
2398+
assert!(drain_ppu_interrupts(&mut ppu).is_empty());
2399+
2400+
tick_ppu(&mut ppu, 0, &oam_bytes);
2401+
2402+
assert_eq!(ppu.snapshot().ly, 144);
2403+
assert_eq!(ppu.snapshot().mode, PpuAccessMode::VBlank);
2404+
assert!(ppu.snapshot().stat_irq_line);
2405+
assert_eq!(
2406+
drain_ppu_interrupts(&mut ppu),
2407+
vec![InterruptSource::VBlank, InterruptSource::LcdStat]
2408+
);
2409+
2410+
tick_ppu(&mut ppu, 1, &oam_bytes);
2411+
2412+
assert_eq!(ppu.snapshot().ly, 144);
2413+
assert_eq!(ppu.snapshot().line_dot, 1);
2414+
assert!(!ppu.snapshot().stat_irq_line);
2415+
assert!(drain_ppu_interrupts(&mut ppu).is_empty());
2416+
}
2417+
2418+
#[test]
2419+
fn mode2_enable_alone_does_not_hold_stat_high_past_vblank_entry() {
2420+
let mut ppu = Ppu::new(ConsoleModel::Dmg);
2421+
2422+
ppu.apply_startup_state(PpuStartupState {
2423+
lcdc: 0x80,
2424+
stat: STAT_MODE2_INTERRUPT_ENABLE_BIT,
2425+
scy: 0x00,
2426+
scx: 0x00,
2427+
ly: 144,
2428+
lyc: 0x00,
2429+
bgp: 0x00,
2430+
wy: 0x00,
2431+
wx: 0x00,
2432+
obj_palette_read_policy: DmgObjPaletteReadPolicy::ReadAsFfUntilWritten,
2433+
});
2434+
ppu.line_dot = 8;
2435+
ppu.refresh_stat_irq_line(false);
2436+
assert!(!ppu.snapshot().stat_irq_line);
2437+
assert!(drain_ppu_interrupts(&mut ppu).is_empty());
2438+
}
2439+
23702440
#[test]
23712441
fn stat_write_quirk_requests_in_mode2_and_coincidence_but_not_plain_mode3() {
23722442
let mut mode2 = Ppu::new(ConsoleModel::Dmg);

crates/gb-test-runner/src/sameboy_tester.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,19 @@ mod tests {
452452
fs::set_permissions(path, permissions).expect("script should be executable");
453453
}
454454

455+
fn success_binary_path(temp_dir: &Path) -> PathBuf {
456+
for candidate in ["/usr/bin/true", "/bin/true"] {
457+
let path = Path::new(candidate);
458+
if path.is_file() {
459+
return path.to_path_buf();
460+
}
461+
}
462+
463+
let tester_binary = temp_dir.join("fake-success");
464+
write_executable(&tester_binary, "#!/bin/sh\nexit 0\n");
465+
tester_binary
466+
}
467+
455468
fn sample_framebuffer_case() -> RomTestCase {
456469
RomTestCase::new(
457470
"acid2",
@@ -625,17 +638,19 @@ mod tests {
625638
b"rom"
626639
);
627640

628-
let tester_binary = temp_dir.join("fake-tester");
629-
write_executable(&tester_binary, "#!/bin/sh\nexit 0\n");
641+
let tester_binary = success_binary_path(&temp_dir);
630642
let error = runner
631643
.clone()
632644
.with_tester_binary(&tester_binary)
633645
.run_case(&case, &tester_binary)
634646
.expect_err("missing image output should fail");
635-
assert!(matches!(
636-
error,
637-
SameBoyTesterExecutionError::MissingImageArtifact { .. }
638-
));
647+
assert!(
648+
matches!(
649+
error,
650+
SameBoyTesterExecutionError::MissingImageArtifact { .. }
651+
),
652+
"unexpected run_case error: {error:?}"
653+
);
639654

640655
let non_strict = case.clone().with_execution_mode(ExecutionMode::Permissive);
641656
let error = runner

0 commit comments

Comments
 (0)