jsbeeb 1.5.0 → 1.7.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 +22 -2
- package/package.json +3 -2
- package/src/6502.js +119 -6
- package/src/6502.opcodes.js +21 -8
- package/src/acia.js +42 -0
- package/src/adc.js +22 -0
- package/src/app/app.js +29 -0
- package/src/app/electron.js +10 -1
- package/src/app/preload.js +1 -0
- package/src/bem-snapshot.js +681 -0
- package/src/disc-drive.js +33 -0
- package/src/disc.js +176 -22
- package/src/fdc.js +3 -1
- package/src/intel-fdc.js +65 -0
- package/src/jsbeeb.css +66 -0
- package/src/machine-session.js +91 -0
- package/src/main.js +231 -6
- package/src/rewind-thumbnail.js +118 -0
- package/src/rewind-ui.js +230 -0
- package/src/rewind.js +84 -0
- package/src/scheduler.js +28 -0
- package/src/snapshot.js +143 -0
- package/src/soundchip.js +40 -5
- package/src/state-utils.js +66 -0
- package/src/teletext.js +69 -0
- package/src/via.js +91 -0
- package/src/video.js +143 -0
- package/src/wd-fdc.js +99 -2
- package/tests/test-machine.js +134 -146
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ and a 128K BBC Master, along with a number of different peripherals.
|
|
|
10
10
|
## Table of Contents
|
|
11
11
|
|
|
12
12
|
- [Keyboard Mappings](#keyboard-mappings)
|
|
13
|
+
- [Emulator Shortcuts](#emulator-shortcuts)
|
|
14
|
+
- [Save State and Rewind](#save-state-and-rewind)
|
|
13
15
|
- [Getting Set Up to Run Locally](#getting-set-up-to-run-locally)
|
|
14
16
|
- [Running as a Desktop Application](#running-as-a-desktop-application)
|
|
15
17
|
- [URL Parameters](#url-parameters)
|
|
@@ -33,6 +35,26 @@ The BBC had a somewhat different-looking keyboard to a modern PC, and so it's us
|
|
|
33
35
|
To play right now, visit [https://bbc.xania.org/](https://bbc.xania.org/). To load the default disc image (Elite in this
|
|
34
36
|
case), press shift-F12 (which is shift-Break on the BBC).
|
|
35
37
|
|
|
38
|
+
### Emulator Shortcuts
|
|
39
|
+
|
|
40
|
+
| Shortcut | Action |
|
|
41
|
+
| -------------- | ------------------------------- |
|
|
42
|
+
| `Ctrl+Home` | Stop and enter debugger |
|
|
43
|
+
| `Ctrl+Insert` | Toggle turbo (fast-as-possible) |
|
|
44
|
+
| `Ctrl+End` | Pause emulation |
|
|
45
|
+
| `Alt+PageDown` | Open rewind scrubber |
|
|
46
|
+
|
|
47
|
+
### Save State and Rewind
|
|
48
|
+
|
|
49
|
+
Save and load full emulator state snapshots from the **State** menu (or `Ctrl+S` / `Ctrl+O` in the Electron app).
|
|
50
|
+
|
|
51
|
+
The emulator continuously captures snapshots into a 30-slot rewind buffer (~1 per second). Open the rewind scrubber from **State > Rewind** or press **Alt+PageDown** to browse recent states as a visual filmstrip:
|
|
52
|
+
|
|
53
|
+
- **Left/Right arrows** — navigate between snapshots (the main screen updates live)
|
|
54
|
+
- **Click** a thumbnail to jump to that point
|
|
55
|
+
- **Enter** — commit selection and close the panel
|
|
56
|
+
- **Escape** — cancel and restore the original state
|
|
57
|
+
|
|
36
58
|
### Joystick Support
|
|
37
59
|
|
|
38
60
|
jsbeeb supports both USB/Bluetooth gamepads and mouse-based analogue joystick emulation. Note that BBC Micro joysticks use inverted axes:
|
|
@@ -184,8 +206,6 @@ If you're looking to help:
|
|
|
184
206
|
- Play lots of games and report issues either on [GitHub](https://github.com/mattgodbolt/jsbeeb/issues) or by email (
|
|
185
207
|
matt@godbolt.org).
|
|
186
208
|
- Core
|
|
187
|
-
- Save state ability
|
|
188
|
-
- Once we have this I'd love to get some "reverse step" debugging support
|
|
189
209
|
- Get the "boo" of the boot "boo-beep" working (disabled currently as the JavaScript startup makes the sound
|
|
190
210
|
dreadfully choppy on Chrome at least).
|
|
191
211
|
- Save disc support
|
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.
|
|
10
|
+
"version": "1.7.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"
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"fflate": "^0.8.2",
|
|
35
35
|
"jquery": "^4.0.0",
|
|
36
36
|
"pako": "^2.1.0",
|
|
37
|
-
"smoothie": "^1.36.1",
|
|
38
37
|
"sharp": "^0.34.5",
|
|
38
|
+
"smoothie": "^1.36.1",
|
|
39
39
|
"underscore": "^1.13.7"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"eslint-plugin-prettier": "^5.5.5",
|
|
47
47
|
"globals": "^17.3.0",
|
|
48
48
|
"husky": "^9.1.7",
|
|
49
|
+
"jsdom": "^29.0.0",
|
|
49
50
|
"lint-staged": "^16.2.7",
|
|
50
51
|
"npm-run-all2": "^8.0.4",
|
|
51
52
|
"pixelmatch": "^7.1.0",
|
package/src/6502.js
CHANGED
|
@@ -616,6 +616,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
616
616
|
this.videoCycles = 0;
|
|
617
617
|
|
|
618
618
|
this.polltime = this.buildPolltime();
|
|
619
|
+
this.is1MHzAccess = this.buildIs1MHzAccess();
|
|
619
620
|
|
|
620
621
|
this._debugRead = this._debugWrite = this._debugInstruction = null;
|
|
621
622
|
this.debugInstruction = new DebugHook(this, "_debugInstruction");
|
|
@@ -1130,6 +1131,96 @@ export class Cpu6502 extends Base6502 {
|
|
|
1130
1131
|
this.resetLine = !resetOn;
|
|
1131
1132
|
}
|
|
1132
1133
|
|
|
1134
|
+
snapshotState() {
|
|
1135
|
+
return {
|
|
1136
|
+
// CPU registers
|
|
1137
|
+
a: this.a,
|
|
1138
|
+
x: this.x,
|
|
1139
|
+
y: this.y,
|
|
1140
|
+
s: this.s,
|
|
1141
|
+
pc: this.pc,
|
|
1142
|
+
p: this.p.asByte(),
|
|
1143
|
+
nmiLevel: this._nmiLevel,
|
|
1144
|
+
nmiEdge: this._nmiEdge,
|
|
1145
|
+
halted: this.halted,
|
|
1146
|
+
takeInt: this.takeInt,
|
|
1147
|
+
// Memory control
|
|
1148
|
+
romsel: this.romsel,
|
|
1149
|
+
acccon: this.acccon,
|
|
1150
|
+
videoDisplayPage: this.videoDisplayPage,
|
|
1151
|
+
// Cycle tracking
|
|
1152
|
+
currentCycles: this.currentCycles,
|
|
1153
|
+
targetCycles: this.targetCycles,
|
|
1154
|
+
cycleSeconds: this.cycleSeconds,
|
|
1155
|
+
peripheralCycles: this.peripheralCycles,
|
|
1156
|
+
videoCycles: this.videoCycles,
|
|
1157
|
+
music5000PageSel: this.music5000PageSel,
|
|
1158
|
+
// RAM only (ROMs don't change at runtime)
|
|
1159
|
+
ram: this.ramRomOs.slice(0, this.romOffset),
|
|
1160
|
+
// Sub-component state
|
|
1161
|
+
scheduler: this.scheduler.snapshotState(),
|
|
1162
|
+
sysvia: this.sysvia.snapshotState(),
|
|
1163
|
+
uservia: this.uservia.snapshotState(),
|
|
1164
|
+
video: this.video.snapshotState(),
|
|
1165
|
+
soundChip: this.soundChip.snapshotState(),
|
|
1166
|
+
acia: this.acia.snapshotState(),
|
|
1167
|
+
adc: this.adconverter.snapshotState(),
|
|
1168
|
+
fdc: this.fdc.snapshotState(),
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
restoreState(state) {
|
|
1173
|
+
// 1. Scheduler epoch first (so task offsets resolve correctly)
|
|
1174
|
+
this.scheduler.restoreState(state.scheduler);
|
|
1175
|
+
|
|
1176
|
+
// 2. CPU registers
|
|
1177
|
+
this.a = state.a;
|
|
1178
|
+
this.x = state.x;
|
|
1179
|
+
this.y = state.y;
|
|
1180
|
+
this.s = state.s;
|
|
1181
|
+
this.pc = state.pc;
|
|
1182
|
+
this.p.setFromByte(state.p);
|
|
1183
|
+
// interrupt is rebuilt by sub-component restores (VIA updateIFR, ACIA updateIrq)
|
|
1184
|
+
this.interrupt = 0;
|
|
1185
|
+
this._nmiLevel = state.nmiLevel;
|
|
1186
|
+
this._nmiEdge = state.nmiEdge;
|
|
1187
|
+
this.halted = state.halted;
|
|
1188
|
+
this.takeInt = state.takeInt;
|
|
1189
|
+
|
|
1190
|
+
// 3. Memory
|
|
1191
|
+
this.ramRomOs.set(state.ram);
|
|
1192
|
+
// Load ROMs if present (e.g. from BEM snapshot import)
|
|
1193
|
+
if (state.roms) {
|
|
1194
|
+
this.ramRomOs.set(state.roms.slice(0, 16 * 16384), this.romOffset);
|
|
1195
|
+
}
|
|
1196
|
+
this.videoDisplayPage = state.videoDisplayPage;
|
|
1197
|
+
this.music5000PageSel = state.music5000PageSel;
|
|
1198
|
+
|
|
1199
|
+
// 4. Rebuild memStat/memLook from romsel/acccon
|
|
1200
|
+
this.romSelect(state.romsel);
|
|
1201
|
+
if (this.model.isMaster) this.writeAcccon(state.acccon);
|
|
1202
|
+
|
|
1203
|
+
// 5. Cycle tracking
|
|
1204
|
+
this.currentCycles = state.currentCycles;
|
|
1205
|
+
this.targetCycles = state.targetCycles;
|
|
1206
|
+
this.cycleSeconds = state.cycleSeconds;
|
|
1207
|
+
this.peripheralCycles = state.peripheralCycles;
|
|
1208
|
+
this.videoCycles = state.videoCycles;
|
|
1209
|
+
|
|
1210
|
+
// 6. Sub-components (these re-register scheduler tasks)
|
|
1211
|
+
this.sysvia.restoreState(state.sysvia);
|
|
1212
|
+
this.uservia.restoreState(state.uservia);
|
|
1213
|
+
this.video.restoreState(state.video);
|
|
1214
|
+
this.soundChip.restoreState(state.soundChip);
|
|
1215
|
+
this.acia.restoreState(state.acia);
|
|
1216
|
+
this.adconverter.restoreState(state.adc);
|
|
1217
|
+
|
|
1218
|
+
// FDC state (v2+). If absent (v1 snapshot), FDC keeps its current state.
|
|
1219
|
+
if (state.fdc) {
|
|
1220
|
+
this.fdc.restoreState(state.fdc);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1133
1224
|
reset(hard) {
|
|
1134
1225
|
if (hard) {
|
|
1135
1226
|
// On the Master, opcodes executing from 0xc000 - 0xdfff can optionally have their memory accesses
|
|
@@ -1191,6 +1282,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
1191
1282
|
this._nmiEdge = false;
|
|
1192
1283
|
this._nmiLevel = false;
|
|
1193
1284
|
this.halted = false;
|
|
1285
|
+
this.breakpointResume = false;
|
|
1194
1286
|
this.music5000PageSel = 0;
|
|
1195
1287
|
this.video.reset(this, this.sysvia, hard);
|
|
1196
1288
|
this.soundChip.reset(hard);
|
|
@@ -1206,15 +1298,30 @@ export class Cpu6502 extends Base6502 {
|
|
|
1206
1298
|
this.sysvia.setKeyLayout(this.config.keyLayout);
|
|
1207
1299
|
}
|
|
1208
1300
|
|
|
1209
|
-
polltimeAddr(cycles, addr) {
|
|
1301
|
+
polltimeAddr(cycles, addr, isWrite) {
|
|
1210
1302
|
cycles = cycles | 0;
|
|
1211
|
-
if (is1MHzAccess(addr)) {
|
|
1303
|
+
if (this.is1MHzAccess(addr, isWrite)) {
|
|
1212
1304
|
cycles += 1 + ((cycles ^ this.currentCycles) & 1);
|
|
1213
1305
|
}
|
|
1214
1306
|
this.polltime(cycles);
|
|
1215
1307
|
}
|
|
1216
1308
|
|
|
1217
|
-
//
|
|
1309
|
+
// Bake in the 1MHz bus check at construction time. On the Master, ACCCON
|
|
1310
|
+
// TST (bit 6) remaps FRED, JIM, and SHEILA reads (&FC00-&FEFF) to the
|
|
1311
|
+
// internal 2MHz bus, but writes still go through the external 1MHz bus.
|
|
1312
|
+
// Non-Master machines just use the static address check with no ACCCON
|
|
1313
|
+
// awareness.
|
|
1314
|
+
buildIs1MHzAccess() {
|
|
1315
|
+
if (this.model.isMaster) {
|
|
1316
|
+
return (addr, isWrite) => {
|
|
1317
|
+
if (!isWrite && this.acccon & 0x40 && addr >= 0xfc00 && addr < 0xff00) return false;
|
|
1318
|
+
return is1MHzAccess(addr);
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
return is1MHzAccess;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Builds common code between polltimeSlow and polltimeFast
|
|
1218
1325
|
buildPolltime() {
|
|
1219
1326
|
const nop = (_cycles) => {};
|
|
1220
1327
|
const tubeStuff = (cycles) => (this.model.tube ? this.tube.execute(cycles) : nop);
|
|
@@ -1289,16 +1396,22 @@ export class Cpu6502 extends Base6502 {
|
|
|
1289
1396
|
}
|
|
1290
1397
|
|
|
1291
1398
|
executeInternal() {
|
|
1292
|
-
|
|
1399
|
+
// Skip the debug-instruction check on the very first instruction only
|
|
1400
|
+
// when resuming from a previous breakpoint stop (so we don't immediately
|
|
1401
|
+
// re-trigger the same breakpoint). Previously this skipped the first
|
|
1402
|
+
// instruction of every execute() chunk, which could miss breakpoints.
|
|
1403
|
+
let skipNext = this.breakpointResume;
|
|
1404
|
+
this.breakpointResume = false;
|
|
1293
1405
|
while (!this.halted && this.currentCycles < this.targetCycles) {
|
|
1294
1406
|
this.oldPcIndex = (this.oldPcIndex + 1) & 0xff;
|
|
1295
1407
|
this.oldPcArray[this.oldPcIndex] = this.pc;
|
|
1296
1408
|
this.memStatOffset = this.memStatOffsetByIFetchBank & (1 << (this.pc >>> 12)) ? 256 : 0;
|
|
1297
1409
|
const opcode = this.readmem(this.pc);
|
|
1298
|
-
if (this._debugInstruction && !
|
|
1410
|
+
if (this._debugInstruction && !skipNext && this._debugInstruction(this.pc, opcode)) {
|
|
1411
|
+
this.breakpointResume = true;
|
|
1299
1412
|
return false;
|
|
1300
1413
|
}
|
|
1301
|
-
|
|
1414
|
+
skipNext = false;
|
|
1302
1415
|
this.incpc();
|
|
1303
1416
|
this.runner.run(opcode);
|
|
1304
1417
|
this.oldAArray[this.oldPcIndex] = this.a;
|
package/src/6502.opcodes.js
CHANGED
|
@@ -88,6 +88,7 @@ class InstructionGen {
|
|
|
88
88
|
let op = `cpu.writemem(${addr}, ${reg});`;
|
|
89
89
|
if (spurious) op += " // spurious";
|
|
90
90
|
this.append(this.cycle, op, true, addr);
|
|
91
|
+
this.ops[this.cycle].isWrite = true;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
zpReadOp(addr, reg) {
|
|
@@ -129,7 +130,8 @@ class InstructionGen {
|
|
|
129
130
|
}
|
|
130
131
|
if (this.cycleAccurate && toSkip && this.ops[i].exact) {
|
|
131
132
|
if (this.ops[i].addr) {
|
|
132
|
-
|
|
133
|
+
const isWrite = this.ops[i].isWrite ? "true" : "false";
|
|
134
|
+
out.push(`cpu.polltimeAddr(${toSkip}, ${this.ops[i].addr}, ${isWrite});`);
|
|
133
135
|
} else {
|
|
134
136
|
out.push(`cpu.polltime(${toSkip});`);
|
|
135
137
|
}
|
|
@@ -140,7 +142,8 @@ class InstructionGen {
|
|
|
140
142
|
}
|
|
141
143
|
if (toSkip) {
|
|
142
144
|
if (this.cycleAccurate && this.ops[this.cycle] && this.ops[this.cycle].addr) {
|
|
143
|
-
|
|
145
|
+
const isWrite = this.ops[this.cycle].isWrite ? "true" : "false";
|
|
146
|
+
out.push(`cpu.polltimeAddr(${toSkip}, ${this.ops[this.cycle].addr}, ${isWrite});`);
|
|
144
147
|
} else {
|
|
145
148
|
out.push(`cpu.polltime(${toSkip});`);
|
|
146
149
|
}
|
|
@@ -1151,7 +1154,9 @@ function makeCpuFunctions(cpu, opcodes, is65c12, cycleAccurate = true) {
|
|
|
1151
1154
|
// so I read the non-carry here.
|
|
1152
1155
|
if (!op.rotate) ig.ifFalse.readOp("addrNonCarry", "REG");
|
|
1153
1156
|
ig.readOp("addrWithCarry", "REG");
|
|
1154
|
-
|
|
1157
|
+
// 65c12 does "two reads and one write" for RMW (not the 6502's
|
|
1158
|
+
// "one read and two writes"). The spurious cycle is a read.
|
|
1159
|
+
ig.readOp("addrWithCarry", "", true);
|
|
1155
1160
|
} else {
|
|
1156
1161
|
// For RMW we always have a spurious read and then a spurious read or write
|
|
1157
1162
|
ig.readOp("addrNonCarry");
|
|
@@ -1236,11 +1241,19 @@ function makeCpuFunctions(cpu, opcodes, is65c12, cycleAccurate = true) {
|
|
|
1236
1241
|
ig.readOp("addrWithCarry", "REG");
|
|
1237
1242
|
ig.spuriousOp("addrWithCarry", "REG");
|
|
1238
1243
|
} else if (op.write) {
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
//
|
|
1243
|
-
|
|
1244
|
+
if (is65c12) {
|
|
1245
|
+
// On the 65c12, the dead cycle reads the Previous Bus Address (PBA)
|
|
1246
|
+
// rather than the partially-formed or target address. Since PBA is
|
|
1247
|
+
// always a zero-page or instruction-fetch address (2MHz), it never
|
|
1248
|
+
// triggers 1MHz bus stretching. We model this as tick(1).
|
|
1249
|
+
ig.tick(1);
|
|
1250
|
+
} else {
|
|
1251
|
+
// Pure stores still exhibit a read at the non-carried address.
|
|
1252
|
+
ig.readOp("addrNonCarry");
|
|
1253
|
+
if (op.zpQuirk) {
|
|
1254
|
+
// with this quirk on undocumented instructions, a page crossing writes to 00XX
|
|
1255
|
+
ig.append("if (addrWithCarry !== addrNonCarry) addrWithCarry &= 0xff;");
|
|
1256
|
+
}
|
|
1244
1257
|
}
|
|
1245
1258
|
}
|
|
1246
1259
|
ig.append(op.op);
|
package/src/acia.js
CHANGED
|
@@ -197,6 +197,48 @@ export class Acia {
|
|
|
197
197
|
this.updateIrq();
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
snapshotState() {
|
|
201
|
+
const scheduler = this.txCompleteTask.scheduler;
|
|
202
|
+
return {
|
|
203
|
+
sr: this.sr,
|
|
204
|
+
cr: this.cr,
|
|
205
|
+
dr: this.dr,
|
|
206
|
+
rs423Selected: this.rs423Selected,
|
|
207
|
+
motorOn: this.motorOn,
|
|
208
|
+
tapeCarrierCount: this.tapeCarrierCount,
|
|
209
|
+
tapeDcdLineLevel: this.tapeDcdLineLevel,
|
|
210
|
+
hadDcdHigh: this.hadDcdHigh,
|
|
211
|
+
serialReceiveRate: this.serialReceiveRate,
|
|
212
|
+
serialReceiveCyclesPerByte: this.serialReceiveCyclesPerByte,
|
|
213
|
+
txCompleteTaskOffset: this.txCompleteTask.scheduled()
|
|
214
|
+
? this.txCompleteTask.expireEpoch - scheduler.epoch
|
|
215
|
+
: null,
|
|
216
|
+
runTapeTaskOffset: this.runTapeTask.scheduled() ? this.runTapeTask.expireEpoch - scheduler.epoch : null,
|
|
217
|
+
runRs423TaskOffset: this.runRs423Task.scheduled() ? this.runRs423Task.expireEpoch - scheduler.epoch : null,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
restoreState(state) {
|
|
222
|
+
this.sr = state.sr;
|
|
223
|
+
this.cr = state.cr;
|
|
224
|
+
this.dr = state.dr;
|
|
225
|
+
this.rs423Selected = state.rs423Selected;
|
|
226
|
+
this.motorOn = state.motorOn;
|
|
227
|
+
this.tapeCarrierCount = state.tapeCarrierCount;
|
|
228
|
+
this.tapeDcdLineLevel = state.tapeDcdLineLevel;
|
|
229
|
+
this.hadDcdHigh = state.hadDcdHigh;
|
|
230
|
+
this.serialReceiveRate = state.serialReceiveRate;
|
|
231
|
+
this.serialReceiveCyclesPerByte = state.serialReceiveCyclesPerByte;
|
|
232
|
+
this.updateIrq();
|
|
233
|
+
|
|
234
|
+
this.txCompleteTask.cancel();
|
|
235
|
+
this.runTapeTask.cancel();
|
|
236
|
+
this.runRs423Task.cancel();
|
|
237
|
+
if (state.txCompleteTaskOffset !== null) this.txCompleteTask.schedule(state.txCompleteTaskOffset);
|
|
238
|
+
if (state.runTapeTaskOffset !== null) this.runTapeTask.schedule(state.runTapeTaskOffset);
|
|
239
|
+
if (state.runRs423TaskOffset !== null) this.runRs423Task.schedule(state.runRs423TaskOffset);
|
|
240
|
+
}
|
|
241
|
+
|
|
200
242
|
setTape(tape) {
|
|
201
243
|
this.tape = tape;
|
|
202
244
|
}
|
package/src/adc.js
CHANGED
|
@@ -99,6 +99,28 @@ export class Adc {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
snapshotState() {
|
|
103
|
+
const scheduler = this.task.scheduler;
|
|
104
|
+
return {
|
|
105
|
+
status: this.status,
|
|
106
|
+
low: this.low,
|
|
107
|
+
high: this.high,
|
|
108
|
+
taskOffset: this.task.scheduled() ? this.task.expireEpoch - scheduler.epoch : null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
restoreState(state) {
|
|
113
|
+
this.status = state.status;
|
|
114
|
+
this.low = state.low;
|
|
115
|
+
this.high = state.high;
|
|
116
|
+
this.task.cancel();
|
|
117
|
+
if (state.taskOffset !== null) {
|
|
118
|
+
this.task.schedule(state.taskOffset);
|
|
119
|
+
// Conversion in progress: CB1 should be high (set by write(), cleared by onComplete())
|
|
120
|
+
this.sysvia.setcb1(true);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
102
124
|
/**
|
|
103
125
|
* Read from the ADC registers
|
|
104
126
|
* @param {number} addr - The address to read from
|
package/src/app/app.js
CHANGED
|
@@ -230,6 +230,35 @@ const template = [
|
|
|
230
230
|
click: showModal("configuration"),
|
|
231
231
|
},
|
|
232
232
|
{ type: "separator" },
|
|
233
|
+
{
|
|
234
|
+
label: "Save State...",
|
|
235
|
+
accelerator: "CmdOrCtrl+S",
|
|
236
|
+
click: sendAction("save-state"),
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
label: "Load State...",
|
|
240
|
+
accelerator: "CmdOrCtrl+O",
|
|
241
|
+
click: async (_, browserWindow) => {
|
|
242
|
+
const result = await dialog.showOpenDialog(browserWindow, {
|
|
243
|
+
title: "Load emulator state",
|
|
244
|
+
filters: [
|
|
245
|
+
{ name: "Snapshot files", extensions: ["gz", "json", "snp"] },
|
|
246
|
+
{ name: "All files", extensions: ["*"] },
|
|
247
|
+
],
|
|
248
|
+
properties: ["openFile"],
|
|
249
|
+
});
|
|
250
|
+
if (!result.canceled) {
|
|
251
|
+
const filePath = getFileParam(result.filePaths[0]);
|
|
252
|
+
browserWindow.webContents.send("load-state", { path: filePath });
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{ type: "separator" },
|
|
257
|
+
{
|
|
258
|
+
label: "Rewind...",
|
|
259
|
+
click: sendAction("rewind"),
|
|
260
|
+
},
|
|
261
|
+
{ type: "separator" },
|
|
233
262
|
{
|
|
234
263
|
label: "Soft Reset",
|
|
235
264
|
click: sendAction("soft-reset"),
|
package/src/app/electron.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export let initialise = function () {};
|
|
7
7
|
|
|
8
8
|
function init(args) {
|
|
9
|
-
const { loadDiscImage, loadTapeImage, processor, modals, actions, config } = args;
|
|
9
|
+
const { loadDiscImage, loadTapeImage, loadStateFile, processor, modals, actions, config } = args;
|
|
10
10
|
const api = window.electronAPI;
|
|
11
11
|
|
|
12
12
|
api.onLoadDisc(async (message) => {
|
|
@@ -34,6 +34,15 @@ function init(args) {
|
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
api.onLoadState(async (message) => {
|
|
38
|
+
if (loadStateFile) {
|
|
39
|
+
const response = await fetch(message.path);
|
|
40
|
+
const blob = await response.blob();
|
|
41
|
+
const file = new File([blob], message.path.split("/").pop());
|
|
42
|
+
await loadStateFile(file);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
37
46
|
// Observe model name changes and update window title
|
|
38
47
|
const modelElement = document.querySelector(".bbc-model");
|
|
39
48
|
if (modelElement) {
|
package/src/app/preload.js
CHANGED
|
@@ -7,6 +7,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|
|
7
7
|
onLoadTape: (callback) => ipcRenderer.on("load-tape", (event, message) => callback(message)),
|
|
8
8
|
onShowModal: (callback) => ipcRenderer.on("show-modal", (event, message) => callback(message)),
|
|
9
9
|
onAction: (callback) => ipcRenderer.on("action", (event, message) => callback(message)),
|
|
10
|
+
onLoadState: (callback) => ipcRenderer.on("load-state", (event, message) => callback(message)),
|
|
10
11
|
setTitle: (title) => ipcRenderer.send("set-title", title),
|
|
11
12
|
saveSettings: (settings) => ipcRenderer.send("save-settings", settings),
|
|
12
13
|
});
|