mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 00:02:16 +00:00
152 lines
5.7 KiB
TypeScript
152 lines
5.7 KiB
TypeScript
import assert from "assert";
|
|
import * as fs from "fs";
|
|
import { describe, it } from "mocha";
|
|
import { KeyFlags } from "../../src/common/emu";
|
|
import { ProbeFlags, ProbeRecorder } from "../../src/common/probe";
|
|
import { hashState } from "../../src/common/statehash";
|
|
import { formatTimestamp, timestamp, timestampsEqual } from "../../src/common/timeline";
|
|
import { EmuTarget, loadPlatform } from "../../src/tools/emutarget";
|
|
|
|
// EmuCore is the one driver behind the CLI's run scripts and the extension's
|
|
// emulator. It runs the machine through the timeline, so stepping, running and
|
|
// rewinding all agree on where the machine is.
|
|
|
|
async function open(platform: string, rom: string): Promise<EmuTarget> {
|
|
const t = await loadPlatform(platform);
|
|
await t.start();
|
|
await t.loadROM(new Uint8Array(fs.readFileSync(rom)));
|
|
return t;
|
|
}
|
|
|
|
function stateOf(t: EmuTarget): number {
|
|
return hashState(t.machine.saveState());
|
|
}
|
|
|
|
const TARGETS = [
|
|
{ platform: 'mw8080bw', rom: 'test/roms/mw8080bw/game2.c.rom' }, // Z80: steps are instructions
|
|
{ platform: 'apple2', rom: 'test/roms/apple2/cosmic.c.rom' }, // 6502: steps are clocks
|
|
];
|
|
|
|
for (const { platform, rom } of TARGETS) {
|
|
describe(`EmuCore on ${platform}`, function () {
|
|
this.timeout(60000);
|
|
|
|
it('finishes the frame it stepped into, the same as running straight through', async function () {
|
|
const straight = await open(platform, rom);
|
|
for (let i = 0; i < 20; i++) straight.advanceFrame();
|
|
|
|
const stepped = await open(platform, rom);
|
|
for (let i = 0; i < 10; i++) stepped.advanceFrame();
|
|
for (let i = 0; i < 100; i++) stepped.stepInsn();
|
|
for (let i = 0; i < 10; i++) stepped.advanceFrame();
|
|
|
|
assert.strictEqual(stepped.frameCount, 20);
|
|
assert.strictEqual(stateOf(stepped), stateOf(straight));
|
|
});
|
|
|
|
it('steps back to the instruction before', async function () {
|
|
const t = await open(platform, rom);
|
|
for (let i = 0; i < 5; i++) t.advanceFrame();
|
|
t.stepInsn();
|
|
const pcs: number[] = [];
|
|
const states: number[] = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
pcs.push(t.getPC());
|
|
states.push(stateOf(t));
|
|
t.stepInsn();
|
|
}
|
|
for (let i = 4; i >= 0; i--) {
|
|
assert.ok(t.stepBack(), `no step back at ${i}`);
|
|
assert.strictEqual(t.getPC(), pcs[i]);
|
|
assert.strictEqual(stateOf(t), states[i]);
|
|
}
|
|
assert.ok(t.isInPast());
|
|
});
|
|
});
|
|
}
|
|
|
|
describe('EmuCore with a probe', function () {
|
|
this.timeout(60000);
|
|
|
|
function executes(rec: ProbeRecorder): number {
|
|
let n = 0;
|
|
for (let i = 0; i < rec.idx; i++) if ((rec.buf[i] & 0xff000000) === ProbeFlags.EXECUTE) n++;
|
|
return n;
|
|
}
|
|
|
|
async function probed() {
|
|
const t = await open('mw8080bw', 'test/roms/mw8080bw/game2.c.rom');
|
|
for (let i = 0; i < 3; i++) t.advanceFrame();
|
|
t.stepInsn(50);
|
|
const rec = new ProbeRecorder(t.machine as any, 0x10000);
|
|
rec.singleFrame = false;
|
|
assert.ok(t.connectProbe(rec));
|
|
return { t, rec };
|
|
}
|
|
|
|
it('logs each stepped instruction once, though stepping replays the frame', async function () {
|
|
const { t, rec } = await probed();
|
|
for (let i = 0; i < 10; i++) t.stepInsn();
|
|
assert.strictEqual(executes(rec), 10);
|
|
});
|
|
|
|
it('keeps the probe connected after a search that uses its own', async function () {
|
|
const { t, rec } = await probed();
|
|
const search = new ProbeRecorder(t.machine as any, 0x10000);
|
|
t.history.findLast({ test: () => false, probe: search });
|
|
t.seek(t.history.last());
|
|
const before = executes(rec);
|
|
t.stepInsn(5);
|
|
assert.strictEqual(executes(rec), before + 5);
|
|
});
|
|
});
|
|
|
|
describe('EmuCore key input', function () {
|
|
this.timeout(60000);
|
|
|
|
it('replays keys, including one pressed while stopped mid-frame', async function () {
|
|
const t = await open('apple2', 'test/roms/apple2/cosmic.c.rom');
|
|
const keys: number[] = [];
|
|
const deliver = (t as any).deliverKey.bind(t);
|
|
(t as any).deliverKey = (key, code, flags) => { keys.push(flags); deliver(key, code, flags); };
|
|
for (let i = 0; i < 2; i++) t.advanceFrame();
|
|
t.setKeyInput(39, 39, KeyFlags.KeyDown); // frame 2
|
|
for (let i = 0; i < 3; i++) t.advanceFrame();
|
|
t.stepInsn();
|
|
t.setKeyInput(39, 39, KeyFlags.KeyUp); // stopped mid-frame: waits for frame 6
|
|
for (let i = 0; i < 4; i++) t.advanceFrame();
|
|
const present = stateOf(t);
|
|
assert.deepStrictEqual(keys, [KeyFlags.KeyDown, KeyFlags.KeyUp]);
|
|
// run forward through the past a frame at a time; no checkpoint since
|
|
// frame 0, so both keys are replayed
|
|
t.seek(timestamp(1, 0));
|
|
let replayed = 0;
|
|
while (t.isInPast()) {
|
|
t.advanceFrame();
|
|
replayed++;
|
|
}
|
|
assert.strictEqual(replayed, 8);
|
|
assert.deepStrictEqual(keys, [KeyFlags.KeyDown, KeyFlags.KeyUp, KeyFlags.KeyDown, KeyFlags.KeyUp]);
|
|
assert.strictEqual(stateOf(t), present);
|
|
});
|
|
|
|
it('diverges without the key, so the key test means something', async function () {
|
|
const withKey = await open('apple2', 'test/roms/apple2/cosmic.c.rom');
|
|
const without = await open('apple2', 'test/roms/apple2/cosmic.c.rom');
|
|
withKey.setKeyInput(39, 39, KeyFlags.KeyDown);
|
|
for (let i = 0; i < 5; i++) { withKey.advanceFrame(); without.advanceFrame(); }
|
|
assert.notStrictEqual(stateOf(withKey), stateOf(without));
|
|
});
|
|
|
|
it('drops the recorded future when a key is pressed in the past', async function () {
|
|
const t = await open('apple2', 'test/roms/apple2/cosmic.c.rom');
|
|
for (let i = 0; i < 10; i++) t.advanceFrame();
|
|
t.seek(timestamp(4, 0));
|
|
t.setKeyInput(39, 39, KeyFlags.KeyDown);
|
|
assert.ok(!t.isInPast());
|
|
assert.ok(timestampsEqual(t.history.last(), timestamp(4, 0)));
|
|
t.advanceFrame();
|
|
assert.strictEqual(t.frameCount, 5);
|
|
});
|
|
});
|