jsbeeb 1.12.0 → 1.13.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.
@@ -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
- this.processor = fake6502(findModel(model), opts || {});
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(0x20e);
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 BBC keyboard.
356
- * @param {number} code - BBC key code (e.g. utils.keyCodes.SPACE)
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.processor.sysvia.keyDown(code);
484
+ this._keyInterface.keyDown(code);
360
485
  }
361
486
 
362
487
  /**
363
- * Release a key on the BBC keyboard.
364
- * @param {number} code - BBC key code
488
+ * Release a key on the keyboard.
489
+ * @param {number} code - key code
365
490
  */
366
491
  keyUp(code) {
367
- this.processor.sysvia.keyUp(code);
492
+ this._keyInterface.keyUp(code);
368
493
  }
369
494
 
370
495
  /**