From 7a32e689c3abb2e42c7dd7a411191a144ab60a77 Mon Sep 17 00:00:00 2001 From: kawa Date: Sat, 17 Oct 2026 10:45:34 +0200 Subject: [PATCH] Run the reference emulator silently when WebKit reports no audio sample rate After a page process has opened many AudioContexts, WebKit can hand out one whose sampleRate is 0. The adapter passed that rate to binjgb, whose emulator_run_until divides by the audio frequency on every call, so the first frame trapped with "Division by zero". This failed the Day-Care cartridge comparisons on WebKit after earlier tests had created hundreds of emulators, and could crash the reference player in Safari. A context without a usable rate is now closed and the emulator runs silently at the fallback rate, as when no AudioContext can be created. Co-Authored-By: Claude Opus 5.5 --- src/emulator/binjgb.ts | 6 ++++++ tests/browser.spec.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/emulator/binjgb.ts b/src/emulator/binjgb.ts index 5fbdd0e..c8a6d6b 100644 --- a/src/emulator/binjgb.ts +++ b/src/emulator/binjgb.ts @@ -59,11 +59,17 @@ export class BinjgbEmulator implements Emulator { // Audio is optional. Failure must not block a silent game. try { this.audio = new AudioContext(); + // WebKit can return a context whose sample rate is 0 (seen after a + // page process has opened many); binjgb divides by the rate. + if (!(this.audio.sampleRate > 0)) + throw new Error("No audio sample rate."); this.gain = this.audio.createGain(); this.gain.gain.value = this.muted ? 0 : 0.4; this.gain.connect(this.audio.destination); } catch { + void this.audio?.close().catch(() => {}); this.audio = undefined; + this.gain = undefined; } this.romPointer = this.module._malloc(rom.length); if (!this.romPointer) throw new Error("Could not allocate ROM memory."); diff --git a/tests/browser.spec.ts b/tests/browser.spec.ts index 36b69f3..72cf460 100644 --- a/tests/browser.spec.ts +++ b/tests/browser.spec.ts @@ -140,6 +140,35 @@ test("actual WASM executes an original cartridge, reads input and round-trips st expect(errors).toEqual([]); }); +test("the emulator runs silently when the browser reports no audio sample rate", async ({ + page, +}) => { + await page.goto("/"); + const result = await page.evaluate(async (bytes) => { + // WebKit hands out contexts with a sample rate of 0 once a page process + // has opened many; binjgb divides by the rate on every frame. + const closed: boolean[] = []; + window.AudioContext = class extends AudioContext { + get sampleRate() { + return 0; + } + close() { + closed.push(true); + return super.close(); + } + }; + const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts"); + const emulator = new BinjgbEmulator(); + await emulator.load(new Uint8Array(bytes), document.createElement("canvas")); + emulator.advanceFrames(12); + const battery = emulator.exportBattery()[0]; + emulator.dispose(); + return { battery, closed: closed.length }; + }, Array.from(syntheticRom())); + expect(result.battery & 1).toBe(1); + expect(result.closed).toBe(1); +}); + test("full player lifecycle with test-only validator replacement", async ({ page, context,