jsbeeb 1.4.0 → 1.6.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/package.json +1 -1
- package/src/6502.js +110 -21
- package/src/6502.opcodes.js +20 -15
- package/src/acia.js +63 -7
- package/src/adc.js +22 -0
- package/src/app/app.js +24 -0
- package/src/app/electron.js +10 -1
- package/src/app/preload.js +1 -0
- package/src/bbc-palette.js +24 -0
- package/src/bem-snapshot.js +681 -0
- package/src/cmos.js +12 -1
- package/src/config.js +23 -0
- package/src/fake6502.js +9 -2
- package/src/fdc.js +51 -54
- package/src/keyboard.js +7 -4
- package/src/machine-session.js +104 -3
- package/src/main.js +230 -33
- package/src/models.js +111 -83
- package/src/rewind.js +71 -0
- package/src/scheduler.js +28 -0
- package/src/snapshot.js +110 -0
- package/src/soundchip.js +117 -0
- package/src/speech-output.js +85 -0
- package/src/state-utils.js +66 -0
- package/src/teletext.js +100 -16
- package/src/via.js +92 -1
- package/src/video.js +349 -34
- package/src/wd-fdc.js +6 -1
- package/src/web/audio-handler.js +1 -1
package/src/via.js
CHANGED
|
@@ -402,6 +402,73 @@ class Via {
|
|
|
402
402
|
this.portBUpdated();
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
snapshotState() {
|
|
406
|
+
return {
|
|
407
|
+
ora: this.ora,
|
|
408
|
+
orb: this.orb,
|
|
409
|
+
ira: this.ira,
|
|
410
|
+
irb: this.irb,
|
|
411
|
+
ddra: this.ddra,
|
|
412
|
+
ddrb: this.ddrb,
|
|
413
|
+
sr: this.sr,
|
|
414
|
+
t1l: this.t1l,
|
|
415
|
+
t2l: this.t2l,
|
|
416
|
+
t1c: this.t1c,
|
|
417
|
+
t2c: this.t2c,
|
|
418
|
+
acr: this.acr,
|
|
419
|
+
pcr: this.pcr,
|
|
420
|
+
ifr: this.ifr,
|
|
421
|
+
ier: this.ier,
|
|
422
|
+
t1hit: this.t1hit,
|
|
423
|
+
t2hit: this.t2hit,
|
|
424
|
+
portapins: this.portapins,
|
|
425
|
+
portbpins: this.portbpins,
|
|
426
|
+
ca1: this.ca1,
|
|
427
|
+
ca2: this.ca2,
|
|
428
|
+
cb1: this.cb1,
|
|
429
|
+
cb2: this.cb2,
|
|
430
|
+
justhit: this.justhit,
|
|
431
|
+
t1_pb7: this.t1_pb7,
|
|
432
|
+
lastPolltime: this.lastPolltime,
|
|
433
|
+
taskOffset: this.task.scheduled() ? this.task.expireEpoch - this.scheduler.epoch : null,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
restoreState(state) {
|
|
438
|
+
this.ora = state.ora;
|
|
439
|
+
this.orb = state.orb;
|
|
440
|
+
this.ira = state.ira;
|
|
441
|
+
this.irb = state.irb;
|
|
442
|
+
this.ddra = state.ddra;
|
|
443
|
+
this.ddrb = state.ddrb;
|
|
444
|
+
this.sr = state.sr;
|
|
445
|
+
this.t1l = state.t1l;
|
|
446
|
+
this.t2l = state.t2l;
|
|
447
|
+
this.t1c = state.t1c;
|
|
448
|
+
this.t2c = state.t2c;
|
|
449
|
+
this.acr = state.acr;
|
|
450
|
+
this.pcr = state.pcr;
|
|
451
|
+
this.ifr = state.ifr;
|
|
452
|
+
this.ier = state.ier;
|
|
453
|
+
this.t1hit = state.t1hit;
|
|
454
|
+
this.t2hit = state.t2hit;
|
|
455
|
+
this.portapins = state.portapins;
|
|
456
|
+
this.portbpins = state.portbpins;
|
|
457
|
+
this.ca1 = state.ca1;
|
|
458
|
+
this.ca2 = state.ca2;
|
|
459
|
+
this.cb1 = state.cb1;
|
|
460
|
+
this.cb2 = state.cb2;
|
|
461
|
+
this.justhit = state.justhit;
|
|
462
|
+
this.t1_pb7 = state.t1_pb7;
|
|
463
|
+
this.lastPolltime = state.lastPolltime;
|
|
464
|
+
this.updateIFR();
|
|
465
|
+
if (state.taskOffset !== null) {
|
|
466
|
+
this.task.reschedule(state.taskOffset);
|
|
467
|
+
} else {
|
|
468
|
+
this.task.cancel();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
405
472
|
setca1(level) {
|
|
406
473
|
if (level === this.ca1) return;
|
|
407
474
|
const pcrSet = !!(this.pcr & 1);
|
|
@@ -460,7 +527,7 @@ class Via {
|
|
|
460
527
|
}
|
|
461
528
|
|
|
462
529
|
export class SysVia extends Via {
|
|
463
|
-
constructor(cpu, scheduler, video, soundChip, cmos, isMaster, initialLayout, getGamepads) {
|
|
530
|
+
constructor(cpu, scheduler, { video, soundChip, cmos, isMaster, initialLayout, getGamepads } = {}) {
|
|
464
531
|
super(cpu, scheduler, 0x01);
|
|
465
532
|
|
|
466
533
|
this.IC32 = 0;
|
|
@@ -484,6 +551,30 @@ export class SysVia extends Via {
|
|
|
484
551
|
this.reset();
|
|
485
552
|
}
|
|
486
553
|
|
|
554
|
+
snapshotState() {
|
|
555
|
+
return {
|
|
556
|
+
...super.snapshotState(),
|
|
557
|
+
IC32: this.IC32,
|
|
558
|
+
capsLockLight: this.capsLockLight,
|
|
559
|
+
shiftLockLight: this.shiftLockLight,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
restoreState(state) {
|
|
564
|
+
super.restoreState(state);
|
|
565
|
+
this.IC32 = state.IC32;
|
|
566
|
+
this.capsLockLight = state.capsLockLight;
|
|
567
|
+
this.shiftLockLight = state.shiftLockLight;
|
|
568
|
+
// Re-apply IC32 side effects (sound chip data bus, CMOS, port A)
|
|
569
|
+
// Must call portBUpdated directly (not recalculatePortBPins which
|
|
570
|
+
// overwrites IC32 based on portbpins) and then re-set IC32.
|
|
571
|
+
this.portBUpdated();
|
|
572
|
+
this.IC32 = state.IC32;
|
|
573
|
+
this.capsLockLight = state.capsLockLight;
|
|
574
|
+
this.shiftLockLight = state.shiftLockLight;
|
|
575
|
+
this.recalculatePortAPins();
|
|
576
|
+
}
|
|
577
|
+
|
|
487
578
|
setKeyLayout(map) {
|
|
488
579
|
this.keycodeToRowCol = utils.getKeyMap(map);
|
|
489
580
|
}
|
package/src/video.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
import { Teletext } from "./teletext.js";
|
|
3
3
|
import * as utils from "./utils.js";
|
|
4
|
+
import { BbcDefaultPalette as NulaDefaultPalette } from "./bbc-palette.js";
|
|
4
5
|
|
|
5
6
|
export const VDISPENABLE = 1 << 0;
|
|
6
7
|
export const HDISPENABLE = 1 << 1;
|
|
@@ -15,43 +16,212 @@ export const OPAQUE_BLACK = 0xff000000;
|
|
|
15
16
|
export const OPAQUE_WHITE = 0xffffffff;
|
|
16
17
|
|
|
17
18
|
////////////////////
|
|
18
|
-
//
|
|
19
|
+
// VideoNULA - programmable 12-bit RGB palette extension (RobC hardware mod).
|
|
20
|
+
// Reference: b-em src/video.c (stardot/b-em).
|
|
21
|
+
// Addresses &FE22 (control) and &FE23 (palette) via 2-byte write protocol.
|
|
22
|
+
|
|
23
|
+
////////////////////
|
|
24
|
+
// ULA interface (includes NULA programmable palette support)
|
|
19
25
|
class Ula {
|
|
20
26
|
constructor(video) {
|
|
21
27
|
this.video = video;
|
|
28
|
+
// NULA state
|
|
29
|
+
this.collook = new Uint32Array(16);
|
|
30
|
+
this.flash = new Uint8Array(8);
|
|
31
|
+
this.paletteWriteFlag = false;
|
|
32
|
+
this.paletteFirstByte = 0;
|
|
33
|
+
this.paletteMode = 0;
|
|
34
|
+
this.horizontalOffset = 0;
|
|
35
|
+
this.leftBlank = 0;
|
|
36
|
+
this.disabled = false;
|
|
37
|
+
this.attributeMode = 0;
|
|
38
|
+
this.attributeText = 0;
|
|
39
|
+
this.reset();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
reset() {
|
|
43
|
+
this.collook.set(NulaDefaultPalette);
|
|
44
|
+
this.flash.fill(1);
|
|
45
|
+
this.paletteWriteFlag = false;
|
|
46
|
+
this.paletteFirstByte = 0;
|
|
47
|
+
this.paletteMode = 0;
|
|
48
|
+
this.horizontalOffset = 0;
|
|
49
|
+
this.leftBlank = 0;
|
|
50
|
+
this.attributeMode = 0;
|
|
51
|
+
this.attributeText = 0;
|
|
52
|
+
// Note: disabled is NOT cleared by reset (matches b-em behaviour).
|
|
53
|
+
// Recompute rendered palette so any custom NULA colours are flushed.
|
|
54
|
+
this._recomputeUlaPal(!!(this.video.ulactrl & 1));
|
|
55
|
+
// Rebuild MODE 7 teletext colours from the restored default palette.
|
|
56
|
+
this.video.teletext.rebuildColours(this.collook);
|
|
22
57
|
}
|
|
23
58
|
|
|
24
59
|
write(addr, val) {
|
|
25
60
|
addr |= 0;
|
|
26
61
|
val |= 0;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
62
|
+
let reg = addr & 3;
|
|
63
|
+
|
|
64
|
+
// When NULA is disabled, mask off bit 1 so &FE22/&FE23 become &FE20/&FE21.
|
|
65
|
+
if (reg >= 2 && this.disabled) {
|
|
66
|
+
reg &= ~2;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
switch (reg) {
|
|
70
|
+
case 0:
|
|
71
|
+
this._writeControl(val);
|
|
72
|
+
break;
|
|
73
|
+
case 1:
|
|
74
|
+
this._writePalette(val);
|
|
75
|
+
break;
|
|
76
|
+
case 2:
|
|
77
|
+
this._writeNulaControl(val);
|
|
78
|
+
break;
|
|
79
|
+
case 3:
|
|
80
|
+
this._writeNulaPalette(val);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
snapshotState() {
|
|
86
|
+
return {
|
|
87
|
+
collook: this.collook.slice(),
|
|
88
|
+
flash: this.flash.slice(),
|
|
89
|
+
paletteWriteFlag: this.paletteWriteFlag,
|
|
90
|
+
paletteFirstByte: this.paletteFirstByte,
|
|
91
|
+
paletteMode: this.paletteMode,
|
|
92
|
+
horizontalOffset: this.horizontalOffset,
|
|
93
|
+
leftBlank: this.leftBlank,
|
|
94
|
+
disabled: this.disabled,
|
|
95
|
+
attributeMode: this.attributeMode,
|
|
96
|
+
attributeText: this.attributeText,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
restoreState(state) {
|
|
101
|
+
this.collook.set(state.collook);
|
|
102
|
+
this.flash.set(state.flash);
|
|
103
|
+
this.paletteWriteFlag = state.paletteWriteFlag;
|
|
104
|
+
this.paletteFirstByte = state.paletteFirstByte;
|
|
105
|
+
this.paletteMode = state.paletteMode;
|
|
106
|
+
this.horizontalOffset = state.horizontalOffset;
|
|
107
|
+
this.leftBlank = state.leftBlank;
|
|
108
|
+
this.disabled = state.disabled;
|
|
109
|
+
this.attributeMode = state.attributeMode;
|
|
110
|
+
this.attributeText = state.attributeText;
|
|
111
|
+
this._recomputeUlaPal(!!(this.video.ulactrl & 1));
|
|
112
|
+
this.video.teletext.rebuildColours(this.collook);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ULA control register (&FE20).
|
|
116
|
+
_writeControl(val) {
|
|
117
|
+
if ((this.video.ulactrl ^ val) & 1) {
|
|
118
|
+
// Flash state has changed - recompute all palette entries.
|
|
119
|
+
this._recomputeUlaPal(!!(val & 1));
|
|
120
|
+
}
|
|
121
|
+
this.video.ulactrl = val;
|
|
122
|
+
this.video.pixelsPerChar = val & 0x10 ? 8 : 16;
|
|
123
|
+
this.video.halfClock = !(val & 0x10);
|
|
124
|
+
const newMode = (val >>> 2) & 3;
|
|
125
|
+
if (newMode !== this.video.ulaMode) {
|
|
126
|
+
this.video.ulaMode = newMode;
|
|
127
|
+
}
|
|
128
|
+
this.video.teletextMode = !!(val & 2);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ULA palette register (&FE21).
|
|
132
|
+
_writePalette(val) {
|
|
133
|
+
const index = (val >>> 4) & 0xf;
|
|
134
|
+
this.video.actualPal[index] = val & 0xf;
|
|
135
|
+
// Default: XOR lower 3 bits with 7 for steady colour.
|
|
136
|
+
let colour = this.collook[(val & 0xf) ^ 7];
|
|
137
|
+
// Flash override: if flash bit set, flash globally enabled, and per-colour flash active.
|
|
138
|
+
if (val & 8 && this.video.ulactrl & 1 && this.flash[(val & 7) ^ 7]) {
|
|
139
|
+
colour = this.collook[val & 0xf];
|
|
140
|
+
}
|
|
141
|
+
if (this.video.ulaPal[index] !== colour) {
|
|
142
|
+
this.video.ulaPal[index] = colour;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// NULA control register (&FE22).
|
|
147
|
+
_writeNulaControl(val) {
|
|
148
|
+
const reg = (val >>> 4) & 0xf;
|
|
149
|
+
const param = val & 0xf;
|
|
150
|
+
switch (reg) {
|
|
151
|
+
case 1:
|
|
152
|
+
this.paletteMode = param & 1;
|
|
153
|
+
break;
|
|
154
|
+
case 2:
|
|
155
|
+
this.horizontalOffset = param & 7;
|
|
156
|
+
break;
|
|
157
|
+
case 3:
|
|
158
|
+
this.leftBlank = param & 0xf;
|
|
159
|
+
break;
|
|
160
|
+
case 4:
|
|
161
|
+
this.reset();
|
|
162
|
+
break;
|
|
163
|
+
case 5:
|
|
164
|
+
this.disabled = true;
|
|
165
|
+
break;
|
|
166
|
+
case 6:
|
|
167
|
+
this.attributeMode = param & 3;
|
|
168
|
+
break;
|
|
169
|
+
case 7:
|
|
170
|
+
this.attributeText = param & 1;
|
|
171
|
+
break;
|
|
172
|
+
case 8:
|
|
173
|
+
this.flash[0] = param & 8 ? 1 : 0;
|
|
174
|
+
this.flash[1] = param & 4 ? 1 : 0;
|
|
175
|
+
this.flash[2] = param & 2 ? 1 : 0;
|
|
176
|
+
this.flash[3] = param & 1 ? 1 : 0;
|
|
177
|
+
this._recomputeUlaPal(!!(this.video.ulactrl & 1));
|
|
178
|
+
break;
|
|
179
|
+
case 9:
|
|
180
|
+
this.flash[4] = param & 8 ? 1 : 0;
|
|
181
|
+
this.flash[5] = param & 4 ? 1 : 0;
|
|
182
|
+
this.flash[6] = param & 2 ? 1 : 0;
|
|
183
|
+
this.flash[7] = param & 1 ? 1 : 0;
|
|
184
|
+
this._recomputeUlaPal(!!(this.video.ulactrl & 1));
|
|
185
|
+
break;
|
|
186
|
+
// Regs 14 (border colour) and 15 (blank colour) are stubbed - rendering not yet implemented.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// NULA palette register (&FE23) - 2-byte write protocol.
|
|
191
|
+
_writeNulaPalette(val) {
|
|
192
|
+
if (this.paletteWriteFlag) {
|
|
193
|
+
const c = (this.paletteFirstByte >>> 4) & 0xf;
|
|
194
|
+
const r = this.paletteFirstByte & 0x0f;
|
|
195
|
+
const g = (val >>> 4) & 0x0f;
|
|
196
|
+
const b = val & 0x0f;
|
|
197
|
+
// Expand 4-bit channels to 8-bit by duplicating the nibble.
|
|
198
|
+
// Store in ABGR format (Uint32Array on little-endian = canvas RGBA).
|
|
199
|
+
this.collook[c] = 0xff000000 | ((b | (b << 4)) << 16) | ((g | (g << 4)) << 8) | (r | (r << 4));
|
|
200
|
+
// Colours 8-15 default to solid (non-flashing) when programmed.
|
|
201
|
+
if (c >= 8) this.flash[c - 8] = 0;
|
|
202
|
+
// Recompute all rendered palette entries from current state.
|
|
203
|
+
this._recomputeUlaPal(!!(this.video.ulactrl & 1));
|
|
204
|
+
// MODE 7 teletext uses its own colour lookup; rebuild when a base colour changes.
|
|
205
|
+
if (c < 8) this.video.teletext.rebuildColours(this.collook);
|
|
35
206
|
} else {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
207
|
+
this.paletteFirstByte = val;
|
|
208
|
+
}
|
|
209
|
+
this.paletteWriteFlag = !this.paletteWriteFlag;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Recompute all 16 ulaPal entries from actualPal + NULA collook + flash state.
|
|
213
|
+
// Follows b-em's palette recomputation logic exactly.
|
|
214
|
+
_recomputeUlaPal(flashEnabled) {
|
|
215
|
+
const video = this.video;
|
|
216
|
+
for (let i = 0; i < 16; ++i) {
|
|
217
|
+
const palVal = video.actualPal[i];
|
|
218
|
+
let colour = this.collook[(palVal & 0xf) ^ 7];
|
|
219
|
+
if (palVal & 8 && flashEnabled && this.flash[(palVal & 7) ^ 7]) {
|
|
220
|
+
colour = this.collook[palVal & 0xf];
|
|
46
221
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.video.halfClock = !(val & 0x10);
|
|
50
|
-
const newMode = (val >>> 2) & 3;
|
|
51
|
-
if (newMode !== this.video.ulaMode) {
|
|
52
|
-
this.video.ulaMode = newMode;
|
|
222
|
+
if (video.ulaPal[i] !== colour) {
|
|
223
|
+
video.ulaPal[i] = colour;
|
|
53
224
|
}
|
|
54
|
-
this.video.teletextMode = !!(val & 2);
|
|
55
225
|
}
|
|
56
226
|
}
|
|
57
227
|
}
|
|
@@ -68,6 +238,14 @@ class Crtc {
|
|
|
68
238
|
]);
|
|
69
239
|
}
|
|
70
240
|
|
|
241
|
+
snapshotState() {
|
|
242
|
+
return { curReg: this.curReg };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
restoreState(state) {
|
|
246
|
+
this.curReg = state.curReg;
|
|
247
|
+
}
|
|
248
|
+
|
|
71
249
|
read(addr) {
|
|
72
250
|
if (!(addr & 1)) return 0;
|
|
73
251
|
switch (this.curReg) {
|
|
@@ -249,6 +427,111 @@ export class Video {
|
|
|
249
427
|
this.paint();
|
|
250
428
|
}
|
|
251
429
|
|
|
430
|
+
snapshotState() {
|
|
431
|
+
return {
|
|
432
|
+
regs: this.regs.slice(),
|
|
433
|
+
bitmapX: this.bitmapX,
|
|
434
|
+
bitmapY: this.bitmapY,
|
|
435
|
+
oddClock: this.oddClock,
|
|
436
|
+
frameCount: this.frameCount,
|
|
437
|
+
doEvenFrameLogic: this.doEvenFrameLogic,
|
|
438
|
+
isEvenRender: this.isEvenRender,
|
|
439
|
+
lastRenderWasEven: this.lastRenderWasEven,
|
|
440
|
+
firstScanline: this.firstScanline,
|
|
441
|
+
inHSync: this.inHSync,
|
|
442
|
+
inVSync: this.inVSync,
|
|
443
|
+
hadVSyncThisRow: this.hadVSyncThisRow,
|
|
444
|
+
checkVertAdjust: this.checkVertAdjust,
|
|
445
|
+
endOfMainLatched: this.endOfMainLatched,
|
|
446
|
+
endOfVertAdjustLatched: this.endOfVertAdjustLatched,
|
|
447
|
+
endOfFrameLatched: this.endOfFrameLatched,
|
|
448
|
+
inVertAdjust: this.inVertAdjust,
|
|
449
|
+
inDummyRaster: this.inDummyRaster,
|
|
450
|
+
hpulseWidth: this.hpulseWidth,
|
|
451
|
+
vpulseWidth: this.vpulseWidth,
|
|
452
|
+
hpulseCounter: this.hpulseCounter,
|
|
453
|
+
vpulseCounter: this.vpulseCounter,
|
|
454
|
+
dispEnabled: this.dispEnabled,
|
|
455
|
+
horizCounter: this.horizCounter,
|
|
456
|
+
vertCounter: this.vertCounter,
|
|
457
|
+
scanlineCounter: this.scanlineCounter,
|
|
458
|
+
vertAdjustCounter: this.vertAdjustCounter,
|
|
459
|
+
addr: this.addr,
|
|
460
|
+
lineStartAddr: this.lineStartAddr,
|
|
461
|
+
nextLineStartAddr: this.nextLineStartAddr,
|
|
462
|
+
ulactrl: this.ulactrl,
|
|
463
|
+
pixelsPerChar: this.pixelsPerChar,
|
|
464
|
+
halfClock: this.halfClock,
|
|
465
|
+
ulaMode: this.ulaMode,
|
|
466
|
+
teletextMode: this.teletextMode,
|
|
467
|
+
displayEnableSkew: this.displayEnableSkew,
|
|
468
|
+
ulaPal: this.ulaPal.slice(),
|
|
469
|
+
actualPal: this.actualPal.slice(),
|
|
470
|
+
cursorOn: this.cursorOn,
|
|
471
|
+
cursorOff: this.cursorOff,
|
|
472
|
+
cursorOnThisFrame: this.cursorOnThisFrame,
|
|
473
|
+
cursorDrawIndex: this.cursorDrawIndex,
|
|
474
|
+
cursorPos: this.cursorPos,
|
|
475
|
+
interlacedSyncAndVideo: this.interlacedSyncAndVideo,
|
|
476
|
+
screenSubtract: this.screenSubtract,
|
|
477
|
+
ula: this.ula.snapshotState(),
|
|
478
|
+
crtc: this.crtc.snapshotState(),
|
|
479
|
+
teletext: this.teletext.snapshotState(),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
restoreState(state) {
|
|
484
|
+
this.regs.set(state.regs);
|
|
485
|
+
this.bitmapX = state.bitmapX;
|
|
486
|
+
this.bitmapY = state.bitmapY;
|
|
487
|
+
this.oddClock = state.oddClock;
|
|
488
|
+
this.frameCount = state.frameCount;
|
|
489
|
+
this.doEvenFrameLogic = state.doEvenFrameLogic;
|
|
490
|
+
this.isEvenRender = state.isEvenRender;
|
|
491
|
+
this.lastRenderWasEven = state.lastRenderWasEven;
|
|
492
|
+
this.firstScanline = state.firstScanline;
|
|
493
|
+
this.inHSync = state.inHSync;
|
|
494
|
+
this.inVSync = state.inVSync;
|
|
495
|
+
this.hadVSyncThisRow = state.hadVSyncThisRow;
|
|
496
|
+
this.checkVertAdjust = state.checkVertAdjust;
|
|
497
|
+
this.endOfMainLatched = state.endOfMainLatched;
|
|
498
|
+
this.endOfVertAdjustLatched = state.endOfVertAdjustLatched;
|
|
499
|
+
this.endOfFrameLatched = state.endOfFrameLatched;
|
|
500
|
+
this.inVertAdjust = state.inVertAdjust;
|
|
501
|
+
this.inDummyRaster = state.inDummyRaster;
|
|
502
|
+
this.hpulseWidth = state.hpulseWidth;
|
|
503
|
+
this.vpulseWidth = state.vpulseWidth;
|
|
504
|
+
this.hpulseCounter = state.hpulseCounter;
|
|
505
|
+
this.vpulseCounter = state.vpulseCounter;
|
|
506
|
+
this.dispEnabled = state.dispEnabled;
|
|
507
|
+
this.horizCounter = state.horizCounter;
|
|
508
|
+
this.vertCounter = state.vertCounter;
|
|
509
|
+
this.scanlineCounter = state.scanlineCounter;
|
|
510
|
+
this.vertAdjustCounter = state.vertAdjustCounter;
|
|
511
|
+
this.addr = state.addr;
|
|
512
|
+
this.lineStartAddr = state.lineStartAddr;
|
|
513
|
+
this.nextLineStartAddr = state.nextLineStartAddr;
|
|
514
|
+
this.ulactrl = state.ulactrl;
|
|
515
|
+
this.pixelsPerChar = state.pixelsPerChar;
|
|
516
|
+
this.halfClock = state.halfClock;
|
|
517
|
+
this.ulaMode = state.ulaMode;
|
|
518
|
+
this.teletextMode = state.teletextMode;
|
|
519
|
+
this.displayEnableSkew = state.displayEnableSkew;
|
|
520
|
+
this.actualPal.set(state.actualPal);
|
|
521
|
+
this.cursorOn = state.cursorOn;
|
|
522
|
+
this.cursorOff = state.cursorOff;
|
|
523
|
+
this.cursorOnThisFrame = state.cursorOnThisFrame;
|
|
524
|
+
this.cursorDrawIndex = state.cursorDrawIndex;
|
|
525
|
+
this.cursorPos = state.cursorPos;
|
|
526
|
+
this.interlacedSyncAndVideo = state.interlacedSyncAndVideo;
|
|
527
|
+
this.screenSubtract = state.screenSubtract;
|
|
528
|
+
this.ula.restoreState(state.ula);
|
|
529
|
+
this.crtc.restoreState(state.crtc);
|
|
530
|
+
this.teletext.restoreState(state.teletext);
|
|
531
|
+
// Restore ulaPal after ULA restore, since ULA recomputation may overwrite it
|
|
532
|
+
this.ulaPal.set(state.ulaPal);
|
|
533
|
+
}
|
|
534
|
+
|
|
252
535
|
reset(cpu, via) {
|
|
253
536
|
this.cpu = cpu;
|
|
254
537
|
this.sysvia = via;
|
|
@@ -320,16 +603,20 @@ export class Video {
|
|
|
320
603
|
destOffset |= 0;
|
|
321
604
|
const offset = table4bppOffset(this.ulaMode, dat);
|
|
322
605
|
const fb32 = this.fb32;
|
|
323
|
-
|
|
606
|
+
// In NULA palette mode, bypass the ULA palette (ulaPal) and look up
|
|
607
|
+
// pixel colours directly from the NULA 12-bit colour table (collook).
|
|
608
|
+
// This skips the XOR-7 logical↔physical colour mapping that the
|
|
609
|
+
// standard ULA applies. Reference: b-em src/video.c lines 1083, 1117.
|
|
610
|
+
const colourLookup = this.ula.paletteMode ? this.ula.collook : this.ulaPal;
|
|
324
611
|
const table4bpp = this.table4bpp;
|
|
325
612
|
// Take advantage of numPixels being either 8 or 16
|
|
326
613
|
if (numPixels === 8) {
|
|
327
614
|
for (let i = 0; i < 8; ++i) {
|
|
328
|
-
fb32[destOffset + i] =
|
|
615
|
+
fb32[destOffset + i] = colourLookup[table4bpp[offset + i]];
|
|
329
616
|
}
|
|
330
617
|
} else {
|
|
331
618
|
for (let i = 0; i < 16; ++i) {
|
|
332
|
-
fb32[destOffset + i] =
|
|
619
|
+
fb32[destOffset + i] = colourLookup[table4bpp[offset + i]];
|
|
333
620
|
}
|
|
334
621
|
}
|
|
335
622
|
}
|
|
@@ -656,9 +943,11 @@ export class Video {
|
|
|
656
943
|
// Read data from address pointer if both horizontal and vertical display enabled.
|
|
657
944
|
const dat = this.readVideoMem();
|
|
658
945
|
if (insideBorder) {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
946
|
+
// Always feed the SAA5050 pipeline: on real hardware IC15
|
|
947
|
+
// permanently connects the video bus to the SAA5050 inputs
|
|
948
|
+
// regardless of ULA mode. Required for the "TTX trick".
|
|
949
|
+
// See https://github.com/mattgodbolt/jsbeeb/issues/546
|
|
950
|
+
this.teletext.fetchData(dat);
|
|
662
951
|
|
|
663
952
|
// Check cursor start.
|
|
664
953
|
if (
|
|
@@ -691,7 +980,17 @@ export class Video {
|
|
|
691
980
|
|
|
692
981
|
if ((this.dispEnabled & EVERYTHINGENABLED) === EVERYTHINGENABLED) {
|
|
693
982
|
if (this.teletextMode) {
|
|
694
|
-
|
|
983
|
+
if (this.halfClock) {
|
|
984
|
+
// Proper MODE 7 (1MHz clock + teletext): render SAA5050 output normally.
|
|
985
|
+
this.teletext.render(this.fb32, offset);
|
|
986
|
+
} else {
|
|
987
|
+
// 2MHz clock + teletext bit set (the "TTX trick"): the Video ULA
|
|
988
|
+
// forces DISPEN to the SAA5050 to 0 in 2MHz modes (0/1/2/3), so
|
|
989
|
+
// the SAA5050 outputs black. Confirmed by Rich Talbot-Watkins (RTW)
|
|
990
|
+
// at ABUG 2026-03-13.
|
|
991
|
+
// See https://github.com/mattgodbolt/jsbeeb/issues/546
|
|
992
|
+
this.fb32.fill(OPAQUE_BLACK, offset, offset + this.pixelsPerChar);
|
|
993
|
+
}
|
|
695
994
|
} else {
|
|
696
995
|
this.blitFb(dat, offset, this.pixelsPerChar, doubledLines);
|
|
697
996
|
}
|
|
@@ -705,6 +1004,14 @@ export class Video {
|
|
|
705
1004
|
}
|
|
706
1005
|
}
|
|
707
1006
|
|
|
1007
|
+
// IC37/IC36: during H blanking with V display active, always feed
|
|
1008
|
+
// the SAA5050 pipeline with the video bus data, forcing bit 6 high.
|
|
1009
|
+
// On real hardware IC37/IC36 operates regardless of ULA mode —
|
|
1010
|
+
// it is wired to the CRTC DISPEN signal, not the ULA teletext bit.
|
|
1011
|
+
if (!(this.dispEnabled & HDISPENABLE) && this.dispEnabled & VDISPENABLE) {
|
|
1012
|
+
this.teletext.fetchData(this.readVideoMem() | 0x40);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
708
1015
|
// CRTC MA always increments, inside display border or not.
|
|
709
1016
|
this.addr = (this.addr + 1) & 0x3fff;
|
|
710
1017
|
|
|
@@ -777,11 +1084,19 @@ export class Video {
|
|
|
777
1084
|
|
|
778
1085
|
export class FakeVideo {
|
|
779
1086
|
constructor() {
|
|
780
|
-
this.
|
|
1087
|
+
this.crtc = {
|
|
1088
|
+
read: function () {
|
|
1089
|
+
return 0xff;
|
|
1090
|
+
},
|
|
1091
|
+
write: utils.noop,
|
|
1092
|
+
};
|
|
1093
|
+
this.ula = {
|
|
781
1094
|
read: function () {
|
|
782
1095
|
return 0xff;
|
|
783
1096
|
},
|
|
784
1097
|
write: utils.noop,
|
|
1098
|
+
reset: utils.noop,
|
|
1099
|
+
disabled: false,
|
|
785
1100
|
};
|
|
786
1101
|
this.regs = new Uint8Array(32);
|
|
787
1102
|
}
|
package/src/wd-fdc.js
CHANGED
|
@@ -804,7 +804,12 @@ export class WdFdc {
|
|
|
804
804
|
if (this._indexPulseCount < 6) return;
|
|
805
805
|
if (this._commandType === 1) this._statusRegister |= Status.typeISpinUpDone;
|
|
806
806
|
if (this._isCommandSettle) {
|
|
807
|
-
|
|
807
|
+
// The WD1770 datasheet specifies 30ms head settle, but empirical testing
|
|
808
|
+
// against b-em (which uses 15ms for all WD1770/2 variants) shows 15ms is
|
|
809
|
+
// correct for BBC hardware. Using 30ms causes disc streaming demos (e.g.
|
|
810
|
+
// STNICC-beeb on Master 128) to hang because sectors don't arrive before
|
|
811
|
+
// the first vsync IRQ. The WD1772 also uses 15ms per its datasheet.
|
|
812
|
+
const settleMs = 15;
|
|
808
813
|
this._startTimer(TimerState.settle, settleMs * 1000);
|
|
809
814
|
} else {
|
|
810
815
|
this._dispatchCommand();
|
package/src/web/audio-handler.js
CHANGED
|
@@ -9,7 +9,7 @@ const rendererUrl = new URL("./audio-renderer.js", import.meta.url).href;
|
|
|
9
9
|
const music5000WorkletUrl = new URL("../music5000-worklet.js", import.meta.url).href;
|
|
10
10
|
|
|
11
11
|
export class AudioHandler {
|
|
12
|
-
constructor(warningNode, statsNode, audioFilterFreq, audioFilterQ, noSeek) {
|
|
12
|
+
constructor({ warningNode, statsNode, audioFilterFreq, audioFilterQ, noSeek } = {}) {
|
|
13
13
|
this.warningNode = warningNode;
|
|
14
14
|
this.warningNode.toggle(false);
|
|
15
15
|
this.chart = new SmoothieChart({
|