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/tests/test-machine.js
DELETED
|
@@ -1,609 +0,0 @@
|
|
|
1
|
-
import { basicIdleAddr, installBasic } from "../src/basic-loader.js";
|
|
2
|
-
import * as fdc from "../src/fdc.js";
|
|
3
|
-
import { fake6502 } from "../src/fake6502.js";
|
|
4
|
-
import { findModel } from "../src/models.js";
|
|
5
|
-
import assert from "assert";
|
|
6
|
-
import * as utils from "../src/utils.js";
|
|
7
|
-
import * as utils_atom from "../src/utils_atom.js";
|
|
8
|
-
import * as Tokeniser from "../src/basic-tokenise.js";
|
|
9
|
-
|
|
10
|
-
const MaxCyclesPerIter = 100 * 1000;
|
|
11
|
-
|
|
12
|
-
export class TestMachine {
|
|
13
|
-
constructor(model, opts) {
|
|
14
|
-
model = model || "B-DFS1.2";
|
|
15
|
-
this.model = findModel(model);
|
|
16
|
-
if (!this.model) throw new Error(`Unknown model "${model}"`);
|
|
17
|
-
this.processor = fake6502(this.model, opts || {});
|
|
18
|
-
this._capturedChars = [];
|
|
19
|
-
this._captureHookInstalled = false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** The keyboard interface for this machine (SysVia for BBC, PPIA for Atom). */
|
|
23
|
-
get _keyInterface() {
|
|
24
|
-
return this.model.isAtom ? this.processor.atomppia : this.processor.sysvia;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async initialise() {
|
|
28
|
-
await this.processor.initialise();
|
|
29
|
-
if (this.model.isAtom) this._startAtomVSync();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The Atom ROM's main loop waits for VSync (bit 7 of Port C) to
|
|
34
|
-
* toggle before scanning the keyboard. The MC6847 video chip drives
|
|
35
|
-
* this in the real emulator, but fake6502 doesn't create one, so we
|
|
36
|
-
* simulate it with a scheduler task at ~60 Hz (NTSC).
|
|
37
|
-
*/
|
|
38
|
-
_startAtomVSync() {
|
|
39
|
-
const ppia = this.processor.atomppia;
|
|
40
|
-
const VsyncPeriod = 16667; // 1 MHz / 60 Hz (NTSC 262-line frame)
|
|
41
|
-
const VsyncPulse = 800;
|
|
42
|
-
let inVsync = false;
|
|
43
|
-
const task = this.processor.scheduler.newTask(() => {
|
|
44
|
-
if (!inVsync) {
|
|
45
|
-
ppia.setVBlankInt(1);
|
|
46
|
-
inVsync = true;
|
|
47
|
-
task.reschedule(VsyncPulse);
|
|
48
|
-
} else {
|
|
49
|
-
ppia.setVBlankInt(0);
|
|
50
|
-
inVsync = false;
|
|
51
|
-
task.reschedule(VsyncPeriod - VsyncPulse);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
task.schedule(VsyncPeriod);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Install the character capture hook (once). All characters sent
|
|
59
|
-
* through WRCHV are accumulated and can be read with drainText().
|
|
60
|
-
* Safe to call multiple times — only installs one hook.
|
|
61
|
-
*/
|
|
62
|
-
startCapture() {
|
|
63
|
-
if (this._captureHookInstalled) return;
|
|
64
|
-
this._captureHookInstalled = true;
|
|
65
|
-
// WRCHV is at 0x0208 on Atom, 0x020E on BBC.
|
|
66
|
-
const wrchvAddr = this.model.isAtom ? 0x0208 : 0x020e;
|
|
67
|
-
this.processor.debugInstruction.add((addr) => {
|
|
68
|
-
const wrchv = this.readword(wrchvAddr);
|
|
69
|
-
if (addr === wrchv) {
|
|
70
|
-
this._capturedChars.push(this.processor.a);
|
|
71
|
-
}
|
|
72
|
-
return false;
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Return all captured characters since the last drain (or since
|
|
78
|
-
* startCapture was called), then clear the buffer.
|
|
79
|
-
* @returns {number[]} array of character codes
|
|
80
|
-
*/
|
|
81
|
-
drainCapturedChars() {
|
|
82
|
-
const chars = this._capturedChars;
|
|
83
|
-
this._capturedChars = [];
|
|
84
|
-
return chars;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Return captured text as a string (printable chars only, with
|
|
89
|
-
* optional newline preservation), then clear the buffer.
|
|
90
|
-
* @param {Object} [opts]
|
|
91
|
-
* @param {boolean} [opts.raw=false] - if true, preserve newlines
|
|
92
|
-
*/
|
|
93
|
-
drainText({ raw = false } = {}) {
|
|
94
|
-
const chars = this.drainCapturedChars();
|
|
95
|
-
return chars
|
|
96
|
-
.map((c) => {
|
|
97
|
-
if (raw && c === 10) return "\n";
|
|
98
|
-
if (c === 13) return "";
|
|
99
|
-
if (c >= 0x20 && c < 0x7f) return String.fromCharCode(c);
|
|
100
|
-
return "";
|
|
101
|
-
})
|
|
102
|
-
.join("");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
runFor(cycles) {
|
|
106
|
-
let left = cycles;
|
|
107
|
-
let stopped = false;
|
|
108
|
-
return new Promise((resolve) => {
|
|
109
|
-
const runAnIter = () => {
|
|
110
|
-
const todo = Math.max(0, Math.min(left, MaxCyclesPerIter));
|
|
111
|
-
if (todo) {
|
|
112
|
-
stopped = !this.processor.execute(todo);
|
|
113
|
-
left -= todo;
|
|
114
|
-
}
|
|
115
|
-
// Not truthiness: a negative or NaN request clamps todo to zero,
|
|
116
|
-
// so left would never move and the loop never end.
|
|
117
|
-
if (left > 0 && !stopped) {
|
|
118
|
-
setTimeout(runAnIter, 0);
|
|
119
|
-
} else {
|
|
120
|
-
resolve(stopped);
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
runAnIter();
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Run until the cursor blink reaches the desired state.
|
|
129
|
-
* This ensures deterministic screenshots regardless of how many
|
|
130
|
-
* cycles were consumed by prior type() or runFor() calls.
|
|
131
|
-
* @param {boolean} on - true for cursor visible, false for hidden
|
|
132
|
-
*/
|
|
133
|
-
async runToCursorState(on) {
|
|
134
|
-
const video = this.processor.video;
|
|
135
|
-
for (let i = 0; i < 100; i++) {
|
|
136
|
-
if (video.cursorOnThisFrame === on) return;
|
|
137
|
-
await this.runFor(40000);
|
|
138
|
-
}
|
|
139
|
-
throw new Error(`Cursor did not reach state ${on} in time (cursorOnThisFrame=${video.cursorOnThisFrame})`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async runUntilFlashHidden() {
|
|
143
|
-
const teletext = this.processor.video.teletext;
|
|
144
|
-
for (let i = 0; i < 100; i++) {
|
|
145
|
-
if (teletext.hideFlashing) return;
|
|
146
|
-
await this.runFor(40000);
|
|
147
|
-
}
|
|
148
|
-
throw new Error("Flashing text did not reach its hidden phase in time");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async runUntilVblank() {
|
|
152
|
-
let hit = false;
|
|
153
|
-
if (this.processor.isMaster) throw new Error("Not yet implemented");
|
|
154
|
-
const hook = this.processor.debugInstruction.add((addr) => {
|
|
155
|
-
if (addr === 0xdd15) {
|
|
156
|
-
hit = true;
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
await this.runFor(10 * 1000 * 1000);
|
|
161
|
-
hook.remove();
|
|
162
|
-
assert(hit, "did not hit appropriate breakpoint in time");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async runUntilInput(secs) {
|
|
166
|
-
if (!secs) secs = 120;
|
|
167
|
-
console.log("Running until keyboard input requested");
|
|
168
|
-
if (this.model.isAtom) {
|
|
169
|
-
// The Atom kernel's keyboard read loop at $FE94 is entered when
|
|
170
|
-
// BASIC (or the OS) waits for a keypress. We detect entry to
|
|
171
|
-
// this routine as the idle point.
|
|
172
|
-
const atomIdleAddr = 0xfe94;
|
|
173
|
-
let hit = false;
|
|
174
|
-
const hook = this.processor.debugInstruction.add((addr) => {
|
|
175
|
-
if (addr === atomIdleAddr) {
|
|
176
|
-
hit = true;
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
await this.runFor(secs * this.model.cyclesPerSecond);
|
|
181
|
-
hook.remove();
|
|
182
|
-
assert(hit, "Atom did not reach keyboard input in time");
|
|
183
|
-
return this.runFor(10 * 1000);
|
|
184
|
-
}
|
|
185
|
-
const idleAddr = basicIdleAddr(this.processor.model);
|
|
186
|
-
let hit = false;
|
|
187
|
-
const hook = this.processor.debugInstruction.add((addr) => {
|
|
188
|
-
if (addr === idleAddr) {
|
|
189
|
-
hit = true;
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
await this.runFor(secs * this.model.cyclesPerSecond);
|
|
194
|
-
hook.remove();
|
|
195
|
-
assert(hit, "did not hit appropriate breakpoint in time");
|
|
196
|
-
return this.runFor(10 * 1000);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async runUntilAddress(targetAddr, secs) {
|
|
200
|
-
if (!secs) secs = 120;
|
|
201
|
-
let hit = false;
|
|
202
|
-
const hook = this.processor.debugInstruction.add((addr) => {
|
|
203
|
-
if (addr === targetAddr) {
|
|
204
|
-
hit = true;
|
|
205
|
-
return true;
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
await this.runFor(secs * this.model.cyclesPerSecond);
|
|
209
|
-
hook.remove();
|
|
210
|
-
assert(hit, "did not hit appropriate breakpoint in time");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async loadDisc(image) {
|
|
214
|
-
const data = await fdc.load(image);
|
|
215
|
-
this.processor.fdc.loadDisc(0, fdc.discFor(this.processor.fdc, image, data));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Load a disc image from raw data (Uint8Array or Buffer).
|
|
220
|
-
* @param {Uint8Array|Buffer} data - raw disc image bytes
|
|
221
|
-
*/
|
|
222
|
-
loadDiscData(data) {
|
|
223
|
-
this.processor.fdc.loadDisc(0, fdc.discFor(this.processor.fdc, "", data));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Reset the machine.
|
|
228
|
-
* @param {boolean} hard - true for power-on reset, false for soft reset
|
|
229
|
-
*/
|
|
230
|
-
reset(hard) {
|
|
231
|
-
this.processor.reset(hard);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Take a snapshot of the entire machine state (CPU, RAM, SWRAM,
|
|
236
|
-
* VIAs, video, FDC, etc). Returns an opaque state object that
|
|
237
|
-
* can be passed to restore().
|
|
238
|
-
*/
|
|
239
|
-
snapshot({ includeRoms = true } = {}) {
|
|
240
|
-
return this.processor.snapshotState({ includeRoms });
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Restore a previously saved snapshot. The machine will be in
|
|
245
|
-
* exactly the state it was when snapshot() was called.
|
|
246
|
-
*/
|
|
247
|
-
restore(state) {
|
|
248
|
-
this.processor.restoreState(state);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async loadBasic(source) {
|
|
252
|
-
const tokeniser = await Tokeniser.create();
|
|
253
|
-
const tokenised = tokeniser.tokenise(source);
|
|
254
|
-
installBasic(tokenised, {
|
|
255
|
-
readByte: (addr) => this.readbyte(addr),
|
|
256
|
-
writeByte: (addr, value) => this.writebyte(addr, value),
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Convert an ASCII character to a {code, shift} pair for the BBC keyboard.
|
|
262
|
-
*/
|
|
263
|
-
_charToKey(ch) {
|
|
264
|
-
switch (ch) {
|
|
265
|
-
case "\n":
|
|
266
|
-
case "\r":
|
|
267
|
-
return { code: 13, shift: false };
|
|
268
|
-
case '"':
|
|
269
|
-
return { code: utils.keyCodes.K2, shift: true };
|
|
270
|
-
case "*":
|
|
271
|
-
return { code: utils.keyCodes.APOSTROPHE, shift: true };
|
|
272
|
-
case "!":
|
|
273
|
-
return { code: utils.keyCodes.K1, shift: true };
|
|
274
|
-
case ".":
|
|
275
|
-
return { code: utils.keyCodes.PERIOD, shift: false };
|
|
276
|
-
case ";":
|
|
277
|
-
return { code: utils.keyCodes.SEMICOLON, shift: false };
|
|
278
|
-
case ":":
|
|
279
|
-
return { code: utils.keyCodes.APOSTROPHE, shift: false };
|
|
280
|
-
case ",":
|
|
281
|
-
return { code: utils.keyCodes.COMMA, shift: false };
|
|
282
|
-
case "&":
|
|
283
|
-
return { code: utils.keyCodes.K6, shift: true };
|
|
284
|
-
case " ":
|
|
285
|
-
return { code: utils.keyCodes.SPACE, shift: false };
|
|
286
|
-
case "-":
|
|
287
|
-
return { code: utils.keyCodes.MINUS, shift: false };
|
|
288
|
-
case "=":
|
|
289
|
-
return { code: utils.keyCodes.MINUS, shift: true };
|
|
290
|
-
case "+":
|
|
291
|
-
return { code: utils.keyCodes.SEMICOLON, shift: true };
|
|
292
|
-
case "^":
|
|
293
|
-
return { code: utils.keyCodes.EQUALS, shift: false };
|
|
294
|
-
case "~":
|
|
295
|
-
return { code: utils.keyCodes.EQUALS, shift: true };
|
|
296
|
-
case "[":
|
|
297
|
-
return { code: utils.keyCodes.LEFT_SQUARE_BRACKET, shift: false };
|
|
298
|
-
case "]":
|
|
299
|
-
return { code: utils.keyCodes.RIGHT_SQUARE_BRACKET, shift: false };
|
|
300
|
-
case "{":
|
|
301
|
-
return { code: utils.keyCodes.LEFT_SQUARE_BRACKET, shift: true };
|
|
302
|
-
case "}":
|
|
303
|
-
return { code: utils.keyCodes.HASH, shift: true };
|
|
304
|
-
case "\\":
|
|
305
|
-
return { code: utils.keyCodes.BACKSLASH, shift: false };
|
|
306
|
-
case "/":
|
|
307
|
-
return { code: utils.keyCodes.SLASH, shift: false };
|
|
308
|
-
case "?":
|
|
309
|
-
return { code: utils.keyCodes.SLASH, shift: true };
|
|
310
|
-
case "<":
|
|
311
|
-
return { code: utils.keyCodes.COMMA, shift: true };
|
|
312
|
-
case ">":
|
|
313
|
-
return { code: utils.keyCodes.PERIOD, shift: true };
|
|
314
|
-
case "(":
|
|
315
|
-
return { code: utils.keyCodes.K8, shift: true };
|
|
316
|
-
case ")":
|
|
317
|
-
return { code: utils.keyCodes.K9, shift: true };
|
|
318
|
-
case "@":
|
|
319
|
-
return { code: utils.keyCodes.BACK_QUOTE, shift: false };
|
|
320
|
-
case "#":
|
|
321
|
-
return { code: utils.keyCodes.K3, shift: true };
|
|
322
|
-
case "$":
|
|
323
|
-
return { code: utils.keyCodes.K4, shift: true };
|
|
324
|
-
case "%":
|
|
325
|
-
return { code: utils.keyCodes.K5, shift: true };
|
|
326
|
-
default: {
|
|
327
|
-
const upper = ch.toUpperCase();
|
|
328
|
-
const isLetter = (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
|
|
329
|
-
if (isLetter) {
|
|
330
|
-
const wantUpper = ch >= "A" && ch <= "Z";
|
|
331
|
-
const capsOn = this.processor.sysvia.capsLockLight;
|
|
332
|
-
// CAPS LOCK on: unshifted = upper, shifted = lower
|
|
333
|
-
// CAPS LOCK off: unshifted = lower, shifted = upper
|
|
334
|
-
const needShift = capsOn ? !wantUpper : wantUpper;
|
|
335
|
-
return { code: upper.charCodeAt(0), shift: needShift };
|
|
336
|
-
}
|
|
337
|
-
return { code: ch.charCodeAt(0), shift: false };
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Type text by installing a debugInstruction hook that presses/releases
|
|
344
|
-
* keys at timed intervals during CPU execution. The hook persists across
|
|
345
|
-
* runFor calls, so breakpoints naturally coexist: if a breakpoint halts
|
|
346
|
-
* execution mid-typing, the remaining characters are typed when execution
|
|
347
|
-
* resumes.
|
|
348
|
-
*/
|
|
349
|
-
async type(text) {
|
|
350
|
-
if (this.model.isAtom) {
|
|
351
|
-
return this._typeAtom(text);
|
|
352
|
-
}
|
|
353
|
-
const fullText = text + "\n"; // append RETURN
|
|
354
|
-
const keys = fullText.split("").map((ch) => this._charToKey(ch));
|
|
355
|
-
// Key hold is counted in CPU cycles, so scale it to keep the hold constant in
|
|
356
|
-
// real time: the OS scans the keyboard on a peripheral-rate interrupt.
|
|
357
|
-
const holdCycles = (40000 * this.processor.cpuMultiplier) | 0;
|
|
358
|
-
let index = 0;
|
|
359
|
-
let phase = "idle"; // "idle" → "down" → "idle"
|
|
360
|
-
let nextEventCycle = 0;
|
|
361
|
-
let done = false;
|
|
362
|
-
|
|
363
|
-
const currentCycle = () =>
|
|
364
|
-
this.processor.cycleSeconds * this.model.cyclesPerSecond + this.processor.currentCycles;
|
|
365
|
-
|
|
366
|
-
const hook = this.processor.debugInstruction.add(() => {
|
|
367
|
-
if (currentCycle() < nextEventCycle) return;
|
|
368
|
-
|
|
369
|
-
if (phase === "down") {
|
|
370
|
-
// Release current key
|
|
371
|
-
const key = keys[index];
|
|
372
|
-
this.processor.sysvia.keyUp(key.code);
|
|
373
|
-
if (key.shift) this.processor.sysvia.keyUp(16);
|
|
374
|
-
index++;
|
|
375
|
-
phase = "idle";
|
|
376
|
-
nextEventCycle = currentCycle() + holdCycles;
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// phase === "idle"
|
|
381
|
-
if (index >= keys.length) {
|
|
382
|
-
hook.remove();
|
|
383
|
-
done = true;
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Press next key
|
|
388
|
-
const key = keys[index];
|
|
389
|
-
if (key.shift) this.processor.sysvia.keyDown(16);
|
|
390
|
-
this.processor.sysvia.keyDown(key.code);
|
|
391
|
-
phase = "down";
|
|
392
|
-
nextEventCycle = currentCycle() + holdCycles;
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
// Drive execution in chunks until all characters are typed or
|
|
396
|
-
// a breakpoint halts the CPU.
|
|
397
|
-
while (!done) {
|
|
398
|
-
const stopped = await this.runFor(holdCycles);
|
|
399
|
-
if (stopped) break;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/** Type text on the Atom using its key mapping and PPIA interface. */
|
|
404
|
-
async _typeAtom(text) {
|
|
405
|
-
// stringToATOMKeys returns a flat array of [col, row] pairs.
|
|
406
|
-
// SHIFT is held across multiple characters; LOCK is tapped to
|
|
407
|
-
// toggle the ROM's internal caps lock state.
|
|
408
|
-
const keySequence = utils_atom.stringToATOMKeys(text + "\n");
|
|
409
|
-
const ppia = this.processor.atomppia;
|
|
410
|
-
const holdCycles = (80000 * this.processor.cpuMultiplier) | 0; // Atom at 1 MHz needs longer hold than BBC at 2 MHz
|
|
411
|
-
const SHIFT = utils_atom.ATOM.SHIFT;
|
|
412
|
-
|
|
413
|
-
let index = 0;
|
|
414
|
-
let phase = "idle";
|
|
415
|
-
let nextEventCycle = 0;
|
|
416
|
-
let done = false;
|
|
417
|
-
let shiftHeld = false;
|
|
418
|
-
|
|
419
|
-
const currentCycle = () =>
|
|
420
|
-
this.processor.cycleSeconds * this.model.cyclesPerSecond + this.processor.currentCycles;
|
|
421
|
-
|
|
422
|
-
const isShift = (entry) => entry[0] === SHIFT[0] && entry[1] === SHIFT[1];
|
|
423
|
-
|
|
424
|
-
const hook = this.processor.debugInstruction.add(() => {
|
|
425
|
-
if (currentCycle() < nextEventCycle) return;
|
|
426
|
-
|
|
427
|
-
if (phase === "down") {
|
|
428
|
-
const entry = keySequence[index];
|
|
429
|
-
if (!isShift(entry)) {
|
|
430
|
-
ppia.keyUpRaw(entry);
|
|
431
|
-
}
|
|
432
|
-
index++;
|
|
433
|
-
phase = "idle";
|
|
434
|
-
nextEventCycle = currentCycle() + holdCycles;
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (index >= keySequence.length) {
|
|
439
|
-
if (shiftHeld) ppia.keyUpRaw(SHIFT);
|
|
440
|
-
hook.remove();
|
|
441
|
-
done = true;
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const entry = keySequence[index];
|
|
446
|
-
if (isShift(entry)) {
|
|
447
|
-
if (shiftHeld) {
|
|
448
|
-
ppia.keyUpRaw(SHIFT);
|
|
449
|
-
shiftHeld = false;
|
|
450
|
-
} else {
|
|
451
|
-
ppia.keyDownRaw(SHIFT);
|
|
452
|
-
shiftHeld = true;
|
|
453
|
-
}
|
|
454
|
-
index++;
|
|
455
|
-
nextEventCycle = currentCycle() + holdCycles;
|
|
456
|
-
} else {
|
|
457
|
-
ppia.keyDownRaw(entry);
|
|
458
|
-
phase = "down";
|
|
459
|
-
nextEventCycle = currentCycle() + holdCycles;
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
while (!done) {
|
|
464
|
-
const stopped = await this.runFor(holdCycles);
|
|
465
|
-
if (stopped) {
|
|
466
|
-
hook.remove();
|
|
467
|
-
if (shiftHeld) ppia.keyUpRaw(SHIFT);
|
|
468
|
-
break;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Press a key on the keyboard.
|
|
475
|
-
* @param {number} code - key code (BBC keyCode or Atom raw key)
|
|
476
|
-
*/
|
|
477
|
-
keyDown(code) {
|
|
478
|
-
this._keyInterface.keyDown(code);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Release a key on the keyboard.
|
|
483
|
-
* @param {number} code - key code
|
|
484
|
-
*/
|
|
485
|
-
keyUp(code) {
|
|
486
|
-
this._keyInterface.keyUp(code);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Load a ROM image directly into a sideways RAM slot.
|
|
491
|
-
* @param {number} slot - slot number (0-15, typically 4-7 for SWRAM)
|
|
492
|
-
* @param {Uint8Array|Buffer} data - ROM data (up to 16384 bytes)
|
|
493
|
-
*/
|
|
494
|
-
loadSidewaysRam(slot, data) {
|
|
495
|
-
const offset = this.processor.romOffset + slot * 16384;
|
|
496
|
-
for (let i = 0; i < data.length && i < 16384; i++) {
|
|
497
|
-
this.processor.ramRomOs[offset + i] = data[i];
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
writebyte(addr, val) {
|
|
502
|
-
this.processor.writemem(addr, val);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
readbyte(addr) {
|
|
506
|
-
return this.processor.readmem(addr);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
readword(addr) {
|
|
510
|
-
return this.readbyte(addr) | (this.readbyte(addr + 1) << 8);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
captureText(onElement) {
|
|
514
|
-
const attributes = {
|
|
515
|
-
x: 0,
|
|
516
|
-
y: 0,
|
|
517
|
-
text: "",
|
|
518
|
-
foreground: 7,
|
|
519
|
-
background: 0,
|
|
520
|
-
mode: 7,
|
|
521
|
-
};
|
|
522
|
-
let currentText = "";
|
|
523
|
-
let params = [];
|
|
524
|
-
let nextN = 0;
|
|
525
|
-
let vduProc = null;
|
|
526
|
-
|
|
527
|
-
function flush() {
|
|
528
|
-
if (currentText.length) {
|
|
529
|
-
attributes.text = currentText;
|
|
530
|
-
onElement(attributes);
|
|
531
|
-
attributes.x += currentText.length; // Approximately...anyway
|
|
532
|
-
}
|
|
533
|
-
currentText = "";
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function onChar(c) {
|
|
537
|
-
if (nextN) {
|
|
538
|
-
params.push(c);
|
|
539
|
-
if (--nextN === 0) {
|
|
540
|
-
if (vduProc) vduProc(params);
|
|
541
|
-
params = [];
|
|
542
|
-
vduProc = null;
|
|
543
|
-
}
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
switch (c) {
|
|
547
|
-
case 1: // Next char to printer
|
|
548
|
-
nextN = 1;
|
|
549
|
-
break;
|
|
550
|
-
case 10:
|
|
551
|
-
attributes.y++;
|
|
552
|
-
break;
|
|
553
|
-
case 12: // CLS
|
|
554
|
-
attributes.x = 0;
|
|
555
|
-
attributes.y = 0;
|
|
556
|
-
break;
|
|
557
|
-
case 13:
|
|
558
|
-
attributes.x = 0;
|
|
559
|
-
break;
|
|
560
|
-
case 17: // Text colour
|
|
561
|
-
nextN = 1;
|
|
562
|
-
vduProc = function (params) {
|
|
563
|
-
if (params[0] & 0x80) attributes.background = params[0] & 0xf;
|
|
564
|
-
else attributes.foreground = params[0] & 0xf;
|
|
565
|
-
};
|
|
566
|
-
break;
|
|
567
|
-
case 18: // GCOL
|
|
568
|
-
nextN = 2;
|
|
569
|
-
break;
|
|
570
|
-
case 19: // logical colour
|
|
571
|
-
nextN = 5;
|
|
572
|
-
break;
|
|
573
|
-
case 22: // mode
|
|
574
|
-
nextN = 1;
|
|
575
|
-
vduProc = function (params) {
|
|
576
|
-
attributes.mode = params[0];
|
|
577
|
-
attributes.x = 0;
|
|
578
|
-
attributes.y = 0;
|
|
579
|
-
};
|
|
580
|
-
break;
|
|
581
|
-
case 25: // plot
|
|
582
|
-
nextN = 5;
|
|
583
|
-
break;
|
|
584
|
-
case 28: // text window
|
|
585
|
-
nextN = 4;
|
|
586
|
-
break;
|
|
587
|
-
case 29: // origin
|
|
588
|
-
nextN = 4;
|
|
589
|
-
break;
|
|
590
|
-
case 31: // text location
|
|
591
|
-
nextN = 2;
|
|
592
|
-
vduProc = function (params) {
|
|
593
|
-
attributes.x = params[0];
|
|
594
|
-
attributes.y = params[1];
|
|
595
|
-
};
|
|
596
|
-
}
|
|
597
|
-
if (c >= 32 && c < 0x7f) {
|
|
598
|
-
currentText += String.fromCharCode(c);
|
|
599
|
-
} else flush();
|
|
600
|
-
return false;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const wrchv = this.readword(0x20e);
|
|
604
|
-
this.processor.debugInstruction.add((addr) => {
|
|
605
|
-
if (addr === wrchv) onChar(this.processor.a);
|
|
606
|
-
return false;
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
}
|