jsbeeb 1.13.0 → 1.14.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 +3 -2
- package/package.json +23 -16
- package/src/6502.js +74 -16
- package/src/app/app.js +2 -15
- package/src/app/args.js +26 -0
- package/src/bem-snapshot.js +154 -15
- package/src/config.js +100 -81
- package/src/fake6502.js +7 -2
- package/src/keyboard.js +3 -2
- package/src/machine-session.js +11 -2
- package/src/main.js +79 -67
- package/src/models.js +24 -4
- package/src/mouse-coordinates.js +16 -0
- package/src/snapshot-helpers.js +16 -2
- package/src/snapshot.js +19 -1
- package/src/soundchip.js +23 -5
- package/src/teletext_adaptor.js +2 -1
- package/src/tube.js +33 -1
- package/src/web/audio-handler.js +4 -1
- package/tests/test-machine.js +7 -5
package/README.md
CHANGED
|
@@ -167,8 +167,9 @@ sudo rpm -i out/dist/jsbeeb-1.0.1.x86_64.rpm
|
|
|
167
167
|
- `autotype` - types whatever you put after. e.g. `&autotype=PRINT"Matt is cool"%0a` (return is URI escaped to `%0a`)
|
|
168
168
|
- `embed` - Remove the margins around the screen, hide most navigation entries and make the page background
|
|
169
169
|
transparent (intended for use when running within an iframe in a third-party site).
|
|
170
|
-
- `cpuMultiplier=X` speeds up the CPU by a factor of `X
|
|
171
|
-
|
|
170
|
+
- `cpuMultiplier=X` speeds up the CPU by a factor of `X` relative to the peripherals: video, sound and the VIAs keep
|
|
171
|
+
running at their real-world rates. May be fractional or below one to slow the CPU down. NB disc loads become
|
|
172
|
+
unreliable with a too-slow CPU, and running too fast might cause the browser to hang.
|
|
172
173
|
- `sbLeft` / `sbRight` / `sbBottom` - a URL to place left of, right of, or below the cub monitor. The left and right
|
|
173
174
|
should be around 648 high and the bottom image should be around 896 wide. Left and right wider than 300 will run into
|
|
174
175
|
problems on smaller screens; bottom taller than 100 or so similarly.
|
package/package.json
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
"name": "jsbeeb",
|
|
8
8
|
"description": "Emulate a BBC Micro",
|
|
9
9
|
"repository": "git@github.com:mattgodbolt/jsbeeb.git",
|
|
10
|
-
"version": "1.
|
|
11
|
-
"//": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
|
|
10
|
+
"version": "1.14.0",
|
|
11
|
+
"//engines": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
|
|
12
12
|
"engines": {
|
|
13
|
-
"node": ">=
|
|
13
|
+
"node": ">=24.15.0"
|
|
14
14
|
},
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "./src/app/app.js",
|
|
@@ -27,33 +27,37 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@popperjs/core": "^2.11.8",
|
|
30
|
-
"argparse": "^
|
|
30
|
+
"argparse": "^3.0.0",
|
|
31
31
|
"bootstrap": "^5.3.8",
|
|
32
32
|
"bootswatch": "^5.3.8",
|
|
33
|
-
"sharp": "^0.
|
|
33
|
+
"sharp": "^0.35.3",
|
|
34
34
|
"smoothie": "^1.36.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@eslint/js": "^10.0.1",
|
|
38
38
|
"@vitest/coverage-v8": "^4.0.18",
|
|
39
|
-
"eslint": "^10.
|
|
39
|
+
"eslint": "^10.7.0",
|
|
40
40
|
"eslint-config-prettier": "^10.1.8",
|
|
41
|
-
"eslint-plugin-prettier": "^5.5.
|
|
42
|
-
"globals": "^17.
|
|
41
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
42
|
+
"globals": "^17.7.0",
|
|
43
43
|
"husky": "^9.1.7",
|
|
44
|
-
"jsdom": "^
|
|
45
|
-
"lint-staged": "^
|
|
46
|
-
"npm-run-all2": "^
|
|
47
|
-
"pixelmatch": "^7.
|
|
48
|
-
"prettier": "^3.
|
|
49
|
-
"vite": "^8.
|
|
44
|
+
"jsdom": "^30.0.1",
|
|
45
|
+
"lint-staged": "^17.1.1",
|
|
46
|
+
"npm-run-all2": "^9.0.2",
|
|
47
|
+
"pixelmatch": "^7.2.0",
|
|
48
|
+
"prettier": "^3.9.6",
|
|
49
|
+
"vite": "^8.1.5",
|
|
50
50
|
"vitest": "^4.0.10"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
|
-
"electron": "^
|
|
54
|
-
"electron-builder": "^26.
|
|
53
|
+
"electron": "^43.2.0",
|
|
54
|
+
"electron-builder": "^26.15.3",
|
|
55
55
|
"electron-store": "^11.0.2"
|
|
56
56
|
},
|
|
57
|
+
"//overrides": "brace-expansion forced to the patched version for GHSA-mh99-v99m-4gvg; the electron-builder dependency chain still pins vulnerable versions. Remove once `npm audit` is clean without it.",
|
|
58
|
+
"overrides": {
|
|
59
|
+
"brace-expansion": "^5.0.8"
|
|
60
|
+
},
|
|
57
61
|
"license": "GPL-3.0-or-later",
|
|
58
62
|
"build": {
|
|
59
63
|
"appId": "org.godbolt.bbc",
|
|
@@ -108,5 +112,8 @@
|
|
|
108
112
|
"prettier --write"
|
|
109
113
|
],
|
|
110
114
|
"*.{md,json,yml,yaml,css,html}": "prettier --write"
|
|
115
|
+
},
|
|
116
|
+
"allowScripts": {
|
|
117
|
+
"electron-winstaller@5.4.0": true
|
|
111
118
|
}
|
|
112
119
|
}
|
package/src/6502.js
CHANGED
|
@@ -15,6 +15,9 @@ import { AtomMMC2 } from "./mmc.js";
|
|
|
15
15
|
|
|
16
16
|
const signExtend = utils.signExtend;
|
|
17
17
|
|
|
18
|
+
// Speed of the second processor relative to the host, as fitted to a Master Turbo.
|
|
19
|
+
export const DefaultTubeCpuMultiplier = 2;
|
|
20
|
+
|
|
18
21
|
function _set(byte, mask, set) {
|
|
19
22
|
return (byte & ~mask) | (set ? mask : 0);
|
|
20
23
|
}
|
|
@@ -407,11 +410,11 @@ class Base6502 {
|
|
|
407
410
|
}
|
|
408
411
|
|
|
409
412
|
class Tube6502 extends Base6502 {
|
|
410
|
-
constructor(model, cpu) {
|
|
413
|
+
constructor(model, cpu, { cpuMultiplier = DefaultTubeCpuMultiplier } = {}) {
|
|
411
414
|
super(model, { cycleAccurate: false });
|
|
412
415
|
|
|
413
416
|
this.cycles = 0;
|
|
414
|
-
this.cpuMultiplier =
|
|
417
|
+
this.cpuMultiplier = cpuMultiplier;
|
|
415
418
|
this.romPaged = true;
|
|
416
419
|
this.memory = new Uint8Array(65536);
|
|
417
420
|
this.rom = new Uint8Array(4096);
|
|
@@ -424,6 +427,10 @@ class Tube6502 extends Base6502 {
|
|
|
424
427
|
this.pc = this.readmem(0xfffc) | (this.readmem(0xfffd) << 8);
|
|
425
428
|
this.p.i = true;
|
|
426
429
|
this.s = (this.s - 3) & 0xff; // Simulate 3 dummy pushes during reset
|
|
430
|
+
// A latched NMI would otherwise survive the reset and be taken on the first instruction
|
|
431
|
+
// after it, defeating the empty R3 FIFO the ULA seeds for exactly that reason.
|
|
432
|
+
this._nmiLevel = this._nmiEdge = false;
|
|
433
|
+
this.takeInt = false;
|
|
427
434
|
this.tube.reset(hard);
|
|
428
435
|
}
|
|
429
436
|
|
|
@@ -482,6 +489,46 @@ class Tube6502 extends Base6502 {
|
|
|
482
489
|
}
|
|
483
490
|
}
|
|
484
491
|
|
|
492
|
+
snapshotState({ includeRoms = false } = {}) {
|
|
493
|
+
return {
|
|
494
|
+
a: this.a,
|
|
495
|
+
x: this.x,
|
|
496
|
+
y: this.y,
|
|
497
|
+
s: this.s,
|
|
498
|
+
pc: this.pc,
|
|
499
|
+
p: this.p.asByte(),
|
|
500
|
+
nmiLevel: this._nmiLevel,
|
|
501
|
+
nmiEdge: this._nmiEdge,
|
|
502
|
+
takeInt: this.takeInt,
|
|
503
|
+
// The parasite has no scheduler of its own, so this is not relative to any epoch.
|
|
504
|
+
cycles: this.cycles,
|
|
505
|
+
romPaged: this.romPaged,
|
|
506
|
+
memory: this.memory.slice(),
|
|
507
|
+
rom: includeRoms ? this.rom.slice() : undefined,
|
|
508
|
+
ula: this.tube.snapshotState(),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
restoreState(state) {
|
|
513
|
+
this.a = state.a;
|
|
514
|
+
this.x = state.x;
|
|
515
|
+
this.y = state.y;
|
|
516
|
+
this.s = state.s;
|
|
517
|
+
this.pc = state.pc;
|
|
518
|
+
this.p.setFromByte(state.p);
|
|
519
|
+
this._nmiLevel = state.nmiLevel;
|
|
520
|
+
this._nmiEdge = state.nmiEdge;
|
|
521
|
+
this.takeInt = state.takeInt;
|
|
522
|
+
this.cycles = state.cycles;
|
|
523
|
+
this.romPaged = state.romPaged;
|
|
524
|
+
this.memory.set(state.memory);
|
|
525
|
+
if (state.rom) this.rom.set(state.rom);
|
|
526
|
+
|
|
527
|
+
// After the registers, so the edge-triggered NMI moves from its saved level to the one
|
|
528
|
+
// the ULA implies: no edge if they agree, and the edge it is owed if they do not.
|
|
529
|
+
this.tube.restoreState(state.ula);
|
|
530
|
+
}
|
|
531
|
+
|
|
485
532
|
async loadOs() {
|
|
486
533
|
console.log("Loading tube rom from roms/" + this.model.os);
|
|
487
534
|
const tubeRom = this.rom;
|
|
@@ -620,10 +667,12 @@ export class Cpu6502 extends Base6502 {
|
|
|
620
667
|
this.cpuMultiplier = this.config.cpuMultiplier;
|
|
621
668
|
this.videoCyclesBatch = this.config.videoCyclesBatch | 0;
|
|
622
669
|
this.peripheralCyclesPerSecond = 2 * 1000 * 1000;
|
|
623
|
-
this.
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
670
|
+
this.hasTube = !!this.config.tube;
|
|
671
|
+
this.hasMusic5000 = !!this.config.hasMusic5000;
|
|
672
|
+
this.hasTeletextAdaptor = !!this.config.hasTeletextAdaptor;
|
|
673
|
+
this.tube = this.hasTube
|
|
674
|
+
? new Tube6502(this.config.tube, this, { cpuMultiplier: this.config.tubeCpuMultiplier })
|
|
675
|
+
: new FakeTube();
|
|
627
676
|
this.music5000PageSel = 0;
|
|
628
677
|
this.econet = econet;
|
|
629
678
|
|
|
@@ -799,7 +848,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
799
848
|
|
|
800
849
|
switch (addr & ~0x0003) {
|
|
801
850
|
case 0xfc10:
|
|
802
|
-
if (this.
|
|
851
|
+
if (this.hasTeletextAdaptor) return this.teletextAdaptor.read(addr - 0xfc10);
|
|
803
852
|
break;
|
|
804
853
|
case 0xfc20:
|
|
805
854
|
case 0xfc24:
|
|
@@ -822,7 +871,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
822
871
|
// IDE
|
|
823
872
|
break;
|
|
824
873
|
case 0xfcfc:
|
|
825
|
-
if (addr === 0xfcff && this.
|
|
874
|
+
if (addr === 0xfcff && this.hasMusic5000) return this.music5000PageSel;
|
|
826
875
|
break;
|
|
827
876
|
case 0xfe00:
|
|
828
877
|
case 0xfe04:
|
|
@@ -909,7 +958,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
909
958
|
return this.tube.read(addr);
|
|
910
959
|
}
|
|
911
960
|
|
|
912
|
-
if (this.
|
|
961
|
+
if (this.hasMusic5000) {
|
|
913
962
|
if ((this.music5000PageSel & 0xf0) === 0x30 && (addr & 0xff00) === 0xfd00) {
|
|
914
963
|
return this.music5000.read(this.music5000PageSel, addr);
|
|
915
964
|
}
|
|
@@ -966,14 +1015,14 @@ export class Cpu6502 extends Base6502 {
|
|
|
966
1015
|
addr &= 0xffff;
|
|
967
1016
|
b |= 0;
|
|
968
1017
|
|
|
969
|
-
if (this.
|
|
1018
|
+
if (this.hasMusic5000 && (addr & 0xff00) === 0xfd00 && (this.music5000PageSel & 0xf0) === 0x30) {
|
|
970
1019
|
this.music5000.write(this.music5000PageSel, addr, b);
|
|
971
1020
|
return;
|
|
972
1021
|
}
|
|
973
1022
|
|
|
974
1023
|
switch (addr & ~0x0003) {
|
|
975
1024
|
case 0xfc10:
|
|
976
|
-
if (this.
|
|
1025
|
+
if (this.hasTeletextAdaptor) return this.teletextAdaptor.write(addr - 0xfc10, b);
|
|
977
1026
|
break;
|
|
978
1027
|
case 0xfc20:
|
|
979
1028
|
case 0xfc24:
|
|
@@ -996,7 +1045,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
996
1045
|
// IDE
|
|
997
1046
|
break;
|
|
998
1047
|
case 0xfcfc:
|
|
999
|
-
if (addr === 0xfcff && this.
|
|
1048
|
+
if (addr === 0xfcff && this.hasMusic5000) {
|
|
1000
1049
|
this.music5000PageSel = b;
|
|
1001
1050
|
}
|
|
1002
1051
|
break;
|
|
@@ -1183,6 +1232,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
1183
1232
|
acia: this.acia.snapshotState(),
|
|
1184
1233
|
adc: this.adconverter.snapshotState(),
|
|
1185
1234
|
fdc: this.fdc.snapshotState(),
|
|
1235
|
+
tube: this.hasTube ? this.tube.snapshotState({ includeRoms }) : undefined,
|
|
1186
1236
|
};
|
|
1187
1237
|
}
|
|
1188
1238
|
|
|
@@ -1243,6 +1293,13 @@ export class Cpu6502 extends Base6502 {
|
|
|
1243
1293
|
if (state.fdc) {
|
|
1244
1294
|
this.fdc.restoreState(state.fdc);
|
|
1245
1295
|
}
|
|
1296
|
+
|
|
1297
|
+
// Tube state (v3+). Rather than leave the parasite running on state the host knows
|
|
1298
|
+
// nothing about, reset it; restoreSnapshot() rejects such a pairing before reaching here.
|
|
1299
|
+
if (this.hasTube) {
|
|
1300
|
+
if (state.tube) this.tube.restoreState(state.tube);
|
|
1301
|
+
else this.tube.reset(true);
|
|
1302
|
+
}
|
|
1246
1303
|
}
|
|
1247
1304
|
|
|
1248
1305
|
reset(hard) {
|
|
@@ -1304,7 +1361,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
1304
1361
|
this.adconverter.reset();
|
|
1305
1362
|
|
|
1306
1363
|
this.touchScreen = new TouchScreen(this.scheduler);
|
|
1307
|
-
if (this.
|
|
1364
|
+
if (this.hasTeletextAdaptor) this.teletextAdaptor = new TeletextAdaptor(this);
|
|
1308
1365
|
if (this.econet) this.filestore = new Filestore(this, this.econet);
|
|
1309
1366
|
}
|
|
1310
1367
|
|
|
@@ -1364,8 +1421,9 @@ export class Cpu6502 extends Base6502 {
|
|
|
1364
1421
|
// Builds common code between polltimeSlow and polltimeFast
|
|
1365
1422
|
buildPolltime() {
|
|
1366
1423
|
const nop = (_cycles) => {};
|
|
1367
|
-
const tubeStuff = (cycles) =>
|
|
1368
|
-
|
|
1424
|
+
const tubeStuff = this.hasTube ? (cycles) => this.tube.execute(cycles) : nop;
|
|
1425
|
+
// The adaptor itself does not exist until reset() runs, so branch on the fitting, not the object.
|
|
1426
|
+
const teletextStuff = this.hasTeletextAdaptor ? (cycles) => this.teletextAdaptor.polltime(cycles) : nop;
|
|
1369
1427
|
const musicStuff = this.music5000 ? (cycles) => this.music5000.polltime(cycles) : nop;
|
|
1370
1428
|
const econetStuff = this.econet
|
|
1371
1429
|
? (cycles) => {
|
|
@@ -1506,7 +1564,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
1506
1564
|
if (this.model.os.length) {
|
|
1507
1565
|
await this.loadOs.apply(this, this.model.os);
|
|
1508
1566
|
}
|
|
1509
|
-
if (this.
|
|
1567
|
+
if (this.hasTube) {
|
|
1510
1568
|
await this.tube.loadOs();
|
|
1511
1569
|
}
|
|
1512
1570
|
this.reset(true);
|
package/src/app/app.js
CHANGED
|
@@ -5,6 +5,8 @@ import * as fs from "fs";
|
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { ArgumentParser } from "argparse";
|
|
7
7
|
|
|
8
|
+
import { getArguments } from "./args.js";
|
|
9
|
+
|
|
8
10
|
const store = new Store();
|
|
9
11
|
|
|
10
12
|
ipcMain.on("set-title", (event, title) => {
|
|
@@ -20,21 +22,6 @@ ipcMain.on("save-settings", (event, settings) => {
|
|
|
20
22
|
|
|
21
23
|
const isMac = process.platform === "darwin";
|
|
22
24
|
|
|
23
|
-
function getArguments() {
|
|
24
|
-
// Heinous hack to get "built" versions working
|
|
25
|
-
let args;
|
|
26
|
-
if (path.basename(process.argv[0]) === "jsbeeb")
|
|
27
|
-
// Is this ia "built" version?
|
|
28
|
-
args = process.argv.slice(1);
|
|
29
|
-
else args = process.argv.slice(2);
|
|
30
|
-
|
|
31
|
-
// Filter out Chrome switches that appear in process.argv. The snap wrapper
|
|
32
|
-
// adds --no-sandbox for compatibility, and `--disable-gpu` might be useful.
|
|
33
|
-
// Note that we don't support snap any more, but these seemed useful to leave.
|
|
34
|
-
const ignoredChromeFlags = ["--no-sandbox", "--disable-gpu"];
|
|
35
|
-
return args.filter((arg) => !ignoredChromeFlags.includes(arg));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
25
|
const parser = new ArgumentParser({
|
|
39
26
|
prog: "jsbeeb",
|
|
40
27
|
add_help: true,
|
package/src/app/args.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the command-line arguments the user actually passed to jsbeeb, with
|
|
5
|
+
* the Electron binary path(s) and known runtime-injected flags stripped.
|
|
6
|
+
*
|
|
7
|
+
* @param {string[]} [argv=process.argv] Full argv array.
|
|
8
|
+
* @param {boolean} [defaultApp=process.defaultApp]
|
|
9
|
+
* True when running under `electron .` (dev). Electron only sets
|
|
10
|
+
* process.defaultApp in that case, so we use it instead of comparing
|
|
11
|
+
* basename(argv[0]) === "jsbeeb" (the previous check), which failed on
|
|
12
|
+
* Windows where the binary is jsbeeb.exe, silently dropping the first
|
|
13
|
+
* user argument. See issue #684.
|
|
14
|
+
* @returns {string[]}
|
|
15
|
+
*/
|
|
16
|
+
export function getArguments(argv = process.argv, defaultApp = process.defaultApp) {
|
|
17
|
+
// In a packaged Electron app argv is [appBinary, ...userArgs]; when running
|
|
18
|
+
// under `electron .` (dev) it is [electronBinary, appPath, ...userArgs].
|
|
19
|
+
const args = defaultApp ? argv.slice(2) : argv.slice(1);
|
|
20
|
+
|
|
21
|
+
// Filter out Chrome switches that appear in argv. The snap wrapper adds
|
|
22
|
+
// --no-sandbox for compatibility, and --disable-gpu might be useful. We
|
|
23
|
+
// don't support snap any more, but these seemed useful to leave.
|
|
24
|
+
const ignoredChromeFlags = ["--no-sandbox", "--disable-gpu"];
|
|
25
|
+
return args.filter((arg) => !ignoredChromeFlags.includes(arg));
|
|
26
|
+
}
|
package/src/bem-snapshot.js
CHANGED
|
@@ -255,7 +255,145 @@ function readString(data, pos) {
|
|
|
255
255
|
return str;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
// b-em writes the tube ULA as a raw struct dump. Version 2 is all single-byte fields, so it has
|
|
259
|
+
// no padding and can be read directly; version 1 used native ints and is not supported here.
|
|
260
|
+
const BemTubeUlaVersion = 2;
|
|
261
|
+
const BemTubeUlaOffsets = {
|
|
262
|
+
ph1: 0,
|
|
263
|
+
ph2: 24,
|
|
264
|
+
ph3: 25,
|
|
265
|
+
ph4: 27,
|
|
266
|
+
hp1: 29, // phl (byte 28) is a latch jsbeeb does not model
|
|
267
|
+
hp2: 30,
|
|
268
|
+
hp3: 31,
|
|
269
|
+
hp4: 33,
|
|
270
|
+
hstat: 35, // hpl (byte 34) likewise unmodelled
|
|
271
|
+
pstat: 39,
|
|
272
|
+
r1stat: 43,
|
|
273
|
+
ph1head: 45, // ph1tail (byte 44) matters only to b-em, which appends there
|
|
274
|
+
ph1count: 46,
|
|
275
|
+
ph3pos: 47,
|
|
276
|
+
hp3pos: 48,
|
|
277
|
+
};
|
|
278
|
+
const BemTubeUlaSize = 49;
|
|
279
|
+
const BemPh1Size = 24;
|
|
280
|
+
// 9 bytes of registers, then RAM, then ROM.
|
|
281
|
+
const BemTubeRegisterBytes = 9;
|
|
282
|
+
const BemTubeRamSize = 65536;
|
|
283
|
+
// b-em's other co-processors are different CPUs, and its "6502 Turbo" has 16MB of RAM.
|
|
284
|
+
const BemSupportedTubes = ["6502 Internal", "6502 External"];
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Convert b-em's tube sections into jsbeeb tube state.
|
|
288
|
+
* @param {object} sections - parsed v3 sections
|
|
289
|
+
* @param {string|null} tubeName - co-processor name from the model section
|
|
290
|
+
* @returns {Promise<object|null>} jsbeeb tube state, or null if no usable co-processor
|
|
291
|
+
*/
|
|
292
|
+
async function parseBemTube(sections, tubeName) {
|
|
293
|
+
// b-em names the model's co-processor even when it failed to start one, so naming a tube but
|
|
294
|
+
// carrying no state for it is valid. Carrying only one of the two sections is not.
|
|
295
|
+
if (!sections["T"] && !sections["P"]) return null;
|
|
296
|
+
if (!sections["T"] || !sections["P"]) {
|
|
297
|
+
throw new Error("Truncated b-em snapshot: it holds only one of the two co-processor sections");
|
|
298
|
+
}
|
|
299
|
+
// b-em only writes these sections for a co-processor it started, which it always names.
|
|
300
|
+
if (!tubeName) {
|
|
301
|
+
throw new Error("Corrupt b-em snapshot: it holds co-processor state but names no co-processor");
|
|
302
|
+
}
|
|
303
|
+
if (!BemSupportedTubes.includes(tubeName)) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Unsupported b-em co-processor "${tubeName}": jsbeeb only emulates the 64K 65C02 second processor`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const ulaSection = sections["T"].data;
|
|
310
|
+
if (ulaSection[0] !== BemTubeUlaVersion) {
|
|
311
|
+
throw new Error(`Unsupported b-em tube ULA state version ${ulaSection[0]}`);
|
|
312
|
+
}
|
|
313
|
+
if (ulaSection.length < 2 + BemTubeUlaSize) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Truncated b-em tube ULA state: expected ${2 + BemTubeUlaSize} bytes, got ${ulaSection.length}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const romPaged = !!ulaSection[1];
|
|
319
|
+
const ula = ulaSection.slice(2, 2 + BemTubeUlaSize);
|
|
320
|
+
const at = (field) => ula[BemTubeUlaOffsets[field]];
|
|
321
|
+
|
|
322
|
+
// b-em keeps the R1 FIFO as a circular buffer; jsbeeb shifts its contents down on read, so
|
|
323
|
+
// the bytes have to be linearised into the order jsbeeb expects to read them.
|
|
324
|
+
const ph1Count = at("ph1count");
|
|
325
|
+
const ph1Head = at("ph1head");
|
|
326
|
+
// Out-of-range counts would restore a FIFO jsbeeb's own reads could never have produced.
|
|
327
|
+
if (ph1Count > BemPh1Size || ph1Head >= BemPh1Size || at("ph3pos") > 2 || at("hp3pos") > 2) {
|
|
328
|
+
throw new Error("Corrupt b-em tube ULA state: FIFO counts out of range");
|
|
329
|
+
}
|
|
330
|
+
const ph1 = new Uint8Array(BemPh1Size);
|
|
331
|
+
for (let i = 0; i < ph1Count; i++) {
|
|
332
|
+
ph1[i] = ula[BemTubeUlaOffsets.ph1 + ((ph1Head + i) % BemPh1Size)];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Those names come from a config file, so size is the real check. It cannot be exact: the
|
|
336
|
+
// trailing ROM's size comes from that same config, and b-em records it as zero if unset.
|
|
337
|
+
const parasite = await decompress(sections["P"].data, "deflate");
|
|
338
|
+
const romBytes = parasite.length - BemTubeRegisterBytes - BemTubeRamSize;
|
|
339
|
+
if (romBytes < 0 || romBytes > BemTubeRamSize) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`Unsupported b-em co-processor: expected ${BemTubeRamSize} bytes of parasite RAM, ` +
|
|
342
|
+
`but the state section holds ${parasite.length - BemTubeRegisterBytes}`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
a: parasite[2],
|
|
348
|
+
x: parasite[3],
|
|
349
|
+
y: parasite[4],
|
|
350
|
+
p: parasite[5] | 0x30,
|
|
351
|
+
s: parasite[6],
|
|
352
|
+
pc: parasite[7] | (parasite[8] << 8),
|
|
353
|
+
// b-em's oldnmi is the NMI level as of the parasite's last instruction, so it may lag the
|
|
354
|
+
// line its ULA state implies. b-em's skipint has no jsbeeb equivalent and is dropped.
|
|
355
|
+
nmiLevel: !!parasite[1],
|
|
356
|
+
nmiEdge: false,
|
|
357
|
+
takeInt: false,
|
|
358
|
+
cycles: 0,
|
|
359
|
+
romPaged,
|
|
360
|
+
memory: parasite.slice(BemTubeRegisterBytes, BemTubeRegisterBytes + BemTubeRamSize),
|
|
361
|
+
// The parasite ROM is loaded from file at boot, so b-em's copy is not needed.
|
|
362
|
+
ula: {
|
|
363
|
+
internalStatusRegister: at("r1stat"),
|
|
364
|
+
hostStatus: ula.slice(BemTubeUlaOffsets.hstat, BemTubeUlaOffsets.hstat + 4),
|
|
365
|
+
parasiteStatus: ula.slice(BemTubeUlaOffsets.pstat, BemTubeUlaOffsets.pstat + 4),
|
|
366
|
+
parasiteToHostData: [
|
|
367
|
+
ph1,
|
|
368
|
+
Uint8Array.of(at("ph2")),
|
|
369
|
+
ula.slice(BemTubeUlaOffsets.ph3, BemTubeUlaOffsets.ph3 + 2),
|
|
370
|
+
Uint8Array.of(at("ph4")),
|
|
371
|
+
],
|
|
372
|
+
hostToParasiteData: [
|
|
373
|
+
Uint8Array.of(at("hp1")),
|
|
374
|
+
Uint8Array.of(at("hp2")),
|
|
375
|
+
ula.slice(BemTubeUlaOffsets.hp3, BemTubeUlaOffsets.hp3 + 2),
|
|
376
|
+
Uint8Array.of(at("hp4")),
|
|
377
|
+
],
|
|
378
|
+
parasiteToHostFifoByteCount1: ph1Count,
|
|
379
|
+
parasiteToHostFifoByteCount3: at("ph3pos"),
|
|
380
|
+
hostToParasiteFifoByteCount3: at("hp3pos"),
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Read the co-processor name from b-em's model section, if one was fitted.
|
|
387
|
+
* @returns {string|null}
|
|
388
|
+
*/
|
|
389
|
+
function readBemTubeName(data, pos) {
|
|
390
|
+
for (let i = 0; i < 4; i++) readString(data, pos); // os, cmos, rom setup, fdc names
|
|
391
|
+
pos.offset += 6; // model flag bytes
|
|
392
|
+
const fitted = data[pos.offset++];
|
|
393
|
+
return fitted ? readString(data, pos) : null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function parseBemV3(buffer) {
|
|
259
397
|
const bytes = new Uint8Array(buffer);
|
|
260
398
|
let offset = 8; // Skip "BEMSNAP3" signature
|
|
261
399
|
|
|
@@ -281,11 +419,13 @@ function parseBemV3(buffer) {
|
|
|
281
419
|
// Parse model section to determine jsbeeb model name.
|
|
282
420
|
// Use jsbeeb synonyms (from models.js) so findModel() resolves them.
|
|
283
421
|
let modelName = "B";
|
|
422
|
+
let tubeName = null;
|
|
284
423
|
if (sections["m"]) {
|
|
285
424
|
const pos = { offset: 0 };
|
|
286
425
|
const data = sections["m"].data;
|
|
287
426
|
readVar(data, pos); // curmodel index (skip)
|
|
288
427
|
const name = readString(data, pos);
|
|
428
|
+
tubeName = readBemTubeName(data, pos);
|
|
289
429
|
if (name.includes("Master")) {
|
|
290
430
|
if (name.includes("ADFS")) modelName = "MasterADFS";
|
|
291
431
|
else if (name.includes("ANFS")) modelName = "MasterANFS";
|
|
@@ -311,23 +451,21 @@ function parseBemV3(buffer) {
|
|
|
311
451
|
if (memSection) {
|
|
312
452
|
// Memory is zlib-compressed; decompression is async.
|
|
313
453
|
// Decompressed layout: 2 bytes (fe30, fe34) + 64KB RAM + 256KB ROM
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
return finishV3Parse(modelName, cpuState, ram, roms, sections);
|
|
325
|
-
});
|
|
454
|
+
const memData = await decompress(memSection.data, "deflate");
|
|
455
|
+
cpuState.fe30 = memData[0];
|
|
456
|
+
cpuState.fe34 = memData[1];
|
|
457
|
+
const ramStart = 2;
|
|
458
|
+
const ramSize = 64 * 1024;
|
|
459
|
+
ram.set(memData.slice(ramStart, ramStart + ramSize));
|
|
460
|
+
const romStart = ramStart + ramSize;
|
|
461
|
+
if (memData.length > romStart) {
|
|
462
|
+
roms = memData.slice(romStart, romStart + 262144);
|
|
463
|
+
}
|
|
326
464
|
}
|
|
327
|
-
return finishV3Parse(modelName, cpuState, ram, roms, sections);
|
|
465
|
+
return finishV3Parse(modelName, cpuState, ram, roms, sections, await parseBemTube(sections, tubeName));
|
|
328
466
|
}
|
|
329
467
|
|
|
330
|
-
function finishV3Parse(modelName, cpuState, ram, roms, sections) {
|
|
468
|
+
function finishV3Parse(modelName, cpuState, ram, roms, sections, tube) {
|
|
331
469
|
// Parse system VIA
|
|
332
470
|
let sysvia = convertViaState(
|
|
333
471
|
{
|
|
@@ -466,5 +604,6 @@ function finishV3Parse(modelName, cpuState, ram, roms, sections) {
|
|
|
466
604
|
uservia,
|
|
467
605
|
buildVideoState(ulaControl, ulaPalette, crtcRegs, nulaCollook, crtcCounters),
|
|
468
606
|
soundChip,
|
|
607
|
+
tube,
|
|
469
608
|
);
|
|
470
609
|
}
|