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/printer.js
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Centronics printer attached to the user VIA: acknowledges each character and keeps the most
|
|
5
|
-
* recent output so it can be shown whenever a printer window is opened.
|
|
3
|
+
* recent output so it can be shown whenever a printer window is opened. Dispatches "output" with
|
|
4
|
+
* each character as its detail, and "first-output" once, when the machine first prints.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
export const MaxBufferedChars = 64 * 1024;
|
|
9
8
|
|
|
10
|
-
export class Printer {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* @param {function(string): void} [handlers.onOutput] called with each character as it is printed
|
|
14
|
-
* @param {function(): void} [handlers.onFirstOutput] called once, when the machine first prints
|
|
15
|
-
*/
|
|
16
|
-
constructor({ onOutput = () => {}, onFirstOutput = () => {} } = {}) {
|
|
17
|
-
this._onOutput = onOutput;
|
|
18
|
-
this._onFirstOutput = onFirstOutput;
|
|
9
|
+
export class Printer extends EventTarget {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
19
12
|
this._uservia = null;
|
|
20
13
|
this._olderText = "";
|
|
21
14
|
this._newerText = "";
|
|
@@ -52,8 +45,8 @@ export class Printer {
|
|
|
52
45
|
}
|
|
53
46
|
if (!this._hasPrinted) {
|
|
54
47
|
this._hasPrinted = true;
|
|
55
|
-
this.
|
|
48
|
+
this.dispatchEvent(new Event("first-output"));
|
|
56
49
|
}
|
|
57
|
-
this.
|
|
50
|
+
this.dispatchEvent(new CustomEvent("output", { detail: char }));
|
|
58
51
|
}
|
|
59
52
|
}
|
package/src/relaynoise.js
CHANGED
package/src/rewind.js
CHANGED
package/src/sample-player.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import * as utils from "./utils.js";
|
|
1
|
+
import { loadData } from "./loader.js";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Base class for audio components that load and play back sample buffers
|
|
@@ -27,7 +26,7 @@ export class SamplePlayer {
|
|
|
27
26
|
const entries = Object.entries(pathMap);
|
|
28
27
|
const decoded = await Promise.all(
|
|
29
28
|
entries.map(async ([, path]) => {
|
|
30
|
-
const data = await
|
|
29
|
+
const data = await loadData(path);
|
|
31
30
|
// Safari doesn't support the promise form of decodeAudioData.
|
|
32
31
|
return new Promise((resolve, reject) => {
|
|
33
32
|
this.context.decodeAudioData(
|
package/src/serial.js
CHANGED
package/src/snapshot-helpers.js
CHANGED
package/src/snapshot.js
CHANGED
package/src/soundchip.js
CHANGED
|
@@ -347,7 +347,7 @@ export class SoundChip {
|
|
|
347
347
|
updateSlowDataBus(slowDataBus, active) {
|
|
348
348
|
this.slowDataBus = slowDataBus;
|
|
349
349
|
this.active = active;
|
|
350
|
-
// TODO
|
|
350
|
+
// TODO(#835) this probably isn't modeled correctly. Currently the sound chip "notices" a new data bus value some
|
|
351
351
|
// fixed number of cycles after WE (write enable) is triggered. In reality, the sound chip likely pulls data off
|
|
352
352
|
// the bus at a fixed point in its cycle, iff WE is active.
|
|
353
353
|
if (active) {
|
package/src/state-utils.js
CHANGED
package/src/sth.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as utils from "./utils.js";
|
|
1
|
+
import { unzipDiscImage } from "./archive.js";
|
|
4
2
|
|
|
5
3
|
// Always https, whatever the page was loaded over: the mirror redirects plain
|
|
6
4
|
// http, so following the page's protocol would cost a redirect on every request
|
|
@@ -55,7 +53,7 @@ export class StairwayToHell {
|
|
|
55
53
|
const response = await fetch(url);
|
|
56
54
|
if (!response.ok) throw new Error(`Unable to load ${url}, http code ${response.status}`);
|
|
57
55
|
try {
|
|
58
|
-
return await
|
|
56
|
+
return await unzipDiscImage(new Uint8Array(await response.arrayBuffer()));
|
|
59
57
|
} catch (error) {
|
|
60
58
|
console.error("Failed to fetch file:", error);
|
|
61
59
|
throw error;
|
package/src/tapes.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import { DataStream, stringToUint8Array } from "./binary.js";
|
|
2
|
+
import { ungzip } from "./archive.js";
|
|
3
|
+
import { hexword } from "./hex.js";
|
|
3
4
|
|
|
4
5
|
function secsToClocks(secs, cpuSpeed) {
|
|
5
6
|
return (cpuSpeed * secs) | 0;
|
|
@@ -240,7 +241,7 @@ class UefTape {
|
|
|
240
241
|
acia.tone(0);
|
|
241
242
|
return secsToClocks(gap, this.cpuSpeed);
|
|
242
243
|
default:
|
|
243
|
-
console.log("Skipping unknown chunk " +
|
|
244
|
+
console.log("Skipping unknown chunk " + hexword(this.curChunk.id));
|
|
244
245
|
this.curChunk = this.readChunk();
|
|
245
246
|
break;
|
|
246
247
|
}
|
|
@@ -306,7 +307,12 @@ class TapefileTape {
|
|
|
306
307
|
}
|
|
307
308
|
|
|
308
309
|
export async function loadTapeFromData(name, data, model) {
|
|
309
|
-
const
|
|
310
|
+
const raw = stringToUint8Array(data);
|
|
311
|
+
if (raw && raw.length > 4 && raw[0] === 0x1f && raw[1] === 0x8b) {
|
|
312
|
+
console.log("Ungzipping " + name);
|
|
313
|
+
data = await ungzip(raw);
|
|
314
|
+
}
|
|
315
|
+
const stream = new DataStream(name, data);
|
|
310
316
|
if (stream.readByte(0) === 0xff && stream.readByte(1) === 0x04) {
|
|
311
317
|
console.log("Detected a 'tapefile' tape");
|
|
312
318
|
return new TapefileTape(stream, model);
|
package/src/teletext.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
import { makeChars } from "./teletext_data.js";
|
|
3
|
-
import { makeFast32 } from "./utils.js";
|
|
4
2
|
import { BbcDefaultPalette as BbcDefaultCollook } from "./bbc-palette.js";
|
|
3
|
+
import { makeFast32 } from "./binary.js";
|
|
5
4
|
|
|
6
5
|
export class Teletext {
|
|
7
6
|
constructor() {
|
|
@@ -341,7 +340,7 @@ export class Teletext {
|
|
|
341
340
|
|
|
342
341
|
// Flashing text starts off in sync with a slow cursor, extinguished together. Multiple MODE
|
|
343
342
|
// changes gradually desynchronise the frame counters.
|
|
344
|
-
// TODO
|
|
343
|
+
// TODO(#1057) setDEW is reached a MOS-dependent number of times before Video.frameCount rises, so
|
|
345
344
|
// the initial sync here holds under MOS 1.20 only.
|
|
346
345
|
get hideFlashing() {
|
|
347
346
|
return this.flashTime < 16;
|
package/src/teletext_adaptor.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as utils from "./utils.js";
|
|
1
|
+
import { loadData } from "./loader.js";
|
|
4
2
|
|
|
5
3
|
// Code ported from Beebem (C to .js) by Jason Robson
|
|
6
4
|
const TELETEXT_IRQ = 5;
|
|
@@ -74,7 +72,7 @@ export class TeletextAdaptor extends EventTarget {
|
|
|
74
72
|
const request = ++this.streamRequest;
|
|
75
73
|
let data;
|
|
76
74
|
try {
|
|
77
|
-
data = await
|
|
75
|
+
data = await loadData(`teletext/txt${channel}.dat`);
|
|
78
76
|
} catch (error) {
|
|
79
77
|
if (request !== this.streamRequest) return;
|
|
80
78
|
console.error(`Teletext adaptor: failed to load channel ${channel}`, error);
|
package/src/teletext_data.js
CHANGED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { installBasic } from "./basic-loader.js";
|
|
2
|
+
import * as fdc from "./fdc.js";
|
|
3
|
+
import { fake6502 } from "./fake6502.js";
|
|
4
|
+
import { findModel } from "./models.js";
|
|
5
|
+
import assert from "assert";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import * as Tokeniser from "./basic-tokenise.js";
|
|
9
|
+
import { VduTextCapture } from "./vdu-capture.js";
|
|
10
|
+
import { setNodeBasePath } from "./loader.js";
|
|
11
|
+
import { Typist } from "./typist.js";
|
|
12
|
+
|
|
13
|
+
const MaxCyclesPerIter = 100 * 1000;
|
|
14
|
+
const RepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
|
|
16
|
+
export class TestMachine {
|
|
17
|
+
constructor(model, opts) {
|
|
18
|
+
model = model || "B-DFS1.2";
|
|
19
|
+
this.model = findModel(model);
|
|
20
|
+
if (!this.model) throw new Error(`Unknown model "${model}"`);
|
|
21
|
+
this.processor = fake6502(this.model, opts || {});
|
|
22
|
+
this.typist = new Typist(this.processor);
|
|
23
|
+
this._capturedChars = [];
|
|
24
|
+
this._captureHookInstalled = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The keyboard interface for this machine (SysVia for BBC, PPIA for Atom). */
|
|
28
|
+
get _keyInterface() {
|
|
29
|
+
return this.processor.keyboardInterface;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async initialise() {
|
|
33
|
+
setNodeBasePath(RepoRoot);
|
|
34
|
+
await this.processor.initialise();
|
|
35
|
+
if (this.model.isAtom) this._startAtomVSync();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The Atom ROM's main loop waits for VSync (bit 7 of Port C) to
|
|
40
|
+
* toggle before scanning the keyboard. The MC6847 video chip drives
|
|
41
|
+
* this in the real emulator, but fake6502 doesn't create one, so we
|
|
42
|
+
* simulate it with a scheduler task at ~60 Hz (NTSC).
|
|
43
|
+
*/
|
|
44
|
+
_startAtomVSync() {
|
|
45
|
+
const ppia = this.processor.atomppia;
|
|
46
|
+
const VsyncPeriod = 16667; // 1 MHz / 60 Hz (NTSC 262-line frame)
|
|
47
|
+
const VsyncPulse = 800;
|
|
48
|
+
let inVsync = false;
|
|
49
|
+
const task = this.processor.scheduler.newTask(() => {
|
|
50
|
+
if (!inVsync) {
|
|
51
|
+
ppia.setVBlankInt(1);
|
|
52
|
+
inVsync = true;
|
|
53
|
+
task.reschedule(VsyncPulse);
|
|
54
|
+
} else {
|
|
55
|
+
ppia.setVBlankInt(0);
|
|
56
|
+
inVsync = false;
|
|
57
|
+
task.reschedule(VsyncPeriod - VsyncPulse);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
task.schedule(VsyncPeriod);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Calls `listener` with each character the machine sends to the VDU, by
|
|
65
|
+
* watching the write-character vector (WRCHV, $020E on the BBC and $0208 on
|
|
66
|
+
* the Atom) as the OS or a program leaves it. Returns a function that stops
|
|
67
|
+
* listening.
|
|
68
|
+
*/
|
|
69
|
+
onVduChar(listener) {
|
|
70
|
+
if (!this._vduListeners) {
|
|
71
|
+
this._vduListeners = [];
|
|
72
|
+
const cpu = this.processor;
|
|
73
|
+
const ram = cpu.ramRomOs;
|
|
74
|
+
const wrchvAddr = this.model.wrchvAddress;
|
|
75
|
+
cpu.debugInstruction.add((addr) => {
|
|
76
|
+
if (addr === (ram[wrchvAddr] | (ram[wrchvAddr + 1] << 8))) {
|
|
77
|
+
for (const listen of this._vduListeners) listen(cpu.a);
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
this._vduListeners.push(listener);
|
|
83
|
+
return () => {
|
|
84
|
+
this._vduListeners = this._vduListeners.filter((other) => other !== listener);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Accumulates every character sent to the VDU for drainText(); safe to call more than once. */
|
|
89
|
+
startCapture() {
|
|
90
|
+
if (this._captureHookInstalled) return;
|
|
91
|
+
this._captureHookInstalled = true;
|
|
92
|
+
this.onVduChar((c) => this._capturedChars.push(c));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Return all captured characters since the last drain (or since
|
|
97
|
+
* startCapture was called), then clear the buffer.
|
|
98
|
+
* @returns {number[]} array of character codes
|
|
99
|
+
*/
|
|
100
|
+
drainCapturedChars() {
|
|
101
|
+
const chars = this._capturedChars;
|
|
102
|
+
this._capturedChars = [];
|
|
103
|
+
return chars;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Return captured text as a string (printable chars only, with
|
|
108
|
+
* optional newline preservation), then clear the buffer.
|
|
109
|
+
* @param {Object} [opts]
|
|
110
|
+
* @param {boolean} [opts.raw=false] - if true, preserve newlines
|
|
111
|
+
*/
|
|
112
|
+
drainText({ raw = false } = {}) {
|
|
113
|
+
const chars = this.drainCapturedChars();
|
|
114
|
+
return chars
|
|
115
|
+
.map((c) => {
|
|
116
|
+
if (raw && c === 10) return "\n";
|
|
117
|
+
if (c === 13) return "";
|
|
118
|
+
if (c >= 0x20 && c < 0x7f) return String.fromCharCode(c);
|
|
119
|
+
return "";
|
|
120
|
+
})
|
|
121
|
+
.join("");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
runFor(cycles) {
|
|
125
|
+
let left = cycles;
|
|
126
|
+
let stopped = false;
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
const runAnIter = () => {
|
|
129
|
+
const todo = Math.max(0, Math.min(left, MaxCyclesPerIter));
|
|
130
|
+
if (todo) {
|
|
131
|
+
stopped = !this.processor.execute(todo);
|
|
132
|
+
left -= todo;
|
|
133
|
+
}
|
|
134
|
+
// Not truthiness: a negative or NaN request clamps todo to zero,
|
|
135
|
+
// so left would never move and the loop never end.
|
|
136
|
+
if (left > 0 && !stopped) {
|
|
137
|
+
setTimeout(runAnIter, 0);
|
|
138
|
+
} else {
|
|
139
|
+
resolve(stopped);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
runAnIter();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Emulated cycles since power-on, undoing the per-second rebasing execute() applies. */
|
|
147
|
+
get elapsedCycles() {
|
|
148
|
+
const cpu = this.processor;
|
|
149
|
+
return cpu.cycleSeconds * this.model.cyclesPerSecond + cpu.currentCycles;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async runUntilVblank() {
|
|
153
|
+
let hit = false;
|
|
154
|
+
if (this.processor.isMaster) throw new Error("Not yet implemented");
|
|
155
|
+
const hook = this.processor.debugInstruction.add((addr) => {
|
|
156
|
+
if (addr === 0xdd15) {
|
|
157
|
+
hit = true;
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
await this.runFor(10 * 1000 * 1000);
|
|
162
|
+
hook.remove();
|
|
163
|
+
assert(hit, "did not hit appropriate breakpoint in time");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async runUntilInput(secs) {
|
|
167
|
+
if (!secs) secs = 120;
|
|
168
|
+
console.log("Running until keyboard input requested");
|
|
169
|
+
const idleAddr = this.model.idleAddress;
|
|
170
|
+
let hit = false;
|
|
171
|
+
const hook = this.processor.debugInstruction.add((addr) => {
|
|
172
|
+
if (addr === idleAddr) {
|
|
173
|
+
hit = true;
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
await this.runFor(secs * this.model.cyclesPerSecond);
|
|
178
|
+
hook.remove();
|
|
179
|
+
assert(hit, "did not hit appropriate breakpoint in time");
|
|
180
|
+
return this.runFor(10 * 1000);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async runUntilAddress(targetAddr, secs) {
|
|
184
|
+
if (!secs) secs = 120;
|
|
185
|
+
let hit = false;
|
|
186
|
+
const hook = this.processor.debugInstruction.add((addr) => {
|
|
187
|
+
if (addr === targetAddr) {
|
|
188
|
+
hit = true;
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
await this.runFor(secs * this.model.cyclesPerSecond);
|
|
193
|
+
hook.remove();
|
|
194
|
+
assert(hit, "did not hit appropriate breakpoint in time");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async loadDisc(image) {
|
|
198
|
+
const data = await fdc.load(image);
|
|
199
|
+
this.processor.fdc.loadDisc(0, fdc.discFor(image, data));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Load a disc image from raw data (Uint8Array or Buffer).
|
|
204
|
+
* @param {Uint8Array|Buffer} data - raw disc image bytes
|
|
205
|
+
*/
|
|
206
|
+
loadDiscData(data) {
|
|
207
|
+
this.processor.fdc.loadDisc(0, fdc.discFor("", data));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Reset the machine.
|
|
212
|
+
* @param {boolean} hard - true for power-on reset, false for soft reset
|
|
213
|
+
*/
|
|
214
|
+
reset(hard) {
|
|
215
|
+
this.processor.reset(hard);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Take a snapshot of the entire machine state (CPU, RAM, SWRAM,
|
|
220
|
+
* VIAs, video, FDC, etc). Returns an opaque state object that
|
|
221
|
+
* can be passed to restore().
|
|
222
|
+
*/
|
|
223
|
+
snapshot({ includeRoms = true } = {}) {
|
|
224
|
+
return this.processor.snapshotState({ includeRoms });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Restore a previously saved snapshot. The machine will be in
|
|
229
|
+
* exactly the state it was when snapshot() was called.
|
|
230
|
+
*/
|
|
231
|
+
restore(state) {
|
|
232
|
+
this.processor.restoreState(state);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async loadBasic(source) {
|
|
236
|
+
const tokeniser = await Tokeniser.create();
|
|
237
|
+
const tokenised = tokeniser.tokenise(source);
|
|
238
|
+
installBasic(tokenised, {
|
|
239
|
+
readByte: (addr) => this.readbyte(addr),
|
|
240
|
+
writeByte: (addr, value) => this.writebyte(addr, value),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Types the text and a RETURN at the machine, running the CPU until the
|
|
246
|
+
* last key is up. The keys arrive from the scheduler, so a breakpoint that
|
|
247
|
+
* stops the CPU part way leaves the rest to be typed when it runs again.
|
|
248
|
+
*/
|
|
249
|
+
async type(text) {
|
|
250
|
+
const lines = text.replace(/\r\n?/g, "\n");
|
|
251
|
+
this.typist.type(this.model.stringToKeys(lines + "\n"), true);
|
|
252
|
+
while (this.typist.isTyping) {
|
|
253
|
+
const stopped = await this.runFor(MaxCyclesPerIter);
|
|
254
|
+
if (stopped) break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Press a key on the keyboard.
|
|
260
|
+
* @param {number} code - key code (BBC keyCode or Atom raw key)
|
|
261
|
+
*/
|
|
262
|
+
keyDown(code) {
|
|
263
|
+
this._keyInterface.keyDown(code);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Release a key on the keyboard.
|
|
268
|
+
* @param {number} code - key code
|
|
269
|
+
*/
|
|
270
|
+
keyUp(code) {
|
|
271
|
+
this._keyInterface.keyUp(code);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Load a ROM image directly into a sideways RAM slot.
|
|
276
|
+
* @param {number} slot - slot number (0-15, typically 4-7 for SWRAM)
|
|
277
|
+
* @param {Uint8Array|Buffer} data - ROM data (up to 16384 bytes)
|
|
278
|
+
*/
|
|
279
|
+
loadSidewaysRam(slot, data) {
|
|
280
|
+
const offset = this.processor.romOffset + slot * 16384;
|
|
281
|
+
for (let i = 0; i < data.length && i < 16384; i++) {
|
|
282
|
+
this.processor.ramRomOs[offset + i] = data[i];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
writebyte(addr, val) {
|
|
287
|
+
this.processor.writemem(addr, val);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
readbyte(addr) {
|
|
291
|
+
return this.processor.readmem(addr);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
readword(addr) {
|
|
295
|
+
return this.readbyte(addr) | (this.readbyte(addr + 1) << 8);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Decodes the machine's VDU output into text elements for `onElement`
|
|
300
|
+
* until the returned capture's `stop()` is called; see VduTextCapture.
|
|
301
|
+
*/
|
|
302
|
+
captureText(onElement) {
|
|
303
|
+
const capture = new VduTextCapture(onElement, { isAtom: this.model.isAtom });
|
|
304
|
+
capture.stop = this.onVduChar((c) => capture.onChar(c));
|
|
305
|
+
return capture;
|
|
306
|
+
}
|
|
307
|
+
}
|
package/src/touchscreen.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as utils from "./utils.js";
|
|
1
|
+
import { Fifo } from "./binary.js";
|
|
4
2
|
|
|
5
3
|
const PollHz = 8; // Made up
|
|
6
4
|
|
|
@@ -13,7 +11,7 @@ export class TouchScreen {
|
|
|
13
11
|
constructor(scheduler, cyclesPerSecond) {
|
|
14
12
|
this.scheduler = scheduler;
|
|
15
13
|
this.pollCycles = cyclesPerSecond / PollHz;
|
|
16
|
-
this.outBuffer = new
|
|
14
|
+
this.outBuffer = new Fifo(16);
|
|
17
15
|
this.pollTask = this.scheduler.newTask(() => this.poll());
|
|
18
16
|
this.reset();
|
|
19
17
|
}
|
package/src/tube.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import * as utils from "./utils.js";
|
|
1
|
+
import { hexbyte, hexword } from "./hex.js";
|
|
3
2
|
|
|
4
3
|
// this one should be declared more globally
|
|
5
4
|
const HOST_CPU_FLAG_IRQ_TUBE_ULA = 8;
|
|
@@ -205,13 +204,13 @@ export class Tube {
|
|
|
205
204
|
}
|
|
206
205
|
this.updateInterrupts();
|
|
207
206
|
if (this.debug) {
|
|
208
|
-
console.log("TUBE ULA: host read " +
|
|
207
|
+
console.log("TUBE ULA: host read " + hexword(address) + " = " + hexbyte(result));
|
|
209
208
|
}
|
|
210
209
|
return result;
|
|
211
210
|
}
|
|
212
211
|
hostWrite(address, value) {
|
|
213
212
|
if (this.debug) {
|
|
214
|
-
console.log("TUBE ULA: host write " +
|
|
213
|
+
console.log("TUBE ULA: host write " + hexword(address) + " = " + hexbyte(value));
|
|
215
214
|
}
|
|
216
215
|
switch (address & 7) {
|
|
217
216
|
case TUBE_ULA_R1_STATUS_ADDRESS: {
|
|
@@ -337,7 +336,7 @@ export class Tube {
|
|
|
337
336
|
}
|
|
338
337
|
this.updateInterrupts();
|
|
339
338
|
if (this.debug) {
|
|
340
|
-
console.log("TUBE ULA: parasite read " +
|
|
339
|
+
console.log("TUBE ULA: parasite read " + hexword(address) + " = " + hexbyte(result));
|
|
341
340
|
}
|
|
342
341
|
return result;
|
|
343
342
|
}
|
|
@@ -382,7 +381,7 @@ export class Tube {
|
|
|
382
381
|
// Boot mode is terminated by the software when it selects any one of the Tube addresses.
|
|
383
382
|
// This deselects the ROM
|
|
384
383
|
if (this.debug) {
|
|
385
|
-
console.log("TUBE ULA: parasite write " +
|
|
384
|
+
console.log("TUBE ULA: parasite write " + hexword(address) + " = " + hexbyte(value));
|
|
386
385
|
}
|
|
387
386
|
switch (address & 7) {
|
|
388
387
|
case TUBE_ULA_R1_DATA_ADDRESS:
|
package/src/typist.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { BBC } from "./keymap.js";
|
|
2
|
+
|
|
3
|
+
const RepeatedKeyGapMs = 30;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Types a key sequence into the keyboard matrix from the processor's
|
|
7
|
+
* scheduler, one key per tick, so the CPU stays on its fast path while the
|
|
8
|
+
* text arrives. An entry is one of the model's raw keys, or a number of
|
|
9
|
+
* milliseconds to wait. SHIFT toggles and stays held; any other key is pressed
|
|
10
|
+
* on one tick and released on the next.
|
|
11
|
+
*/
|
|
12
|
+
export class Typist {
|
|
13
|
+
constructor(processor) {
|
|
14
|
+
this.processor = processor;
|
|
15
|
+
this.keyInterface = processor.keyboardInterface;
|
|
16
|
+
this._shiftKey = processor.model.keys.SHIFT;
|
|
17
|
+
this._keys = [];
|
|
18
|
+
this._lastKey = undefined;
|
|
19
|
+
this._clocksPerMs = 0;
|
|
20
|
+
this._task = processor.scheduler.newTask(() => this._deliver());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Array} keys raw keys and millisecond delays, consumed as they are sent
|
|
25
|
+
* @param {boolean} checkCapsAndShiftLocks bracket the keys with a lock toggle when
|
|
26
|
+
* the machine's lock lights say the letters would come out in the wrong case
|
|
27
|
+
*/
|
|
28
|
+
type(keys, checkCapsAndShiftLocks) {
|
|
29
|
+
if (this.isTyping) this.cancel();
|
|
30
|
+
this.keyInterface.disableKeyboard();
|
|
31
|
+
// The task lives on the processor's scheduler, which is polled with peripheral
|
|
32
|
+
// cycles, so the delays stay in real time whatever the CPU multiplier is.
|
|
33
|
+
this._clocksPerMs = this.processor.peripheralCyclesPerSecond / 1000;
|
|
34
|
+
|
|
35
|
+
if (checkCapsAndShiftLocks) {
|
|
36
|
+
let toggleKey = null;
|
|
37
|
+
if (!this.keyInterface.capsLockLight) toggleKey = BBC.CAPSLOCK;
|
|
38
|
+
else if (this.keyInterface.shiftLockLight) toggleKey = BBC.SHIFTLOCK;
|
|
39
|
+
if (toggleKey) {
|
|
40
|
+
keys.unshift(toggleKey);
|
|
41
|
+
keys.push(toggleKey);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this._keys = keys;
|
|
46
|
+
this._lastKey = undefined;
|
|
47
|
+
this._task.schedule(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
cancel() {
|
|
51
|
+
if (!this.isTyping) return;
|
|
52
|
+
this._task.cancel();
|
|
53
|
+
this._releaseLastKey();
|
|
54
|
+
this._lastKey = undefined;
|
|
55
|
+
this._keys = [];
|
|
56
|
+
this.keyInterface.enableKeyboard();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get isTyping() {
|
|
60
|
+
return this._keys.length > 0 || this._task.scheduled();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_releaseLastKey() {
|
|
64
|
+
if (this._lastKey && this._lastKey !== this._shiftKey) this.keyInterface.keyToggleRaw(this._lastKey);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_deliver() {
|
|
68
|
+
this._releaseLastKey();
|
|
69
|
+
|
|
70
|
+
if (this._keys.length === 0) {
|
|
71
|
+
this._lastKey = undefined;
|
|
72
|
+
this.keyInterface.enableKeyboard();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const releaseGapMs = this.processor.model.pasteReleaseGapMs;
|
|
77
|
+
if (this._lastKey && this._lastKey !== this._shiftKey && releaseGapMs) {
|
|
78
|
+
this._lastKey = undefined;
|
|
79
|
+
this._task.schedule(releaseGapMs * this._clocksPerMs);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const key = this._keys[0];
|
|
84
|
+
const repeated = this._lastKey === key;
|
|
85
|
+
this._lastKey = key;
|
|
86
|
+
if (repeated) {
|
|
87
|
+
this._lastKey = undefined;
|
|
88
|
+
this._task.schedule(RepeatedKeyGapMs * this._clocksPerMs);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let delayMs = this.processor.model.pasteKeyDelayMs;
|
|
93
|
+
if (typeof key === "number") {
|
|
94
|
+
delayMs = key;
|
|
95
|
+
this._lastKey = undefined;
|
|
96
|
+
} else {
|
|
97
|
+
this.keyInterface.keyToggleRaw(key);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this._keys.shift();
|
|
101
|
+
this._task.schedule(delayMs * this._clocksPerMs);
|
|
102
|
+
}
|
|
103
|
+
}
|