jsbeeb 1.22.4 → 1.24.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.
Files changed (112) hide show
  1. package/README.md +2 -2
  2. package/package.json +16 -11
  3. package/src/6502.js +39 -42
  4. package/src/6502.opcodes.js +198 -123
  5. package/src/6847.js +4 -5
  6. package/src/6847_fontdata.js +0 -2
  7. package/src/acia.js +2 -3
  8. package/src/app/app.js +0 -1
  9. package/src/app/args.js +0 -2
  10. package/src/app/electron.js +4 -6
  11. package/src/app-bench.js +1 -3
  12. package/src/archive.js +243 -0
  13. package/src/basic-loader.js +0 -7
  14. package/src/basic-tokenise.js +19 -6
  15. package/src/bbc-palette.js +0 -2
  16. package/src/bbcdiscs.js +0 -2
  17. package/src/bem-snapshot.js +1 -3
  18. package/src/binary.js +175 -0
  19. package/src/cmos.js +0 -2
  20. package/src/ddnoise.js +0 -1
  21. package/src/debounce.js +7 -0
  22. package/src/disc-surface.js +0 -2
  23. package/src/disc.js +10 -9
  24. package/src/fake6502.js +7 -23
  25. package/src/fdc.js +9 -51
  26. package/src/filestore.js +10 -17
  27. package/src/hex.js +38 -0
  28. package/src/index.js +11 -0
  29. package/src/intel-fdc.js +18 -20
  30. package/src/jsbeeb.css +33 -0
  31. package/src/{utils_atom.js → keymap-atom.js} +2 -18
  32. package/src/{utils.js → keymap.js} +47 -610
  33. package/src/loader.js +63 -0
  34. package/src/machine-session.js +37 -177
  35. package/src/machine-spec.js +59 -0
  36. package/src/main.js +69 -184
  37. package/src/media-resolver.js +68 -0
  38. package/src/mmc.js +3 -2
  39. package/src/models.js +40 -4
  40. package/src/music5000.js +0 -2
  41. package/src/ppia.js +3 -4
  42. package/src/printer.js +7 -14
  43. package/src/relaynoise.js +0 -1
  44. package/src/rewind.js +0 -2
  45. package/src/sample-player.js +2 -3
  46. package/src/serial.js +0 -2
  47. package/src/snapshot-helpers.js +0 -2
  48. package/src/snapshot.js +0 -2
  49. package/src/soundchip.js +1 -1
  50. package/src/state-utils.js +0 -2
  51. package/src/sth.js +2 -4
  52. package/src/tapes.js +10 -4
  53. package/src/teletext.js +2 -3
  54. package/src/teletext_adaptor.js +2 -4
  55. package/src/teletext_data.js +0 -2
  56. package/src/test-machine.js +307 -0
  57. package/src/touchscreen.js +2 -4
  58. package/src/tube.js +5 -6
  59. package/src/typist.js +103 -0
  60. package/src/uef-snapshot.js +0 -2
  61. package/src/url-params.js +4 -5
  62. package/src/vdu-capture.js +113 -0
  63. package/src/via.js +3 -4
  64. package/src/video-filters/pal-composite.js +0 -2
  65. package/src/video-filters/passthrough-filter.js +0 -2
  66. package/src/video-filters/pixel-grid.js +0 -2
  67. package/src/video-filters/shader-program.js +0 -2
  68. package/src/video-filters/xbr-filter.js +0 -2
  69. package/src/video.js +12 -11
  70. package/src/wd-fdc.js +20 -20
  71. package/src/web/analogue-inputs.js +6 -8
  72. package/src/web/analytics.js +13 -0
  73. package/src/web/audio-handler.js +2 -2
  74. package/src/{audio-utils.js → web/audio-utils.js} +0 -2
  75. package/src/web/autoboot.js +10 -13
  76. package/src/{canvas.js → web/canvas.js} +4 -5
  77. package/src/web/config.js +42 -72
  78. package/src/web/console-surface.js +25 -0
  79. package/src/web/debug.js +8 -49
  80. package/src/web/disc-visualiser.js +0 -2
  81. package/src/web/display.js +21 -4
  82. package/src/{dom-utils.js → web/dom-utils.js} +1 -11
  83. package/src/web/drives.js +1 -1
  84. package/src/web/emulation-loop.js +54 -33
  85. package/src/web/front-panel.js +13 -12
  86. package/src/{gamepads.js → web/gamepads.js} +2 -6
  87. package/src/web/google-drive-picker.js +14 -9
  88. package/src/{google-drive.js → web/google-drive.js} +9 -10
  89. package/src/web/hfe-picker.js +5 -4
  90. package/src/web/icons.js +42 -0
  91. package/src/web/keyboard-setup.js +29 -40
  92. package/src/{keyboard.js → web/keyboard.js} +18 -119
  93. package/src/web/layout.js +4 -0
  94. package/src/web/local-disc.js +44 -0
  95. package/src/web/machine.js +27 -20
  96. package/src/web/media-loader.js +38 -147
  97. package/src/{microphone-input.js → web/microphone-input.js} +1 -1
  98. package/src/web/modals.js +6 -9
  99. package/src/web/page-actions.js +88 -0
  100. package/src/web/quick-settings.js +16 -15
  101. package/src/web/reporting.js +0 -7
  102. package/src/{rewind-thumbnail.js → web/rewind-thumbnail.js} +0 -2
  103. package/src/web/rewind-ui.js +29 -29
  104. package/src/web/run-controls.js +39 -0
  105. package/src/web/settings.js +64 -135
  106. package/src/web/snapshot-ui.js +10 -16
  107. package/src/{speech-output.js → web/speech-output.js} +0 -2
  108. package/src/web/sth-picker.js +9 -7
  109. package/src/web/url-state.js +20 -6
  110. package/src/econet/L3FS.dat +0 -0
  111. package/src/econet/scsi.dat +0 -0
  112. package/tests/test-machine.js +0 -609
package/README.md CHANGED
@@ -96,8 +96,8 @@ Some things to know:
96
96
  was at fault.
97
97
  - On the Atom, use the Atom's own key names (`LOCK`, `UP_DOWN`, `LEFT_RIGHT` and so on) rather than the BBC's.
98
98
 
99
- The definitive lists are `keyCodes` (host) and `BBC` (BBC micro) in [`src/utils.js`](src/utils.js), and `ATOM` in
100
- [`src/utils_atom.js`](src/utils_atom.js).
99
+ The definitive lists are `keyCodes` (host) and `BBC` (BBC micro) in [`src/keymap.js`](src/keymap.js), and `ATOM` in
100
+ [`src/keymap-atom.js`](src/keymap-atom.js).
101
101
 
102
102
  ### Emulator Shortcuts
103
103
 
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "name": "jsbeeb",
8
8
  "description": "Emulate a BBC Micro",
9
9
  "repository": "git@github.com:mattgodbolt/jsbeeb.git",
10
- "version": "1.22.4",
10
+ "version": "1.24.0",
11
11
  "//engines": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
12
12
  "engines": {
13
13
  "node": ">=24.15.0"
@@ -15,27 +15,26 @@
15
15
  "type": "module",
16
16
  "main": "./src/app/app.js",
17
17
  "exports": {
18
- ".": "./src/app/app.js",
18
+ ".": "./src/index.js",
19
19
  "./machine-session": "./src/machine-session.js",
20
- "./src/*": "./src/*",
21
- "./tests/test-machine.js": "./tests/test-machine.js"
20
+ "./src/*": "./src/*"
22
21
  },
23
22
  "files": [
24
23
  "src/",
25
- "tests/test-machine.js",
26
24
  "public/roms/"
27
25
  ],
28
26
  "dependencies": {
29
27
  "@popperjs/core": "^2.11.8",
30
28
  "argparse": "^3.0.0",
31
29
  "bootstrap": "^5.3.8",
30
+ "bootstrap-icons": "^1.13.1",
32
31
  "bootswatch": "^5.3.8",
33
32
  "pako": "^3.0.1",
34
- "sharp": "^0.35.3",
35
33
  "smoothie": "^1.36.1"
36
34
  },
37
35
  "devDependencies": {
38
36
  "@eslint/js": "^10.0.1",
37
+ "@playwright/test": "^1.62.1",
39
38
  "@vitest/coverage-v8": "^4.0.18",
40
39
  "eslint": "^10.7.0",
41
40
  "eslint-config-prettier": "^10.1.8",
@@ -52,7 +51,8 @@
52
51
  "vitest": "^4.0.10"
53
52
  },
54
53
  "optionalDependencies": {
55
- "electron": "^43.2.0",
54
+ "electron": "^44.0.0",
55
+ "sharp": "^0.35.3",
56
56
  "electron-builder": "^26.15.3",
57
57
  "electron-store": "^11.0.2"
58
58
  },
@@ -89,9 +89,12 @@
89
89
  "start": "vite",
90
90
  "build": "vite build",
91
91
  "preview": "vite preview",
92
- "ci-checks": "eslint .",
93
- "lint": "eslint . --fix",
92
+ "ci-checks": "npm-run-all lint format:check check-workflow-suites",
93
+ "check-workflow-suites": "node tools/check-workflow-suites.js",
94
+ "lint": "eslint .",
95
+ "lint-fix": "eslint . --fix",
94
96
  "format": "prettier --write .",
97
+ "format:check": "prettier --check .",
95
98
  "pre-commit": "lint-staged",
96
99
  "test": "npm-run-all test:*",
97
100
  "test:unit": "vitest run --silent=true tests/unit",
@@ -99,6 +102,7 @@
99
102
  "test:cpu": "node tests/test-suite.js",
100
103
  "test:shader": "vitest run --silent=true tests/shader",
101
104
  "coverage:unit": "vitest run tests/unit --coverage",
105
+ "dom-unit-tests": "vitest run --silent=true --project unit $(grep -rl domFromIndexHtml tests/unit)",
102
106
  "coverage:all-tests": "vitest run --coverage",
103
107
  "benchmark": "node src/app-bench.js",
104
108
  "mirror-sth": "node tools/mirror-sth.js --out .sth-mirror",
@@ -115,14 +119,15 @@
115
119
  "mirror-bbcdiscs:upload:index": "aws s3 cp .bbcdiscs-mirror/hfe/manifest.json s3://bbc.xania.org/archive/bbcdiscs/hfe/manifest.json --cache-control 'public, max-age=300' && aws s3 cp .bbcdiscs-mirror/manifest.json s3://bbc.xania.org/archive/bbcdiscs/manifest.json --cache-control 'public, max-age=300'",
116
120
  "electron": "npm run build && ELECTRON_DISABLE_SANDBOX=1 electron .",
117
121
  "electron:build": "electron-builder",
118
- "test:smoke": "npm run build && node tests/browser/smoke.js"
122
+ "test:e2e": "npm run build && playwright test --project chrome"
119
123
  },
120
124
  "lint-staged": {
121
125
  "*.js": [
122
- "vitest related --run --exclude tests/integration --exclude tests/shader --no-file-parallelism",
126
+ "vitest related --run --project unit --no-file-parallelism",
123
127
  "eslint --cache --fix",
124
128
  "prettier --write"
125
129
  ],
130
+ "index.html": "npm run dom-unit-tests --",
126
131
  "*.{md,json,yml,yaml,css,html}": "prettier --write"
127
132
  },
128
133
  "allowScripts": {
package/src/6502.js CHANGED
@@ -1,5 +1,3 @@
1
- "use strict";
2
- import * as utils from "./utils.js";
3
1
  import * as via from "./via.js";
4
2
  import { Acia } from "./acia.js";
5
3
  import { Serial } from "./serial.js";
@@ -12,8 +10,10 @@ import { Filestore } from "./filestore.js";
12
10
  import { FakeRelayNoise } from "./relaynoise.js";
13
11
  import { AtomPPIA } from "./ppia.js";
14
12
  import { AtomMMC2 } from "./mmc.js";
15
-
16
- const signExtend = utils.signExtend;
13
+ import { unzipRomImage } from "./archive.js";
14
+ import { signExtend } from "./binary.js";
15
+ import { hexbyte, hexword } from "./hex.js";
16
+ import { loadData } from "./loader.js";
17
17
 
18
18
  function _set(byte, mask, set) {
19
19
  return (byte & ~mask) | (set ? mask : 0);
@@ -255,7 +255,7 @@ class Base6502 {
255
255
  this.polltime(3);
256
256
  } else {
257
257
  this.polltime(2);
258
- // TODO: check 65c12 BRK interrupt poll timing.
258
+ // TODO(#1063) check 65c12 BRK interrupt poll timing.
259
259
  this.checkInt();
260
260
  this.polltime(1);
261
261
  }
@@ -540,7 +540,7 @@ class Tube6502 extends Base6502 {
540
540
  async loadOs() {
541
541
  console.log("Loading tube rom from roms/" + this.model.os);
542
542
  const tubeRom = this.rom;
543
- const data = await utils.loadData("roms/" + this.model.os);
543
+ const data = await loadData("roms/" + this.model.os);
544
544
  const len = data.length;
545
545
  if (len !== 2048) throw new Error("Broken ROM file (length=" + len + ")");
546
546
  for (let i = 0; i < len; ++i) {
@@ -561,25 +561,6 @@ class FakeTube {
561
561
  reset() {}
562
562
  }
563
563
 
564
- class FakeUserPort {
565
- write() {}
566
-
567
- read() {
568
- return 0xff;
569
- }
570
- }
571
-
572
- function fixUpConfig(config) {
573
- if (config === undefined) config = {};
574
- if (!config.keyLayout) config.keyLayout = "physical";
575
- if (!config.cpuMultiplier) config.cpuMultiplier = 1;
576
- if (!config.userPort) config.userPort = new FakeUserPort();
577
- if (config.printerPort === undefined) config.printerPort = null;
578
- config.extraRoms = config.extraRoms || [];
579
- config.debugFlags = config.debugFlags || {};
580
- return config;
581
- }
582
-
583
564
  class DebugHook {
584
565
  constructor(cpu, functionName) {
585
566
  this.cpu = cpu;
@@ -645,7 +626,8 @@ export class Cpu6502 extends Base6502 {
645
626
  } = {},
646
627
  ) {
647
628
  super(model, { cycleAccurate });
648
- this.config = fixUpConfig(config);
629
+ this.config = config;
630
+ this.keyLayout = config.keyLayout;
649
631
  this.debugFlags = this.config.debugFlags;
650
632
  this.cmos = cmos;
651
633
  this.debugger = dbgr;
@@ -702,12 +684,14 @@ export class Cpu6502 extends Base6502 {
702
684
  soundChip: this.soundChip,
703
685
  cmos: this.cmos,
704
686
  isMaster: this.model.isMaster,
705
- initialLayout: this.config.keyLayout,
687
+ initialLayout: this.keyLayout,
706
688
  getGamepads: this.config.getGamepads,
707
689
  });
708
690
  this.uservia = new via.UserVia(this, this.scheduler, this.model.isMaster, this.config.userPort);
709
691
  this.acia = new Acia(this, this.soundChip.toneGenerator, this.scheduler, this.relayNoise);
710
692
  this.serial = new Serial(this.acia);
693
+ this.keyboardInterface = this.sysvia;
694
+ this.tapeInterface = this.acia;
711
695
  this.adconverter = new Adc(this.sysvia, this.scheduler);
712
696
  this.touchScreen = new TouchScreen(this.scheduler, this.model.cyclesPerSecond);
713
697
  this.soundChip.setScheduler(this.scheduler);
@@ -826,7 +810,7 @@ export class Cpu6502 extends Base6502 {
826
810
  readArea(addr, len) {
827
811
  let str = "";
828
812
  for (let i = 0; i < len; ++i) {
829
- str += utils.hexbyte(this.readmem(addr + i));
813
+ str += hexbyte(this.readmem(addr + i));
830
814
  }
831
815
  return str;
832
816
  }
@@ -1003,7 +987,7 @@ export class Cpu6502 extends Base6502 {
1003
987
  const offset = this.memLook[statOffset];
1004
988
  return this.ramRomOs[offset + addr];
1005
989
  } else {
1006
- return 0xff; // TODO; peekDevice -- this.peekDevice(addr);
990
+ return 0xff; // TODO(#1062) peekDevice: this.peekDevice(addr);
1007
991
  }
1008
992
  }
1009
993
 
@@ -1165,9 +1149,9 @@ export class Cpu6502 extends Base6502 {
1165
1149
  if (name.indexOf("http") !== 0) name = "roms/" + name;
1166
1150
  console.log("Loading ROM from " + name);
1167
1151
  const ramRomOs = this.ramRomOs;
1168
- let data = await utils.loadData(name);
1152
+ let data = await loadData(name);
1169
1153
  if (/\.zip/i.test(name)) {
1170
- data = (await utils.unzipRomImage(data)).data;
1154
+ data = (await unzipRomImage(data)).data;
1171
1155
  }
1172
1156
  ramRomOs.set(data, offset);
1173
1157
  }
@@ -1177,7 +1161,7 @@ export class Cpu6502 extends Base6502 {
1177
1161
  os = "roms/" + os;
1178
1162
  console.log(`Loading OS from ${os}`);
1179
1163
  const ramRomOs = this.ramRomOs;
1180
- const data = await utils.loadData(os);
1164
+ const data = await loadData(os);
1181
1165
  const len = data.length;
1182
1166
  if (len < 16384 || len & 16383) throw new Error(`Broken OS ROM file (length=${len})`);
1183
1167
  ramRomOs.set(data, this.osOffset);
@@ -1410,7 +1394,7 @@ export class Cpu6502 extends Base6502 {
1410
1394
  }
1411
1395
 
1412
1396
  updateKeyLayout() {
1413
- this.sysvia.setKeyLayout(this.config.keyLayout);
1397
+ this.sysvia.setKeyLayout(this.keyLayout);
1414
1398
  }
1415
1399
 
1416
1400
  polltimeAddr(cycles, addr, isWrite) {
@@ -1555,6 +1539,12 @@ export class Cpu6502 extends Base6502 {
1555
1539
  this.halted = true;
1556
1540
  }
1557
1541
 
1542
+ /** Changes the host key layout for the keyboard, and for every reset after it. */
1543
+ setKeyLayout(layout) {
1544
+ this.keyLayout = layout;
1545
+ this.sysvia.setKeyLayout(layout);
1546
+ }
1547
+
1558
1548
  dumpTrace(maxToShow, func) {
1559
1549
  if (!maxToShow) maxToShow = 256;
1560
1550
  if (maxToShow > 256) maxToShow = 256;
@@ -1564,11 +1554,11 @@ export class Cpu6502 extends Base6502 {
1564
1554
  function (pc, a, x, y) {
1565
1555
  const dis = disassembler.disassemble(pc, true)[0];
1566
1556
  console.log(
1567
- utils.hexword(pc),
1557
+ hexword(pc),
1568
1558
  (dis + " ").substring(0, 15),
1569
- utils.hexbyte(a),
1570
- utils.hexbyte(x),
1571
- utils.hexbyte(y),
1559
+ hexbyte(a),
1560
+ hexbyte(x),
1561
+ hexbyte(y),
1572
1562
  );
1573
1563
  };
1574
1564
  for (let i = maxToShow - 2; i >= 0; --i) {
@@ -1613,12 +1603,19 @@ const NumBranquartBanks = 16;
1613
1603
  const AtomRomBlockSize = 0x1000; // 4KB
1614
1604
 
1615
1605
  export class AtomCpu6502 extends Cpu6502 {
1606
+ setKeyLayout(layout) {
1607
+ this.keyLayout = layout;
1608
+ this.atomppia.setKeyLayout(layout);
1609
+ }
1610
+
1616
1611
  constructor(model, options) {
1617
1612
  super(model, options);
1618
1613
 
1619
1614
  // Atom peripherals
1620
- this.atomppia = new AtomPPIA(this, this.config.keyLayout, this.scheduler);
1615
+ this.atomppia = new AtomPPIA(this, this.keyLayout, this.scheduler);
1621
1616
  this.atommc = new AtomMMC2(this);
1617
+ this.keyboardInterface = this.atomppia;
1618
+ this.tapeInterface = this.atomppia;
1622
1619
 
1623
1620
  // Branquart bank selection
1624
1621
  this.branquartLatch = 0;
@@ -1785,7 +1782,7 @@ export class AtomCpu6502 extends Cpu6502 {
1785
1782
  const bankRoms = this.model.banks || [];
1786
1783
  os = "roms/" + os;
1787
1784
  console.log(`Loading Atom OS from ${os}`);
1788
- const data = await utils.loadData(os);
1785
+ const data = await loadData(os);
1789
1786
  const len = data.length;
1790
1787
 
1791
1788
  if (len < AtomRomBlockSize || len % AtomRomBlockSize !== 0) {
@@ -1825,9 +1822,9 @@ export class AtomCpu6502 extends Cpu6502 {
1825
1822
  // Override loadRom to accept 4KB ROMs
1826
1823
  async loadRom(name, offset) {
1827
1824
  if (name.indexOf("http") !== 0) name = "roms/" + name;
1828
- let data = await utils.loadData(name);
1825
+ let data = await loadData(name);
1829
1826
  if (/\.zip/i.test(name)) {
1830
- data = (await utils.unzipRomImage(data)).data;
1827
+ data = (await unzipRomImage(data)).data;
1831
1828
  }
1832
1829
  const len = data.length;
1833
1830
  if (len !== 16384 && len !== 8192 && len !== 4096) {
@@ -1838,7 +1835,7 @@ export class AtomCpu6502 extends Cpu6502 {
1838
1835
 
1839
1836
  // Atom key layout goes through PPIA, not SysVia
1840
1837
  updateKeyLayout() {
1841
- this.atomppia.setKeyLayout(this.config.keyLayout);
1838
+ this.atomppia.setKeyLayout(this.keyLayout);
1842
1839
  }
1843
1840
 
1844
1841
  snapshotState(options) {