jsbeeb 1.11.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.
package/README.md CHANGED
@@ -4,8 +4,9 @@
4
4
 
5
5
  [![jsbeeb](public/images/jsbeeb-example.png)](https://bbc.xania.org/)
6
6
 
7
- A BBC Micro emulator written in JavaScript and running in modern browsers. Emulates a 32K BBC B (with sideways RAM)
8
- and a 128K BBC Master, along with a number of different peripherals.
7
+ A BBC Micro and other 8-bit Acorn emulator written in JavaScript and running in modern browsers. Emulates a 32K BBC B
8
+ (with sideways RAM), a 128K BBC Master, and an Acorn Atom (with AtoMMC2 SD card interface), along with a number of
9
+ different peripherals.
9
10
 
10
11
  ## Table of Contents
11
12
 
@@ -177,7 +178,15 @@ sudo rpm -i out/dist/jsbeeb-1.0.1.x86_64.rpm
177
178
  Doesn't support the sth: pseudo URL unlike `disc` and `tape`, but if given a ZIP file will attempt to use the `.rom`
178
179
  file assumed to be within.
179
180
  - (mostly internal use) `logFdcCommands`, `logFdcStateChanges` - turn on logging in the disc controller.
180
- - `audioDebug=true` turns on some audio debug graphs.
181
+ - `audioDebug` - show audio queue stats chart.
182
+
183
+ ### Atom-specific parameters
184
+
185
+ - `model=Atom` - select the Acorn Atom (MMC) model. Other Atom variants: `Atom-Tape`, `Atom-Tape-FP`, `Atom-DOS`.
186
+ - `mmc=XXX` - load an MMC/SD card image (ZIP) for the Atom.
187
+
188
+ Atom models can also be selected automatically by hostname: any hostname starting with `atom` (e.g. `atom.xania.org`)
189
+ defaults to the Atom model.
181
190
 
182
191
  ## Patches
183
192
 
@@ -258,6 +267,11 @@ Cheers to [Ed Spittles](https://github.com/BigEd) for testing various interrupt
258
267
 
259
268
  Thanks to Chris Jordan for his thorough testing, bug reports, ideas and help.
260
269
 
270
+ Huge thanks to [Andrew Hague](https://github.com/CommanderCoder) (CommanderCoder) for the Acorn Atom emulation
271
+ support. Andrew developed the original Atom implementation including the MC6847 video chip, 8255 PPIA, AtoMMC2 SD card
272
+ interface, Atom keyboard mapping, tape support, and speaker output. His work in [PR #505](https://github.com/mattgodbolt/jsbeeb/pull/505)
273
+ was incrementally merged and refined into the codebase.
274
+
261
275
  A lot of the early development used the amazing [Visual 6502](http://visual6502.org/) as reference for intra-instruction
262
276
  timings. Amazing stuff.
263
277
 
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.11.0",
10
+ "version": "1.13.0",
11
11
  "//": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
12
12
  "engines": {
13
13
  "node": ">=22.12.0"
@@ -30,28 +30,27 @@
30
30
  "argparse": "^2.0.1",
31
31
  "bootstrap": "^5.3.8",
32
32
  "bootswatch": "^5.3.8",
33
- "event-emitter-es6": "^1.1.5",
34
33
  "sharp": "^0.34.5",
35
34
  "smoothie": "^1.36.1"
36
35
  },
37
36
  "devDependencies": {
38
37
  "@eslint/js": "^10.0.1",
39
38
  "@vitest/coverage-v8": "^4.0.18",
40
- "eslint": "^10.0.0",
39
+ "eslint": "^10.2.0",
41
40
  "eslint-config-prettier": "^10.1.8",
42
41
  "eslint-plugin-prettier": "^5.5.5",
43
- "globals": "^17.3.0",
42
+ "globals": "^17.5.0",
44
43
  "husky": "^9.1.7",
45
- "jsdom": "^29.0.0",
46
- "lint-staged": "^16.2.7",
44
+ "jsdom": "^29.0.2",
45
+ "lint-staged": "^16.4.0",
47
46
  "npm-run-all2": "^8.0.4",
48
47
  "pixelmatch": "^7.1.0",
49
- "prettier": "^3.8.1",
50
- "vite": "^8.0.3",
48
+ "prettier": "^3.8.2",
49
+ "vite": "^8.0.8",
51
50
  "vitest": "^4.0.10"
52
51
  },
53
52
  "optionalDependencies": {
54
- "electron": "^41.1.1",
53
+ "electron": "^41.2.0",
55
54
  "electron-builder": "^26.8.1",
56
55
  "electron-store": "^11.0.2"
57
56
  },
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/6502.js CHANGED
@@ -10,6 +10,8 @@ import { TouchScreen } from "./touchscreen.js";
10
10
  import { TeletextAdaptor } from "./teletext_adaptor.js";
11
11
  import { Filestore } from "./filestore.js";
12
12
  import { FakeRelayNoise } from "./relaynoise.js";
13
+ import { AtomPPIA } from "./ppia.js";
14
+ import { AtomMMC2 } from "./mmc.js";
13
15
 
14
16
  const signExtend = utils.signExtend;
15
17
 
@@ -1208,6 +1210,13 @@ export class Cpu6502 extends Base6502 {
1208
1210
  if (state.roms) {
1209
1211
  this.ramRomOs.set(state.roms.slice(0, 16 * 16384), this.romOffset);
1210
1212
  }
1213
+ // Selectively overwrite individual sideways RAM banks (e.g. from BeebEm UEF import)
1214
+ // without touching ROM banks that jsbeeb has already loaded.
1215
+ if (state.swRamBanks) {
1216
+ for (const [bank, data] of Object.entries(state.swRamBanks)) {
1217
+ this.ramRomOs.set(data.slice(0, 16384), this.romOffset + Number(bank) * 16384);
1218
+ }
1219
+ }
1211
1220
  this.videoDisplayPage = state.videoDisplayPage;
1212
1221
  this.music5000PageSel = state.music5000PageSel;
1213
1222
 
@@ -1238,53 +1247,69 @@ export class Cpu6502 extends Base6502 {
1238
1247
 
1239
1248
  reset(hard) {
1240
1249
  if (hard) {
1241
- // On the Master, opcodes executing from 0xc000 - 0xdfff can optionally have their memory accesses
1242
- // redirected to shadow RAM.
1243
- this.memStatOffsetByIFetchBank = this.model.isMaster ? (1 << 0xc) | (1 << 0xd) : 0x0000;
1244
- if (!this.model.isTest) {
1245
- for (let i = 0; i < 128; ++i) this.memStat[i] = this.memStat[256 + i] = 1;
1246
- for (let i = 128; i < 256; ++i) this.memStat[i] = this.memStat[256 + i] = 2;
1247
- for (let i = 0; i < 128; ++i) this.memLook[i] = this.memLook[256 + i] = 0;
1248
- for (let i = 128; i < 192; ++i) this.memLook[i] = this.memLook[256 + i] = this.romOffset - 0x8000;
1249
- for (let i = 192; i < 256; ++i) this.memLook[i] = this.memLook[256 + i] = this.osOffset - 0xc000;
1250
-
1251
- for (let i = 0xfc; i < 0xff; ++i) this.memStat[i] = this.memStat[256 + i] = 0;
1252
- } else {
1253
- // Test sets everything as RAM.
1254
- for (let i = 0; i < 256; ++i) {
1255
- this.memStat[i] = this.memStat[256 + i] = 1;
1256
- this.memLook[i] = this.memLook[256 + i] = 0;
1257
- }
1258
- }
1259
- // DRAM content is not guaranteed to contain any particular
1260
- // value on start up, so we choose values that help avoid
1261
- // bugs in various games.
1262
- for (let i = 0; i < this.romOffset; ++i) {
1263
- if (i < 0x100) {
1264
- // For Clogger.
1265
- this.ramRomOs[i] = 0x00;
1266
- } else {
1267
- // For Eagle Empire.
1268
- this.ramRomOs[i] = 0xff;
1269
- }
1270
- }
1271
- this.videoDisplayPage = 0;
1272
- if (this.config.printerPort) this.uservia.ca2changecallback = this.config.printerPort.outputStrobe;
1273
-
1274
- this.sysvia.reset();
1275
- this.uservia.reset();
1276
- this.acia.reset();
1277
- this.serial.reset();
1278
- this.ddNoise.spinDown();
1279
- this.fdc.powerOnReset();
1280
- this.adconverter.reset();
1281
-
1282
- this.touchScreen = new TouchScreen(this.scheduler);
1283
- if (this.model.hasTeletextAdaptor) this.teletextAdaptor = new TeletextAdaptor(this);
1284
- if (this.econet) this.filestore = new Filestore(this, this.econet);
1250
+ this.setupMemoryMap();
1251
+ this.resetPeripherals(hard);
1285
1252
  } else {
1286
1253
  this.fdc.reset();
1287
1254
  }
1255
+ this.resetCpuState(hard);
1256
+ }
1257
+
1258
+ // Override in subclasses for different memory maps.
1259
+ setupMemoryMap() {
1260
+ // On the Master, opcodes executing from 0xc000 - 0xdfff can optionally have their memory accesses
1261
+ // redirected to shadow RAM.
1262
+ this.memStatOffsetByIFetchBank = this.model.isMaster ? (1 << 0xc) | (1 << 0xd) : 0x0000;
1263
+ if (!this.model.isTest) {
1264
+ for (let i = 0; i < 128; ++i) this.memStat[i] = this.memStat[256 + i] = 1;
1265
+ for (let i = 128; i < 256; ++i) this.memStat[i] = this.memStat[256 + i] = 2;
1266
+ for (let i = 0; i < 128; ++i) this.memLook[i] = this.memLook[256 + i] = 0;
1267
+ for (let i = 128; i < 192; ++i) this.memLook[i] = this.memLook[256 + i] = this.romOffset - 0x8000;
1268
+ for (let i = 192; i < 256; ++i) this.memLook[i] = this.memLook[256 + i] = this.osOffset - 0xc000;
1269
+
1270
+ for (let i = 0xfc; i < 0xff; ++i) this.memStat[i] = this.memStat[256 + i] = 0;
1271
+ } else {
1272
+ // Test sets everything as RAM.
1273
+ for (let i = 0; i < 256; ++i) {
1274
+ this.memStat[i] = this.memStat[256 + i] = 1;
1275
+ this.memLook[i] = this.memLook[256 + i] = 0;
1276
+ }
1277
+ }
1278
+ // DRAM content is not guaranteed to contain any particular
1279
+ // value on start up, so we choose values that help avoid
1280
+ // bugs in various games.
1281
+ for (let i = 0; i < this.romOffset; ++i) {
1282
+ if (i < 0x100) {
1283
+ // For Clogger.
1284
+ this.ramRomOs[i] = 0x00;
1285
+ } else {
1286
+ // For Eagle Empire.
1287
+ this.ramRomOs[i] = 0xff;
1288
+ }
1289
+ }
1290
+ this.videoDisplayPage = 0;
1291
+ }
1292
+
1293
+ // Override in subclasses for different peripheral sets.
1294
+ // Only called on hard reset.
1295
+ resetPeripherals() {
1296
+ if (this.config.printerPort) this.uservia.ca2changecallback = this.config.printerPort.outputStrobe;
1297
+
1298
+ this.sysvia.reset();
1299
+ this.uservia.reset();
1300
+ this.acia.reset();
1301
+ this.serial.reset();
1302
+ this.ddNoise.spinDown();
1303
+ this.fdc.powerOnReset();
1304
+ this.adconverter.reset();
1305
+
1306
+ this.touchScreen = new TouchScreen(this.scheduler);
1307
+ if (this.model.hasTeletextAdaptor) this.teletextAdaptor = new TeletextAdaptor(this);
1308
+ if (this.econet) this.filestore = new Filestore(this, this.econet);
1309
+ }
1310
+
1311
+ // Universal CPU state reset. Shared by all machine types.
1312
+ resetCpuState(hard) {
1288
1313
  this.tube.reset(hard);
1289
1314
  if (hard) {
1290
1315
  this.targetCycles = 0;
@@ -1488,3 +1513,285 @@ export class Cpu6502 extends Base6502 {
1488
1513
  this.debugger.setCpu(this);
1489
1514
  }
1490
1515
  }
1516
+
1517
+ // Acorn Atom memory map:
1518
+ // 0x0000-0x7FFF RAM (32KB, or 40KB with extensions up to 0x9FFF)
1519
+ // 0x0A00-0x0AFF FDC (8271) -- hole in RAM region
1520
+ // 0x8000-0x9FFF Video RAM (shared with MC6847 VDG)
1521
+ // 0xA000-0xAFFF Banked ROM/RAM (Branquart, 16 x 4KB banks via latch at 0xBFFF)
1522
+ // 0xB000-0xB003 PPIA (8255)
1523
+ // 0xB400-0xB40F AtomMMC
1524
+ // 0xB800-0xB80F VIA (6522)
1525
+ // 0xBFFF Bank select latch (write-only)
1526
+ // 0xC000-0xEFFF ROM (BASIC, FP, DOS in 4KB blocks)
1527
+ // 0xF000-0xFFFF OS kernel ROM (4KB)
1528
+ //
1529
+ // Branquart banking: bits 0-3 of the latch at 0xBFFF select which 4KB
1530
+ // bank is visible at 0xA000-0xAFFF. Banks can contain ROM or RAM.
1531
+ // The latch is write-only; bit 6 is a lock bit.
1532
+
1533
+ // Storage for Branquart banks starts after the BBC ROM area.
1534
+ const BranquartOffset = 128 * 1024 + 17 * 16 * 16384;
1535
+ const BranquartBankSize = 0x1000; // 4KB per bank
1536
+ const NumBranquartBanks = 16;
1537
+ const AtomRomBlockSize = 0x1000; // 4KB
1538
+
1539
+ export class AtomCpu6502 extends Cpu6502 {
1540
+ constructor(model, options) {
1541
+ super(model, options);
1542
+
1543
+ // Atom peripherals
1544
+ this.atomppia = new AtomPPIA(this, this.config.keyLayout, this.scheduler);
1545
+ this.atommc = new AtomMMC2(this);
1546
+
1547
+ // Branquart bank selection
1548
+ this.branquartLatch = 0;
1549
+ // Track which banks are ROM (false) vs RAM (true) for write protection.
1550
+ this.branquartRam = new Array(NumBranquartBanks).fill(true);
1551
+
1552
+ // Expand ramRomOs to include Branquart bank storage.
1553
+ const expandedSize = BranquartOffset + NumBranquartBanks * BranquartBankSize;
1554
+ if (this.ramRomOs.length < expandedSize) {
1555
+ const expanded = new Uint8Array(expandedSize);
1556
+ expanded.set(this.ramRomOs);
1557
+ this.ramRomOs = expanded;
1558
+ }
1559
+
1560
+ // Atom runs at 1 MHz
1561
+ this.peripheralCyclesPerSecond = 1 * 1000 * 1000;
1562
+ // reset() and debugger.setCpu() are called by initialise() after loadOs().
1563
+ }
1564
+
1565
+ // Select which Branquart bank is visible at 0xA000-0xAFFF by
1566
+ // updating the memLook table. This keeps reads on the fast path.
1567
+ selectBranquartBank(bank) {
1568
+ bank &= 0x0f;
1569
+ const offset = BranquartOffset + bank * BranquartBankSize - 0xa000;
1570
+ for (let i = 0xa0; i < 0xb0; ++i) {
1571
+ this.memLook[i] = this.memLook[256 + i] = offset;
1572
+ }
1573
+ // ROM banks are read-only, RAM banks are writable
1574
+ const stat = this.branquartRam[bank] ? 1 : 2;
1575
+ for (let i = 0xa0; i < 0xb0; ++i) {
1576
+ this.memStat[i] = this.memStat[256 + i] = stat;
1577
+ }
1578
+ }
1579
+
1580
+ setupMemoryMap() {
1581
+ this.memStatOffsetByIFetchBank = 0; // no shadow RAM on Atom
1582
+
1583
+ // 0x0000-0x9FFF: RAM (including video RAM at 0x8000)
1584
+ for (let i = 0; i < 0xa0; ++i) {
1585
+ this.memStat[i] = this.memStat[256 + i] = 1;
1586
+ this.memLook[i] = this.memLook[256 + i] = 0;
1587
+ }
1588
+
1589
+ // 0xA000-0xAFFF: Branquart banked region (set up via selectBranquartBank)
1590
+ this.branquartLatch = 0;
1591
+ this.selectBranquartBank(0);
1592
+
1593
+ // 0xB000-0xBFFF: Device I/O (PPIA, MMC, VIA, bank latch)
1594
+ for (let i = 0xb0; i < 0xc0; ++i) {
1595
+ this.memStat[i] = this.memStat[256 + i] = 0;
1596
+ }
1597
+
1598
+ // 0xC000-0xEFFF: ROM (4KB blocks loaded into romOffset area)
1599
+ for (let i = 0xc0; i < 0xf0; ++i) {
1600
+ this.memStat[i] = this.memStat[256 + i] = 2;
1601
+ this.memLook[i] = this.memLook[256 + i] = this.romOffset - 0xa000;
1602
+ }
1603
+
1604
+ // 0xF000-0xFFFF: OS kernel ROM
1605
+ for (let i = 0xf0; i < 0x100; ++i) {
1606
+ this.memStat[i] = this.memStat[256 + i] = 2;
1607
+ this.memLook[i] = this.memLook[256 + i] = this.osOffset - 0xf000;
1608
+ }
1609
+
1610
+ // FDC at 0x0A00 (device hole in RAM, if model uses FDC)
1611
+ if (this.model.Fdc) {
1612
+ this.memStat[0x0a] = this.memStat[256 + 0x0a] = 0;
1613
+ }
1614
+
1615
+ // Randomise video RAM and seed bytes
1616
+ for (let i = 8; i < 13; ++i) this.ramRomOs[i] = (256 * Math.random()) | 0;
1617
+ for (let i = 0x8000; i < 0x9000; ++i) this.ramRomOs[i] = (256 * Math.random()) | 0;
1618
+
1619
+ this.videoDisplayPage = 0;
1620
+ }
1621
+
1622
+ resetPeripherals() {
1623
+ super.resetPeripherals();
1624
+ this.atomppia.reset();
1625
+ this.atommc.reset(true);
1626
+ }
1627
+
1628
+ readDevice(addr) {
1629
+ addr &= 0xffff;
1630
+ switch (addr & ~0x0003) {
1631
+ case 0x0a00:
1632
+ case 0x0a04:
1633
+ return this.fdc.read(addr);
1634
+ case 0xb000:
1635
+ case 0xb004:
1636
+ return this.atomppia.read(addr);
1637
+ case 0xb008:
1638
+ case 0xb00c:
1639
+ return 0x00;
1640
+ case 0xb400:
1641
+ case 0xb404:
1642
+ case 0xb408:
1643
+ case 0xb40c:
1644
+ return this.atommc.read(addr);
1645
+ case 0xb800:
1646
+ case 0xb804:
1647
+ case 0xb808:
1648
+ case 0xb80c:
1649
+ return this.uservia.read(addr);
1650
+ }
1651
+ return addr >>> 8; // open bus
1652
+ }
1653
+
1654
+ writeDevice(addr, b) {
1655
+ addr &= 0xffff;
1656
+ b |= 0;
1657
+ switch (addr & ~0x0003) {
1658
+ case 0x0a00:
1659
+ case 0x0a04:
1660
+ return this.fdc.write(addr, b);
1661
+ case 0xb000:
1662
+ case 0xb004:
1663
+ case 0xb008:
1664
+ case 0xb00c:
1665
+ return this.atomppia.write(addr, b);
1666
+ case 0xb400:
1667
+ case 0xb404:
1668
+ case 0xb408:
1669
+ case 0xb40c:
1670
+ return this.atommc.write(addr, b);
1671
+ case 0xb800:
1672
+ case 0xb804:
1673
+ case 0xb808:
1674
+ case 0xb80c:
1675
+ return this.uservia.write(addr, b);
1676
+ case 0xbffc:
1677
+ if (addr === 0xbfff) {
1678
+ this.branquartLatch = b & 0xff;
1679
+ this.selectBranquartBank(b & 0x0f);
1680
+ }
1681
+ return;
1682
+ }
1683
+ }
1684
+
1685
+ writemem(addr, b) {
1686
+ addr &= 0xffff;
1687
+ b |= 0;
1688
+ if (this._debugWrite) this._debugWrite(addr, b);
1689
+ const statOffset = this.memStatOffset + (addr >>> 8);
1690
+ const stat = this.memStat[statOffset];
1691
+ if (stat === 1) {
1692
+ // Notify VDG of CPU address bus activity for snow effect
1693
+ if (this.video.video6847) {
1694
+ this.video.video6847.cpuAddrAccess(addr);
1695
+ }
1696
+ const offset = this.memLook[statOffset];
1697
+ this.ramRomOs[offset + addr] = b;
1698
+ return;
1699
+ }
1700
+ if (stat === 2) return; // ROM: write ignored
1701
+ // Device page
1702
+ this.writeDevice(addr, b);
1703
+ }
1704
+
1705
+ // Atom ROMs are 4KB, not 16KB like BBC.
1706
+ // OS layout: osOffset holds 4KB kernel at 0xF000.
1707
+ // Extra ROMs (BASIC, FP, DOS) are 4KB blocks at romOffset+0..romOffset+0x5000
1708
+ // mapped at 0xC000, 0xD000, 0xE000 etc.
1709
+ async loadOs(os) {
1710
+ const extraRoms = Array.prototype.slice.call(arguments, 1).concat(this.config.extraRoms);
1711
+ const bankRoms = this.model.banks || [];
1712
+ os = "roms/" + os;
1713
+ console.log(`Loading Atom OS from ${os}`);
1714
+ const data = await utils.loadData(os);
1715
+ const len = data.length;
1716
+
1717
+ if (len < AtomRomBlockSize || len % AtomRomBlockSize !== 0) {
1718
+ throw new Error(`Broken Atom ROM file (length=${len})`);
1719
+ }
1720
+
1721
+ // Clear the OS area and load the kernel at osOffset (mapped to 0xF000)
1722
+ this.ramRomOs.fill(0x00, this.osOffset, this.osOffset + 0x4000);
1723
+ this.ramRomOs.set(data.subarray(0, AtomRomBlockSize), this.osOffset);
1724
+
1725
+ // Load extra ROMs (BASIC, FP, DOS) into 4KB blocks.
1726
+ // romIndex counts down from 5: slot 4 maps to 0xE000 (romOffset+0x4000),
1727
+ // slot 3 to 0xD000 (romOffset+0x3000), slot 2 to 0xC000 (romOffset+0x2000).
1728
+ let romIndex = 5;
1729
+ const awaiting = [];
1730
+ for (const rom of extraRoms) {
1731
+ romIndex--;
1732
+ if (romIndex < 2)
1733
+ throw new Error("Too many extra ROMs for Atom (max 3 addressable slots at 0xC000-0xEFFF)");
1734
+ if (rom !== "") {
1735
+ awaiting.push(this.loadRom(rom, this.romOffset + romIndex * AtomRomBlockSize));
1736
+ }
1737
+ }
1738
+
1739
+ // Load Branquart bank ROMs (max 16 banks)
1740
+ const numBanks = Math.min(bankRoms.length, NumBranquartBanks);
1741
+ for (let bankIndex = 0; bankIndex < numBanks; bankIndex++) {
1742
+ if (bankRoms[bankIndex] !== "") {
1743
+ awaiting.push(this.loadRom(bankRoms[bankIndex], BranquartOffset + bankIndex * BranquartBankSize));
1744
+ this.branquartRam[bankIndex] = false; // mark as ROM
1745
+ }
1746
+ }
1747
+
1748
+ return await Promise.all(awaiting);
1749
+ }
1750
+
1751
+ // Override loadRom to accept 4KB ROMs
1752
+ async loadRom(name, offset) {
1753
+ const data = await utils.loadData("roms/" + name);
1754
+ const len = data.length;
1755
+ if (len !== 16384 && len !== 8192 && len !== 4096) {
1756
+ throw new Error(`Broken ROM file ${name} (length=${len})`);
1757
+ }
1758
+ this.ramRomOs.set(data, offset);
1759
+ }
1760
+
1761
+ // Atom key layout goes through PPIA, not SysVia
1762
+ updateKeyLayout() {
1763
+ this.atomppia.setKeyLayout(this.config.keyLayout);
1764
+ }
1765
+
1766
+ snapshotState(options) {
1767
+ const state = super.snapshotState(options);
1768
+ // Atom-specific state
1769
+ state.branquartLatch = this.branquartLatch;
1770
+ state.branquartRam = [...this.branquartRam];
1771
+ // Save Branquart bank contents
1772
+ state.branquartBanks = this.ramRomOs.slice(
1773
+ BranquartOffset,
1774
+ BranquartOffset + NumBranquartBanks * BranquartBankSize,
1775
+ );
1776
+ state.atomppia = this.atomppia.snapshotState();
1777
+ return state;
1778
+ }
1779
+
1780
+ // No-op: the Atom doesn't have BBC sideways ROM banking.
1781
+ // This prevents super.restoreState() from corrupting the Atom memory map.
1782
+ romSelect() {}
1783
+
1784
+ restoreState(state) {
1785
+ super.restoreState(state);
1786
+
1787
+ // Restore Atom-specific state after the parent has handled
1788
+ // CPU registers, memory, cycle tracking, and sub-components.
1789
+ if (state.branquartRam) this.branquartRam = [...state.branquartRam];
1790
+ if (state.branquartBanks) {
1791
+ this.ramRomOs.set(state.branquartBanks, BranquartOffset);
1792
+ }
1793
+ this.branquartLatch = state.branquartLatch || 0;
1794
+ this.selectBranquartBank(this.branquartLatch & 0x0f);
1795
+ if (state.atomppia) this.atomppia.restoreState(state.atomppia);
1796
+ }
1797
+ }