Files

295 lines
12 KiB
TypeScript

import assert from "assert";
import fs from "fs";
import { describe, it } from "mocha";
import { mockAudio, mockGlobals } from "../../src/tools/nodemock";
mockGlobals();
mockAudio();
// imported after the globals are mocked -- the machine pulls in the Javatari
// core, which reaches for window/navigator/document at run time
import { JavatariMachine } from "../../src/machine/vcs";
import { EmuHalt, KeyFlags, Keys } from "../../src/common/emu";
import { hashState } from "../../src/common/statehash";
import { NullProbe } from "../../src/common/devices";
import { History } from "../../src/common/history";
import {
formatTimestamp,
MachineCore,
timestamp,
timestampsEqual,
} from "../../src/common/timeline";
// The VCS machine runs the Javatari TIA frame loop with a trap before every
// CPU clock, so a step is one clock. This pins the same guarantees
// testtimelinemachine.ts pins for the BasicScanline machines: exact replay
// from a snapshot, exact sub-frame seeking, and a KIL that parks where it
// happened.
const ROM = './test/roms/vcs/brickgame.rom';
/** Counts what the machine hands the probe, so a run can be checked for shape. */
class CountingProbe extends NullProbe {
frames = 0;
scanlines = 0;
clocks = 0;
executes = 0;
waits = 0;
ioWrites = 0;
pcs = new Set<number>();
logNewFrame() { this.frames++; }
logNewScanline() { this.scanlines++; }
logClocks(n?: number) { this.clocks += n || 0; }
logExecute(pc?: number) { this.executes++; this.pcs.add(pc); }
logWait() { this.waits++; }
logIOWrite() { this.ioWrites++; }
}
function newCore() {
const m = new JavatariMachine();
m.connectVideo(new Uint32Array(m.getVideoParams().width * m.getVideoParams().height));
m.loadROM(new Uint8Array(fs.readFileSync(ROM)));
m.reset();
return { m, core: new MachineCore(m, 'clock') };
}
describe('JavatariMachine (VCS)', function () {
this.timeout(60000);
it('detects the cartridge format and runs a frame', function () {
const { m, core } = newCore();
assert.strictEqual(m.getCartridgeFormat(), '4K');
const r = core.runUntil(timestamp(1, 0));
assert.ok(timestampsEqual(r.at, timestamp(1, 0)), formatTimestamp(r.at));
assert.ok(timestampsEqual(core.now(), timestamp(1, 0)));
});
it('replays to the identical state from a snapshot 20 frames back', function () {
const { m, core } = newCore();
core.runUntil(timestamp(20, 0), () => false);
const snap = core.snapshot();
core.runUntil(timestamp(40, 0), () => false);
const expected = hashState(m.saveState());
const pc = m.cpu.getPC();
core.restore(snap, timestamp(20, 0));
const r = core.runUntil(timestamp(40, 0), () => false);
assert.ok(timestampsEqual(r.at, timestamp(40, 0)), formatTimestamp(r.at));
assert.strictEqual(m.cpu.getPC(), pc);
assert.strictEqual(hashState(m.saveState()), expected);
});
it('reaches a sub-frame position identically forwards and backwards', function () {
// where the machine should be at 12:250, reached only by running forward
const fwd = newCore();
fwd.core.runUntil(timestamp(12, 250), () => false);
const expected = hashState(fwd.m.saveState());
// now overshoot, rewind to the frame boundary, and come back
const { m, core } = newCore();
core.runUntil(timestamp(12, 0), () => false);
const snap = core.snapshot();
core.runUntil(timestamp(12, 900), () => false);
assert.notStrictEqual(hashState(m.saveState()), expected);
core.restore(snap, timestamp(12, 0));
core.runUntil(timestamp(12, 250), () => false);
assert.strictEqual(hashState(m.saveState()), expected);
});
it('traps on a condition several frames ahead and reports where', function () {
const { m, core } = newCore();
core.runUntil(timestamp(5, 0), () => false);
const snap5 = core.snapshot();
// find some PC the ROM reaches later, whatever it is
const seen = new Set<number>();
core.runUntil(timestamp(6, 0), () => { seen.add(m.cpu.getPC()); return false; });
core.restore(snap5, timestamp(5, 0));
const targetPC = [...seen][Math.floor(seen.size / 2)];
const r = core.runUntil(timestamp(30, 0), () => m.cpu.getPC() === targetPC);
assert.strictEqual(r.trapped, true);
assert.strictEqual(m.cpu.getPC(), targetPC);
// the same search from the same snapshot must land on the same instant
const hit = r.at;
const expected = hashState(m.saveState());
core.restore(snap5, timestamp(5, 0));
const r2 = core.runUntil(timestamp(30, 0), () => m.cpu.getPC() === targetPC);
assert.ok(timestampsEqual(r2.at, hit), `${formatTimestamp(r2.at)} != ${formatTimestamp(hit)}`);
assert.strictEqual(hashState(m.saveState()), expected);
});
it('runs a hundred frames in a sane amount of time', function () {
const { core } = newCore();
const t0 = Date.now();
core.runUntil(timestamp(100, 0), () => false);
const ms = Date.now() - t0;
// the smoke test measured about 1ms per frame; this is a wide bound so a
// slow CI machine passes but a pathological slowdown fails
assert.ok(ms < 5000, `100 frames took ${ms}ms`);
});
it('leaves the machine alone when asked what memory holds', function () {
// read() drives the real bus, so it latches the open-bus register the way
// the hardware does; readConst() is what the disassembler and the debugger
// use, and it must not move the machine or a replay stops matching
const { m, core } = newCore();
core.runUntil(timestamp(5, 0), () => false);
const before = hashState(m.saveState());
m.readConst(0x80);
m.readConst(0xfffc);
m.readConst(0x0280);
m.readConst(0x00);
assert.strictEqual(hashState(m.saveState()), before);
m.read(0x80);
assert.notStrictEqual(hashState(m.saveState()), before, 'read() should latch the bus');
});
it('logs through the probe without perturbing the run', function () {
const plain = newCore();
plain.core.runUntil(timestamp(3, 0), () => false);
const expected = hashState(plain.m.saveState());
const { m, core } = newCore();
const probe = new CountingProbe();
m.connectProbe(probe);
assert.strictEqual(m.probing, true);
core.runUntil(timestamp(3, 0), () => false);
// probing must not change what the machine does, or a recorded run and the
// replay of it would not agree
assert.strictEqual(hashState(m.saveState()), expected);
assert.strictEqual(probe.frames, 3);
assert.ok(probe.scanlines > 700, `only ${probe.scanlines} scanlines`);
assert.ok(probe.executes > 1000, `only ${probe.executes} instructions`);
// 76 CPU clocks a line, a little over 260 lines a frame
assert.ok(probe.clocks > 50000, `only ${probe.clocks} clocks`);
assert.ok(probe.waits > 0, 'no WSYNC wait was logged');
assert.ok(probe.ioWrites > 0, 'no TIA writes were logged');
// the PCs a probe sees are the ones the CPU is really at
assert.ok([...probe.pcs].every(pc => pc >= 0 && pc < 0x10000));
// and the bus is un-wrapped again once the probe is dropped
m.connectProbe(null);
assert.strictEqual(m.probing, false);
m.connectProbe(probe);
assert.strictEqual(m.probing, true);
});
it('delivers keys to the joysticks and the console switches', function () {
const { m, core } = newCore();
const idle = hashState(m.saveState());
// joystick 1 is the arrows
m.setKeyInput(Keys.UP.c, 0, KeyFlags.KeyDown);
assert.notStrictEqual(hashState(m.saveState()), idle, 'the stick did not move');
m.setKeyInput(Keys.UP.c, 0, KeyFlags.KeyUp);
assert.strictEqual(hashState(m.saveState()), idle, 'the stick did not come back');
// a key the machine has no use for must not touch it
m.setKeyInput(Keys.VK_F7.c, 0, KeyFlags.KeyDown);
assert.strictEqual(hashState(m.saveState()), idle);
// the difficulty switches live in the PIA port the program reads
m.setKeyInput(Keys.VK_F4.c, 0, KeyFlags.KeyDown);
assert.notStrictEqual(hashState(m.saveState()), idle);
core.runUntil(timestamp(1, 0), () => false);
});
});
describe('History on a VCS machine', function () {
this.timeout(60000);
it('replays a recorded run back to the same present', function () {
const { m, core } = newCore();
const hist = new History(core, { checkpointInterval: 5 });
for (var i = 0; i < 40; i++) hist.recordFrame();
const present = hashState(m.saveState());
hist.seek(timestamp(3, 200));
assert.notStrictEqual(hashState(m.saveState()), present);
hist.seek(hist.last());
assert.strictEqual(hashState(m.saveState()), present);
});
it('steps back to an instruction inside a frame', function () {
const { m, core } = newCore();
const hist = new History(core, { checkpointInterval: 4 });
for (var i = 0; i < 6; i++) hist.recordFrame();
const present = hist.last();
// the last stable instruction before the frame-4 boundary
const hit = hist.findLast(() => m.cpu.isStable(), hist.first(), timestamp(4, 0));
assert.ok(hit, 'no stable instruction found');
assert.ok(timestampsEqual(core.now(), hit), formatTimestamp(core.now()));
assert.ok(m.cpu.isStable());
const hitState = hashState(m.saveState());
const hitPC = m.cpu.getPC();
// seek away and back: the instruction must come back exactly
hist.seek(hist.first());
hist.seek(hit);
assert.strictEqual(hashState(m.saveState()), hitState);
assert.strictEqual(m.cpu.getPC(), hitPC);
// and carrying on to the present must match the original present
hist.seek(present);
assert.ok(timestampsEqual(core.now(), present), formatTimestamp(core.now()));
});
});
// A 6502 that counts $80 up to 5 (a handful of frames at 1 MHz), then executes
// KIL, which the machine raises as EmuHalt from the onHalt hook.
describe('JavatariMachine on a 6502 that halts', function () {
this.timeout(60000);
const KIL_ADDR = 0xf008;
function newRom() {
const rom = new Uint8Array(0x1000);
rom.set([
0xe6, 0x80, // f000 loop: inc $80
0xa5, 0x80, // f002 lda $80
0xc9, 5, // f004 cmp #5
0xd0, 0xf8, // f006 bne loop
0x02, // f008 kil
]);
rom[0xffc] = 0x00; rom[0xffd] = 0xf0; // reset vector -> $f000
const m = new JavatariMachine();
m.connectVideo(new Uint32Array(m.getVideoParams().width * m.getVideoParams().height));
m.loadROM(rom);
m.reset();
return { m, core: new MachineCore(m, 'clock') };
}
it('parks on the KIL instruction, whether metered or not', function () {
const a = newRom();
const ra = a.core.runUntil(timestamp(100, 0));
assert.ok(ra.halt instanceof EmuHalt, 'expected a halt');
assert.strictEqual(a.m.cpu.getPC(), KIL_ADDR);
assert.strictEqual(a.m.readConst(0x80), 5);
const b = newRom();
const rb = b.core.runUntil(timestamp(100, 0), () => false);
assert.ok(rb.halt instanceof EmuHalt);
assert.ok(timestampsEqual(ra.at, rb.at), `${formatTimestamp(ra.at)} != ${formatTimestamp(rb.at)}`);
assert.strictEqual(hashState(a.m.saveState()), hashState(b.m.saveState()));
});
it('records to the halt and rewinds to the step before it', function () {
const { m, core } = newRom();
const hist = new History(core, { checkpointInterval: 4 });
var r;
for (var i = 0; i < 100 && !r?.halt; i++) r = hist.recordFrame();
assert.ok(r.halt instanceof EmuHalt, 'expected a halt');
const haltAt = hist.last();
assert.strictEqual(m.cpu.getPC(), KIL_ADDR);
// the 6502 steps by clocks, so the previous instruction is the last stable
// step before now: the bne that fell through to the KIL
const prev = hist.findLast(() => m.cpu.isStable(), hist.first(), hist.now());
assert.ok(prev, 'no previous instruction');
assert.strictEqual(m.cpu.getPC(), 0xf006);
// and forward to the present again, without halting
assert.strictEqual(hist.seek(haltAt).halt, undefined);
assert.strictEqual(m.cpu.getPC(), KIL_ADDR);
});
});