From bb9bb9a2de6bb28fe2e0be8bded5615da0dd1705 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Mon, 28 Sep 2026 19:26:51 +0200 Subject: [PATCH] nes: fixed for emuworker and new debug adapter --- extension/test/emuworker.test.ts | 29 +++++++++++++++ jsnes | 2 +- src/platform/nes.ts | 61 ++++++++++++++++++++++---------- test/cli/testcli.js | 7 ++++ test/cli/testdeterminism.js | 3 ++ 5 files changed, 82 insertions(+), 20 deletions(-) diff --git a/extension/test/emuworker.test.ts b/extension/test/emuworker.test.ts index 4880e4ae..4edffea4 100644 --- a/extension/test/emuworker.test.ts +++ b/extension/test/emuworker.test.ts @@ -233,6 +233,35 @@ describe('extension emuworker', function () { assert.equal(statuses[statuses.length - 1].state, 'paused'); }); + it('steps through C source on the NES, which has no Machine of its own', async function () { + // jsnes stops between instructions through the trap its frame() takes + var dir = path.join(ROOT, 'presets/nes'); + var mainText = fs.readFileSync(path.join(dir, 'hello.c'), 'utf-8'); + var read = async (rel: string) => { + var p = path.join(dir, rel); + return fs.existsSync(p) ? new Uint8Array(fs.readFileSync(p)) : null; + }; + var nes = await new Builder(ROOT).build({ + platform: 'nes', mainPath: 'hello.c', mainText, + files: new ProjectFileProvider(read, ROOT, 'nes'), + }); + assert.deepEqual(nes.diagnostics, []); + await rpc.call('start', 'nes', nes.output, nes.files, { paused: true }); + await debug('setBuild', { listings: nes.listings, symbols: nes.symbolmap, mainPath: 'hello.c', paths: nes.paths }); + var [bp] = await debug('setBreakpoints', [{ id: 1, type: 'source', file: 'hello.c', line: 19, enabled: true }]); + assert.ok(bp.verified, bp.message); + await debug('continue'); + assert.equal((await nextStop()).reason, 'breakpoint'); + assert.equal((await debug('location')).source.line, 19); + await debug('step', 'over', 'line'); + assert.equal((await nextStop()).reason, 'step'); + assert.equal((await debug('location')).source.line, 20); + await debug('stepBack', 'line'); + assert.equal((await nextStop()).reason, 'step'); + assert.equal((await debug('location')).source.line, 19); + await debug('setBreakpoints', []); + }); + it('drives the debug adapter through the worker, as the extension does', async function () { var backend = new WorkerDebugBackend((method, ...args) => rpc.call('debug', method, ...args), async () => { await rpc.call('start', 'mw8080bw', built.output, built.files, { paused: true }); diff --git a/jsnes b/jsnes index a6a026b6..dd2fa697 160000 --- a/jsnes +++ b/jsnes @@ -1 +1 @@ -Subproject commit a6a026b698f9a68372cc94b46182e3abedc26672 +Subproject commit dd2fa697a05368782d23add9375c8478e7eb38c6 diff --git a/src/platform/nes.ts b/src/platform/nes.ts index 8ba83911..f44e2443 100644 --- a/src/platform/nes.ts +++ b/src/platform/nes.ts @@ -6,7 +6,7 @@ import { hex, byteArrayToString, replaceAll } from "../common/util"; import { CodeAnalyzer_nes } from "../common/analysis"; import { SampleAudio } from "../common/audio"; import { ProbeRecorder } from "../common/probe"; -import { NullProbe, Probeable, ProbeAll } from "../common/devices"; +import { NullProbe, Probeable, ProbeAll, TrapCondition } from "../common/devices"; import jsnes = require('../../jsnes'); // Lazy mousetrap require: mousetrap references `document` at module load time, @@ -77,11 +77,36 @@ const JSNES_KEYCODE_MAP = makeKeycodeMap([ ]); /** Replace every typed array in `o` with a copy, breaking aliasing. */ -function copyTypedArrays(o: any) { - if (!o) return; - for (var k in o) { - if (ArrayBuffer.isView(o[k])) o[k] = (o[k] as any).slice(0); +/** + * The JSNES core seen as a Machine, so the headless debugger (EmuCore, + * MachineCore) can stop between instructions and step. JSNES runs whole + * instructions, so every stop is stable. + */ +class JSNESMachine { + cpuCyclesPerLine = 114; // width of the probe scope + cpu = { + getPC: () => this.p.getPC(), + getSP: () => this.p.getSP(), + isStable: () => true, + }; + + constructor(readonly p: JSNESPlatform) { } + + advanceFrame(trap: TrapCondition): number { + var steps = 0; + // frame() asks the trap before each instruction, so count those + this.p.nes.frame(() => { + if (trap && trap()) return true; + steps++; + return false; + }); + return steps; } + saveState() { return this.p.saveState(); } + loadState(s) { this.p.loadState(s); } + read(a: number) { return this.p.readAddress(a); } + readConst(a: number) { return this.p.readAddress(a); } + connectProbe(probe: ProbeAll) { this.p.connectProbe(probe); } } class JSNESPlatform extends Base6502Platform implements Platform, Probeable { @@ -98,7 +123,7 @@ class JSNESPlatform extends Base6502Platform implements Platform, Probeable { ntlastbuf: Uint32Array; showDebugView = false; - machine = { cpuCyclesPerLine: 114 }; // TODO: hack for width of probe scope + machine = new JSNESMachine(this); constructor(mainElement) { super(); @@ -318,6 +343,11 @@ class JSNESPlatform extends Base6502Platform implements Platform, Probeable { return this.nes.ppu.curX; } + // the debugger asks for these before every instruction; skip toJSON() + getPC() { return (this.nes.cpu.REG_PC + this.debugPCDelta) & 0xffff; } + getSP() { return this.nes.cpu.REG_SP & 0xff; } + isStable() { return true; } + getCPUState() { var c = this.nes.cpu.toJSON(); this.copy6502REGvars(c); @@ -335,13 +365,8 @@ class JSNESPlatform extends Base6502Platform implements Platform, Probeable { } s.c = s.cpu; this.copy6502REGvars(s.c); - // jsnes's utils.toJSON() stores *references* to the live typed arrays - // (jsnes/src/utils.js), so a save state keeps changing as the emulator - // runs on. Copy every one of them, or a checkpoint is worthless the moment - // it is taken -- restoring it would be a no-op self-copy. - copyTypedArrays(s.cpu); - copyTypedArrays(s.ppu); - copyTypedArrays(s.mmap); + // our jsnes fork's toJSON() copies every array, so the state doesn't + // change as the emulator runs on s.b = s.cpu.mem; s.ctrl = this.saveControlsState(); return s; @@ -353,10 +378,7 @@ class JSNESPlatform extends Base6502Platform implements Platform, Probeable { //this.nes.cpu.fromJSON(state.cpu); //this.nes.mmap.fromJSON(state.mmap); //this.nes.ppu.fromJSON(state.ppu); - // fromJSON() copies typed arrays in with .set(), so nothing here aliases - // the state -- but the plain `mem` reference does, and the debugger reads - // through it - this.nes.cpu.mem = state.cpu.mem.slice(0); + // fromJSON() copies arrays in, so nothing here aliases the state this.loadControlsState(state.ctrl); //$.extend(this.nes, state); this.installIntercepts(); @@ -368,8 +390,9 @@ class JSNESPlatform extends Base6502Platform implements Platform, Probeable { }; } loadControlsState(state) { - this.nes.controllers[1].state = state.c1; - this.nes.controllers[2].state = state.c2; + // copy, or later button presses would change the saved state + this.nes.controllers[1].state = state.c1.slice(0); + this.nes.controllers[2].state = state.c2.slice(0); } readAddress(addr) { return this.nes.cpu.mem[addr]; diff --git a/test/cli/testcli.js b/test/cli/testcli.js index 28bfee5f..7f520861 100644 --- a/test/cli/testcli.js +++ b/test/cli/testcli.js @@ -244,6 +244,13 @@ describe('8bws CLI', function () { // 6502 registers and flags should be shown while stepping assert.ok(/A=\$[0-9A-F]{2} X=\$[0-9A-F]{2} Y=\$[0-9A-F]{2} SP=\$[0-9A-F]{2} [NnVvDdIiZzCc]{6}/.test(out), out); }); + it('should step and step back on nes (jsnes)', function () { + var out = cli('run', '--platform', 'nes', '-e', 'run 5; step 3; back 2; pc 1', 'test/roms/nes/shoot2.c.rom'); + assert.ok(!/does not support|only checks the PC at frame boundaries/.test(out), out); + // stopped 3 instructions into frame 5, then back to the 1st + assert.ok(/^\[5:3\] PC=/m.test(out), out); + assert.ok(/^\[5:1\] back 2: PC=\$[0-9A-F]{4}/m.test(out), out); + }); it('should record an instruction history', function () { var out = cli('run', '--platform', 'apple2', '-e', 'run 20; hist 5', 'test/roms/apple2/cosmic.c.rom'); assert.ok(/instructions shown/.test(out), out); diff --git a/test/cli/testdeterminism.js b/test/cli/testdeterminism.js index df3d3844..b368942a 100644 --- a/test/cli/testdeterminism.js +++ b/test/cli/testdeterminism.js @@ -56,4 +56,7 @@ describe('Deterministic replay', function () { it('should replay zx exactly (joymask must round-trip)', async function () { await checkDeterministic('zx', 20); }); + it('should replay nes exactly (jsnes save states must not alias live arrays)', async function () { + assert.strictEqual(await checkDeterministic('nes', 20), 'insn'); + }); });