jsbeeb 1.15.0 → 1.17.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 +8 -2
- package/package.json +15 -6
- package/src/6502.js +14 -7
- package/src/6847.js +50 -8
- package/src/acia.js +2 -1
- package/src/app/app.js +4 -0
- package/src/bbcdiscs.js +84 -0
- package/src/canvas.js +85 -19
- package/src/disc-drive.js +54 -12
- package/src/disc-hfe.js +19 -3
- package/src/disc-surface.js +350 -0
- package/src/disc-visualiser.js +569 -0
- package/src/disc.js +226 -62
- package/src/econet.js +6 -2
- package/src/fdc.js +83 -11
- package/src/google-drive.js +4 -4
- package/src/intel-fdc.js +8 -1
- package/src/jsbeeb.css +191 -0
- package/src/keyboard.js +14 -15
- package/src/machine-session.js +2 -1
- package/src/main.js +389 -152
- package/src/models.js +15 -3
- package/src/sth.js +25 -22
- package/src/tapes.js +10 -12
- package/src/teletext_adaptor.js +6 -4
- package/src/touchscreen.js +6 -6
- package/src/tube.js +89 -37
- package/src/url-params.js +28 -0
- package/src/utils.js +3 -0
- package/src/video-filters/pal-composite.js +14 -48
- package/src/video-filters/passthrough-filter.js +11 -39
- package/src/video-filters/pixel-grid.js +55 -0
- package/src/video-filters/shader-program.js +53 -0
- package/src/video-filters/shaders/xbr.frag.glsl +278 -0
- package/src/video-filters/shaders/xbr.vert.glsl +7 -0
- package/src/video-filters/xbr-filter.js +107 -0
- package/src/video.js +56 -16
- package/src/wd-fdc.js +25 -8
- package/src/web/audio-handler.js +4 -2
- package/src/web/toast.js +79 -0
- package/tests/test-machine.js +8 -6
package/src/video.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Teletext } from "./teletext.js";
|
|
|
3
3
|
import * as utils from "./utils.js";
|
|
4
4
|
import { BbcDefaultPalette as NulaDefaultPalette } from "./bbc-palette.js";
|
|
5
5
|
import { Video6847 } from "./6847.js";
|
|
6
|
+
import { encodeLineGrid, texelsPerPixel, LineGridRows } from "./video-filters/pixel-grid.js";
|
|
6
7
|
|
|
7
8
|
export const VDISPENABLE = 1 << 0;
|
|
8
9
|
export const HDISPENABLE = 1 << 1;
|
|
@@ -16,6 +17,8 @@ export const EVERYTHINGENABLED =
|
|
|
16
17
|
export const OPAQUE_BLACK = 0xff000000;
|
|
17
18
|
export const OPAQUE_WHITE = 0xffffffff;
|
|
18
19
|
|
|
20
|
+
export const MinPaintedFrameRows = 64;
|
|
21
|
+
|
|
19
22
|
////////////////////
|
|
20
23
|
// VideoNULA - programmable 12-bit RGB palette extension (RobC hardware mod).
|
|
21
24
|
// Reference: b-em src/video.c (stardot/b-em).
|
|
@@ -127,6 +130,7 @@ class Ula {
|
|
|
127
130
|
this.video.ulaMode = newMode;
|
|
128
131
|
}
|
|
129
132
|
this.video.teletextMode = !!(val & 2);
|
|
133
|
+
this.video.updateLineGridUla();
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
// ULA palette register (&FE21).
|
|
@@ -386,6 +390,17 @@ export class Video {
|
|
|
386
390
|
this.doubledScanlines = true;
|
|
387
391
|
this.frameSkipCount = 0;
|
|
388
392
|
this.screenSubtract = 0;
|
|
393
|
+
// Describes the logical pixel grid of each framebuffer row: how many
|
|
394
|
+
// texels wide and tall one BBC pixel is on that line. Display filters
|
|
395
|
+
// need this to see the picture as pixels rather than as raster samples;
|
|
396
|
+
// see video-filters/pixel-grid.js. One byte per row, written as each
|
|
397
|
+
// character cell renders, so a mode change between rows is recorded
|
|
398
|
+
// faithfully. A row whose mode changes part way along keeps only the
|
|
399
|
+
// last mode on it — enough for a raster split, not for a mid-line one.
|
|
400
|
+
this.lineGrid = new Uint8Array(LineGridRows);
|
|
401
|
+
this.lineGridUla = 0;
|
|
402
|
+
this.lineGridUlaDoubled = 0;
|
|
403
|
+
this.updateLineGridUla();
|
|
389
404
|
|
|
390
405
|
this.topBorder = 12;
|
|
391
406
|
this.bottomBorder = 13;
|
|
@@ -524,6 +539,8 @@ export class Video {
|
|
|
524
539
|
this.halfClock = state.halfClock;
|
|
525
540
|
this.ulaMode = state.ulaMode;
|
|
526
541
|
this.teletextMode = state.teletextMode;
|
|
542
|
+
// Derived from the above, so it is recomputed rather than snapshotted.
|
|
543
|
+
this.updateLineGridUla();
|
|
527
544
|
this.displayEnableSkew = state.displayEnableSkew;
|
|
528
545
|
this.actualPal.set(state.actualPal);
|
|
529
546
|
this.cursorOn = state.cursorOn;
|
|
@@ -555,20 +572,24 @@ export class Video {
|
|
|
555
572
|
|
|
556
573
|
clearPaintBuffer() {
|
|
557
574
|
const fb32 = this.fb32;
|
|
575
|
+
// The line grid is cleared exactly where the pixels are: in interlaced
|
|
576
|
+
// modes the other field's rows survive, and so must their grid.
|
|
558
577
|
if (this.interlacedSyncAndVideo || !this.doubledScanlines) {
|
|
559
578
|
let line = this.frameCount & 1;
|
|
560
579
|
while (line < 625) {
|
|
561
580
|
const start = line * 1024;
|
|
562
581
|
fb32.fill(OPAQUE_BLACK, start, start + 1024);
|
|
582
|
+
this.lineGrid[line] = 0;
|
|
563
583
|
line += 2;
|
|
564
584
|
}
|
|
565
585
|
} else {
|
|
566
586
|
fb32.fill(OPAQUE_BLACK);
|
|
587
|
+
this.lineGrid.fill(0);
|
|
567
588
|
}
|
|
568
589
|
}
|
|
569
590
|
|
|
570
|
-
|
|
571
|
-
if (this.dispEnabled & FRAMESKIPENABLE) {
|
|
591
|
+
flyback() {
|
|
592
|
+
if (this.bitmapY >= MinPaintedFrameRows && this.dispEnabled & FRAMESKIPENABLE) {
|
|
572
593
|
this.paint();
|
|
573
594
|
this.clearPaintBuffer();
|
|
574
595
|
}
|
|
@@ -610,6 +631,21 @@ export class Video {
|
|
|
610
631
|
debugCopyFb(this.fb32, this.debugPrevScreen);
|
|
611
632
|
}
|
|
612
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Recompute the ULA-dependent half of the line grid descriptor: everything
|
|
636
|
+
* except whether this particular scanline was doubled. Called whenever the
|
|
637
|
+
* ULA control register changes, so the render loop only has to store it.
|
|
638
|
+
*
|
|
639
|
+
* MODE 7 counts as one texel per pixel: the SAA5050 emulation writes each
|
|
640
|
+
* of its 16 texels per character individually, so its output is already at
|
|
641
|
+
* the framebuffer's own resolution.
|
|
642
|
+
*/
|
|
643
|
+
updateLineGridUla() {
|
|
644
|
+
const texelsWide = this.teletextMode ? 1 : texelsPerPixel(this.ulaMode);
|
|
645
|
+
this.lineGridUla = encodeLineGrid(texelsWide, false);
|
|
646
|
+
this.lineGridUlaDoubled = encodeLineGrid(texelsWide, true);
|
|
647
|
+
}
|
|
648
|
+
|
|
613
649
|
blitFb(dat, destOffset, numPixels) {
|
|
614
650
|
destOffset |= 0;
|
|
615
651
|
const offset = table4bppOffset(this.ulaMode, dat);
|
|
@@ -816,14 +852,12 @@ export class Video {
|
|
|
816
852
|
// an approximation that works if hsyncs are spaced evenly.
|
|
817
853
|
this.bitmapY += 2;
|
|
818
854
|
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
// Arbitrary moment when TV will give up and start flyback in the absence of an explicit VSync signal
|
|
822
|
-
this.paintAndClear();
|
|
823
|
-
}
|
|
855
|
+
// Arbitrary moment when TV will give up and start flyback in the absence of an explicit VSync signal
|
|
856
|
+
return this.bitmapY >= 768;
|
|
824
857
|
} else if (this.hpulseCounter === (this.regs[3] & 0x0f)) {
|
|
825
858
|
this.inHSync = false;
|
|
826
859
|
}
|
|
860
|
+
return false;
|
|
827
861
|
}
|
|
828
862
|
|
|
829
863
|
cb2changed(level, output) {
|
|
@@ -871,7 +905,7 @@ export class Video {
|
|
|
871
905
|
// This emulates the Hitachi 6845SP CRTC.
|
|
872
906
|
// Other variants have different quirks.
|
|
873
907
|
// Handle HSync
|
|
874
|
-
if (this.inHSync
|
|
908
|
+
if (this.inHSync && this.handleHSync()) this.flyback();
|
|
875
909
|
|
|
876
910
|
// Handle delayed display enable due to skew
|
|
877
911
|
const displayEnablePos = this.displayEnableSkew + (this.teletextMode ? 2 : 0);
|
|
@@ -935,11 +969,7 @@ export class Video {
|
|
|
935
969
|
this.hadVSyncThisRow = true;
|
|
936
970
|
this.vpulseCounter = 0;
|
|
937
971
|
|
|
938
|
-
|
|
939
|
-
// otherwise small values.
|
|
940
|
-
if (this.regs[0] && this.regs[4]) {
|
|
941
|
-
this.paintAndClear();
|
|
942
|
-
}
|
|
972
|
+
this.flyback();
|
|
943
973
|
}
|
|
944
974
|
|
|
945
975
|
if (vSyncStarting || vSyncEnding) {
|
|
@@ -974,7 +1004,7 @@ export class Video {
|
|
|
974
1004
|
// Render data depending on display enable state.
|
|
975
1005
|
if (this.bitmapX >= 0 && this.bitmapX < 1024 && this.bitmapY < 625) {
|
|
976
1006
|
let doubledLines = false;
|
|
977
|
-
let
|
|
1007
|
+
let bitmapRow = this.bitmapY;
|
|
978
1008
|
// There's a painting subtlety here: if we're in an
|
|
979
1009
|
// interlace mode but R6>R4 then we'll get stuck
|
|
980
1010
|
// painting just an odd or even frame, so we double up
|
|
@@ -984,12 +1014,22 @@ export class Video {
|
|
|
984
1014
|
this.isEvenRender === this.lastRenderWasEven
|
|
985
1015
|
) {
|
|
986
1016
|
doubledLines = true;
|
|
987
|
-
|
|
1017
|
+
bitmapRow &= ~1;
|
|
988
1018
|
}
|
|
989
1019
|
|
|
990
|
-
offset =
|
|
1020
|
+
const offset = bitmapRow * 1024 + this.bitmapX;
|
|
991
1021
|
|
|
992
1022
|
if ((this.dispEnabled & EVERYTHINGENABLED) === EVERYTHINGENABLED) {
|
|
1023
|
+
// Note this row's logical pixel size for display
|
|
1024
|
+
// filters; see video-filters/pixel-grid.js. The ULA half
|
|
1025
|
+
// of the descriptor is precomputed on register writes so
|
|
1026
|
+
// this stays a store or two in the hottest loop we have.
|
|
1027
|
+
if (doubledLines) {
|
|
1028
|
+
this.lineGrid[bitmapRow] = this.lineGridUlaDoubled;
|
|
1029
|
+
this.lineGrid[bitmapRow + 1] = this.lineGridUlaDoubled;
|
|
1030
|
+
} else {
|
|
1031
|
+
this.lineGrid[bitmapRow] = this.lineGridUla;
|
|
1032
|
+
}
|
|
993
1033
|
if (this.teletextMode) {
|
|
994
1034
|
if (this.halfClock) {
|
|
995
1035
|
// Proper MODE 7 (1MHz clock + teletext): render SAA5050 output normally.
|
package/src/wd-fdc.js
CHANGED
|
@@ -46,6 +46,17 @@ const CommandBits = Object.freeze({
|
|
|
46
46
|
typeIIDeleted: 0x01,
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Type IV (force interrupt) condition bits, taken from the command's low nibble.
|
|
51
|
+
*
|
|
52
|
+
* @readonly
|
|
53
|
+
* @enum {Number}
|
|
54
|
+
*/
|
|
55
|
+
const ForceInterruptBits = Object.freeze({
|
|
56
|
+
indexPulse: 0x04,
|
|
57
|
+
immediate: 0x08,
|
|
58
|
+
});
|
|
59
|
+
|
|
49
60
|
/**
|
|
50
61
|
* The drive control register is documented here:
|
|
51
62
|
* https://www.cloud9.co.uk/james/BBCMicro/Documentation/wd1770.html
|
|
@@ -512,6 +523,8 @@ export class WdFdc {
|
|
|
512
523
|
// insofar as index pulse appears to be reported in the status register.
|
|
513
524
|
// - Interrupt on index pulse is only active for the current command.
|
|
514
525
|
if (this._statusRegister & Status.busy) {
|
|
526
|
+
// Any pending timer belongs to the command being aborted.
|
|
527
|
+
this._clearTimer();
|
|
515
528
|
this._commandDone(false);
|
|
516
529
|
} else {
|
|
517
530
|
if (this._state !== State.idle) throw new Error(`Unexpected state when force interrupt: ${this._state}`);
|
|
@@ -523,13 +536,10 @@ export class WdFdc {
|
|
|
523
536
|
this._currentDrive.startSpinning();
|
|
524
537
|
}
|
|
525
538
|
}
|
|
526
|
-
if (forceInterruptBits
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
} else {
|
|
531
|
-
throw new Error(`1700 force interrupt flags not handled: ${forceInterruptBits}`);
|
|
532
|
-
}
|
|
539
|
+
if (forceInterruptBits & ForceInterruptBits.immediate) this._setIntRq(true);
|
|
540
|
+
this._isInterruptOnIndexPulse = !!(forceInterruptBits & ForceInterruptBits.indexPulse);
|
|
541
|
+
// The remaining two bits select interrupts on the ready line's transitions. The BBC ties
|
|
542
|
+
// the 1770's READY input active, so neither transition can ever occur.
|
|
533
543
|
}
|
|
534
544
|
|
|
535
545
|
_timerFired() {
|
|
@@ -1404,8 +1414,15 @@ export class WdFdc {
|
|
|
1404
1414
|
* @param {Number} drive
|
|
1405
1415
|
* @param {Disc} disc
|
|
1406
1416
|
*/
|
|
1407
|
-
|
|
1417
|
+
/**
|
|
1418
|
+
* @param {Number} drive
|
|
1419
|
+
* @param {Disc} disc
|
|
1420
|
+
* @param {Number} [tracksPerStep] where to leave the drive's 40/80 switch, which by default
|
|
1421
|
+
* follows the disc, since no drive can tell what pitch the disc in it was written at
|
|
1422
|
+
*/
|
|
1423
|
+
loadDisc(drive, disc, tracksPerStep = disc?.is40Track ? 2 : 1) {
|
|
1408
1424
|
this._drives[drive].setDisc(disc);
|
|
1425
|
+
this._drives[drive].tracksPerStep = tracksPerStep;
|
|
1409
1426
|
}
|
|
1410
1427
|
|
|
1411
1428
|
get motorOn() {
|
package/src/web/audio-handler.js
CHANGED
|
@@ -15,6 +15,7 @@ export class AudioHandler {
|
|
|
15
15
|
this.cpuSpeed = cpuSpeed;
|
|
16
16
|
this.isAtom = isAtom;
|
|
17
17
|
this.warningNode = warningNode;
|
|
18
|
+
this.noAudioWorklet = false;
|
|
18
19
|
toggle(this.warningNode, false);
|
|
19
20
|
this.stats = {};
|
|
20
21
|
if (statsNode) {
|
|
@@ -41,14 +42,15 @@ export class AudioHandler {
|
|
|
41
42
|
} else {
|
|
42
43
|
if (this.audioContext && !this.audioContext.audioWorklet) {
|
|
43
44
|
this.audioContext = null;
|
|
45
|
+
this.noAudioWorklet = true;
|
|
44
46
|
console.log("Unable to initialise audio: no audio worklet API");
|
|
45
|
-
toggle(this.warningNode, true);
|
|
46
47
|
const localhost = new URL(window.location);
|
|
47
48
|
localhost.hostname = "localhost";
|
|
48
49
|
this.warningNode.innerHTML = `No audio worklet API was found - there will be no audio.
|
|
49
50
|
If you are running a local jsbeeb, you must either use a host of
|
|
50
51
|
<a href="${localhost}">localhost</a>,
|
|
51
52
|
or serve the content over <em>https</em>.`;
|
|
53
|
+
toggle(this.warningNode, true);
|
|
52
54
|
}
|
|
53
55
|
this.soundChip = new FakeSoundChip();
|
|
54
56
|
this.ddNoise = new FakeDdNoise();
|
|
@@ -56,7 +58,6 @@ export class AudioHandler {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
this.warningNode.addEventListener("mousedown", () => this.tryResume());
|
|
59
|
-
toggle(this.warningNode, false);
|
|
60
61
|
|
|
61
62
|
// Initialise Music 5000 audio context
|
|
62
63
|
this.audioContextM5000 = createAudioContext({ sampleRate: 46875 });
|
|
@@ -145,6 +146,7 @@ export class AudioHandler {
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
checkStatus() {
|
|
149
|
+
if (this.noAudioWorklet) return;
|
|
148
150
|
if (!this.audioContext && !this.audioContextM5000) return;
|
|
149
151
|
const suspended =
|
|
150
152
|
(this.audioContext && this.audioContext.state === "suspended") ||
|
package/src/web/toast.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as bootstrap from "bootstrap";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Passing notices: something happened that is worth knowing and needs nothing doing about it.
|
|
5
|
+
* The error dialog is for the other kind.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
let nextQuietId = 0;
|
|
9
|
+
|
|
10
|
+
function toastContainer() {
|
|
11
|
+
const existing = document.querySelector(".toast-container");
|
|
12
|
+
if (existing) return existing;
|
|
13
|
+
const container = document.createElement("div");
|
|
14
|
+
container.className = "toast-container position-fixed bottom-0 end-0 p-3";
|
|
15
|
+
document.body.appendChild(container);
|
|
16
|
+
return container;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Storage can be unreachable or full, and a notice that nothing needs doing about is not worth
|
|
20
|
+
// failing over: forget the answer and say the thing again.
|
|
21
|
+
function remembered(key) {
|
|
22
|
+
try {
|
|
23
|
+
return !!window.localStorage.getItem(key);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.log(`Unable to read ${key}: ${e}`);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function remember(key, wanted) {
|
|
31
|
+
try {
|
|
32
|
+
if (wanted) window.localStorage.setItem(key, "yes");
|
|
33
|
+
else window.localStorage.removeItem(key);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.log(`Unable to remember ${key}: ${e}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} message
|
|
41
|
+
* @param {object} [options]
|
|
42
|
+
* @param {string} [options.title] a heading, for a notice whose message does not say where it came from
|
|
43
|
+
* @param {string} [options.quietKey] offer to stop showing this kind of notice, remembering the answer here
|
|
44
|
+
*/
|
|
45
|
+
export function toast(message, { title = "", quietKey = "" } = {}) {
|
|
46
|
+
if (quietKey && remembered(quietKey)) return;
|
|
47
|
+
|
|
48
|
+
const element = document.createElement("div");
|
|
49
|
+
element.className = "toast text-bg-dark";
|
|
50
|
+
element.setAttribute("role", "status");
|
|
51
|
+
element.setAttribute("aria-live", "polite");
|
|
52
|
+
element.setAttribute("aria-atomic", "true");
|
|
53
|
+
const quietId = `toast-quiet-${nextQuietId++}`;
|
|
54
|
+
element.innerHTML = `
|
|
55
|
+
<div class="toast-header text-bg-dark">
|
|
56
|
+
<strong class="me-auto"></strong>
|
|
57
|
+
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="toast-body">
|
|
60
|
+
<div class="message"></div>
|
|
61
|
+
<div class="form-check mt-2">
|
|
62
|
+
<input class="form-check-input" type="checkbox" id="${quietId}" />
|
|
63
|
+
<label class="form-check-label small" for="${quietId}">Stop telling me this</label>
|
|
64
|
+
</div>
|
|
65
|
+
</div>`;
|
|
66
|
+
// Disc names and the like come from the outside world, so they are set as text, never as markup.
|
|
67
|
+
element.querySelector(".message").textContent = message;
|
|
68
|
+
element.querySelector(".toast-header strong").textContent = title;
|
|
69
|
+
element.querySelector(".toast-header").hidden = !title;
|
|
70
|
+
|
|
71
|
+
const quiet = element.querySelector(".form-check");
|
|
72
|
+
quiet.hidden = !quietKey;
|
|
73
|
+
quiet.querySelector("input").addEventListener("change", (event) => remember(quietKey, event.target.checked));
|
|
74
|
+
|
|
75
|
+
toastContainer().appendChild(element);
|
|
76
|
+
element.addEventListener("hidden.bs.toast", () => element.remove());
|
|
77
|
+
bootstrap.Toast.getOrCreateInstance(element).show();
|
|
78
|
+
return element;
|
|
79
|
+
}
|
package/tests/test-machine.js
CHANGED
|
@@ -178,7 +178,7 @@ export class TestMachine {
|
|
|
178
178
|
return true;
|
|
179
179
|
}
|
|
180
180
|
});
|
|
181
|
-
await this.runFor(secs *
|
|
181
|
+
await this.runFor(secs * this.model.cyclesPerSecond);
|
|
182
182
|
hook.remove();
|
|
183
183
|
assert(hit, "Atom did not reach keyboard input in time");
|
|
184
184
|
return this.runFor(10 * 1000);
|
|
@@ -191,7 +191,7 @@ export class TestMachine {
|
|
|
191
191
|
return true;
|
|
192
192
|
}
|
|
193
193
|
});
|
|
194
|
-
await this.runFor(secs *
|
|
194
|
+
await this.runFor(secs * this.model.cyclesPerSecond);
|
|
195
195
|
hook.remove();
|
|
196
196
|
assert(hit, "did not hit appropriate breakpoint in time");
|
|
197
197
|
return this.runFor(10 * 1000);
|
|
@@ -206,14 +206,14 @@ export class TestMachine {
|
|
|
206
206
|
return true;
|
|
207
207
|
}
|
|
208
208
|
});
|
|
209
|
-
await this.runFor(secs *
|
|
209
|
+
await this.runFor(secs * this.model.cyclesPerSecond);
|
|
210
210
|
hook.remove();
|
|
211
211
|
assert(hit, "did not hit appropriate breakpoint in time");
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
async loadDisc(image) {
|
|
215
215
|
const data = await fdc.load(image);
|
|
216
|
-
this.processor.fdc.loadDisc(0, fdc.discFor(this.processor.fdc,
|
|
216
|
+
this.processor.fdc.loadDisc(0, fdc.discFor(this.processor.fdc, image, data));
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
/**
|
|
@@ -370,7 +370,8 @@ export class TestMachine {
|
|
|
370
370
|
let nextEventCycle = 0;
|
|
371
371
|
let done = false;
|
|
372
372
|
|
|
373
|
-
const currentCycle = () =>
|
|
373
|
+
const currentCycle = () =>
|
|
374
|
+
this.processor.cycleSeconds * this.model.cyclesPerSecond + this.processor.currentCycles;
|
|
374
375
|
|
|
375
376
|
const hook = this.processor.debugInstruction.add(() => {
|
|
376
377
|
if (currentCycle() < nextEventCycle) return;
|
|
@@ -425,7 +426,8 @@ export class TestMachine {
|
|
|
425
426
|
let done = false;
|
|
426
427
|
let shiftHeld = false;
|
|
427
428
|
|
|
428
|
-
const currentCycle = () =>
|
|
429
|
+
const currentCycle = () =>
|
|
430
|
+
this.processor.cycleSeconds * this.model.cyclesPerSecond + this.processor.currentCycles;
|
|
429
431
|
|
|
430
432
|
const isShift = (entry) => entry[0] === SHIFT[0] && entry[1] === SHIFT[1];
|
|
431
433
|
|