Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | 2x 2x 2x 2x | import { RomBankBytes } from "./6502.js";
import { installBasic } from "./basic-loader.js";
import * as fdc from "./fdc.js";
import { fake6502 } from "./fake6502.js";
import { findModel } from "./models.js";
import assert from "assert";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { inspect } from "node:util";
import * as Tokeniser from "./basic-tokenise.js";
import { VduTextCapture } from "./vdu-capture.js";
import { loadData, setNodeBasePath } from "./loader.js";
import { Typist } from "./typist.js";
import { keyCodes } from "./keymap.js";
import { cycleCount } from "./lockstep.js";
export const MaxCyclesPerIter = 100 * 1000;
const VblankSearchCycles = 10 * 1000 * 1000;
export const RepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const HostKeyCodes = new Set(Object.values(keyCodes));
function requireKnownKeyCode(method, code) {
if (typeof code === "number") {
throw new Error(
`${method}: ${code} is a numeric key code; since 2.0 keys are named by physical position, ` +
`as KeyboardEvent.code names them, e.g. "ShiftLeft". See keyCodes in keymap.js`,
);
}
if (!HostKeyCodes.has(code)) {
const shown = typeof code === "string" ? JSON.stringify(code) : inspect(code);
throw new Error(
`${method}: ${shown} is not a key jsbeeb knows; keys are named ` +
`by physical position, as KeyboardEvent.code names them, e.g. "ShiftLeft" or "KeyA". ` +
`See keyCodes in keymap.js`,
);
}
}
export class TestMachine {
constructor(model, opts) {
model = model || "B-DFS1.2";
this.model = findModel(model);
if (!this.model) throw new Error(`Unknown model "${model}"`);
this.processor = fake6502(this.model, opts || {});
this.typist = new Typist(this.processor);
this._capturedChars = [];
this._captureHookInstalled = false;
/** Called after each execute a run makes, where the CPU is between instructions, or null. */
this.betweenExecutes = null;
}
async initialise() {
setNodeBasePath(RepoRoot);
await this.processor.initialise();
}
/**
* Calls `listener` with each character the machine sends to the VDU, by
* watching the write-character vector (WRCHV, $020E on the BBC and $0208 on
* the Atom) as the OS or a program leaves it. Returns a function that stops
* listening.
*/
onVduChar(listener) {
if (!this._vduListeners) {
this._vduListeners = [];
const cpu = this.processor;
const ram = cpu.ramRomOs;
const wrchvAddr = this.model.wrchvAddress;
cpu.debugInstruction.add((addr) => {
if (addr === (ram[wrchvAddr] | (ram[wrchvAddr + 1] << 8))) {
for (const listen of this._vduListeners) listen(cpu.a);
}
return false;
});
}
this._vduListeners.push(listener);
return () => {
this._vduListeners = this._vduListeners.filter((other) => other !== listener);
};
}
/** Accumulates every character sent to the VDU for drainText(); safe to call more than once. */
startCapture() {
if (this._captureHookInstalled) return;
this._captureHookInstalled = true;
this.onVduChar((c) => this._capturedChars.push(c));
}
/**
* Return all captured characters since the last drain (or since
* startCapture was called), then clear the buffer.
* @returns {number[]} array of character codes
*/
drainCapturedChars() {
const chars = this._capturedChars;
this._capturedChars = [];
return chars;
}
/**
* Return captured text as a string (printable chars only, with
* optional newline preservation), then clear the buffer.
* @param {Object} [opts]
* @param {boolean} [opts.raw=false] - if true, preserve newlines
*/
drainText({ raw = false } = {}) {
const chars = this.drainCapturedChars();
return chars
.map((c) => {
if (raw && c === 10) return "\n";
if (c === 13) return "";
if (c >= 0x20 && c < 0x7f) return String.fromCharCode(c);
return "";
})
.join("");
}
/**
* Run for `cycles` emulated cycles, or until something stops the CPU.
* Resolves true if it was stopped short. Rejects if a hook or device throws,
* leaving the cycle target where the run stopped.
*/
runFor(cycles) {
let left = cycles;
let stopped = false;
const cpu = this.processor;
return new Promise((resolve, reject) => {
const runAnIter = () => {
const todo = Math.max(0, Math.min(left, MaxCyclesPerIter));
if (todo) {
try {
stopped = !cpu.execute(todo);
} catch (e) {
cpu.targetCycles = cpu.currentCycles;
reject(e);
return;
}
left -= todo;
}
// execute() adds each request to a running targetCycles, so budget
// left unspent by an early stop would silently lengthen the next run.
if (stopped) cpu.targetCycles = cpu.currentCycles;
this.betweenExecutes?.();
// Not truthiness: a negative or NaN request clamps todo to zero,
// so left would never move and the loop never end.
if (left > 0 && !stopped) {
setTimeout(runAnIter, 0);
} else {
resolve(stopped);
}
};
runAnIter();
});
}
/** Emulated cycles since power-on, undoing the per-second rebasing execute() applies. */
get elapsedCycles() {
return cycleCount(this.processor);
}
async runUntilVblank() {
const { vsyncAddress } = this.model;
if (vsyncAddress === null) throw new Error(`runUntilVblank does not know the ${this.model.name} OS`);
await this._runUntilAddress(vsyncAddress, VblankSearchCycles);
}
async runUntilInput(secs) {
console.log("Running until keyboard input requested");
await this.runUntilAddress(this.model.idleAddress, secs);
return this.runFor(10 * 1000);
}
/** The sideways bank paged in at &8000 to &BFFF. */
get pagedBank() {
return this.processor.pagedBank;
}
/** Whether sideways `bank` is paged in at `address`; see the CPU's isBankPagedAt. */
isBankPagedAt(bank, address) {
return this.processor.isBankPagedAt(bank, address);
}
/**
* Run until the PC reaches `targetAddr`, with sideways `bank` paged in if
* one is given, or throw after `secs` of emulated time.
*/
async runUntilAddress(targetAddr, secs, { bank } = {}) {
if (!secs) secs = 120;
await this._runUntilAddress(targetAddr, secs * this.model.cyclesPerSecond, { bank });
}
/**
* Stop any run when the CPU executes, reads or writes `address`, only
* while sideways `bank` is paged in there if one is given, calling
* `onHit` first with the byte read or written. An execute breakpoint
* never fires for the instruction the CPU resumes at after a stop.
* Returns a handle whose `remove()` ends the watch.
* @param {"execute"|"read"|"write"|"access"} type
* @param {number} address
* @param {{bank: (number|undefined)}} opts
* @param {function(number=): void} onHit
*/
addBreakpoint(type, address, { bank }, onHit) {
const { breakpoints } = this.processor;
const id = breakpoints.add({
kind: type,
start: address,
bank,
owner: "TestMachine",
onHit: ({ value }) => onHit(value),
});
return { remove: () => breakpoints.remove(id) };
}
async _runUntilAddress(address, cycles, { bank } = {}) {
let hit = false;
const hook = this.addBreakpoint("execute", address, { bank }, () => {
hit = true;
});
try {
await this.runFor(cycles);
} finally {
hook.remove();
}
assert(hit, "did not hit appropriate breakpoint in time");
}
async loadDisc(image) {
const data = await loadData(image);
this.processor.fdc.loadDisc(0, fdc.discFor(image, data));
}
/**
* Load a disc image from raw data (Uint8Array or Buffer).
* @param {Uint8Array|Buffer} data - raw disc image bytes
*/
loadDiscData(data) {
this.processor.fdc.loadDisc(0, fdc.discFor("", data));
}
/**
* Reset the machine.
* @param {boolean} hard - true for power-on reset, false for soft reset
*/
reset(hard) {
this.processor.reset(hard);
}
/**
* Take a snapshot of the entire machine state (CPU, RAM, SWRAM,
* VIAs, video, FDC, etc). Returns an opaque state object that
* can be passed to restore().
*/
snapshot({ includeRoms = true } = {}) {
return this.processor.snapshotState({ includeRoms });
}
/**
* Restore a previously saved snapshot. The machine will be in
* exactly the state it was when snapshot() was called.
*/
restore(state) {
this.processor.restoreState(state);
}
async loadBasic(source) {
const tokeniser = await Tokeniser.create();
const tokenised = tokeniser.tokenise(source);
installBasic(tokenised, {
readByte: (addr) => this.readbyte(addr),
writeByte: (addr, value) => this.writebyte(addr, value),
});
}
/**
* Types the text at the machine, then a RETURN unless it already ends in
* one, running the CPU until the last key is up. The keys arrive between
* instructions, so a breakpoint that stops the CPU part way leaves the rest
* to be typed when it runs again.
*/
async type(text) {
const lines = text.replace(/\r\n?/g, "\n");
const entered = lines.endsWith("\n") ? lines : `${lines}\n`;
this.typist.type(this.model.stringToKeys(entered), true);
while (this.typist.isTyping) {
const stopped = await this.runFor(MaxCyclesPerIter);
if (stopped) break;
}
}
/**
* Press a key on the keyboard. Throws on a code that is not in `keyCodes`.
* @param {string} code - the host key by physical position, as `keyCodes` in keymap.js names it
* @param {boolean} [shiftDown] - whether the host's shift is held, which only a natural layout maps on
*/
keyDown(code, shiftDown = false) {
requireKnownKeyCode("keyDown", code);
this.processor.keyboardInterface.keyDown(code, shiftDown);
}
/**
* Release a key on the keyboard. Throws on a code that is not in `keyCodes`.
* @param {string} code - the host key by physical position
*/
keyUp(code) {
requireKnownKeyCode("keyUp", code);
this.processor.keyboardInterface.keyUp(code);
}
/**
* Load a ROM image directly into a sideways RAM slot.
* @param {number} slot - slot number (0-15, typically 4-7 for SWRAM)
* @param {Uint8Array|Buffer} data - ROM data (up to one 16KB bank)
*/
loadSidewaysRam(slot, data) {
this.processor.romBank(slot).set(data.subarray(0, RomBankBytes));
}
writebyte(addr, val) {
this.processor.writemem(addr, val);
}
readbyte(addr) {
return this.processor.readmem(addr);
}
readword(addr) {
return this.readbyte(addr) | (this.readbyte(addr + 1) << 8);
}
/**
* Decodes the machine's VDU output into text elements for `onElement`
* until the returned capture's `stop()` is called; see VduTextCapture.
*/
captureText(onElement) {
const capture = new VduTextCapture(onElement, { isAtom: this.model.isAtom });
capture.stop = this.onVduChar((c) => capture.onChar(c));
return capture;
}
}
|