jsbeeb 1.12.0 → 1.13.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 +16 -2
- package/package.json +8 -8
- package/public/roms/atom/ATMMC3E.rom +0 -0
- package/public/roms/atom/Atom_Basic.rom +0 -0
- package/public/roms/atom/Atom_DOS.rom +0 -0
- package/public/roms/atom/Atom_FloatingPoint.rom +0 -0
- package/public/roms/atom/Atom_Kernel.rom +0 -0
- package/public/roms/atom/Atom_Kernel_E.rom +0 -0
- package/public/roms/atom/PCHARME.ROM +0 -0
- package/public/roms/atom/gags.rom +0 -0
- package/public/roms/atom/werom.rom +0 -0
- package/src/6502.js +344 -44
- package/src/6847.js +724 -0
- package/src/6847_fontdata.js +124 -0
- package/src/app/app.js +2 -15
- package/src/app/args.js +26 -0
- package/src/disc.js +2 -20
- package/src/fake6502.js +3 -2
- package/src/jsbeeb.css +23 -0
- package/src/keyboard.js +45 -23
- package/src/machine-session.js +85 -59
- package/src/main.js +142 -41
- package/src/mmc.js +1053 -0
- package/src/models.js +42 -1
- package/src/ppia.js +477 -0
- package/src/soundchip.js +99 -1
- package/src/tapes.js +73 -16
- package/src/url-params.js +7 -2
- package/src/utils.js +74 -1
- package/src/utils_atom.js +508 -0
- package/src/video.js +12 -1
- package/src/web/audio-handler.js +8 -3
- package/tests/test-machine.js +133 -8
package/tests/test-machine.js
CHANGED
|
@@ -3,6 +3,7 @@ import { fake6502 } from "../src/fake6502.js";
|
|
|
3
3
|
import { findModel } from "../src/models.js";
|
|
4
4
|
import assert from "assert";
|
|
5
5
|
import * as utils from "../src/utils.js";
|
|
6
|
+
import * as utils_atom from "../src/utils_atom.js";
|
|
6
7
|
import * as Tokeniser from "../src/basic-tokenise.js";
|
|
7
8
|
|
|
8
9
|
const MaxCyclesPerIter = 100 * 1000;
|
|
@@ -10,13 +11,46 @@ const MaxCyclesPerIter = 100 * 1000;
|
|
|
10
11
|
export class TestMachine {
|
|
11
12
|
constructor(model, opts) {
|
|
12
13
|
model = model || "B-DFS1.2";
|
|
13
|
-
|
|
14
|
+
const modelObj = findModel(model);
|
|
15
|
+
this.model = modelObj;
|
|
16
|
+
this.processor = fake6502(modelObj, opts || {});
|
|
14
17
|
this._capturedChars = [];
|
|
15
18
|
this._captureHookInstalled = false;
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
/** The keyboard interface for this machine (SysVia for BBC, PPIA for Atom). */
|
|
22
|
+
get _keyInterface() {
|
|
23
|
+
return this.model.isAtom ? this.processor.atomppia : this.processor.sysvia;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
async initialise() {
|
|
19
27
|
await this.processor.initialise();
|
|
28
|
+
if (this.model.isAtom) this._startAtomVSync();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The Atom ROM's main loop waits for VSync (bit 7 of Port C) to
|
|
33
|
+
* toggle before scanning the keyboard. The MC6847 video chip drives
|
|
34
|
+
* this in the real emulator, but fake6502 doesn't create one, so we
|
|
35
|
+
* simulate it with a scheduler task at ~60 Hz (NTSC).
|
|
36
|
+
*/
|
|
37
|
+
_startAtomVSync() {
|
|
38
|
+
const ppia = this.processor.atomppia;
|
|
39
|
+
const VsyncPeriod = 16667; // 1 MHz / 60 Hz (NTSC 262-line frame)
|
|
40
|
+
const VsyncPulse = 800;
|
|
41
|
+
let inVsync = false;
|
|
42
|
+
const task = this.processor.scheduler.newTask(() => {
|
|
43
|
+
if (!inVsync) {
|
|
44
|
+
ppia.setVBlankInt(1);
|
|
45
|
+
inVsync = true;
|
|
46
|
+
task.reschedule(VsyncPulse);
|
|
47
|
+
} else {
|
|
48
|
+
ppia.setVBlankInt(0);
|
|
49
|
+
inVsync = false;
|
|
50
|
+
task.reschedule(VsyncPeriod - VsyncPulse);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
task.schedule(VsyncPeriod);
|
|
20
54
|
}
|
|
21
55
|
|
|
22
56
|
/**
|
|
@@ -27,8 +61,10 @@ export class TestMachine {
|
|
|
27
61
|
startCapture() {
|
|
28
62
|
if (this._captureHookInstalled) return;
|
|
29
63
|
this._captureHookInstalled = true;
|
|
64
|
+
// WRCHV is at 0x0208 on Atom, 0x020E on BBC.
|
|
65
|
+
const wrchvAddr = this.model.isAtom ? 0x0208 : 0x020e;
|
|
30
66
|
this.processor.debugInstruction.add((addr) => {
|
|
31
|
-
const wrchv = this.readword(
|
|
67
|
+
const wrchv = this.readword(wrchvAddr);
|
|
32
68
|
if (addr === wrchv) {
|
|
33
69
|
this._capturedChars.push(this.processor.a);
|
|
34
70
|
}
|
|
@@ -130,6 +166,23 @@ export class TestMachine {
|
|
|
130
166
|
async runUntilInput(secs) {
|
|
131
167
|
if (!secs) secs = 120;
|
|
132
168
|
console.log("Running until keyboard input requested");
|
|
169
|
+
if (this.model.isAtom) {
|
|
170
|
+
// The Atom kernel's keyboard read loop at $FE94 is entered when
|
|
171
|
+
// BASIC (or the OS) waits for a keypress. We detect entry to
|
|
172
|
+
// this routine as the idle point.
|
|
173
|
+
const atomIdleAddr = 0xfe94;
|
|
174
|
+
let hit = false;
|
|
175
|
+
const hook = this.processor.debugInstruction.add((addr) => {
|
|
176
|
+
if (addr === atomIdleAddr) {
|
|
177
|
+
hit = true;
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
await this.runFor(secs * 1 * 1000 * 1000); // Atom is 1 MHz
|
|
182
|
+
hook.remove();
|
|
183
|
+
assert(hit, "Atom did not reach keyboard input in time");
|
|
184
|
+
return this.runFor(10 * 1000);
|
|
185
|
+
}
|
|
133
186
|
const idleAddr = this.processor.model.isMaster ? 0xe7e6 : 0xe581;
|
|
134
187
|
let hit = false;
|
|
135
188
|
const hook = this.processor.debugInstruction.add((addr) => {
|
|
@@ -304,6 +357,9 @@ export class TestMachine {
|
|
|
304
357
|
* resumes.
|
|
305
358
|
*/
|
|
306
359
|
async type(text) {
|
|
360
|
+
if (this.model.isAtom) {
|
|
361
|
+
return this._typeAtom(text);
|
|
362
|
+
}
|
|
307
363
|
const fullText = text + "\n"; // append RETURN
|
|
308
364
|
const keys = fullText.split("").map((ch) => this._charToKey(ch));
|
|
309
365
|
const holdCycles = 40000;
|
|
@@ -351,20 +407,89 @@ export class TestMachine {
|
|
|
351
407
|
}
|
|
352
408
|
}
|
|
353
409
|
|
|
410
|
+
/** Type text on the Atom using its key mapping and PPIA interface. */
|
|
411
|
+
async _typeAtom(text) {
|
|
412
|
+
// stringToATOMKeys returns a flat array of [col, row] pairs.
|
|
413
|
+
// SHIFT is held across multiple characters; LOCK is tapped to
|
|
414
|
+
// toggle the ROM's internal caps lock state.
|
|
415
|
+
const keySequence = utils_atom.stringToATOMKeys(text + "\n");
|
|
416
|
+
const ppia = this.processor.atomppia;
|
|
417
|
+
const holdCycles = 80000; // Atom at 1 MHz needs longer hold than BBC at 2 MHz
|
|
418
|
+
const SHIFT = utils_atom.ATOM.SHIFT;
|
|
419
|
+
|
|
420
|
+
let index = 0;
|
|
421
|
+
let phase = "idle";
|
|
422
|
+
let nextEventCycle = 0;
|
|
423
|
+
let done = false;
|
|
424
|
+
let shiftHeld = false;
|
|
425
|
+
|
|
426
|
+
const currentCycle = () => this.processor.cycleSeconds * 1000000 + this.processor.currentCycles;
|
|
427
|
+
|
|
428
|
+
const isShift = (entry) => entry[0] === SHIFT[0] && entry[1] === SHIFT[1];
|
|
429
|
+
|
|
430
|
+
const hook = this.processor.debugInstruction.add(() => {
|
|
431
|
+
if (currentCycle() < nextEventCycle) return;
|
|
432
|
+
|
|
433
|
+
if (phase === "down") {
|
|
434
|
+
const entry = keySequence[index];
|
|
435
|
+
if (!isShift(entry)) {
|
|
436
|
+
ppia.keyUpRaw(entry);
|
|
437
|
+
}
|
|
438
|
+
index++;
|
|
439
|
+
phase = "idle";
|
|
440
|
+
nextEventCycle = currentCycle() + holdCycles;
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (index >= keySequence.length) {
|
|
445
|
+
if (shiftHeld) ppia.keyUpRaw(SHIFT);
|
|
446
|
+
hook.remove();
|
|
447
|
+
done = true;
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const entry = keySequence[index];
|
|
452
|
+
if (isShift(entry)) {
|
|
453
|
+
if (shiftHeld) {
|
|
454
|
+
ppia.keyUpRaw(SHIFT);
|
|
455
|
+
shiftHeld = false;
|
|
456
|
+
} else {
|
|
457
|
+
ppia.keyDownRaw(SHIFT);
|
|
458
|
+
shiftHeld = true;
|
|
459
|
+
}
|
|
460
|
+
index++;
|
|
461
|
+
nextEventCycle = currentCycle() + holdCycles;
|
|
462
|
+
} else {
|
|
463
|
+
ppia.keyDownRaw(entry);
|
|
464
|
+
phase = "down";
|
|
465
|
+
nextEventCycle = currentCycle() + holdCycles;
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
while (!done) {
|
|
470
|
+
const stopped = await this.runFor(holdCycles);
|
|
471
|
+
if (stopped) {
|
|
472
|
+
hook.remove();
|
|
473
|
+
if (shiftHeld) ppia.keyUpRaw(SHIFT);
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
354
479
|
/**
|
|
355
|
-
* Press a key on the
|
|
356
|
-
* @param {number} code -
|
|
480
|
+
* Press a key on the keyboard.
|
|
481
|
+
* @param {number} code - key code (BBC keyCode or Atom raw key)
|
|
357
482
|
*/
|
|
358
483
|
keyDown(code) {
|
|
359
|
-
this.
|
|
484
|
+
this._keyInterface.keyDown(code);
|
|
360
485
|
}
|
|
361
486
|
|
|
362
487
|
/**
|
|
363
|
-
* Release a key on the
|
|
364
|
-
* @param {number} code -
|
|
488
|
+
* Release a key on the keyboard.
|
|
489
|
+
* @param {number} code - key code
|
|
365
490
|
*/
|
|
366
491
|
keyUp(code) {
|
|
367
|
-
this.
|
|
492
|
+
this._keyInterface.keyUp(code);
|
|
368
493
|
}
|
|
369
494
|
|
|
370
495
|
/**
|