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/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.6.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"
|
package/src/6502.js
CHANGED
|
@@ -101,12 +101,12 @@ class Flags {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
class Base6502 {
|
|
104
|
-
constructor(model) {
|
|
104
|
+
constructor(model, { cycleAccurate = true } = {}) {
|
|
105
105
|
this.model = model;
|
|
106
106
|
this.a = this.x = this.y = this.s = 0;
|
|
107
107
|
this.p = new Flags();
|
|
108
108
|
this.pc = 0;
|
|
109
|
-
this.opcodes = model.opcodesFactory(this);
|
|
109
|
+
this.opcodes = model.opcodesFactory(this, { cycleAccurate });
|
|
110
110
|
this.disassembler = this.opcodes.disassembler;
|
|
111
111
|
this.forceTracing = false;
|
|
112
112
|
this.runner = this.opcodes.runInstruction;
|
|
@@ -405,9 +405,10 @@ class Base6502 {
|
|
|
405
405
|
|
|
406
406
|
class Tube6502 extends Base6502 {
|
|
407
407
|
constructor(model, cpu) {
|
|
408
|
-
super(model);
|
|
408
|
+
super(model, { cycleAccurate: false });
|
|
409
409
|
|
|
410
410
|
this.cycles = 0;
|
|
411
|
+
this.cpuMultiplier = 2;
|
|
411
412
|
this.romPaged = true;
|
|
412
413
|
this.memory = new Uint8Array(65536);
|
|
413
414
|
this.rom = new Uint8Array(4096);
|
|
@@ -468,7 +469,7 @@ class Tube6502 extends Base6502 {
|
|
|
468
469
|
}
|
|
469
470
|
|
|
470
471
|
execute(cycles) {
|
|
471
|
-
this.cycles += cycles *
|
|
472
|
+
this.cycles += (cycles * this.cpuMultiplier) | 0;
|
|
472
473
|
if (this.cycles < 3) return;
|
|
473
474
|
while (this.cycles > 0) {
|
|
474
475
|
const opcode = this.readmem(this.pc);
|
|
@@ -570,19 +571,22 @@ function is1MHzAccess(addr) {
|
|
|
570
571
|
}
|
|
571
572
|
|
|
572
573
|
export class Cpu6502 extends Base6502 {
|
|
573
|
-
constructor(
|
|
574
|
-
|
|
574
|
+
constructor(
|
|
575
|
+
model,
|
|
576
|
+
{ dbgr, video, soundChip, ddNoise, music5000, cmos, config, econet, cycleAccurate = true } = {},
|
|
577
|
+
) {
|
|
578
|
+
super(model, { cycleAccurate });
|
|
575
579
|
this.config = fixUpConfig(config);
|
|
576
580
|
this.debugFlags = this.config.debugFlags;
|
|
577
581
|
this.cmos = cmos;
|
|
578
582
|
this.debugger = dbgr;
|
|
579
583
|
|
|
580
|
-
this.video =
|
|
584
|
+
this.video = video;
|
|
581
585
|
this.crtc = this.video.crtc;
|
|
582
586
|
this.ula = this.video.ula;
|
|
583
|
-
this.soundChip =
|
|
584
|
-
this.music5000 =
|
|
585
|
-
this.ddNoise =
|
|
587
|
+
this.soundChip = soundChip;
|
|
588
|
+
this.music5000 = music5000;
|
|
589
|
+
this.ddNoise = ddNoise;
|
|
586
590
|
this.memStatOffsetByIFetchBank = 0;
|
|
587
591
|
this.memStatOffset = 0;
|
|
588
592
|
this.memStat = new Uint8Array(512);
|
|
@@ -602,8 +606,11 @@ export class Cpu6502 extends Base6502 {
|
|
|
602
606
|
this.videoCyclesBatch = this.config.videoCyclesBatch | 0;
|
|
603
607
|
this.peripheralCyclesPerSecond = 2 * 1000 * 1000;
|
|
604
608
|
this.tube = model.tube ? new Tube6502(model.tube, this) : new FakeTube();
|
|
609
|
+
if (model.tube && this.config.tubeCpuMultiplier) {
|
|
610
|
+
this.tube.cpuMultiplier = this.config.tubeCpuMultiplier;
|
|
611
|
+
}
|
|
605
612
|
this.music5000PageSel = 0;
|
|
606
|
-
this.econet =
|
|
613
|
+
this.econet = econet;
|
|
607
614
|
|
|
608
615
|
this.peripheralCycles = 0;
|
|
609
616
|
this.videoCycles = 0;
|
|
@@ -616,16 +623,14 @@ export class Cpu6502 extends Base6502 {
|
|
|
616
623
|
this.debugWrite = new DebugHook(this, "_debugWrite");
|
|
617
624
|
|
|
618
625
|
this.scheduler = new Scheduler();
|
|
619
|
-
this.sysvia = new via.SysVia(
|
|
620
|
-
this,
|
|
621
|
-
this.
|
|
622
|
-
this.
|
|
623
|
-
this.
|
|
624
|
-
this.
|
|
625
|
-
this.
|
|
626
|
-
|
|
627
|
-
this.config.getGamepads,
|
|
628
|
-
);
|
|
626
|
+
this.sysvia = new via.SysVia(this, this.scheduler, {
|
|
627
|
+
video: this.video,
|
|
628
|
+
soundChip: this.soundChip,
|
|
629
|
+
cmos: this.cmos,
|
|
630
|
+
isMaster: this.model.isMaster,
|
|
631
|
+
initialLayout: this.config.keyLayout,
|
|
632
|
+
getGamepads: this.config.getGamepads,
|
|
633
|
+
});
|
|
629
634
|
this.uservia = new via.UserVia(this, this.scheduler, this.model.isMaster, this.config.userPort);
|
|
630
635
|
this.acia = new Acia(this, this.soundChip.toneGenerator, this.scheduler, this.touchScreen);
|
|
631
636
|
this.serial = new Serial(this.acia);
|
|
@@ -1125,6 +1130,90 @@ export class Cpu6502 extends Base6502 {
|
|
|
1125
1130
|
this.resetLine = !resetOn;
|
|
1126
1131
|
}
|
|
1127
1132
|
|
|
1133
|
+
snapshotState() {
|
|
1134
|
+
return {
|
|
1135
|
+
// CPU registers
|
|
1136
|
+
a: this.a,
|
|
1137
|
+
x: this.x,
|
|
1138
|
+
y: this.y,
|
|
1139
|
+
s: this.s,
|
|
1140
|
+
pc: this.pc,
|
|
1141
|
+
p: this.p.asByte(),
|
|
1142
|
+
nmiLevel: this._nmiLevel,
|
|
1143
|
+
nmiEdge: this._nmiEdge,
|
|
1144
|
+
halted: this.halted,
|
|
1145
|
+
takeInt: this.takeInt,
|
|
1146
|
+
// Memory control
|
|
1147
|
+
romsel: this.romsel,
|
|
1148
|
+
acccon: this.acccon,
|
|
1149
|
+
videoDisplayPage: this.videoDisplayPage,
|
|
1150
|
+
// Cycle tracking
|
|
1151
|
+
currentCycles: this.currentCycles,
|
|
1152
|
+
targetCycles: this.targetCycles,
|
|
1153
|
+
cycleSeconds: this.cycleSeconds,
|
|
1154
|
+
peripheralCycles: this.peripheralCycles,
|
|
1155
|
+
videoCycles: this.videoCycles,
|
|
1156
|
+
music5000PageSel: this.music5000PageSel,
|
|
1157
|
+
// RAM only (ROMs don't change at runtime)
|
|
1158
|
+
ram: this.ramRomOs.slice(0, this.romOffset),
|
|
1159
|
+
// Sub-component state
|
|
1160
|
+
scheduler: this.scheduler.snapshotState(),
|
|
1161
|
+
sysvia: this.sysvia.snapshotState(),
|
|
1162
|
+
uservia: this.uservia.snapshotState(),
|
|
1163
|
+
video: this.video.snapshotState(),
|
|
1164
|
+
soundChip: this.soundChip.snapshotState(),
|
|
1165
|
+
acia: this.acia.snapshotState(),
|
|
1166
|
+
adc: this.adconverter.snapshotState(),
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
restoreState(state) {
|
|
1171
|
+
// 1. Scheduler epoch first (so task offsets resolve correctly)
|
|
1172
|
+
this.scheduler.restoreState(state.scheduler);
|
|
1173
|
+
|
|
1174
|
+
// 2. CPU registers
|
|
1175
|
+
this.a = state.a;
|
|
1176
|
+
this.x = state.x;
|
|
1177
|
+
this.y = state.y;
|
|
1178
|
+
this.s = state.s;
|
|
1179
|
+
this.pc = state.pc;
|
|
1180
|
+
this.p.setFromByte(state.p);
|
|
1181
|
+
// interrupt is rebuilt by sub-component restores (VIA updateIFR, ACIA updateIrq)
|
|
1182
|
+
this.interrupt = 0;
|
|
1183
|
+
this._nmiLevel = state.nmiLevel;
|
|
1184
|
+
this._nmiEdge = state.nmiEdge;
|
|
1185
|
+
this.halted = state.halted;
|
|
1186
|
+
this.takeInt = state.takeInt;
|
|
1187
|
+
|
|
1188
|
+
// 3. Memory
|
|
1189
|
+
this.ramRomOs.set(state.ram);
|
|
1190
|
+
// Load ROMs if present (e.g. from BEM snapshot import)
|
|
1191
|
+
if (state.roms) {
|
|
1192
|
+
this.ramRomOs.set(state.roms.slice(0, 16 * 16384), this.romOffset);
|
|
1193
|
+
}
|
|
1194
|
+
this.videoDisplayPage = state.videoDisplayPage;
|
|
1195
|
+
this.music5000PageSel = state.music5000PageSel;
|
|
1196
|
+
|
|
1197
|
+
// 4. Rebuild memStat/memLook from romsel/acccon
|
|
1198
|
+
this.romSelect(state.romsel);
|
|
1199
|
+
if (this.model.isMaster) this.writeAcccon(state.acccon);
|
|
1200
|
+
|
|
1201
|
+
// 5. Cycle tracking
|
|
1202
|
+
this.currentCycles = state.currentCycles;
|
|
1203
|
+
this.targetCycles = state.targetCycles;
|
|
1204
|
+
this.cycleSeconds = state.cycleSeconds;
|
|
1205
|
+
this.peripheralCycles = state.peripheralCycles;
|
|
1206
|
+
this.videoCycles = state.videoCycles;
|
|
1207
|
+
|
|
1208
|
+
// 6. Sub-components (these re-register scheduler tasks)
|
|
1209
|
+
this.sysvia.restoreState(state.sysvia);
|
|
1210
|
+
this.uservia.restoreState(state.uservia);
|
|
1211
|
+
this.video.restoreState(state.video);
|
|
1212
|
+
this.soundChip.restoreState(state.soundChip);
|
|
1213
|
+
this.acia.restoreState(state.acia);
|
|
1214
|
+
this.adconverter.restoreState(state.adc);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1128
1217
|
reset(hard) {
|
|
1129
1218
|
if (hard) {
|
|
1130
1219
|
// On the Master, opcodes executing from 0xc000 - 0xdfff can optionally have their memory accesses
|
package/src/6502.opcodes.js
CHANGED
|
@@ -41,8 +41,9 @@ function push(reg) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
class InstructionGen {
|
|
44
|
-
constructor(is65c12) {
|
|
44
|
+
constructor(is65c12, cycleAccurate = true) {
|
|
45
45
|
this.is65c12 = is65c12;
|
|
46
|
+
this.cycleAccurate = cycleAccurate;
|
|
46
47
|
this.ops = {};
|
|
47
48
|
this.cycle = 0;
|
|
48
49
|
}
|
|
@@ -106,6 +107,10 @@ class InstructionGen {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
spuriousOp(addr, reg) {
|
|
110
|
+
if (!this.cycleAccurate) {
|
|
111
|
+
this.cycle++;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
109
114
|
if (this.is65c12) {
|
|
110
115
|
this.readOp(addr, "", true);
|
|
111
116
|
} else {
|
|
@@ -122,7 +127,7 @@ class InstructionGen {
|
|
|
122
127
|
toSkip++;
|
|
123
128
|
continue;
|
|
124
129
|
}
|
|
125
|
-
if (toSkip && this.ops[i].exact) {
|
|
130
|
+
if (this.cycleAccurate && toSkip && this.ops[i].exact) {
|
|
126
131
|
if (this.ops[i].addr) {
|
|
127
132
|
out.push(`cpu.polltimeAddr(${toSkip}, ${this.ops[i].addr});`);
|
|
128
133
|
} else {
|
|
@@ -134,7 +139,7 @@ class InstructionGen {
|
|
|
134
139
|
toSkip++;
|
|
135
140
|
}
|
|
136
141
|
if (toSkip) {
|
|
137
|
-
if (this.ops[this.cycle] && this.ops[this.cycle].addr) {
|
|
142
|
+
if (this.cycleAccurate && this.ops[this.cycle] && this.ops[this.cycle].addr) {
|
|
138
143
|
out.push(`cpu.polltimeAddr(${toSkip}, ${this.ops[this.cycle].addr});`);
|
|
139
144
|
} else {
|
|
140
145
|
out.push(`cpu.polltime(${toSkip});`);
|
|
@@ -145,17 +150,17 @@ class InstructionGen {
|
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
split(condition) {
|
|
148
|
-
return new SplitInstruction(this, condition, this.is65c12);
|
|
153
|
+
return new SplitInstruction(this, condition, this.is65c12, this.cycleAccurate);
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
class SplitInstruction {
|
|
153
|
-
constructor(preamble, condition, is65c12) {
|
|
158
|
+
constructor(preamble, condition, is65c12, cycleAccurate = true) {
|
|
154
159
|
this.preamble = preamble;
|
|
155
160
|
this.condition = condition;
|
|
156
|
-
this.ifTrue = new InstructionGen(is65c12);
|
|
161
|
+
this.ifTrue = new InstructionGen(is65c12, cycleAccurate);
|
|
157
162
|
this.ifTrue.tick(preamble.cycle);
|
|
158
|
-
this.ifFalse = new InstructionGen(is65c12);
|
|
163
|
+
this.ifFalse = new InstructionGen(is65c12, cycleAccurate);
|
|
159
164
|
this.ifFalse.tick(preamble.cycle);
|
|
160
165
|
|
|
161
166
|
["append", "prepend", "readOp", "writeOp", "spuriousOp"].forEach((op) => {
|
|
@@ -1052,7 +1057,7 @@ class Disassemble6502 {
|
|
|
1052
1057
|
}
|
|
1053
1058
|
}
|
|
1054
1059
|
|
|
1055
|
-
function makeCpuFunctions(cpu, opcodes, is65c12) {
|
|
1060
|
+
function makeCpuFunctions(cpu, opcodes, is65c12, cycleAccurate = true) {
|
|
1056
1061
|
function getInstruction(opcodeString, needsReg) {
|
|
1057
1062
|
const split = opcodeString.split(" ");
|
|
1058
1063
|
const opcode = split[0];
|
|
@@ -1060,7 +1065,7 @@ function makeCpuFunctions(cpu, opcodes, is65c12) {
|
|
|
1060
1065
|
const op = getOp(opcode, arg);
|
|
1061
1066
|
if (!op) return null;
|
|
1062
1067
|
|
|
1063
|
-
let ig = new InstructionGen(is65c12);
|
|
1068
|
+
let ig = new InstructionGen(is65c12, cycleAccurate);
|
|
1064
1069
|
if (needsReg) ig.append("let REG = 0|0;");
|
|
1065
1070
|
|
|
1066
1071
|
switch (arg) {
|
|
@@ -1398,14 +1403,14 @@ ${indent}`)
|
|
|
1398
1403
|
};
|
|
1399
1404
|
}
|
|
1400
1405
|
|
|
1401
|
-
export function Cpu6502(cpu) {
|
|
1402
|
-
return makeCpuFunctions(cpu, opcodes6502, false);
|
|
1406
|
+
export function Cpu6502(cpu, { cycleAccurate = true } = {}) {
|
|
1407
|
+
return makeCpuFunctions(cpu, opcodes6502, false, cycleAccurate);
|
|
1403
1408
|
}
|
|
1404
1409
|
|
|
1405
|
-
export function Cpu65c12(cpu) {
|
|
1406
|
-
return makeCpuFunctions(cpu, opcodes65c12, true);
|
|
1410
|
+
export function Cpu65c12(cpu, { cycleAccurate = true } = {}) {
|
|
1411
|
+
return makeCpuFunctions(cpu, opcodes65c12, true, cycleAccurate);
|
|
1407
1412
|
}
|
|
1408
1413
|
|
|
1409
|
-
export function Cpu65c02(cpu) {
|
|
1410
|
-
return makeCpuFunctions(cpu, opcodes65c02, true);
|
|
1414
|
+
export function Cpu65c02(cpu, { cycleAccurate = true } = {}) {
|
|
1415
|
+
return makeCpuFunctions(cpu, opcodes65c02, true, cycleAccurate);
|
|
1411
1416
|
}
|
package/src/acia.js
CHANGED
|
@@ -118,20 +118,34 @@ export class Acia {
|
|
|
118
118
|
selectRs423(selected) {
|
|
119
119
|
this.rs423Selected = !!selected;
|
|
120
120
|
if (this.rs423Selected) {
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
121
|
+
// When a handler is present it acts as the remote device and is
|
|
122
|
+
// always ready to receive, so CTS is low (active = clear to send).
|
|
123
|
+
// Without a handler there is nothing on the other end and CTS
|
|
124
|
+
// stays high, which inhibits TDRE so the OS output buffer drains
|
|
125
|
+
// silently rather than hanging.
|
|
126
|
+
if (this.rs423Handler) {
|
|
127
|
+
this.sr &= ~0x08; // CTS low — clear to send
|
|
128
|
+
} else {
|
|
129
|
+
this.sr |= 0x08; // CTS high — not connected
|
|
130
|
+
}
|
|
126
131
|
} else {
|
|
127
|
-
// Cassette selected.
|
|
128
|
-
// CTS is always low, meaning actually Clear To Send.
|
|
132
|
+
// Cassette selected — CTS is always low (clear to send).
|
|
129
133
|
this.sr &= ~0x08;
|
|
130
134
|
}
|
|
131
135
|
this.dcdLineUpdated();
|
|
132
136
|
this.runRs423Task.ensureScheduled(this.rs423Selected, this.serialReceiveCyclesPerByte);
|
|
133
137
|
}
|
|
134
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Attach or detach an RS-423 peripheral handler at runtime.
|
|
141
|
+
* The handler must implement onTransmit(byte) and tryReceive(rts).
|
|
142
|
+
* If RS-423 is already selected, the CTS line is updated immediately.
|
|
143
|
+
*/
|
|
144
|
+
setRs423Handler(handler) {
|
|
145
|
+
this.rs423Handler = handler;
|
|
146
|
+
if (this.rs423Selected) this.selectRs423(true);
|
|
147
|
+
}
|
|
148
|
+
|
|
135
149
|
dcdLineUpdated() {
|
|
136
150
|
// AUG: "It will always be low when the RS423 interface is selected".
|
|
137
151
|
const level = this.rs423Selected ? false : this.tapeDcdLineLevel;
|
|
@@ -183,6 +197,48 @@ export class Acia {
|
|
|
183
197
|
this.updateIrq();
|
|
184
198
|
}
|
|
185
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
|
+
|
|
186
242
|
setTape(tape) {
|
|
187
243
|
this.tape = tape;
|
|
188
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,30 @@ 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" },
|
|
233
257
|
{
|
|
234
258
|
label: "Soft Reset",
|
|
235
259
|
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
|
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// The 16 standard BBC Micro colours in ABGR format (0xffBBGGRR, little-endian canvas RGBA).
|
|
4
|
+
// Colours 0-7 are the primary palette; 8-15 duplicate them as the default solid/non-flash set.
|
|
5
|
+
// Imported by both video.js (NulaDefaultPalette) and teletext.js (BbcDefaultCollook) to avoid
|
|
6
|
+
// circular imports between those two modules.
|
|
7
|
+
export const BbcDefaultPalette = new Uint32Array([
|
|
8
|
+
0xff000000, // 0: black
|
|
9
|
+
0xff0000ff, // 1: red
|
|
10
|
+
0xff00ff00, // 2: green
|
|
11
|
+
0xff00ffff, // 3: yellow
|
|
12
|
+
0xffff0000, // 4: blue
|
|
13
|
+
0xffff00ff, // 5: magenta
|
|
14
|
+
0xffffff00, // 6: cyan
|
|
15
|
+
0xffffffff, // 7: white
|
|
16
|
+
0xff000000, // 8: black (solid duplicate)
|
|
17
|
+
0xff0000ff, // 9: red
|
|
18
|
+
0xff00ff00, // 10: green
|
|
19
|
+
0xff00ffff, // 11: yellow
|
|
20
|
+
0xffff0000, // 12: blue
|
|
21
|
+
0xffff00ff, // 13: magenta
|
|
22
|
+
0xffffff00, // 14: cyan
|
|
23
|
+
0xffffffff, // 15: white
|
|
24
|
+
]);
|