nes: fixed for emuworker and new debug adapter

This commit is contained in:
Steven Hugg committed 2026-09-28 19:28:38 +02:00
1 parent 8bdd47cf57
commit bb9bb9a2de
5 files changed
+82 -20

No files matched your search

+29
View File
@@ -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 });
+1 -1
Submodule jsnes updated: a6a026b698...dd2fa697a0.
+42 -19
View File
@@ -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];
+7
View File
@@ -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);
+3
View File
@@ -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');
});
});