mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
runscript: back, rewind, seek, rbreak, now; label output [frame:step]
This commit is contained in:
1 parent
e5067d48fe
commit
9cfb4865ce
2 files changed
+163
-1
No files matched your search
+74
-1
@@ -11,6 +11,7 @@ import { ProbeFlags, ProbeRecorder } from '../common/probe';
|
||||
import { hex } from '../common/util';
|
||||
import type { SymbolMap } from '../common/baseplatform';
|
||||
import { lookupSymbol } from '../common/symbols/symbolfile';
|
||||
import { formatTimestamp, timestamp, Timestamp } from '../common/timeline';
|
||||
import { hexdump, write } from './cliformat';
|
||||
import { DEFAULT_MAX_FRAMES, EmuTarget } from './emutarget';
|
||||
|
||||
@@ -22,6 +23,13 @@ export const RUN_SCRIPT_HELP = [
|
||||
' trace [MAXLINES] ADDR - run until PC==ADDR, then log every',
|
||||
' instruction until the routine returns',
|
||||
' hist [MAXLINES] - last N instructions from the trace buffer',
|
||||
'Time travel (replays the recording; output is labeled [frame:step]):',
|
||||
' back [N] - step back N instructions (default 1)',
|
||||
' rewind [N] - back to the start of the Nth frame before (default 1)',
|
||||
' seek FRAME[:STEP] - go to a recorded moment',
|
||||
' rbreak ADDR - run back to the last time PC==ADDR',
|
||||
' now - print the current moment and the recorded range',
|
||||
' (run and step in the past replay the recording; a key starts a new future)',
|
||||
'Inspection & input:',
|
||||
' key KEY - press key (down, 3 frames, up)',
|
||||
' keydown KEY / keyup KEY - raw key down/up events',
|
||||
@@ -148,7 +156,7 @@ export class RunScript {
|
||||
|
||||
//// helpers used by the commands
|
||||
|
||||
private log(msg: string) { this.out(`[frame ${this.target.frameCount}] ${msg}\n`); }
|
||||
private log(msg: string) { this.out(`[${formatTimestamp(this.target.now())}] ${msg}\n`); }
|
||||
|
||||
private addr(tok: string): number {
|
||||
try { return parseNum(tok); }
|
||||
@@ -192,6 +200,17 @@ export class RunScript {
|
||||
}
|
||||
}
|
||||
|
||||
private requireRewind() {
|
||||
if (!this.target.supportsRewind) {
|
||||
throw new Error(`'${this.target.id}' cannot rewind (it can't save its state)`);
|
||||
}
|
||||
}
|
||||
|
||||
private parseTimestamp(tok: string): Timestamp {
|
||||
const [f, st] = tok.split(':');
|
||||
return timestamp(parseNum(f), st ? parseNum(st) : 0);
|
||||
}
|
||||
|
||||
private requireStep() {
|
||||
if (!this.target.supportsStep) {
|
||||
throw new Error(`'${this.target.id}' does not support instruction stepping`);
|
||||
@@ -279,6 +298,55 @@ export class RunScript {
|
||||
this.out(`(${shown} instructions shown, ${p.idx} events recorded)\n`);
|
||||
}
|
||||
|
||||
cmdBack(tokens: string[]) {
|
||||
this.requireRewind();
|
||||
this.requireStep();
|
||||
const n = tokens[1] ? parseNum(tokens[1]) : 1;
|
||||
if (!this.target.stepBack(n)) throw new Error(`the recording doesn't reach back ${n} instruction${n == 1 ? '' : 's'}`);
|
||||
this.log(`back ${n}: PC=$${hex(this.target.getPC(), 4)}`);
|
||||
this.out(this.disasmLine(this.target.getPC(), true) + '\n');
|
||||
}
|
||||
|
||||
cmdSeek(tokens: string[]) {
|
||||
this.requireRewind();
|
||||
if (!tokens[1]) throw new Error('seek requires FRAME[:STEP]');
|
||||
this.target.seek(this.parseTimestamp(tokens[1]));
|
||||
this.log(this.where());
|
||||
}
|
||||
|
||||
cmdRewind(tokens: string[]) {
|
||||
this.requireRewind();
|
||||
const n = tokens[1] ? parseNum(tokens[1]) : 1;
|
||||
const t = this.target.now();
|
||||
// from partway into a frame, its own start counts as the first
|
||||
const frame = t.step > 0 ? t.frame - n + 1 : t.frame - n;
|
||||
const first = this.target.history.first();
|
||||
this.target.seek(timestamp(Math.max(frame, first.frame), 0));
|
||||
this.log(this.where());
|
||||
}
|
||||
|
||||
cmdReverseBreak(tokens: string[]) {
|
||||
this.requireRewind();
|
||||
if (!tokens[1]) throw new Error('rbreak requires an address');
|
||||
const addr = this.addr(tokens[1]);
|
||||
const hit = this.target.reverseRunUntil(() => this.target.getPC() === addr);
|
||||
const pc = this.target.getPC();
|
||||
this.log(`rbreak $${hex(addr, 4)}: ${hit ? 'HIT' : 'MISSED'} (pc=${pc != null ? '$' + hex(pc, 4) : '?'})`);
|
||||
}
|
||||
|
||||
cmdNow() {
|
||||
this.log(this.where());
|
||||
}
|
||||
|
||||
/** "at F:S (past; recorded A to B)" */
|
||||
private where(): string {
|
||||
const t = this.target;
|
||||
const h = t.history;
|
||||
if (!h) return `at frame ${t.frameCount} (no recording)`;
|
||||
const range = `recorded ${formatTimestamp(h.first())} to ${formatTimestamp(h.last())}`;
|
||||
return `at ${formatTimestamp(t.now())} (${t.isInPast() ? 'past' : 'present'}; ${range})`;
|
||||
}
|
||||
|
||||
cmdKey(tokens: string[]) {
|
||||
if (!tokens[1]) throw new Error('key requires a key name');
|
||||
const { key, flags } = parseKeyValue(tokens[1]);
|
||||
@@ -365,5 +433,10 @@ const COMMANDS: { [name: string]: Command } = {
|
||||
'pc': RunScript.prototype.cmdPC,
|
||||
'info': RunScript.prototype.cmdInfo,
|
||||
'reset': RunScript.prototype.cmdReset,
|
||||
'back': RunScript.prototype.cmdBack,
|
||||
'seek': RunScript.prototype.cmdSeek,
|
||||
'rewind': RunScript.prototype.cmdRewind,
|
||||
'rbreak': RunScript.prototype.cmdReverseBreak,
|
||||
'now': RunScript.prototype.cmdNow,
|
||||
'echo': RunScript.prototype.cmdEcho,
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import assert from "assert";
|
||||
import * as fs from "fs";
|
||||
import { describe, it } from "mocha";
|
||||
import { hashState } from "../../src/common/statehash";
|
||||
import { formatTimestamp } from "../../src/common/timeline";
|
||||
import { hex } from "../../src/common/util";
|
||||
import { EmuTarget, loadPlatform } from "../../src/tools/emutarget";
|
||||
import { RunScript } from "../../src/tools/runscript";
|
||||
|
||||
// The run-script commands that move backwards in time: back, seek, rewind,
|
||||
// rbreak, now. They drive EmuCore's timeline, so going back and forward again
|
||||
// has to land on the same state.
|
||||
|
||||
async function open() {
|
||||
const t = await loadPlatform('mw8080bw');
|
||||
await t.start();
|
||||
await t.loadROM(new Uint8Array(fs.readFileSync('test/roms/mw8080bw/game2.c.rom')));
|
||||
let out = '';
|
||||
const script = new RunScript(t, (s: string) => { out += s; });
|
||||
return { t, script, output: () => { const s = out; out = ''; return s; } };
|
||||
}
|
||||
|
||||
function stateOf(t: EmuTarget) { return hashState(t.machine.saveState()); }
|
||||
|
||||
describe('RunScript time travel', function () {
|
||||
this.timeout(60000);
|
||||
|
||||
it('labels output with the frame and step', async function () {
|
||||
const { script, output } = await open();
|
||||
script.run('run 3; step 2');
|
||||
const out = output();
|
||||
assert.match(out, /^\[3:0\] ran 3 frames$/m);
|
||||
assert.match(out, /^\[3:2\] PC=\$[0-9A-F]{4}$/m);
|
||||
});
|
||||
|
||||
it('steps back to where it stepped from', async function () {
|
||||
const { t, script, output } = await open();
|
||||
script.run('run 5; step 1');
|
||||
const pc = t.getPC();
|
||||
const state = stateOf(t);
|
||||
script.run('step 7; back 7');
|
||||
assert.strictEqual(t.getPC(), pc);
|
||||
assert.strictEqual(stateOf(t), state);
|
||||
assert.match(output(), new RegExp(`PC=\\$${hex(pc, 4)}`));
|
||||
});
|
||||
|
||||
it('seeks to a frame and step, and back to the present', async function () {
|
||||
const { t, script, output } = await open();
|
||||
script.run('run 12');
|
||||
const present = stateOf(t);
|
||||
script.run('seek 4:10; now');
|
||||
assert.strictEqual(formatTimestamp(t.now()), '4:10');
|
||||
assert.match(output(), /at 4:10 \(past; recorded 0:0 to 12:0\)/);
|
||||
script.run('seek 12');
|
||||
assert.strictEqual(stateOf(t), present);
|
||||
assert.ok(!t.isInPast());
|
||||
});
|
||||
|
||||
it('rewinds whole frames, to a frame start', async function () {
|
||||
const { t, script } = await open();
|
||||
script.run('run 10; rewind 3');
|
||||
assert.strictEqual(formatTimestamp(t.now()), '7:0');
|
||||
script.run('step 5; rewind');
|
||||
assert.strictEqual(formatTimestamp(t.now()), '7:0');
|
||||
});
|
||||
|
||||
it('runs back to the last time the PC was somewhere', async function () {
|
||||
const { t, script, output } = await open();
|
||||
script.run('run 10; step 1');
|
||||
const pc = t.getPC();
|
||||
const when = t.now();
|
||||
script.run('run 5');
|
||||
output();
|
||||
script.run(`rbreak $${hex(pc, 4)}`);
|
||||
assert.match(output(), /HIT/);
|
||||
assert.strictEqual(t.getPC(), pc);
|
||||
assert.ok(t.isInPast());
|
||||
// the last visit, which is no earlier than the one we saw
|
||||
assert.ok(t.now().frame >= when.frame);
|
||||
});
|
||||
|
||||
it('reports a miss and stays put', async function () {
|
||||
const { t, script, output } = await open();
|
||||
script.run('run 5');
|
||||
script.run('rbreak $FFFF');
|
||||
assert.match(output(), /MISSED/);
|
||||
assert.strictEqual(formatTimestamp(t.now()), '5:0');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user