jsbeeb 1.21.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/machine-session.js +65 -16
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"name": "jsbeeb",
|
|
8
8
|
"description": "Emulate a BBC Micro",
|
|
9
9
|
"repository": "git@github.com:mattgodbolt/jsbeeb.git",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.22.0",
|
|
11
11
|
"//engines": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=24.15.0"
|
package/src/machine-session.js
CHANGED
|
@@ -41,9 +41,9 @@ export class MachineSession {
|
|
|
41
41
|
this.modelName = modelName;
|
|
42
42
|
this._opts = opts;
|
|
43
43
|
|
|
44
|
-
// Raw RGBA framebuffer
|
|
44
|
+
// Raw RGBA framebuffer. The Video chip renders into _fb32 (cleared each frame).
|
|
45
45
|
// _completeFb8 is a snapshot taken at paint time (the equivalent of the browser canvas)
|
|
46
|
-
// and is what screenshot() reads from
|
|
46
|
+
// and is what screenshot() reads from, always a complete frame, never mid-render.
|
|
47
47
|
this._fb8 = new Uint8Array(FB_WIDTH * FB_HEIGHT * 4);
|
|
48
48
|
this._fb32 = new Uint32Array(this._fb8.buffer);
|
|
49
49
|
this._completeFb8 = new Uint8Array(FB_WIDTH * FB_HEIGHT * 4);
|
|
@@ -84,16 +84,16 @@ export class MachineSession {
|
|
|
84
84
|
hasTeletextAdaptor: opts.hasTeletextAdaptor,
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
// Accumulated VDU text output
|
|
87
|
+
// Accumulated VDU text output, drained by callers
|
|
88
88
|
this._pendingOutput = [];
|
|
89
89
|
this._flushCapture = () => {};
|
|
90
90
|
|
|
91
|
-
// Breakpoint management
|
|
91
|
+
// Breakpoint management, with persistent hooks that survive across run calls
|
|
92
92
|
this._breakpoints = new Map(); // id → { hook, type, address, hit }
|
|
93
93
|
this._nextBreakpointId = 1;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/** Load ROMs and hardware
|
|
96
|
+
/** Load ROMs and hardware; call once before anything else */
|
|
97
97
|
async initialise() {
|
|
98
98
|
setNodeBasePath(_jsbeebRoot);
|
|
99
99
|
await this._machine.initialise();
|
|
@@ -117,12 +117,12 @@ export class MachineSession {
|
|
|
117
117
|
*
|
|
118
118
|
* WRCHV discovery: RAM at the OS write-character vector (0x20E on BBC,
|
|
119
119
|
* 0x208 on Atom) initialises to 0x0000 before the OS runs. We read
|
|
120
|
-
* directly from cpu.ramRomOs (two array lookups
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
120
|
+
* directly from cpu.ramRomOs (two array lookups, no readmem() dispatch
|
|
121
|
+
* overhead) on every instruction, waiting for the value to change from
|
|
122
|
+
* its initial 0xFFFF. Once the OS installs a real handler we use that
|
|
123
|
+
* address for the lifetime of the session. Programs that later install
|
|
124
|
+
* a custom VDU driver are handled seamlessly because we always re-read
|
|
125
|
+
* from the live memory.
|
|
126
126
|
*
|
|
127
127
|
* Text elements: { x, y, text, foreground, background, mode }
|
|
128
128
|
* Screenshots (via the real Video chip) are the right tool for anything
|
|
@@ -130,7 +130,7 @@ export class MachineSession {
|
|
|
130
130
|
*/
|
|
131
131
|
_installCaptureHook() {
|
|
132
132
|
const cpu = this._machine.processor;
|
|
133
|
-
const ram = cpu.ramRomOs; // direct Uint8Array
|
|
133
|
+
const ram = cpu.ramRomOs; // direct Uint8Array, no dispatch overhead
|
|
134
134
|
// WRCHV vector: BBC at $020E, Atom at $0208.
|
|
135
135
|
const wrchvAddr = this._isAtom ? 0x208 : 0x20e;
|
|
136
136
|
const initialWrchv = ram[wrchvAddr] | (ram[wrchvAddr + 1] << 8); // 0xFFFF pre-boot
|
|
@@ -156,6 +156,26 @@ export class MachineSession {
|
|
|
156
156
|
// that hasn't been terminated by a control character.
|
|
157
157
|
this._flushCapture = flush;
|
|
158
158
|
|
|
159
|
+
// Expose the decoder's cursor so snapshot()/restore() can rewind it too:
|
|
160
|
+
// text captured after a restore would otherwise carry the coordinates
|
|
161
|
+
// and colours the abandoned run had reached.
|
|
162
|
+
this._snapshotCapture = () => ({
|
|
163
|
+
attributes: { ...attributes },
|
|
164
|
+
currentText,
|
|
165
|
+
params: [...params],
|
|
166
|
+
nextN,
|
|
167
|
+
});
|
|
168
|
+
this._restoreCapture = (state) => {
|
|
169
|
+
Object.assign(attributes, state.attributes);
|
|
170
|
+
currentText = state.currentText;
|
|
171
|
+
params = [...state.params];
|
|
172
|
+
nextN = state.nextN;
|
|
173
|
+
// vduProc is a closure and does not survive, so a snapshot taken
|
|
174
|
+
// mid-sequence resumes with nextN still swallowing the parameters:
|
|
175
|
+
// the sequence is dropped rather than printed as text.
|
|
176
|
+
vduProc = null;
|
|
177
|
+
};
|
|
178
|
+
|
|
159
179
|
const isAtom = this._isAtom;
|
|
160
180
|
|
|
161
181
|
function onChar(c) {
|
|
@@ -244,7 +264,7 @@ export class MachineSession {
|
|
|
244
264
|
}
|
|
245
265
|
|
|
246
266
|
cpu.debugInstruction.add((addr) => {
|
|
247
|
-
// Two direct array reads
|
|
267
|
+
// Two direct array reads, no function-call dispatch overhead.
|
|
248
268
|
// Once the OS sets WRCHV (it changes from 0xFFFF), we start
|
|
249
269
|
// capturing. Programs that install a custom VDU driver mid-run
|
|
250
270
|
// are handled transparently because we re-read on every call.
|
|
@@ -280,6 +300,35 @@ export class MachineSession {
|
|
|
280
300
|
this._pendingOutput = [];
|
|
281
301
|
}
|
|
282
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Capture machine state and captured text as an opaque object for restore().
|
|
305
|
+
* Leaves the running session undisturbed.
|
|
306
|
+
*
|
|
307
|
+
* @param {Object} [opts]
|
|
308
|
+
* @param {boolean} [opts.includeRoms=true] - carry the ROM contents too.
|
|
309
|
+
* Only safe to omit when restoring into a session built from the same
|
|
310
|
+
* model, which is then left with whatever ROMs it already had.
|
|
311
|
+
*/
|
|
312
|
+
snapshot({ includeRoms = true } = {}) {
|
|
313
|
+
return {
|
|
314
|
+
machine: this._machine.snapshot({ includeRoms }),
|
|
315
|
+
pendingOutput: this._pendingOutput.map((element) => ({ ...element })),
|
|
316
|
+
capture: this._snapshotCapture(),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Put back a state from snapshot(). `elapsedCycles` rewinds with the
|
|
322
|
+
* machine; breakpoints belong to the session, not the machine, so they and
|
|
323
|
+
* their hit flags are left alone, and `frameCount` keeps counting the way it
|
|
324
|
+
* does across a hard reset.
|
|
325
|
+
*/
|
|
326
|
+
restore(state) {
|
|
327
|
+
this._machine.restore(state.machine);
|
|
328
|
+
this._pendingOutput = state.pendingOutput.map((element) => ({ ...element }));
|
|
329
|
+
this._restoreCapture(state.capture);
|
|
330
|
+
}
|
|
331
|
+
|
|
283
332
|
/** Tokenise BBC BASIC source and write it into PAGE */
|
|
284
333
|
async loadBasic(source) {
|
|
285
334
|
await this._machine.loadBasic(source);
|
|
@@ -476,7 +525,7 @@ export class MachineSession {
|
|
|
476
525
|
*
|
|
477
526
|
* @param {Object} [opts]
|
|
478
527
|
* @param {boolean} [opts.clear=true] - If true (default), clear the buffer
|
|
479
|
-
* after returning it. Pass false to peek without consuming
|
|
528
|
+
* after returning it. Pass false to peek without consuming; the same
|
|
480
529
|
* elements will be returned again on the next call.
|
|
481
530
|
*
|
|
482
531
|
* Each element: { x, y, text, foreground, background, mode }
|
|
@@ -527,14 +576,14 @@ export class MachineSession {
|
|
|
527
576
|
/**
|
|
528
577
|
* Capture the current screen as a PNG.
|
|
529
578
|
* Returns a Buffer containing a 1024×625 PNG (the full emulated display,
|
|
530
|
-
* including borders
|
|
579
|
+
* including borders, matching what the browser renders).
|
|
531
580
|
*
|
|
532
581
|
* The active display area is roughly:
|
|
533
582
|
* x: leftBorder .. 1024-rightBorder
|
|
534
583
|
* y: topBorder .. 625-bottomBorder
|
|
535
584
|
*/
|
|
536
585
|
async screenshot() {
|
|
537
|
-
// Read from _completeFb8
|
|
586
|
+
// Read from _completeFb8, the last fully-painted frame snapshotted in paint_ext.
|
|
538
587
|
// _fb8/_fb32 is the live render buffer (cleared and partially refilled each frame).
|
|
539
588
|
return sharp(Buffer.from(this._completeFb8.buffer), {
|
|
540
589
|
raw: { width: FB_WIDTH, height: FB_HEIGHT, channels: 4 },
|