jsbeeb 1.22.4 → 1.24.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/README.md +2 -2
- package/package.json +16 -11
- package/src/6502.js +39 -42
- package/src/6502.opcodes.js +198 -123
- package/src/6847.js +4 -5
- package/src/6847_fontdata.js +0 -2
- package/src/acia.js +2 -3
- package/src/app/app.js +0 -1
- package/src/app/args.js +0 -2
- package/src/app/electron.js +4 -6
- package/src/app-bench.js +1 -3
- package/src/archive.js +243 -0
- package/src/basic-loader.js +0 -7
- package/src/basic-tokenise.js +19 -6
- package/src/bbc-palette.js +0 -2
- package/src/bbcdiscs.js +0 -2
- package/src/bem-snapshot.js +1 -3
- package/src/binary.js +175 -0
- package/src/cmos.js +0 -2
- package/src/ddnoise.js +0 -1
- package/src/debounce.js +7 -0
- package/src/disc-surface.js +0 -2
- package/src/disc.js +10 -9
- package/src/fake6502.js +7 -23
- package/src/fdc.js +9 -51
- package/src/filestore.js +10 -17
- package/src/hex.js +38 -0
- package/src/index.js +11 -0
- package/src/intel-fdc.js +18 -20
- package/src/jsbeeb.css +33 -0
- package/src/{utils_atom.js → keymap-atom.js} +2 -18
- package/src/{utils.js → keymap.js} +47 -610
- package/src/loader.js +63 -0
- package/src/machine-session.js +37 -177
- package/src/machine-spec.js +59 -0
- package/src/main.js +69 -184
- package/src/media-resolver.js +68 -0
- package/src/mmc.js +3 -2
- package/src/models.js +40 -4
- package/src/music5000.js +0 -2
- package/src/ppia.js +3 -4
- package/src/printer.js +7 -14
- package/src/relaynoise.js +0 -1
- package/src/rewind.js +0 -2
- package/src/sample-player.js +2 -3
- package/src/serial.js +0 -2
- package/src/snapshot-helpers.js +0 -2
- package/src/snapshot.js +0 -2
- package/src/soundchip.js +1 -1
- package/src/state-utils.js +0 -2
- package/src/sth.js +2 -4
- package/src/tapes.js +10 -4
- package/src/teletext.js +2 -3
- package/src/teletext_adaptor.js +2 -4
- package/src/teletext_data.js +0 -2
- package/src/test-machine.js +307 -0
- package/src/touchscreen.js +2 -4
- package/src/tube.js +5 -6
- package/src/typist.js +103 -0
- package/src/uef-snapshot.js +0 -2
- package/src/url-params.js +4 -5
- package/src/vdu-capture.js +113 -0
- package/src/via.js +3 -4
- package/src/video-filters/pal-composite.js +0 -2
- package/src/video-filters/passthrough-filter.js +0 -2
- package/src/video-filters/pixel-grid.js +0 -2
- package/src/video-filters/shader-program.js +0 -2
- package/src/video-filters/xbr-filter.js +0 -2
- package/src/video.js +12 -11
- package/src/wd-fdc.js +20 -20
- package/src/web/analogue-inputs.js +6 -8
- package/src/web/analytics.js +13 -0
- package/src/web/audio-handler.js +2 -2
- package/src/{audio-utils.js → web/audio-utils.js} +0 -2
- package/src/web/autoboot.js +10 -13
- package/src/{canvas.js → web/canvas.js} +4 -5
- package/src/web/config.js +42 -72
- package/src/web/console-surface.js +25 -0
- package/src/web/debug.js +8 -49
- package/src/web/disc-visualiser.js +0 -2
- package/src/web/display.js +21 -4
- package/src/{dom-utils.js → web/dom-utils.js} +1 -11
- package/src/web/drives.js +1 -1
- package/src/web/emulation-loop.js +54 -33
- package/src/web/front-panel.js +13 -12
- package/src/{gamepads.js → web/gamepads.js} +2 -6
- package/src/web/google-drive-picker.js +14 -9
- package/src/{google-drive.js → web/google-drive.js} +9 -10
- package/src/web/hfe-picker.js +5 -4
- package/src/web/icons.js +42 -0
- package/src/web/keyboard-setup.js +29 -40
- package/src/{keyboard.js → web/keyboard.js} +18 -119
- package/src/web/layout.js +4 -0
- package/src/web/local-disc.js +44 -0
- package/src/web/machine.js +27 -20
- package/src/web/media-loader.js +38 -147
- package/src/{microphone-input.js → web/microphone-input.js} +1 -1
- package/src/web/modals.js +6 -9
- package/src/web/page-actions.js +88 -0
- package/src/web/quick-settings.js +16 -15
- package/src/web/reporting.js +0 -7
- package/src/{rewind-thumbnail.js → web/rewind-thumbnail.js} +0 -2
- package/src/web/rewind-ui.js +29 -29
- package/src/web/run-controls.js +39 -0
- package/src/web/settings.js +64 -135
- package/src/web/snapshot-ui.js +10 -16
- package/src/{speech-output.js → web/speech-output.js} +0 -2
- package/src/web/sth-picker.js +9 -7
- package/src/web/url-state.js +20 -6
- package/src/econet/L3FS.dat +0 -0
- package/src/econet/scsi.dat +0 -0
- package/tests/test-machine.js +0 -609
package/src/loader.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { stringToUint8Array } from "./binary.js";
|
|
2
|
+
|
|
3
|
+
export const runningInNode = typeof window === "undefined";
|
|
4
|
+
|
|
5
|
+
function loadDataHttp(url) {
|
|
6
|
+
return new Promise(function (resolve, reject) {
|
|
7
|
+
const request = new XMLHttpRequest();
|
|
8
|
+
request.open("GET", url, true);
|
|
9
|
+
request.overrideMimeType("text/plain; charset=x-user-defined");
|
|
10
|
+
request.onload = function () {
|
|
11
|
+
if (request.status !== 200) {
|
|
12
|
+
reject(new Error("Unable to load " + url + ", http code " + request.status));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (typeof request.response !== "string") {
|
|
16
|
+
resolve(request.response);
|
|
17
|
+
} else {
|
|
18
|
+
resolve(stringToUint8Array(request.response));
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
request.onerror = function () {
|
|
22
|
+
reject(new Error("A network error occurred loading " + url));
|
|
23
|
+
};
|
|
24
|
+
request.send(null);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let _nodeBasePath = null;
|
|
29
|
+
|
|
30
|
+
export function setNodeBasePath(basePath) {
|
|
31
|
+
_nodeBasePath = basePath;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function loadDataNode(url) {
|
|
35
|
+
if (url.startsWith("file:")) {
|
|
36
|
+
const fs = await import("fs");
|
|
37
|
+
const { fileURLToPath } = await import("url");
|
|
38
|
+
return fs.readFileSync(fileURLToPath(url));
|
|
39
|
+
}
|
|
40
|
+
if (/^https?:\/\//.test(url)) {
|
|
41
|
+
const response = await fetch(url);
|
|
42
|
+
if (!response.ok) throw new Error(`Unable to load ${url}, http code ${response.status}`);
|
|
43
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
44
|
+
}
|
|
45
|
+
const fs = await import("fs");
|
|
46
|
+
const nodePath = await import("path");
|
|
47
|
+
if (_nodeBasePath) {
|
|
48
|
+
const publicPath = nodePath.join(_nodeBasePath, "public", url);
|
|
49
|
+
if (fs.existsSync(publicPath)) return fs.readFileSync(publicPath);
|
|
50
|
+
return fs.readFileSync(nodePath.join(_nodeBasePath, url));
|
|
51
|
+
}
|
|
52
|
+
if (url[0] === "/") url = "." + url;
|
|
53
|
+
if (fs.existsSync("public/" + url)) return fs.readFileSync("public/" + url);
|
|
54
|
+
return fs.readFileSync(url);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function loadData(url) {
|
|
58
|
+
if (runningInNode) {
|
|
59
|
+
return loadDataNode(url);
|
|
60
|
+
} else {
|
|
61
|
+
return loadDataHttp(url);
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/machine-session.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
import { readFileSync } from "fs";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
import path from "path";
|
|
11
|
-
import { TestMachine } from "
|
|
11
|
+
import { TestMachine } from "./test-machine.js";
|
|
12
|
+
import { VduTextCapture } from "./vdu-capture.js";
|
|
12
13
|
import { InstrumentedSoundChip, FakeSoundChip } from "./soundchip.js";
|
|
13
14
|
|
|
14
15
|
// Resolve the jsbeeb package root from our own location (src/machine-session.js
|
|
@@ -16,10 +17,12 @@ import { InstrumentedSoundChip, FakeSoundChip } from "./soundchip.js";
|
|
|
16
17
|
// files relative to this package regardless of the calling process's cwd.
|
|
17
18
|
const _jsbeebRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
18
19
|
import * as fdc from "./fdc.js";
|
|
20
|
+
import { MediaResolver } from "./media-resolver.js";
|
|
21
|
+
import { StairwayToHell } from "./sth.js";
|
|
22
|
+
import { BbcDiscArchive } from "./bbcdiscs.js";
|
|
19
23
|
import { Video } from "./video.js";
|
|
20
24
|
import { findModel } from "./models.js";
|
|
21
|
-
import { setNodeBasePath } from "./
|
|
22
|
-
import sharp from "sharp";
|
|
25
|
+
import { setNodeBasePath } from "./loader.js";
|
|
23
26
|
|
|
24
27
|
// BBC framebuffer is 1024×625 pixels
|
|
25
28
|
const FB_WIDTH = 1024;
|
|
@@ -86,7 +89,7 @@ export class MachineSession {
|
|
|
86
89
|
|
|
87
90
|
// Accumulated VDU text output, drained by callers
|
|
88
91
|
this._pendingOutput = [];
|
|
89
|
-
this.
|
|
92
|
+
this._capture = new VduTextCapture((element) => this._pendingOutput.push(element), { isAtom: this._isAtom });
|
|
90
93
|
|
|
91
94
|
// Breakpoint management, with persistent hooks that survive across run calls
|
|
92
95
|
this._breakpoints = new Map(); // id → { hook, type, address, hit }
|
|
@@ -100,7 +103,7 @@ export class MachineSession {
|
|
|
100
103
|
if (this._opts.discImage) {
|
|
101
104
|
this.loadDisc(this._opts.discImage);
|
|
102
105
|
}
|
|
103
|
-
this.
|
|
106
|
+
this._machine.onVduChar((c) => this._capture.onChar(c));
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
/**
|
|
@@ -112,170 +115,6 @@ export class MachineSession {
|
|
|
112
115
|
return this.drainOutput();
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
/**
|
|
116
|
-
* Install the VDU character-output capture hook.
|
|
117
|
-
*
|
|
118
|
-
* WRCHV discovery: RAM at the OS write-character vector (0x20E on BBC,
|
|
119
|
-
* 0x208 on Atom) initialises to 0x0000 before the OS runs. We read
|
|
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
|
-
*
|
|
127
|
-
* Text elements: { x, y, text, foreground, background, mode }
|
|
128
|
-
* Screenshots (via the real Video chip) are the right tool for anything
|
|
129
|
-
* visual; this capture is a lightweight aid for text-mode output only.
|
|
130
|
-
*/
|
|
131
|
-
_installCaptureHook() {
|
|
132
|
-
const cpu = this._machine.processor;
|
|
133
|
-
const ram = cpu.ramRomOs; // direct Uint8Array, no dispatch overhead
|
|
134
|
-
// WRCHV vector: BBC at $020E, Atom at $0208.
|
|
135
|
-
const wrchvAddr = this._isAtom ? 0x208 : 0x20e;
|
|
136
|
-
const initialWrchv = ram[wrchvAddr] | (ram[wrchvAddr + 1] << 8); // 0xFFFF pre-boot
|
|
137
|
-
|
|
138
|
-
const attributes = { x: 0, y: 0, text: "", foreground: 7, background: 0, mode: 7 };
|
|
139
|
-
let currentText = "";
|
|
140
|
-
let params = [];
|
|
141
|
-
let nextN = 0;
|
|
142
|
-
let vduProc = null;
|
|
143
|
-
|
|
144
|
-
const onElement = (elem) => this._pendingOutput.push({ ...elem });
|
|
145
|
-
|
|
146
|
-
function flush() {
|
|
147
|
-
if (currentText.length) {
|
|
148
|
-
attributes.text = currentText;
|
|
149
|
-
onElement({ ...attributes });
|
|
150
|
-
attributes.x += currentText.length;
|
|
151
|
-
}
|
|
152
|
-
currentText = "";
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Expose flush so drainOutput can capture trailing text
|
|
156
|
-
// that hasn't been terminated by a control character.
|
|
157
|
-
this._flushCapture = flush;
|
|
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
|
-
|
|
179
|
-
const isAtom = this._isAtom;
|
|
180
|
-
|
|
181
|
-
function onChar(c) {
|
|
182
|
-
if (nextN) {
|
|
183
|
-
params.push(c);
|
|
184
|
-
if (--nextN === 0) {
|
|
185
|
-
if (vduProc) vduProc(params);
|
|
186
|
-
params = [];
|
|
187
|
-
vduProc = null;
|
|
188
|
-
}
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
switch (c) {
|
|
192
|
-
case 10: // LF
|
|
193
|
-
flush();
|
|
194
|
-
attributes.y++;
|
|
195
|
-
break;
|
|
196
|
-
case 12: // CLS
|
|
197
|
-
flush();
|
|
198
|
-
attributes.x = 0;
|
|
199
|
-
attributes.y = 0;
|
|
200
|
-
break;
|
|
201
|
-
case 13: // CR
|
|
202
|
-
flush();
|
|
203
|
-
attributes.x = 0;
|
|
204
|
-
break;
|
|
205
|
-
default:
|
|
206
|
-
// BBC VDU control codes (multi-byte sequences).
|
|
207
|
-
// The Atom doesn't use these; skip them to avoid
|
|
208
|
-
// swallowing printable characters.
|
|
209
|
-
if (!isAtom) {
|
|
210
|
-
switch (c) {
|
|
211
|
-
case 1: // next char to printer only
|
|
212
|
-
nextN = 1;
|
|
213
|
-
return;
|
|
214
|
-
case 17: // COLOUR n
|
|
215
|
-
nextN = 1;
|
|
216
|
-
vduProc = (p) => {
|
|
217
|
-
if (p[0] & 0x80) attributes.background = p[0] & 0xf;
|
|
218
|
-
else attributes.foreground = p[0] & 0xf;
|
|
219
|
-
};
|
|
220
|
-
return;
|
|
221
|
-
case 18: // GCOL
|
|
222
|
-
nextN = 2;
|
|
223
|
-
return;
|
|
224
|
-
case 19: // define logical colour
|
|
225
|
-
nextN = 5;
|
|
226
|
-
return;
|
|
227
|
-
case 22: // MODE n
|
|
228
|
-
nextN = 1;
|
|
229
|
-
vduProc = (p) => {
|
|
230
|
-
flush();
|
|
231
|
-
attributes.mode = p[0];
|
|
232
|
-
attributes.x = 0;
|
|
233
|
-
attributes.y = 0;
|
|
234
|
-
attributes.foreground = 7;
|
|
235
|
-
attributes.background = 0;
|
|
236
|
-
};
|
|
237
|
-
return;
|
|
238
|
-
case 25: // PLOT
|
|
239
|
-
nextN = 5;
|
|
240
|
-
return;
|
|
241
|
-
case 28: // define text window
|
|
242
|
-
nextN = 4;
|
|
243
|
-
return;
|
|
244
|
-
case 29: // define graphics origin
|
|
245
|
-
nextN = 4;
|
|
246
|
-
return;
|
|
247
|
-
case 31: // TAB(x,y)
|
|
248
|
-
nextN = 2;
|
|
249
|
-
vduProc = (p) => {
|
|
250
|
-
flush();
|
|
251
|
-
attributes.x = p[0];
|
|
252
|
-
attributes.y = p[1];
|
|
253
|
-
};
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (c >= 32 && c < 0x7f) {
|
|
258
|
-
currentText += String.fromCharCode(c);
|
|
259
|
-
} else {
|
|
260
|
-
flush();
|
|
261
|
-
}
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
cpu.debugInstruction.add((addr) => {
|
|
267
|
-
// Two direct array reads, no function-call dispatch overhead.
|
|
268
|
-
// Once the OS sets WRCHV (it changes from 0xFFFF), we start
|
|
269
|
-
// capturing. Programs that install a custom VDU driver mid-run
|
|
270
|
-
// are handled transparently because we re-read on every call.
|
|
271
|
-
const wrchv = ram[wrchvAddr] | (ram[wrchvAddr + 1] << 8);
|
|
272
|
-
if (wrchv !== initialWrchv && addr === wrchv) {
|
|
273
|
-
onChar(cpu.a);
|
|
274
|
-
}
|
|
275
|
-
return false;
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
118
|
/**
|
|
280
119
|
* Press a key (by browser keyCode).
|
|
281
120
|
* Use utils.keyCodes for named keys, or ASCII charCode for letters/digits.
|
|
@@ -313,7 +152,7 @@ export class MachineSession {
|
|
|
313
152
|
return {
|
|
314
153
|
machine: this._machine.snapshot({ includeRoms }),
|
|
315
154
|
pendingOutput: this._pendingOutput.map((element) => ({ ...element })),
|
|
316
|
-
capture: this.
|
|
155
|
+
capture: this._capture.snapshot(),
|
|
317
156
|
};
|
|
318
157
|
}
|
|
319
158
|
|
|
@@ -326,7 +165,7 @@ export class MachineSession {
|
|
|
326
165
|
restore(state) {
|
|
327
166
|
this._machine.restore(state.machine);
|
|
328
167
|
this._pendingOutput = state.pendingOutput.map((element) => ({ ...element }));
|
|
329
|
-
this.
|
|
168
|
+
this._capture.restore(state.capture);
|
|
330
169
|
}
|
|
331
170
|
|
|
332
171
|
/** Tokenise BBC BASIC source and write it into PAGE */
|
|
@@ -364,10 +203,9 @@ export class MachineSession {
|
|
|
364
203
|
await this._machine.runFor(cycles);
|
|
365
204
|
}
|
|
366
205
|
|
|
367
|
-
/** Emulated cycles since power-on
|
|
206
|
+
/** Emulated cycles since power-on */
|
|
368
207
|
get elapsedCycles() {
|
|
369
|
-
|
|
370
|
-
return cpu.cycleSeconds * cpu.model.cyclesPerSecond + cpu.currentCycles;
|
|
208
|
+
return this._machine.elapsedCycles;
|
|
371
209
|
}
|
|
372
210
|
|
|
373
211
|
/**
|
|
@@ -516,8 +354,28 @@ export class MachineSession {
|
|
|
516
354
|
*/
|
|
517
355
|
loadDisc(imagePath) {
|
|
518
356
|
const data = new Uint8Array(readFileSync(imagePath));
|
|
519
|
-
|
|
520
|
-
|
|
357
|
+
this._machine.processor.fdc.loadDisc(0, fdc.discFor(imagePath, data));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Put a disc in a drive by any reference the web page's URL accepts: a bare
|
|
362
|
+
* name from the built-in discs, `sth:` or `hfe:` for the archives, or a URL.
|
|
363
|
+
* Returns the name of the image loaded and any others the archive held.
|
|
364
|
+
*/
|
|
365
|
+
async loadDiscImage(ref, drive = 0) {
|
|
366
|
+
const { name, data, ignored } = await this.mediaResolver().resolve("disc", ref);
|
|
367
|
+
this._machine.processor.fdc.loadDisc(drive, fdc.discFor(name, data));
|
|
368
|
+
return { name, ignored };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
mediaResolver() {
|
|
372
|
+
if (!this._mediaResolver) {
|
|
373
|
+
const quiet = () => {};
|
|
374
|
+
this._mediaResolver = new MediaResolver();
|
|
375
|
+
this._mediaResolver.addSource("sth", (file) => new StairwayToHell(quiet, quiet, quiet).fetch(file));
|
|
376
|
+
this._mediaResolver.addSource("hfe", (path) => new BbcDiscArchive(quiet, quiet, quiet).fetch(path));
|
|
377
|
+
}
|
|
378
|
+
return this._mediaResolver;
|
|
521
379
|
}
|
|
522
380
|
|
|
523
381
|
/**
|
|
@@ -532,7 +390,7 @@ export class MachineSession {
|
|
|
532
390
|
* Also includes a flat `screenText` reconstruction.
|
|
533
391
|
*/
|
|
534
392
|
drainOutput({ clear = true } = {}) {
|
|
535
|
-
this.
|
|
393
|
+
this._capture.flush();
|
|
536
394
|
const elements = clear ? this._pendingOutput.splice(0) : [...this._pendingOutput];
|
|
537
395
|
return {
|
|
538
396
|
elements,
|
|
@@ -585,6 +443,7 @@ export class MachineSession {
|
|
|
585
443
|
async screenshot() {
|
|
586
444
|
// Read from _completeFb8, the last fully-painted frame snapshotted in paint_ext.
|
|
587
445
|
// _fb8/_fb32 is the live render buffer (cleared and partially refilled each frame).
|
|
446
|
+
const { default: sharp } = await import("sharp");
|
|
588
447
|
return sharp(Buffer.from(this._completeFb8.buffer), {
|
|
589
448
|
raw: { width: FB_WIDTH, height: FB_HEIGHT, channels: 4 },
|
|
590
449
|
})
|
|
@@ -609,6 +468,7 @@ export class MachineSession {
|
|
|
609
468
|
const w = FB_WIDTH - left - right;
|
|
610
469
|
const h = FB_HEIGHT - top - bottom;
|
|
611
470
|
|
|
471
|
+
const { default: sharp } = await import("sharp");
|
|
612
472
|
return sharp(Buffer.from(this._completeFb8.buffer), {
|
|
613
473
|
raw: { width: FB_WIDTH, height: FB_HEIGHT, channels: 4 },
|
|
614
474
|
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Cmos } from "./cmos.js";
|
|
2
|
+
import { FakeDdNoise } from "./ddnoise.js";
|
|
3
|
+
import { FakeMusic5000 } from "./music5000.js";
|
|
4
|
+
import { FakeRelayNoise } from "./relaynoise.js";
|
|
5
|
+
import { FakeSoundChip } from "./soundchip.js";
|
|
6
|
+
import { FakeVideo } from "./video.js";
|
|
7
|
+
|
|
8
|
+
const NullUserPort = {
|
|
9
|
+
write() {},
|
|
10
|
+
read() {
|
|
11
|
+
return 0xff;
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const SpecDefaults = {
|
|
16
|
+
keyLayout: "physical",
|
|
17
|
+
cpuMultiplier: 1,
|
|
18
|
+
tubeCpuMultiplier: 1,
|
|
19
|
+
videoCyclesBatch: 0,
|
|
20
|
+
tube: null,
|
|
21
|
+
hasMusic5000: false,
|
|
22
|
+
hasTeletextAdaptor: false,
|
|
23
|
+
extraRoms: [],
|
|
24
|
+
userPort: NullUserPort,
|
|
25
|
+
printerPort: null,
|
|
26
|
+
getGamepads: () => [],
|
|
27
|
+
debugFlags: { logFdcCommands: false, logFdcStateChanges: false },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What a machine is fitted with and how it is driven, complete and frozen:
|
|
32
|
+
* every field is present, an unknown one is an error, and an undefined
|
|
33
|
+
* override means the default. It is the `config` a CPU is built with.
|
|
34
|
+
*/
|
|
35
|
+
export function machineSpec(overrides = {}) {
|
|
36
|
+
const unknown = Object.keys(overrides).filter((field) => !Object.hasOwn(SpecDefaults, field));
|
|
37
|
+
if (unknown.length) throw new Error(`Unknown machine spec fields: ${unknown.join(", ")}`);
|
|
38
|
+
const given = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== undefined));
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
...SpecDefaults,
|
|
41
|
+
...given,
|
|
42
|
+
extraRoms: Object.freeze([...(given.extraRoms ?? SpecDefaults.extraRoms)]),
|
|
43
|
+
debugFlags: Object.freeze({ ...SpecDefaults.debugFlags, ...given.debugFlags }),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Peripherals that go nowhere, for a machine run headless. */
|
|
48
|
+
export function nullIo({ video = new FakeVideo(), soundChip = new FakeSoundChip() } = {}) {
|
|
49
|
+
return {
|
|
50
|
+
dbgr: { setCpu() {} },
|
|
51
|
+
video,
|
|
52
|
+
soundChip,
|
|
53
|
+
ddNoise: new FakeDdNoise(),
|
|
54
|
+
relayNoise: new FakeRelayNoise(),
|
|
55
|
+
music5000: new FakeMusic5000(),
|
|
56
|
+
cmos: new Cmos(),
|
|
57
|
+
econet: null,
|
|
58
|
+
};
|
|
59
|
+
}
|