jsbeeb 1.23.0 → 1.24.1
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 +14 -10
- package/src/6502.js +39 -42
- package/src/6502.opcodes.js +7 -13
- 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 +11 -24
- 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/{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 +66 -180
- package/src/media-resolver.js +68 -0
- package/src/mmc.js +3 -2
- package/src/models.js +18 -3
- 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 +281 -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 +38 -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 +41 -74
- package/src/web/console-surface.js +25 -0
- package/src/web/debug.js +6 -5
- 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/keyboard-setup.js +18 -38
- 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/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,281 @@
|
|
|
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
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Calls `listener` with each character the machine sends to the VDU, by
|
|
39
|
+
* watching the write-character vector (WRCHV, $020E on the BBC and $0208 on
|
|
40
|
+
* the Atom) as the OS or a program leaves it. Returns a function that stops
|
|
41
|
+
* listening.
|
|
42
|
+
*/
|
|
43
|
+
onVduChar(listener) {
|
|
44
|
+
if (!this._vduListeners) {
|
|
45
|
+
this._vduListeners = [];
|
|
46
|
+
const cpu = this.processor;
|
|
47
|
+
const ram = cpu.ramRomOs;
|
|
48
|
+
const wrchvAddr = this.model.wrchvAddress;
|
|
49
|
+
cpu.debugInstruction.add((addr) => {
|
|
50
|
+
if (addr === (ram[wrchvAddr] | (ram[wrchvAddr + 1] << 8))) {
|
|
51
|
+
for (const listen of this._vduListeners) listen(cpu.a);
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
this._vduListeners.push(listener);
|
|
57
|
+
return () => {
|
|
58
|
+
this._vduListeners = this._vduListeners.filter((other) => other !== listener);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Accumulates every character sent to the VDU for drainText(); safe to call more than once. */
|
|
63
|
+
startCapture() {
|
|
64
|
+
if (this._captureHookInstalled) return;
|
|
65
|
+
this._captureHookInstalled = true;
|
|
66
|
+
this.onVduChar((c) => this._capturedChars.push(c));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Return all captured characters since the last drain (or since
|
|
71
|
+
* startCapture was called), then clear the buffer.
|
|
72
|
+
* @returns {number[]} array of character codes
|
|
73
|
+
*/
|
|
74
|
+
drainCapturedChars() {
|
|
75
|
+
const chars = this._capturedChars;
|
|
76
|
+
this._capturedChars = [];
|
|
77
|
+
return chars;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Return captured text as a string (printable chars only, with
|
|
82
|
+
* optional newline preservation), then clear the buffer.
|
|
83
|
+
* @param {Object} [opts]
|
|
84
|
+
* @param {boolean} [opts.raw=false] - if true, preserve newlines
|
|
85
|
+
*/
|
|
86
|
+
drainText({ raw = false } = {}) {
|
|
87
|
+
const chars = this.drainCapturedChars();
|
|
88
|
+
return chars
|
|
89
|
+
.map((c) => {
|
|
90
|
+
if (raw && c === 10) return "\n";
|
|
91
|
+
if (c === 13) return "";
|
|
92
|
+
if (c >= 0x20 && c < 0x7f) return String.fromCharCode(c);
|
|
93
|
+
return "";
|
|
94
|
+
})
|
|
95
|
+
.join("");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
runFor(cycles) {
|
|
99
|
+
let left = cycles;
|
|
100
|
+
let stopped = false;
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const runAnIter = () => {
|
|
103
|
+
const todo = Math.max(0, Math.min(left, MaxCyclesPerIter));
|
|
104
|
+
if (todo) {
|
|
105
|
+
stopped = !this.processor.execute(todo);
|
|
106
|
+
left -= todo;
|
|
107
|
+
}
|
|
108
|
+
// Not truthiness: a negative or NaN request clamps todo to zero,
|
|
109
|
+
// so left would never move and the loop never end.
|
|
110
|
+
if (left > 0 && !stopped) {
|
|
111
|
+
setTimeout(runAnIter, 0);
|
|
112
|
+
} else {
|
|
113
|
+
resolve(stopped);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
runAnIter();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Emulated cycles since power-on, undoing the per-second rebasing execute() applies. */
|
|
121
|
+
get elapsedCycles() {
|
|
122
|
+
const cpu = this.processor;
|
|
123
|
+
return cpu.cycleSeconds * this.model.cyclesPerSecond + cpu.currentCycles;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async runUntilVblank() {
|
|
127
|
+
let hit = false;
|
|
128
|
+
if (this.processor.isMaster) throw new Error("Not yet implemented");
|
|
129
|
+
const hook = this.processor.debugInstruction.add((addr) => {
|
|
130
|
+
if (addr === 0xdd15) {
|
|
131
|
+
hit = true;
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
await this.runFor(10 * 1000 * 1000);
|
|
136
|
+
hook.remove();
|
|
137
|
+
assert(hit, "did not hit appropriate breakpoint in time");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async runUntilInput(secs) {
|
|
141
|
+
if (!secs) secs = 120;
|
|
142
|
+
console.log("Running until keyboard input requested");
|
|
143
|
+
const idleAddr = this.model.idleAddress;
|
|
144
|
+
let hit = false;
|
|
145
|
+
const hook = this.processor.debugInstruction.add((addr) => {
|
|
146
|
+
if (addr === idleAddr) {
|
|
147
|
+
hit = true;
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
await this.runFor(secs * this.model.cyclesPerSecond);
|
|
152
|
+
hook.remove();
|
|
153
|
+
assert(hit, "did not hit appropriate breakpoint in time");
|
|
154
|
+
return this.runFor(10 * 1000);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async runUntilAddress(targetAddr, secs) {
|
|
158
|
+
if (!secs) secs = 120;
|
|
159
|
+
let hit = false;
|
|
160
|
+
const hook = this.processor.debugInstruction.add((addr) => {
|
|
161
|
+
if (addr === targetAddr) {
|
|
162
|
+
hit = true;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
await this.runFor(secs * this.model.cyclesPerSecond);
|
|
167
|
+
hook.remove();
|
|
168
|
+
assert(hit, "did not hit appropriate breakpoint in time");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async loadDisc(image) {
|
|
172
|
+
const data = await fdc.load(image);
|
|
173
|
+
this.processor.fdc.loadDisc(0, fdc.discFor(image, data));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Load a disc image from raw data (Uint8Array or Buffer).
|
|
178
|
+
* @param {Uint8Array|Buffer} data - raw disc image bytes
|
|
179
|
+
*/
|
|
180
|
+
loadDiscData(data) {
|
|
181
|
+
this.processor.fdc.loadDisc(0, fdc.discFor("", data));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Reset the machine.
|
|
186
|
+
* @param {boolean} hard - true for power-on reset, false for soft reset
|
|
187
|
+
*/
|
|
188
|
+
reset(hard) {
|
|
189
|
+
this.processor.reset(hard);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Take a snapshot of the entire machine state (CPU, RAM, SWRAM,
|
|
194
|
+
* VIAs, video, FDC, etc). Returns an opaque state object that
|
|
195
|
+
* can be passed to restore().
|
|
196
|
+
*/
|
|
197
|
+
snapshot({ includeRoms = true } = {}) {
|
|
198
|
+
return this.processor.snapshotState({ includeRoms });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Restore a previously saved snapshot. The machine will be in
|
|
203
|
+
* exactly the state it was when snapshot() was called.
|
|
204
|
+
*/
|
|
205
|
+
restore(state) {
|
|
206
|
+
this.processor.restoreState(state);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async loadBasic(source) {
|
|
210
|
+
const tokeniser = await Tokeniser.create();
|
|
211
|
+
const tokenised = tokeniser.tokenise(source);
|
|
212
|
+
installBasic(tokenised, {
|
|
213
|
+
readByte: (addr) => this.readbyte(addr),
|
|
214
|
+
writeByte: (addr, value) => this.writebyte(addr, value),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Types the text and a RETURN at the machine, running the CPU until the
|
|
220
|
+
* last key is up. The keys arrive from the scheduler, so a breakpoint that
|
|
221
|
+
* stops the CPU part way leaves the rest to be typed when it runs again.
|
|
222
|
+
*/
|
|
223
|
+
async type(text) {
|
|
224
|
+
const lines = text.replace(/\r\n?/g, "\n");
|
|
225
|
+
this.typist.type(this.model.stringToKeys(lines + "\n"), true);
|
|
226
|
+
while (this.typist.isTyping) {
|
|
227
|
+
const stopped = await this.runFor(MaxCyclesPerIter);
|
|
228
|
+
if (stopped) break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Press a key on the keyboard.
|
|
234
|
+
* @param {number} code - key code (BBC keyCode or Atom raw key)
|
|
235
|
+
*/
|
|
236
|
+
keyDown(code) {
|
|
237
|
+
this._keyInterface.keyDown(code);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Release a key on the keyboard.
|
|
242
|
+
* @param {number} code - key code
|
|
243
|
+
*/
|
|
244
|
+
keyUp(code) {
|
|
245
|
+
this._keyInterface.keyUp(code);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Load a ROM image directly into a sideways RAM slot.
|
|
250
|
+
* @param {number} slot - slot number (0-15, typically 4-7 for SWRAM)
|
|
251
|
+
* @param {Uint8Array|Buffer} data - ROM data (up to 16384 bytes)
|
|
252
|
+
*/
|
|
253
|
+
loadSidewaysRam(slot, data) {
|
|
254
|
+
const offset = this.processor.romOffset + slot * 16384;
|
|
255
|
+
for (let i = 0; i < data.length && i < 16384; i++) {
|
|
256
|
+
this.processor.ramRomOs[offset + i] = data[i];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
writebyte(addr, val) {
|
|
261
|
+
this.processor.writemem(addr, val);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
readbyte(addr) {
|
|
265
|
+
return this.processor.readmem(addr);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
readword(addr) {
|
|
269
|
+
return this.readbyte(addr) | (this.readbyte(addr + 1) << 8);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Decodes the machine's VDU output into text elements for `onElement`
|
|
274
|
+
* until the returned capture's `stop()` is called; see VduTextCapture.
|
|
275
|
+
*/
|
|
276
|
+
captureText(onElement) {
|
|
277
|
+
const capture = new VduTextCapture(onElement, { isAtom: this.model.isAtom });
|
|
278
|
+
capture.stop = this.onVduChar((c) => capture.onChar(c));
|
|
279
|
+
return capture;
|
|
280
|
+
}
|
|
281
|
+
}
|
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
|
+
}
|
package/src/uef-snapshot.js
CHANGED
package/src/url-params.js
CHANGED
|
@@ -295,17 +295,16 @@ export function guessModelFromHostname(hostname) {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
/**
|
|
298
|
-
* Parse disc
|
|
298
|
+
* Parse disc images from the query parameters
|
|
299
299
|
* @param {Object} parsedQuery - The query parameters
|
|
300
|
-
* @returns {Object} Object containing disc
|
|
300
|
+
* @returns {Object} Object containing disc information
|
|
301
301
|
* - discImage: disc image URL (?disc= or ?disc1=)
|
|
302
302
|
* - secondDiscImage: second disc URL (?disc2=)
|
|
303
|
-
* - tapeImage: tape image URL (?tape=)
|
|
304
303
|
* - mmcImage: MMC/SD card image URL (?mmc=, Atom only)
|
|
305
304
|
*/
|
|
306
305
|
export function parseMediaParams(parsedQuery) {
|
|
307
|
-
const { disc, disc1, disc2,
|
|
306
|
+
const { disc, disc1, disc2, mmc } = parsedQuery;
|
|
308
307
|
const discImage = disc || disc1;
|
|
309
308
|
|
|
310
|
-
return { discImage, secondDiscImage: disc2,
|
|
309
|
+
return { discImage, secondDiscImage: disc2, mmcImage: mmc };
|
|
311
310
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns the bytes a program sends to the VDU into text elements,
|
|
3
|
+
* `{ x, y, text, foreground, background, mode }`, following the cursor and
|
|
4
|
+
* colour codes and swallowing the parameters of the rest. Text-mode output
|
|
5
|
+
* only; anything visual wants a screenshot.
|
|
6
|
+
*/
|
|
7
|
+
// How many parameter bytes follow each BBC VDU code that takes any.
|
|
8
|
+
const SequenceParamCounts = {
|
|
9
|
+
1: 1, // next character to the printer only
|
|
10
|
+
17: 1, // COLOUR n
|
|
11
|
+
18: 2, // GCOL
|
|
12
|
+
19: 5, // define logical colour
|
|
13
|
+
22: 1, // MODE n
|
|
14
|
+
25: 5, // PLOT
|
|
15
|
+
28: 4, // define text window
|
|
16
|
+
29: 4, // define graphics origin
|
|
17
|
+
31: 2, // TAB(x,y)
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class VduTextCapture {
|
|
21
|
+
constructor(onElement, { isAtom = false } = {}) {
|
|
22
|
+
this.onElement = onElement;
|
|
23
|
+
this.isAtom = isAtom;
|
|
24
|
+
this.attributes = { x: 0, y: 0, text: "", foreground: 7, background: 0, mode: 7 };
|
|
25
|
+
this.currentText = "";
|
|
26
|
+
this.params = [];
|
|
27
|
+
this.nextN = 0;
|
|
28
|
+
this.vduProc = null;
|
|
29
|
+
this.sequenceHandlers = {
|
|
30
|
+
17: (p) => {
|
|
31
|
+
if (p[0] & 0x80) this.attributes.background = p[0] & 0xf;
|
|
32
|
+
else this.attributes.foreground = p[0] & 0xf;
|
|
33
|
+
},
|
|
34
|
+
22: (p) => {
|
|
35
|
+
Object.assign(this.attributes, { mode: p[0], x: 0, y: 0, foreground: 7, background: 0 });
|
|
36
|
+
},
|
|
37
|
+
31: (p) => {
|
|
38
|
+
this.attributes.x = p[0];
|
|
39
|
+
this.attributes.y = p[1];
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
flush() {
|
|
45
|
+
if (this.currentText.length) {
|
|
46
|
+
this.onElement({ ...this.attributes, text: this.currentText });
|
|
47
|
+
this.attributes.x += this.currentText.length;
|
|
48
|
+
}
|
|
49
|
+
this.currentText = "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onChar(c) {
|
|
53
|
+
const attributes = this.attributes;
|
|
54
|
+
if (this.nextN) {
|
|
55
|
+
this.params.push(c);
|
|
56
|
+
if (--this.nextN === 0) {
|
|
57
|
+
if (this.vduProc) this.vduProc(this.params);
|
|
58
|
+
this.params = [];
|
|
59
|
+
this.vduProc = null;
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
switch (c) {
|
|
64
|
+
case 10:
|
|
65
|
+
this.flush();
|
|
66
|
+
attributes.y++;
|
|
67
|
+
return;
|
|
68
|
+
case 12:
|
|
69
|
+
this.flush();
|
|
70
|
+
attributes.x = 0;
|
|
71
|
+
attributes.y = 0;
|
|
72
|
+
return;
|
|
73
|
+
case 13:
|
|
74
|
+
this.flush();
|
|
75
|
+
attributes.x = 0;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// The Atom has no multi-byte VDU sequences; treating its bytes as
|
|
79
|
+
// parameters would swallow printable characters.
|
|
80
|
+
const paramCount = this.isAtom ? undefined : SequenceParamCounts[c];
|
|
81
|
+
if (paramCount !== undefined) {
|
|
82
|
+
this.flush();
|
|
83
|
+
this.nextN = paramCount;
|
|
84
|
+
this.vduProc = this.sequenceHandlers[c] ?? null;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (c >= 32 && c < 0x7f) this.currentText += String.fromCharCode(c);
|
|
88
|
+
else this.flush();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The decoder's position in the byte stream, for a machine snapshot to carry. */
|
|
92
|
+
snapshot() {
|
|
93
|
+
return {
|
|
94
|
+
attributes: { ...this.attributes },
|
|
95
|
+
currentText: this.currentText,
|
|
96
|
+
params: [...this.params],
|
|
97
|
+
nextN: this.nextN,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Puts back a snapshot(). The handler for a half-read sequence is a
|
|
103
|
+
* closure and does not survive, so such a sequence is dropped rather
|
|
104
|
+
* than printed as text.
|
|
105
|
+
*/
|
|
106
|
+
restore(state) {
|
|
107
|
+
Object.assign(this.attributes, state.attributes);
|
|
108
|
+
this.currentText = state.currentText;
|
|
109
|
+
this.params = [...state.params];
|
|
110
|
+
this.nextN = state.nextN;
|
|
111
|
+
this.vduProc = null;
|
|
112
|
+
}
|
|
113
|
+
}
|