mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 00:02:16 +00:00
refactored some more stuff for headless
This commit is contained in:
1 parent
03d98e784c
commit
4ddbc7ccec
12 files changed
+611
-434
No files matched your search
@@ -0,0 +1,49 @@
|
||||
// Starts each platform in out/emuworker.js and reports whether frames arrive.
|
||||
// usage: node scripts/survey.js [platform...] (after npm run build)
|
||||
const { Worker } = require('worker_threads');
|
||||
const fs = require('fs'), path = require('path');
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const OUT = ROOT + '/extension/out';
|
||||
function rpcWorker(file) {
|
||||
const w = new Worker(path.join(OUT, file), { workerData: { rootDir: ROOT }, stdout: true, stderr: true });
|
||||
let id = 1; const pending = new Map(); const events = [];
|
||||
w.on('message', m => {
|
||||
if (m.t === 'result') { const p = pending.get(m.id); pending.delete(m.id); m.error ? p.rej(new Error(m.error.message)) : p.res(m.value); }
|
||||
else if (m.t === 'event') events.push(m);
|
||||
});
|
||||
w.on('error', e => { for (const p of pending.values()) p.rej(e); });
|
||||
return { w, events, call: (method, ...args) => new Promise((res, rej) => { const i = id++; pending.set(i, { res, rej }); w.postMessage({ t: 'call', id: i, method, args }); }) };
|
||||
}
|
||||
function romFor(id) {
|
||||
for (const d of [id, id.split('-')[0], id.split('.')[0]]) {
|
||||
const dir = path.join(ROOT, 'test/roms', d);
|
||||
if (fs.existsSync(dir)) { const f = fs.readdirSync(dir).filter(f => /\.rom$|\.bin$/.test(f))[0]; if (f) return { rom: new Uint8Array(fs.readFileSync(path.join(dir, f))), name: d + '/' + f }; }
|
||||
}
|
||||
return { rom: new Uint8Array(+(process.env.ROMSIZE || 0x8000)), name: 'zeros' };
|
||||
}
|
||||
const withTimeout = (p, ms) => Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms))]);
|
||||
(async () => {
|
||||
const b = rpcWorker('buildworker.js');
|
||||
const ids = process.argv[2] ? process.argv.slice(2) : await b.call('listPlatforms');
|
||||
await b.w.terminate();
|
||||
for (const id of ids) {
|
||||
const { rom, name } = romFor(id);
|
||||
const e = rpcWorker('emuworker.js');
|
||||
let result;
|
||||
try {
|
||||
await withTimeout(e.call('start', id, rom), 8000);
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
const frames = e.events.filter(x => x.name === 'frame');
|
||||
const halted = e.events.find(x => x.name === 'status' && x.data?.state === 'halted');
|
||||
if (halted) result = 'HALTED: ' + halted.data.message;
|
||||
else if (!frames.length) result = 'NO FRAMES';
|
||||
else {
|
||||
const px = new Uint32Array(frames[frames.length-1].data.pixels);
|
||||
result = `ok ${frames.length} frames ${frames[0].data.width}x${frames[0].data.height}` + (px.some(p => p !== px[0]) ? '' : ' (blank)');
|
||||
}
|
||||
} catch (err) { result = 'FAIL: ' + String(err.message).split('\n')[0].slice(0, 140); }
|
||||
console.log(`${id}\t${name}\t${result}`);
|
||||
await e.w.terminate().catch(() => {});
|
||||
}
|
||||
process.exit(0);
|
||||
})();
|
||||
File renamed without changes.
@@ -0,0 +1,97 @@
|
||||
// Breakpoints and how they resolve to an address, shared by every host (the
|
||||
// IDE, the CLI, the VS Code debug adapter). Two kinds:
|
||||
// - 'source' -- file + line, resolved to a PC via the build listing
|
||||
// - 'address' -- a target that's a symbol, $hex or decimal address
|
||||
// Both support an optional condition expression (see breakcond.ts).
|
||||
// The IDE's store (persistence, gutter markers) is in src/ide/breakpoints.ts.
|
||||
|
||||
import type { Platform, SymbolMap } from "./baseplatform";
|
||||
import type { CodeListing } from "./workertypes";
|
||||
import { CondFn, CondContext, compileCondition, parseTarget } from "./breakcond";
|
||||
import { lookupSymbol } from "./symbols/symbolfile";
|
||||
|
||||
export interface Breakpoint {
|
||||
id: number;
|
||||
type: 'source' | 'address';
|
||||
file?: string; // source: project path
|
||||
line?: number; // source: 1-based line number
|
||||
target?: string; // address: symbol, $hex or decimal
|
||||
enabled: boolean;
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
export interface ResolvedBreakpoint {
|
||||
bp: Breakpoint;
|
||||
pc?: number; // resolved address (undefined if not resolvable now)
|
||||
condFn?: CondFn;
|
||||
error?: string; // why the breakpoint can't resolve right now
|
||||
}
|
||||
|
||||
// What resolving a breakpoint needs from the host.
|
||||
export interface BreakpointContext {
|
||||
/** name -> address, from the build or a label file */
|
||||
symbols?: SymbolMap;
|
||||
/** the build's listing for a source file, for source breakpoints */
|
||||
getListingForFile?(path: string): CodeListing | undefined;
|
||||
/** CPU fields, memory and raster position, for conditions */
|
||||
platform?: Platform;
|
||||
}
|
||||
|
||||
function makeCondContext(ctx: BreakpointContext): CondContext {
|
||||
let platform = ctx.platform;
|
||||
let cpuFields = new Set<string>();
|
||||
try {
|
||||
let c = platform && platform.getCPUState && platform.getCPUState();
|
||||
if (c) cpuFields = new Set(Object.keys(c));
|
||||
} catch (e) {
|
||||
}
|
||||
// platform accessors reachable with the '#' sigil; the value is read live
|
||||
// at eval time so it reflects the current raster position, etc.
|
||||
let hw: { [name: string]: () => number | undefined } = null;
|
||||
if (platform) {
|
||||
if (platform.getRasterScanline) {
|
||||
hw = hw || {};
|
||||
hw['scanline'] = () => platform.getRasterScanline();
|
||||
}
|
||||
if (platform.getRasterLineClock) {
|
||||
hw = hw || {};
|
||||
hw['lineclock'] = () => platform.getRasterLineClock();
|
||||
}
|
||||
}
|
||||
return {
|
||||
cpuFields,
|
||||
symbol: (name) => lookupSymbol(ctx.symbols, name),
|
||||
readMem: platform && platform.readAddress ? (a) => platform.readAddress(a) : undefined,
|
||||
readVRAM: platform && platform.readVRAMAddress ? (a) => platform.readVRAMAddress(a) : undefined,
|
||||
hw,
|
||||
};
|
||||
}
|
||||
|
||||
// Can this platform actually stop at a breakpoint?
|
||||
export function canUseBreakpoints(platform: Platform): boolean {
|
||||
return !!(platform && (platform.runEval || platform.runToPC || platform.runEvalAtPC));
|
||||
}
|
||||
|
||||
export function resolveBreakpoint(bp: Breakpoint, ctx: BreakpointContext): ResolvedBreakpoint {
|
||||
try {
|
||||
let pc: number;
|
||||
if (bp.type == 'source') {
|
||||
let lst = ctx.getListingForFile && ctx.getListingForFile(bp.file);
|
||||
let sf = lst && (lst.sourcefile || lst.assemblyfile);
|
||||
if (!sf) return { bp, error: "no debug info (build first?)" };
|
||||
pc = sf.line2offset.get(bp.line);
|
||||
if (!(pc >= 0)) return { bp, error: "line has no code" };
|
||||
} else {
|
||||
let r = parseTarget(bp.target, (name) => lookupSymbol(ctx.symbols, name));
|
||||
if (r.error) return { bp, error: r.error };
|
||||
pc = r.pc;
|
||||
}
|
||||
let condFn: CondFn = undefined;
|
||||
if (bp.condition && bp.condition.trim()) {
|
||||
condFn = compileCondition(bp.condition, makeCondContext(ctx));
|
||||
}
|
||||
return { bp, pc, condFn };
|
||||
} catch (e) {
|
||||
return { bp, error: String(e.message || e) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// Unified headless driver for the emulators in this codebase.
|
||||
//
|
||||
// A *Platform* (src/platform/*) is the full object the IDE drives: it owns its
|
||||
// own video/audio/timer and implements the Platform interface in
|
||||
// common/baseplatform.ts. Most platforms are built on a *Machine* (src/machine/*),
|
||||
// the bare emulation core made of the interfaces in common/devices.ts -- Bus,
|
||||
// FrameBased, HasCPU, VideoSource and friends.
|
||||
//
|
||||
// EmuCore drives a Platform, reaching through to its Machine when there is
|
||||
// one for the things only the core can do (exact traps, single stepping, the
|
||||
// probe). The CLI and the run-script interpreter never have to ask which of
|
||||
// those a target supports; the capability sniffing lives here.
|
||||
//
|
||||
// No DOM and no Node: start() swaps in headless video for the platform, and a
|
||||
// host that already started the platform (the IDE) can wrap it without
|
||||
// calling start(). The Node side (mocks, loading platform modules) is in
|
||||
// src/tools/emutarget.ts.
|
||||
|
||||
import {
|
||||
CpuState, DisasmLine, EmuState, Machine, Platform, hasProbe, isDebuggable,
|
||||
} from "./baseplatform";
|
||||
import { ProbeAll, TrapCondition } from "./devices";
|
||||
import { disassemble6502 } from "./cpu/disasm6502";
|
||||
import { disassembleZ80 } from "./cpu/disasmz80";
|
||||
import { disassembleSM83 } from "./cpu/disasmSM83";
|
||||
import { disassembleF8 } from "./cpu/disasmF8";
|
||||
import { disassembleHuC6280 } from "./cpu/disasmHuC6280";
|
||||
import { CPU6809 } from "./cpu/6809";
|
||||
import * as emu from "./emu";
|
||||
|
||||
export interface VideoOutput {
|
||||
pixels: Uint32Array;
|
||||
width: number;
|
||||
height: number;
|
||||
/** display rotation in degrees (RasterVideo's `rotate` option) */
|
||||
rotate?: number;
|
||||
/** display aspect ratio, if not width/height */
|
||||
aspect?: number;
|
||||
}
|
||||
|
||||
export interface DebugSection {
|
||||
category: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/** Disassemblers, keyed by the `arch` name used in the worker's platform params. */
|
||||
export const DISASSEMBLERS: { [arch: string]: (addr: number, read: (a: number) => number) => DisasmLine } = {
|
||||
'6502': (a, r) => disassemble6502(a, r(a), r(a + 1), r(a + 2)),
|
||||
'huc6280': (a, r) => disassembleHuC6280(a, r(a), r(a + 1), r(a + 2)),
|
||||
'z80': (a, r) => disassembleZ80(a, r(a), r(a + 1), r(a + 2), r(a + 3)),
|
||||
'gbz80': (a, r) => disassembleSM83(a, r(a), r(a + 1), r(a + 2)),
|
||||
'sm83': (a, r) => disassembleSM83(a, r(a), r(a + 1), r(a + 2)),
|
||||
'f8': (a, r) => disassembleF8(a, r(a), r(a + 1), r(a + 2)),
|
||||
'6809': (a, r) => Object.create(CPU6809()).disasm(r(a), r(a + 1), r(a + 2), r(a + 3), r(a + 4), a),
|
||||
};
|
||||
|
||||
/** CPU class name -> arch, for the platforms that don't implement disassemble(). */
|
||||
const CPU_ARCH: { [cls: string]: string } = {
|
||||
'MOS6502': '6502', 'HuC6280': 'huc6280',
|
||||
'Z80': 'z80', 'ZilogZ80': 'z80',
|
||||
'SM83': 'sm83', 'F8CPU': 'f8', 'CPU6809': '6809',
|
||||
};
|
||||
|
||||
/** Work out which disassembler a CPU object wants. */
|
||||
function archOf(cpu: any): string {
|
||||
const arch = CPU_ARCH[cpu?.constructor?.name];
|
||||
if (arch) return arch;
|
||||
// CPU6809 is a factory returning a plain object, so it has no class name to
|
||||
// match; it is also the only CPU here that carries its own disassembler.
|
||||
if (typeof cpu?.disasm === 'function') return '6809';
|
||||
return '';
|
||||
}
|
||||
|
||||
// Frames to run before giving up on a `runUntil` predicate that never fires.
|
||||
export const DEFAULT_MAX_FRAMES = 1000;
|
||||
|
||||
// Upper bound on clocks in one instruction, so stepping can't spin forever.
|
||||
const MAX_CYCLES_PER_INSN = 64;
|
||||
|
||||
/**
|
||||
* Headless stand-ins for RasterVideo/VectorVideo/AnimationTimer. Platform
|
||||
* modules pick these up because Platform.start() reads them off the emu module.
|
||||
*/
|
||||
function installHeadlessVideo() {
|
||||
// Platform.start() builds its video/timer by reading these classes off the
|
||||
// emu module, so we swap in headless stand-ins for the duration of start()
|
||||
// and put the real ones back afterwards. Leaving the stubs installed would
|
||||
// leak into every other module sharing the (cached) emu import -- which is
|
||||
// exactly what happens when mocha reuses a worker across test files.
|
||||
let pixels: Uint32Array | null = null;
|
||||
let params: { width: number, height: number, rotate?: number, aspect?: number } | null = null;
|
||||
let frameRate = 60;
|
||||
// the handler a platform registers on its canvas; it's how the IDE
|
||||
// delivers keys, whether or not there's a Machine behind the platform
|
||||
let keyHandler: ((key: number, code: number, flags: number) => void) | null = null;
|
||||
const setKeyboardEvents = function (callback) { keyHandler ??= callback; };
|
||||
const RasterVideo: any = function (_el: any, width: number, height: number, options?: { rotate?: number, aspect?: number }) {
|
||||
const buffer = new ArrayBuffer(width * height * 4);
|
||||
const datau8 = new Uint8Array(buffer);
|
||||
const datau32 = new Uint32Array(buffer);
|
||||
// the first one is the screen; later ones are debug views (nes nametables)
|
||||
if (!pixels) {
|
||||
params = { width, height, rotate: options?.rotate, aspect: options?.aspect };
|
||||
pixels = datau32;
|
||||
}
|
||||
this.create = function () { this.width = width; this.height = height; };
|
||||
this.setKeyboardEvents = setKeyboardEvents;
|
||||
this.getFrameData = function () { return datau32; };
|
||||
this.getImageData = function () { return { data: datau8, width, height }; };
|
||||
this.updateFrame = function () { };
|
||||
this.clearRect = function () { };
|
||||
this.setupMouseEvents = function () { };
|
||||
this.canvas = this;
|
||||
this.getContext = function () { return this; };
|
||||
this.fillRect = function () { };
|
||||
this.fillStyle = '';
|
||||
this.putImageData = function () { };
|
||||
this.style = {};
|
||||
};
|
||||
const VectorVideo: any = function () {
|
||||
this.create = function () { this.drawops = 0; };
|
||||
this.setKeyboardEvents = setKeyboardEvents;
|
||||
this.clear = function () { };
|
||||
this.drawLine = function () { this.drawops++; };
|
||||
};
|
||||
const AnimationTimer: any = function (fps: number) {
|
||||
if (fps > 0) frameRate = fps;
|
||||
this.running = false;
|
||||
this.start = function () { };
|
||||
this.stop = function () { };
|
||||
this.isRunning = function () { return this.running; };
|
||||
};
|
||||
const original = emu.setVideoClasses({ RasterVideo, VectorVideo, AnimationTimer });
|
||||
return {
|
||||
get(): VideoOutput | null {
|
||||
return pixels && params ? { pixels, ...params } : null;
|
||||
},
|
||||
get frameRate() { return frameRate; },
|
||||
get keyHandler() { return keyHandler; },
|
||||
restore() {
|
||||
emu.setVideoClasses(original);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class EmuCore {
|
||||
frameCount = 0;
|
||||
private video: ReturnType<typeof installHeadlessVideo> | null = null;
|
||||
|
||||
constructor(readonly id: string, readonly platform: Platform) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The underlying Machine, if there is one. Platforms built on
|
||||
* BaseMachinePlatform expose theirs, which is what lets us stop mid-frame
|
||||
* through the TrapCondition the FrameBased interface already defines,
|
||||
* instead of guessing at cycle counts.
|
||||
*/
|
||||
get machine(): Machine | null {
|
||||
return (this.platform as any).machine || null;
|
||||
}
|
||||
|
||||
async start() {
|
||||
// start() is where platforms construct their video and timer, so install
|
||||
// the headless stand-ins just for that call, then restore the real classes.
|
||||
const headless = installHeadlessVideo();
|
||||
this.video = headless;
|
||||
try {
|
||||
await this.platform.start();
|
||||
} finally {
|
||||
headless.restore();
|
||||
}
|
||||
}
|
||||
reset() { this.platform.reset(); }
|
||||
loadROM(data: Uint8Array, title = 'ROM') { this.platform.loadROM(title, data); }
|
||||
|
||||
loadBIOS(data: Uint8Array, title = 'BIOS'): boolean {
|
||||
if (!this.platform.loadBIOS) return false;
|
||||
this.platform.loadBIOS(title, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
read(addr: number): number {
|
||||
if (this.platform.readAddress) return this.platform.readAddress(addr);
|
||||
const m = this.machine;
|
||||
if (m) return m.readConst ? m.readConst(addr) : m.read(addr);
|
||||
throw new Error(`platform '${this.id}' cannot read memory`);
|
||||
}
|
||||
|
||||
getCPUState(): CpuState | null {
|
||||
try { return this.platform.getCPUState ? this.platform.getCPUState() : null; }
|
||||
catch (e) { return null; }
|
||||
}
|
||||
|
||||
getPC(): number | null {
|
||||
// Platforms may correct the PC (see debugPCDelta), so prefer getPC().
|
||||
try { if (this.platform.getPC) return this.platform.getPC(); } catch (e) { }
|
||||
const s = this.getCPUState();
|
||||
return s ? s.PC : null;
|
||||
}
|
||||
|
||||
disassemble(addr: number): DisasmLine | null {
|
||||
const read = (a: number) => this.read(a);
|
||||
try {
|
||||
if (this.platform.disassemble) return this.platform.disassemble(addr, read);
|
||||
// a few platforms leave it out; fall back to the machine's CPU
|
||||
const disasm = DISASSEMBLERS[archOf(this.machine?.cpu)];
|
||||
return disasm ? disasm(addr, read) : null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
setKeyInput(key: number, code: number, flags: number) {
|
||||
const handler = this.video?.keyHandler;
|
||||
if (handler) return handler(key, code, flags);
|
||||
const target: any = this.machine || this.platform;
|
||||
if (typeof target.setKeyInput !== 'function') {
|
||||
throw new Error(`platform '${this.id}' does not accept key input`);
|
||||
}
|
||||
target.setKeyInput(key, code, flags);
|
||||
}
|
||||
|
||||
connectProbe(probe: ProbeAll | null): boolean {
|
||||
const m = this.machine;
|
||||
if (!m || !hasProbe(m)) return false;
|
||||
m.connectProbe(probe);
|
||||
return true;
|
||||
}
|
||||
|
||||
getVideo(): VideoOutput | null { return this.video ? this.video.get() : null; }
|
||||
/** Frames per second the platform's timer asked for (60 if unknown). */
|
||||
get frameRate(): number { return this.video ? this.video.frameRate : 60; }
|
||||
saveState(): EmuState | null { return this.platform.saveState ? this.platform.saveState() : null; }
|
||||
|
||||
getDebugInfo(): DebugSection[] {
|
||||
const state = this.saveState();
|
||||
const p: any = this.platform;
|
||||
if (!state || !isDebuggable(p) || !p.getDebugCategories) return [];
|
||||
const sections: DebugSection[] = [];
|
||||
for (const category of p.getDebugCategories() || []) {
|
||||
try {
|
||||
const text = p.getDebugInfo(category, state);
|
||||
if (text) sections.push({ category, text });
|
||||
} catch (e) { /* a category that doesn't apply to this state */ }
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
//// execution control, driven by what the target turns out to support
|
||||
|
||||
/** True if execution can be stopped mid-frame at an exact instruction. */
|
||||
get supportsTrap(): boolean { return this.machine != null; }
|
||||
|
||||
/** True if single instructions can be stepped. */
|
||||
get supportsStep(): boolean { return this.supportsTrap; }
|
||||
|
||||
/**
|
||||
* A function that advances the CPU by the smallest amount the machine
|
||||
* supports -- one clock for ClockBased CPUs, one instruction for
|
||||
* InstructionBased ones -- without disturbing the frame loop. Null if the
|
||||
* machine only knows how to run whole frames.
|
||||
*/
|
||||
private clockStepper(): (() => void) | null {
|
||||
const m = this.machine as any;
|
||||
if (!m) return null;
|
||||
if (typeof m.advanceCPU === 'function') return () => m.advanceCPU(); // BasicHeadlessMachine
|
||||
if (typeof m.advanceFrameClock === 'function') return () => m.advanceFrameClock(null, 1); // WASM
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish any partly-executed instruction, so getPC() names a real
|
||||
* instruction. Frames can end mid-instruction on clock-based CPUs.
|
||||
*/
|
||||
settle(): void {
|
||||
const clock = this.clockStepper();
|
||||
if (!clock) return;
|
||||
for (let i = 0; i < MAX_CYCLES_PER_INSN && !this.isStable(); i++) clock();
|
||||
}
|
||||
|
||||
advanceFrame(trap?: TrapCondition | null): void {
|
||||
const m = this.machine;
|
||||
// With a trap we drive the Machine directly -- Platform.advance() only
|
||||
// honors traps registered as breakpoints, and installing/removing those
|
||||
// rewinds BaseDebugPlatform's saved state.
|
||||
if (trap && m) {
|
||||
m.advanceFrame(trap);
|
||||
} else {
|
||||
const p = this.platform as any;
|
||||
if (p.nextFrame) p.nextFrame();
|
||||
else if (p.advance) p.advance(false);
|
||||
}
|
||||
this.frameCount++;
|
||||
}
|
||||
|
||||
isStable(): boolean {
|
||||
return this.machine ? this.machine.cpu.isStable() : true;
|
||||
}
|
||||
|
||||
/** Execute exactly one instruction. Returns false if the target can't step. */
|
||||
stepInsn(): boolean {
|
||||
if (!this.machine) return false;
|
||||
const clock = this.clockStepper();
|
||||
if (clock) {
|
||||
this.settle(); // don't count an in-flight instruction as this step
|
||||
clock();
|
||||
for (let i = 0; i < MAX_CYCLES_PER_INSN && !this.isStable(); i++) clock();
|
||||
return true;
|
||||
}
|
||||
// No clock-level entry point: stop at the next instruction boundary via
|
||||
// the frame trap. This restarts the frame, so it is the last resort.
|
||||
let boundaries = 0;
|
||||
for (let frames = 0; frames < 2 && boundaries < 2; frames++) {
|
||||
this.advanceFrame(() => this.isStable() && ++boundaries >= 2);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run until `pred` is true, up to `maxFrames` frames. Exact to the
|
||||
* instruction on targets with a Machine; frame-granular otherwise.
|
||||
*/
|
||||
runUntil(pred: () => boolean, maxFrames = DEFAULT_MAX_FRAMES): boolean {
|
||||
const start = this.frameCount;
|
||||
while (this.frameCount - start < maxFrames) {
|
||||
if (this.supportsTrap) {
|
||||
let hit = false;
|
||||
this.advanceFrame(() => (hit = this.isStable() && pred()));
|
||||
if (hit) return true;
|
||||
} else {
|
||||
this.advanceFrame();
|
||||
if (pred()) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Run until the PC reaches one of `addrs`. */
|
||||
runToPC(addrs: Set<number>, maxFrames = DEFAULT_MAX_FRAMES): boolean {
|
||||
return this.runUntil(() => addrs.has(this.getPC()), maxFrames);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
import type { SymbolMap } from "../baseplatform";
|
||||
|
||||
/** Parse a cc65/ca65 or VICE label file into a symbol map. */
|
||||
export function parseSymbolFile(text: string): SymbolMap {
|
||||
const symbols: SymbolMap = {};
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const m1 = line.match(/^\s*([A-Za-z_][\w]*)\s*=\s*\$?([0-9A-Fa-f]+)\s*;/); // ca65/cc65 list
|
||||
const m2 = line.match(/^\s*(?:al|add_label)\s+([0-9A-Fa-f]+)\s+\.?([A-Za-z_][\w]*)/); // VICE
|
||||
if (m1) symbols[m1[1]] = parseInt(m1[2], 16);
|
||||
else if (m2) symbols[m2[2]] = parseInt(m2[1], 16);
|
||||
}
|
||||
return symbols;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a symbol the way a user types it: exactly, then with the leading
|
||||
* underscore C compilers add (`main` finds `_main`), then without a leading
|
||||
* '.' (VICE labels).
|
||||
*/
|
||||
export function lookupSymbol(symbols: SymbolMap | null | undefined, name: string): number | undefined {
|
||||
if (!symbols) return undefined;
|
||||
if (name in symbols) return symbols[name];
|
||||
if ('_' + name in symbols) return symbols['_' + name];
|
||||
const bare = name.replace(/^\./, '');
|
||||
if (bare !== name && bare in symbols) return symbols[bare];
|
||||
return undefined;
|
||||
}
|
||||
+16
-81
@@ -1,30 +1,14 @@
|
||||
// Unified breakpoint store, shared by the editor gutter markers and the
|
||||
// Breakpoints window. Two kinds of breakpoints:
|
||||
// - 'source' -- file + line, resolved to a PC via the build listing
|
||||
// (these render as gutter markers in the editor)
|
||||
// - 'address' -- a target that's a symbol, $hex or decimal address
|
||||
// Both support an optional condition expression (see breakcond.ts).
|
||||
// The store persists to localStorage, scoped by platform + main file.
|
||||
// The IDE's breakpoint store, shared by the editor gutter markers and the
|
||||
// Breakpoints window. It persists to localStorage, scoped by platform + main
|
||||
// file. Breakpoint types and resolution are in src/common/breakpoints.ts.
|
||||
|
||||
import { current_project, platform } from "./ui";
|
||||
import { CondFn, CondContext, compileCondition, parseTarget } from "./breakcond";
|
||||
import {
|
||||
Breakpoint, BreakpointContext, ResolvedBreakpoint,
|
||||
canUseBreakpoints as platformCanUseBreakpoints, resolveBreakpoint as resolveIn,
|
||||
} from "../common/breakpoints";
|
||||
|
||||
export interface Breakpoint {
|
||||
id: number;
|
||||
type: 'source' | 'address';
|
||||
file?: string; // source: project path
|
||||
line?: number; // source: 1-based line number
|
||||
target?: string; // address: symbol, $hex or decimal
|
||||
enabled: boolean;
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
export interface ResolvedBreakpoint {
|
||||
bp: Breakpoint;
|
||||
pc?: number; // resolved address (undefined if not resolvable now)
|
||||
condFn?: CondFn;
|
||||
error?: string; // why the breakpoint can't resolve right now
|
||||
}
|
||||
export type { Breakpoint, ResolvedBreakpoint };
|
||||
|
||||
const STORAGE_KEY = '8bitworkshop.breakpoints.v1';
|
||||
|
||||
@@ -145,73 +129,24 @@ class BreakpointStore {
|
||||
|
||||
export const bpStore = new BreakpointStore();
|
||||
|
||||
function makeSymbolLookup(): (name: string) => number | undefined {
|
||||
return (name: string): number | undefined => {
|
||||
let sm = platform && platform.debugSymbols && platform.debugSymbols.symbolmap;
|
||||
if (!sm) return undefined;
|
||||
// C symbols often get a leading underscore from the compiler
|
||||
return (name in sm) ? sm[name] : sm['_' + name];
|
||||
};
|
||||
}
|
||||
|
||||
function makeCondContext(): CondContext {
|
||||
let cpuFields = new Set<string>();
|
||||
try {
|
||||
let c = platform && platform.getCPUState && platform.getCPUState();
|
||||
if (c) cpuFields = new Set(Object.keys(c));
|
||||
} catch (e) {
|
||||
}
|
||||
// platform accessors reachable with the '#' sigil; the value is read live
|
||||
// at eval time so it reflects the current raster position, etc.
|
||||
let hw: { [name: string]: () => number | undefined } = null;
|
||||
if (platform) {
|
||||
if (platform.getRasterScanline) {
|
||||
hw = hw || {};
|
||||
hw['scanline'] = () => platform.getRasterScanline();
|
||||
}
|
||||
if (platform.getRasterLineClock) {
|
||||
hw = hw || {};
|
||||
hw['lineclock'] = () => platform.getRasterLineClock();
|
||||
}
|
||||
}
|
||||
function ideContext(): BreakpointContext {
|
||||
return {
|
||||
cpuFields,
|
||||
symbol: makeSymbolLookup(),
|
||||
readMem: platform && platform.readAddress ? (a) => platform.readAddress(a) : undefined,
|
||||
readVRAM: platform && platform.readVRAMAddress ? (a) => platform.readVRAMAddress(a) : undefined,
|
||||
hw,
|
||||
symbols: platform && platform.debugSymbols && platform.debugSymbols.symbolmap,
|
||||
getListingForFile: (path) => current_project && current_project.getListingForFile(path),
|
||||
platform,
|
||||
};
|
||||
}
|
||||
|
||||
// Can this platform actually stop at a breakpoint?
|
||||
export function canUseBreakpoints(): boolean {
|
||||
return !!(platform && (platform.runEval || platform.runToPC || platform.runEvalAtPC));
|
||||
return platformCanUseBreakpoints(platform);
|
||||
}
|
||||
|
||||
export function resolveBreakpoint(bp: Breakpoint): ResolvedBreakpoint {
|
||||
try {
|
||||
let pc: number;
|
||||
if (bp.type == 'source') {
|
||||
let lst = current_project.getListingForFile(bp.file);
|
||||
let sf = lst && (lst.sourcefile || lst.assemblyfile);
|
||||
if (!sf) return { bp, error: "no debug info (build first?)" };
|
||||
pc = sf.line2offset.get(bp.line);
|
||||
if (!(pc >= 0)) return { bp, error: "line has no code" };
|
||||
} else {
|
||||
let r = parseTarget(bp.target, makeSymbolLookup());
|
||||
if (r.error) return { bp, error: r.error };
|
||||
pc = r.pc;
|
||||
}
|
||||
let condFn: CondFn = undefined;
|
||||
if (bp.condition && bp.condition.trim()) {
|
||||
condFn = compileCondition(bp.condition, makeCondContext());
|
||||
}
|
||||
return { bp, pc, condFn };
|
||||
} catch (e) {
|
||||
return { bp, error: String(e.message || e) };
|
||||
}
|
||||
return resolveIn(bp, ideContext());
|
||||
}
|
||||
|
||||
export function resolveBreakpoints(): ResolvedBreakpoint[] {
|
||||
return bpStore.getAll().map(bp => resolveBreakpoint(bp));
|
||||
let ctx = ideContext();
|
||||
return bpStore.getAll().map(bp => resolveIn(bp, ctx));
|
||||
}
|
||||
+1
-1
@@ -38,7 +38,7 @@ import { ProjectWindows } from "./windows";
|
||||
import { setupSplits, showTab } from "./layout";
|
||||
export { setupSplits, showTab };
|
||||
import { bpStore, canUseBreakpoints, resolveBreakpoints } from "./breakpoints";
|
||||
import { CondFn } from "./breakcond";
|
||||
import { CondFn } from "../common/breakcond";
|
||||
import { findListingLocation as findListingLocationPure } from "./search/listinglocation";
|
||||
import DOMPurify = require("dompurify");
|
||||
|
||||
|
||||
+2
-1
@@ -16,7 +16,8 @@ import * as path from 'path';
|
||||
import { isProbablyBinary } from '../common/util';
|
||||
import { fail, hasOutput, note, output, setJsonMode } from './cliformat';
|
||||
import { EmuTarget, loadPlatform } from './emutarget';
|
||||
import { RUN_SCRIPT_HELP, RunScript, parseNum, parseSymbolFile } from './runscript';
|
||||
import { RUN_SCRIPT_HELP, RunScript, parseNum } from './runscript';
|
||||
import { parseSymbolFile } from '../common/symbols/symbolfile';
|
||||
import type { CompileResult } from './testlib';
|
||||
|
||||
interface Args {
|
||||
|
||||
+8
-335
@@ -1,343 +1,16 @@
|
||||
// Unified headless driver for the emulators in this codebase.
|
||||
//
|
||||
// A *Platform* (src/platform/*) is the full object the IDE drives: it owns its
|
||||
// own video/audio/timer and implements the Platform interface in
|
||||
// common/baseplatform.ts. Most platforms are built on a *Machine* (src/machine/*),
|
||||
// the bare emulation core made of the interfaces in common/devices.ts -- Bus,
|
||||
// FrameBased, HasCPU, VideoSource and friends.
|
||||
//
|
||||
// EmuTarget drives a Platform headlessly, reaching through to its Machine when
|
||||
// there is one for the things only the core can do (exact traps, single
|
||||
// stepping, the probe). The CLI and the run-script interpreter never have to
|
||||
// ask which of those a target supports; the capability sniffing lives here.
|
||||
// The Node side of headless emulation: browser mocks and platform loading.
|
||||
// The emulator driver itself is EmuCore (src/common/emucore.ts); EmuTarget is
|
||||
// the name the CLI and the extension know it by.
|
||||
|
||||
import {
|
||||
CpuState, DisasmLine, EmuState, Machine, Platform, hasProbe, isDebuggable,
|
||||
} from "../common/baseplatform";
|
||||
import { ProbeAll, TrapCondition } from "../common/devices";
|
||||
import { disassemble6502 } from "../common/cpu/disasm6502";
|
||||
import { disassembleZ80 } from "../common/cpu/disasmz80";
|
||||
import { disassembleSM83 } from "../common/cpu/disasmSM83";
|
||||
import { disassembleF8 } from "../common/cpu/disasmF8";
|
||||
import { disassembleHuC6280 } from "../common/cpu/disasmHuC6280";
|
||||
import { CPU6809 } from "../common/cpu/6809";
|
||||
import { EmuCore } from "../common/emucore";
|
||||
import { PLATFORMS } from "../common/emu";
|
||||
import * as emu from "../common/emu";
|
||||
import { getRootBasePlatform } from "../common/util";
|
||||
import { importPlatform } from "../platform/_index";
|
||||
import { mockAudio, mockDOM, mockFetch, mockGlobals } from "./nodemock";
|
||||
|
||||
export interface VideoOutput {
|
||||
pixels: Uint32Array;
|
||||
width: number;
|
||||
height: number;
|
||||
/** display rotation in degrees (RasterVideo's `rotate` option) */
|
||||
rotate?: number;
|
||||
/** display aspect ratio, if not width/height */
|
||||
aspect?: number;
|
||||
}
|
||||
|
||||
export interface DebugSection {
|
||||
category: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/** Disassemblers, keyed by the `arch` name used in the worker's platform params. */
|
||||
export const DISASSEMBLERS: { [arch: string]: (addr: number, read: (a: number) => number) => DisasmLine } = {
|
||||
'6502': (a, r) => disassemble6502(a, r(a), r(a + 1), r(a + 2)),
|
||||
'huc6280': (a, r) => disassembleHuC6280(a, r(a), r(a + 1), r(a + 2)),
|
||||
'z80': (a, r) => disassembleZ80(a, r(a), r(a + 1), r(a + 2), r(a + 3)),
|
||||
'gbz80': (a, r) => disassembleSM83(a, r(a), r(a + 1), r(a + 2)),
|
||||
'sm83': (a, r) => disassembleSM83(a, r(a), r(a + 1), r(a + 2)),
|
||||
'f8': (a, r) => disassembleF8(a, r(a), r(a + 1), r(a + 2)),
|
||||
'6809': (a, r) => Object.create(CPU6809()).disasm(r(a), r(a + 1), r(a + 2), r(a + 3), r(a + 4), a),
|
||||
};
|
||||
|
||||
/** CPU class name -> arch, for the platforms that don't implement disassemble(). */
|
||||
const CPU_ARCH: { [cls: string]: string } = {
|
||||
'MOS6502': '6502', 'HuC6280': 'huc6280',
|
||||
'Z80': 'z80', 'ZilogZ80': 'z80',
|
||||
'SM83': 'sm83', 'F8CPU': 'f8', 'CPU6809': '6809',
|
||||
};
|
||||
|
||||
/** Work out which disassembler a CPU object wants. */
|
||||
function archOf(cpu: any): string {
|
||||
const arch = CPU_ARCH[cpu?.constructor?.name];
|
||||
if (arch) return arch;
|
||||
// CPU6809 is a factory returning a plain object, so it has no class name to
|
||||
// match; it is also the only CPU here that carries its own disassembler.
|
||||
if (typeof cpu?.disasm === 'function') return '6809';
|
||||
return '';
|
||||
}
|
||||
|
||||
// Frames to run before giving up on a `runUntil` predicate that never fires.
|
||||
export const DEFAULT_MAX_FRAMES = 1000;
|
||||
|
||||
// Upper bound on clocks in one instruction, so stepping can't spin forever.
|
||||
const MAX_CYCLES_PER_INSN = 64;
|
||||
|
||||
/**
|
||||
* Headless stand-ins for RasterVideo/VectorVideo/AnimationTimer. Platform
|
||||
* modules pick these up because Platform.start() reads them off the emu module.
|
||||
*/
|
||||
function installHeadlessVideo() {
|
||||
// Platform.start() builds its video/timer by reading these classes off the
|
||||
// emu module, so we swap in headless stand-ins for the duration of start()
|
||||
// and put the real ones back afterwards. Leaving the stubs installed would
|
||||
// leak into every other module sharing the (cached) emu import -- which is
|
||||
// exactly what happens when mocha reuses a worker across test files.
|
||||
let pixels: Uint32Array | null = null;
|
||||
let params: { width: number, height: number, rotate?: number, aspect?: number } | null = null;
|
||||
let frameRate = 60;
|
||||
// the handler a platform registers on its canvas; it's how the IDE
|
||||
// delivers keys, whether or not there's a Machine behind the platform
|
||||
let keyHandler: ((key: number, code: number, flags: number) => void) | null = null;
|
||||
const setKeyboardEvents = function (callback) { keyHandler ??= callback; };
|
||||
const RasterVideo: any = function (_el: any, width: number, height: number, options?: { rotate?: number, aspect?: number }) {
|
||||
const buffer = new ArrayBuffer(width * height * 4);
|
||||
const datau8 = new Uint8Array(buffer);
|
||||
const datau32 = new Uint32Array(buffer);
|
||||
// the first one is the screen; later ones are debug views (nes nametables)
|
||||
if (!pixels) {
|
||||
params = { width, height, rotate: options?.rotate, aspect: options?.aspect };
|
||||
pixels = datau32;
|
||||
}
|
||||
this.create = function () { this.width = width; this.height = height; };
|
||||
this.setKeyboardEvents = setKeyboardEvents;
|
||||
this.getFrameData = function () { return datau32; };
|
||||
this.getImageData = function () { return { data: datau8, width, height }; };
|
||||
this.updateFrame = function () { };
|
||||
this.clearRect = function () { };
|
||||
this.setupMouseEvents = function () { };
|
||||
this.canvas = this;
|
||||
this.getContext = function () { return this; };
|
||||
this.fillRect = function () { };
|
||||
this.fillStyle = '';
|
||||
this.putImageData = function () { };
|
||||
this.style = {};
|
||||
};
|
||||
const VectorVideo: any = function () {
|
||||
this.create = function () { this.drawops = 0; };
|
||||
this.setKeyboardEvents = setKeyboardEvents;
|
||||
this.clear = function () { };
|
||||
this.drawLine = function () { this.drawops++; };
|
||||
};
|
||||
const AnimationTimer: any = function (fps: number) {
|
||||
if (fps > 0) frameRate = fps;
|
||||
this.running = false;
|
||||
this.start = function () { };
|
||||
this.stop = function () { };
|
||||
this.isRunning = function () { return this.running; };
|
||||
};
|
||||
const original = emu.setVideoClasses({ RasterVideo, VectorVideo, AnimationTimer });
|
||||
return {
|
||||
get(): VideoOutput | null {
|
||||
return pixels && params ? { pixels, ...params } : null;
|
||||
},
|
||||
get frameRate() { return frameRate; },
|
||||
get keyHandler() { return keyHandler; },
|
||||
restore() {
|
||||
emu.setVideoClasses(original);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class EmuTarget {
|
||||
frameCount = 0;
|
||||
private video: ReturnType<typeof installHeadlessVideo> | null = null;
|
||||
|
||||
constructor(readonly id: string, readonly platform: Platform) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The underlying Machine, if there is one. Platforms built on
|
||||
* BaseMachinePlatform expose theirs, which is what lets us stop mid-frame
|
||||
* through the TrapCondition the FrameBased interface already defines,
|
||||
* instead of guessing at cycle counts.
|
||||
*/
|
||||
get machine(): Machine | null {
|
||||
return (this.platform as any).machine || null;
|
||||
}
|
||||
|
||||
async start() {
|
||||
// start() is where platforms construct their video and timer, so install
|
||||
// the headless stand-ins just for that call, then restore the real classes.
|
||||
const headless = installHeadlessVideo();
|
||||
this.video = headless;
|
||||
try {
|
||||
await this.platform.start();
|
||||
} finally {
|
||||
headless.restore();
|
||||
}
|
||||
}
|
||||
reset() { this.platform.reset(); }
|
||||
loadROM(data: Uint8Array, title = 'ROM') { this.platform.loadROM(title, data); }
|
||||
|
||||
loadBIOS(data: Uint8Array, title = 'BIOS'): boolean {
|
||||
if (!this.platform.loadBIOS) return false;
|
||||
this.platform.loadBIOS(title, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
read(addr: number): number {
|
||||
if (this.platform.readAddress) return this.platform.readAddress(addr);
|
||||
const m = this.machine;
|
||||
if (m) return m.readConst ? m.readConst(addr) : m.read(addr);
|
||||
throw new Error(`platform '${this.id}' cannot read memory`);
|
||||
}
|
||||
|
||||
getCPUState(): CpuState | null {
|
||||
try { return this.platform.getCPUState ? this.platform.getCPUState() : null; }
|
||||
catch (e) { return null; }
|
||||
}
|
||||
|
||||
getPC(): number | null {
|
||||
// Platforms may correct the PC (see debugPCDelta), so prefer getPC().
|
||||
try { if (this.platform.getPC) return this.platform.getPC(); } catch (e) { }
|
||||
const s = this.getCPUState();
|
||||
return s ? s.PC : null;
|
||||
}
|
||||
|
||||
disassemble(addr: number): DisasmLine | null {
|
||||
const read = (a: number) => this.read(a);
|
||||
try {
|
||||
if (this.platform.disassemble) return this.platform.disassemble(addr, read);
|
||||
// a few platforms leave it out; fall back to the machine's CPU
|
||||
const disasm = DISASSEMBLERS[archOf(this.machine?.cpu)];
|
||||
return disasm ? disasm(addr, read) : null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
setKeyInput(key: number, code: number, flags: number) {
|
||||
const handler = this.video?.keyHandler;
|
||||
if (handler) return handler(key, code, flags);
|
||||
const target: any = this.machine || this.platform;
|
||||
if (typeof target.setKeyInput !== 'function') {
|
||||
throw new Error(`platform '${this.id}' does not accept key input`);
|
||||
}
|
||||
target.setKeyInput(key, code, flags);
|
||||
}
|
||||
|
||||
connectProbe(probe: ProbeAll | null): boolean {
|
||||
const m = this.machine;
|
||||
if (!m || !hasProbe(m)) return false;
|
||||
m.connectProbe(probe);
|
||||
return true;
|
||||
}
|
||||
|
||||
getVideo(): VideoOutput | null { return this.video ? this.video.get() : null; }
|
||||
/** Frames per second the platform's timer asked for (60 if unknown). */
|
||||
get frameRate(): number { return this.video ? this.video.frameRate : 60; }
|
||||
saveState(): EmuState | null { return this.platform.saveState ? this.platform.saveState() : null; }
|
||||
|
||||
getDebugInfo(): DebugSection[] {
|
||||
const state = this.saveState();
|
||||
const p: any = this.platform;
|
||||
if (!state || !isDebuggable(p) || !p.getDebugCategories) return [];
|
||||
const sections: DebugSection[] = [];
|
||||
for (const category of p.getDebugCategories() || []) {
|
||||
try {
|
||||
const text = p.getDebugInfo(category, state);
|
||||
if (text) sections.push({ category, text });
|
||||
} catch (e) { /* a category that doesn't apply to this state */ }
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
//// execution control, driven by what the target turns out to support
|
||||
|
||||
/** True if execution can be stopped mid-frame at an exact instruction. */
|
||||
get supportsTrap(): boolean { return this.machine != null; }
|
||||
|
||||
/** True if single instructions can be stepped. */
|
||||
get supportsStep(): boolean { return this.supportsTrap; }
|
||||
|
||||
/**
|
||||
* A function that advances the CPU by the smallest amount the machine
|
||||
* supports -- one clock for ClockBased CPUs, one instruction for
|
||||
* InstructionBased ones -- without disturbing the frame loop. Null if the
|
||||
* machine only knows how to run whole frames.
|
||||
*/
|
||||
private clockStepper(): (() => void) | null {
|
||||
const m = this.machine as any;
|
||||
if (!m) return null;
|
||||
if (typeof m.advanceCPU === 'function') return () => m.advanceCPU(); // BasicHeadlessMachine
|
||||
if (typeof m.advanceFrameClock === 'function') return () => m.advanceFrameClock(null, 1); // WASM
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish any partly-executed instruction, so getPC() names a real
|
||||
* instruction. Frames can end mid-instruction on clock-based CPUs.
|
||||
*/
|
||||
settle(): void {
|
||||
const clock = this.clockStepper();
|
||||
if (!clock) return;
|
||||
for (let i = 0; i < MAX_CYCLES_PER_INSN && !this.isStable(); i++) clock();
|
||||
}
|
||||
|
||||
advanceFrame(trap?: TrapCondition | null): void {
|
||||
const m = this.machine;
|
||||
// With a trap we drive the Machine directly -- Platform.advance() only
|
||||
// honors traps registered as breakpoints, and installing/removing those
|
||||
// rewinds BaseDebugPlatform's saved state.
|
||||
if (trap && m) {
|
||||
m.advanceFrame(trap);
|
||||
} else {
|
||||
const p = this.platform as any;
|
||||
if (p.nextFrame) p.nextFrame();
|
||||
else if (p.advance) p.advance(false);
|
||||
}
|
||||
this.frameCount++;
|
||||
}
|
||||
|
||||
isStable(): boolean {
|
||||
return this.machine ? this.machine.cpu.isStable() : true;
|
||||
}
|
||||
|
||||
/** Execute exactly one instruction. Returns false if the target can't step. */
|
||||
stepInsn(): boolean {
|
||||
if (!this.machine) return false;
|
||||
const clock = this.clockStepper();
|
||||
if (clock) {
|
||||
this.settle(); // don't count an in-flight instruction as this step
|
||||
clock();
|
||||
for (let i = 0; i < MAX_CYCLES_PER_INSN && !this.isStable(); i++) clock();
|
||||
return true;
|
||||
}
|
||||
// No clock-level entry point: stop at the next instruction boundary via
|
||||
// the frame trap. This restarts the frame, so it is the last resort.
|
||||
let boundaries = 0;
|
||||
for (let frames = 0; frames < 2 && boundaries < 2; frames++) {
|
||||
this.advanceFrame(() => this.isStable() && ++boundaries >= 2);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run until `pred` is true, up to `maxFrames` frames. Exact to the
|
||||
* instruction on targets with a Machine; frame-granular otherwise.
|
||||
*/
|
||||
runUntil(pred: () => boolean, maxFrames = DEFAULT_MAX_FRAMES): boolean {
|
||||
const start = this.frameCount;
|
||||
while (this.frameCount - start < maxFrames) {
|
||||
if (this.supportsTrap) {
|
||||
let hit = false;
|
||||
this.advanceFrame(() => (hit = this.isStable() && pred()));
|
||||
if (hit) return true;
|
||||
} else {
|
||||
this.advanceFrame();
|
||||
if (pred()) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Run until the PC reaches one of `addrs`. */
|
||||
runToPC(addrs: Set<number>, maxFrames = DEFAULT_MAX_FRAMES): boolean {
|
||||
return this.runUntil(() => addrs.has(this.getPC()), maxFrames);
|
||||
}
|
||||
}
|
||||
export { EmuCore as EmuTarget, DISASSEMBLERS, DEFAULT_MAX_FRAMES } from "../common/emucore";
|
||||
export type { VideoOutput, DebugSection } from "../common/emucore";
|
||||
type EmuTarget = EmuCore;
|
||||
|
||||
/** Load a platform module by ID (e.g. "nes", "c64.wasm", "atari8-5200"). */
|
||||
export async function loadPlatform(platformId: string): Promise<EmuTarget> {
|
||||
@@ -349,7 +22,7 @@ export async function loadPlatform(platformId: string): Promise<EmuTarget> {
|
||||
if (!PlatformClass) {
|
||||
throw new Error(`Platform '${platformId}' not found. Available: ${Object.keys(PLATFORMS).sort().join(', ')}`);
|
||||
}
|
||||
return new EmuTarget(platformId, new PlatformClass(null));
|
||||
return new EmuCore(platformId, new PlatformClass(null));
|
||||
}
|
||||
|
||||
let mocksInstalled = false;
|
||||
|
||||
+5
-15
@@ -9,6 +9,8 @@
|
||||
import { KeyFlags } from '../common/emu';
|
||||
import { ProbeFlags, ProbeRecorder } from '../common/probe';
|
||||
import { hex } from '../common/util';
|
||||
import type { SymbolMap } from '../common/baseplatform';
|
||||
import { lookupSymbol } from '../common/symbols/symbolfile';
|
||||
import { hexdump, write } from './cliformat';
|
||||
import { DEFAULT_MAX_FRAMES, EmuTarget } from './emutarget';
|
||||
|
||||
@@ -105,13 +107,13 @@ export function formatRegs(state: any): string {
|
||||
const PROBE_BUFFER_SIZE = 0x400000;
|
||||
|
||||
export class RunScript {
|
||||
symbols: { [name: string]: number } = {};
|
||||
symbols: SymbolMap = {};
|
||||
private addr2symbol: { [addr: number]: string } = {};
|
||||
private probe: ProbeRecorder | null = null;
|
||||
|
||||
constructor(readonly target: EmuTarget, private out = write) { }
|
||||
|
||||
addSymbols(symbols: { [name: string]: number }) {
|
||||
addSymbols(symbols: SymbolMap) {
|
||||
Object.assign(this.symbols, symbols);
|
||||
for (const [name, addr] of Object.entries(this.symbols)) this.addr2symbol[addr] = name;
|
||||
}
|
||||
@@ -151,7 +153,7 @@ export class RunScript {
|
||||
private addr(tok: string): number {
|
||||
try { return parseNum(tok); }
|
||||
catch (e) {
|
||||
const v = this.symbols[tok] ?? this.symbols[tok.replace(/^\./, '')];
|
||||
const v = lookupSymbol(this.symbols, tok);
|
||||
if (v == null) throw new Error(`unknown address or symbol '${tok}'`);
|
||||
return v;
|
||||
}
|
||||
@@ -368,15 +370,3 @@ const COMMANDS: { [name: string]: Command } = {
|
||||
'reset': RunScript.prototype.cmdReset,
|
||||
'echo': RunScript.prototype.cmdEcho,
|
||||
};
|
||||
|
||||
/** Parse a cc65/ca65 or VICE label file into a symbol map. */
|
||||
export function parseSymbolFile(text: string): { [name: string]: number } {
|
||||
const symbols: { [name: string]: number } = {};
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const m1 = line.match(/^\s*([A-Za-z_][\w]*)\s*=\s*\$?([0-9A-Fa-f]+)\s*;/); // ca65/cc65 list
|
||||
const m2 = line.match(/^\s*(?:al|add_label)\s+([0-9A-Fa-f]+)\s+\.?([A-Za-z_][\w]*)/); // VICE
|
||||
if (m1) symbols[m1[1]] = parseInt(m1[2], 16);
|
||||
else if (m2) symbols[m2[2]] = parseInt(m2[1], 16);
|
||||
}
|
||||
return symbols;
|
||||
}
|
||||
@@ -85,6 +85,10 @@ describe('8bws CLI', function () {
|
||||
assert.ok(/break \$[0-9A-F]+: HIT/.test(out), out);
|
||||
assert.ok(/_main:/.test(out), out);
|
||||
});
|
||||
it('should find a C symbol without its underscore', function () {
|
||||
var out = cli('run', '--platform', 'gb', 'presets/gb/hello.c', '-e', 'break main 600; pc 1');
|
||||
assert.ok(/break \$[0-9A-F]+: HIT/.test(out), out);
|
||||
});
|
||||
it('should write a screenshot', function () {
|
||||
var png = path.join(os.tmpdir(), '8bws-test-shot.png');
|
||||
if (fs.existsSync(png)) fs.unlinkSync(png);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe } from "mocha";
|
||||
import { compileCondition, parseTarget } from "../../src/ide/breakcond";
|
||||
import { compileCondition, parseTarget } from "../../src/common/breakcond";
|
||||
import { Breakpoint, BreakpointContext, canUseBreakpoints, resolveBreakpoint } from "../../src/common/breakpoints";
|
||||
import { lookupSymbol, parseSymbolFile } from "../../src/common/symbols/symbolfile";
|
||||
import { SourceFile } from "../../src/common/workertypes";
|
||||
import assert from "assert";
|
||||
|
||||
const ctx = {
|
||||
@@ -142,3 +145,59 @@ describe('Breakpoints', () => {
|
||||
assert.ok(parseTarget('12ab', sym).error); // neither number nor symbol
|
||||
});
|
||||
});
|
||||
|
||||
describe('Breakpoint resolution (shared by IDE, CLI, debug adapter)', () => {
|
||||
|
||||
const symbols = { _main: 0x1234, mainloop: 0x800, vblank: 0x900 };
|
||||
const sourcefile = new SourceFile([{ line: 10, offset: 0x1234 } as any], '');
|
||||
const ctx: BreakpointContext = {
|
||||
symbols,
|
||||
getListingForFile: (path) => path == 'game.c' ? { lines: [], sourcefile } as any : undefined,
|
||||
platform: {
|
||||
getCPUState: () => ({ PC: 0x1234, A: 7 }),
|
||||
readAddress: (a: number) => (a == 0x10 ? 0x2a : 0),
|
||||
} as any,
|
||||
};
|
||||
const bp = (patch: Partial<Breakpoint>): Breakpoint => ({ id: 1, type: 'address', enabled: true, ...patch });
|
||||
|
||||
it('should resolve a symbol, with or without the C underscore', () => {
|
||||
assert.equal(resolveBreakpoint(bp({ target: 'mainloop' }), ctx).pc, 0x800);
|
||||
assert.equal(resolveBreakpoint(bp({ target: '_main' }), ctx).pc, 0x1234);
|
||||
assert.equal(resolveBreakpoint(bp({ target: 'main' }), ctx).pc, 0x1234);
|
||||
assert.ok(resolveBreakpoint(bp({ target: 'nosuch' }), ctx).error);
|
||||
});
|
||||
it('should resolve a source line through the listing', () => {
|
||||
assert.equal(resolveBreakpoint(bp({ type: 'source', file: 'game.c', line: 10 }), ctx).pc, 0x1234);
|
||||
assert.equal(resolveBreakpoint(bp({ type: 'source', file: 'game.c', line: 11 }), ctx).error, 'line has no code');
|
||||
assert.equal(resolveBreakpoint(bp({ type: 'source', file: 'other.c', line: 10 }), ctx).error, 'no debug info (build first?)');
|
||||
});
|
||||
it('should compile a condition against the host platform', () => {
|
||||
let r = resolveBreakpoint(bp({ target: '$800', condition: 'A == 7 && [$10] == 42' }), ctx);
|
||||
assert.equal(r.pc, 0x800);
|
||||
assert.equal(r.condFn({ A: 7 }), true);
|
||||
assert.equal(r.condFn({ A: 6 }), false);
|
||||
});
|
||||
it('should work without a platform or listings', () => {
|
||||
assert.equal(resolveBreakpoint(bp({ target: '$c000' }), {}).pc, 0xc000);
|
||||
assert.ok(resolveBreakpoint(bp({ type: 'source', file: 'game.c', line: 10 }), {}).error);
|
||||
});
|
||||
it('should report whether a platform can stop at breakpoints', () => {
|
||||
assert.equal(canUseBreakpoints(null), false);
|
||||
assert.equal(canUseBreakpoints({ runEval: () => { } } as any), true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Symbol files', () => {
|
||||
it('should parse ca65 and VICE label files', () => {
|
||||
let syms = parseSymbolFile('main = $1234 ;\nal 00C000 .vblank\nadd_label 0800 loop\n');
|
||||
assert.deepEqual(syms, { main: 0x1234, vblank: 0xc000, loop: 0x800 });
|
||||
});
|
||||
it('should look up names as a user types them', () => {
|
||||
let syms = { _main: 1, loop: 2 };
|
||||
assert.equal(lookupSymbol(syms, '_main'), 1);
|
||||
assert.equal(lookupSymbol(syms, 'main'), 1);
|
||||
assert.equal(lookupSymbol(syms, '.loop'), 2);
|
||||
assert.equal(lookupSymbol(syms, 'nope'), undefined);
|
||||
assert.equal(lookupSymbol(null, 'main'), undefined);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user