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/src/rewind.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Circular buffer of emulator state snapshots for rewind functionality.
|
|
5
|
+
* Snapshots are stored directly without deep-copying, since
|
|
6
|
+
* snapshotState() already clones all TypedArrays via .slice().
|
|
7
|
+
*/
|
|
8
|
+
export class RewindBuffer {
|
|
9
|
+
/**
|
|
10
|
+
* @param {number} maxSnapshots - maximum number of snapshots to retain
|
|
11
|
+
*/
|
|
12
|
+
constructor(maxSnapshots = 30) {
|
|
13
|
+
this.maxSnapshots = maxSnapshots;
|
|
14
|
+
this.snapshots = new Array(maxSnapshots);
|
|
15
|
+
this.count = 0;
|
|
16
|
+
this.writeIndex = 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Push a snapshot into the buffer.
|
|
21
|
+
* The caller must ensure the snapshot's typed arrays are already
|
|
22
|
+
* independent copies (e.g. from snapshotState() which uses .slice()).
|
|
23
|
+
* Overwrites the oldest snapshot when full.
|
|
24
|
+
* @param {object} snapshot - emulator state snapshot (already cloned)
|
|
25
|
+
*/
|
|
26
|
+
push(snapshot) {
|
|
27
|
+
this.snapshots[this.writeIndex] = snapshot;
|
|
28
|
+
this.writeIndex = (this.writeIndex + 1) % this.maxSnapshots;
|
|
29
|
+
if (this.count < this.maxSnapshots) this.count++;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Pop the most recent snapshot from the buffer.
|
|
34
|
+
* @returns {object|null} the most recent snapshot, or null if empty
|
|
35
|
+
*/
|
|
36
|
+
pop() {
|
|
37
|
+
if (this.count === 0) return null;
|
|
38
|
+
this.writeIndex = (this.writeIndex - 1 + this.maxSnapshots) % this.maxSnapshots;
|
|
39
|
+
this.count--;
|
|
40
|
+
const snapshot = this.snapshots[this.writeIndex];
|
|
41
|
+
this.snapshots[this.writeIndex] = null;
|
|
42
|
+
return snapshot;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Peek at the most recent snapshot without removing it.
|
|
47
|
+
* @returns {object|null} the most recent snapshot, or null if empty
|
|
48
|
+
*/
|
|
49
|
+
peek() {
|
|
50
|
+
if (this.count === 0) return null;
|
|
51
|
+
const index = (this.writeIndex - 1 + this.maxSnapshots) % this.maxSnapshots;
|
|
52
|
+
return this.snapshots[index];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Clear all snapshots from the buffer.
|
|
57
|
+
*/
|
|
58
|
+
clear() {
|
|
59
|
+
this.snapshots.fill(null);
|
|
60
|
+
this.count = 0;
|
|
61
|
+
this.writeIndex = 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Return all snapshots in order from oldest to newest.
|
|
66
|
+
* @returns {object[]} array of snapshots
|
|
67
|
+
*/
|
|
68
|
+
getAll() {
|
|
69
|
+
const result = new Array(this.count);
|
|
70
|
+
const start = (this.writeIndex - this.count + this.maxSnapshots) % this.maxSnapshots;
|
|
71
|
+
for (let i = 0; i < this.count; i++) {
|
|
72
|
+
result[i] = this.snapshots[(start + i) % this.maxSnapshots];
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Number of snapshots currently stored.
|
|
79
|
+
* @returns {number}
|
|
80
|
+
*/
|
|
81
|
+
get length() {
|
|
82
|
+
return this.count;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/scheduler.js
CHANGED
|
@@ -87,6 +87,34 @@ export class Scheduler {
|
|
|
87
87
|
return this.scheduled.expireEpoch - this.epoch;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Cancel all scheduled tasks.
|
|
92
|
+
* Used during state restore - components will re-register their own tasks.
|
|
93
|
+
*/
|
|
94
|
+
cancelAll() {
|
|
95
|
+
while (this.scheduled) {
|
|
96
|
+
this.scheduled.cancel();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Capture the scheduler's state for snapshotting.
|
|
102
|
+
* @returns {{epoch: number}}
|
|
103
|
+
*/
|
|
104
|
+
snapshotState() {
|
|
105
|
+
return { epoch: this.epoch };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Restore the scheduler's state from a snapshot.
|
|
110
|
+
* Cancels all existing tasks - components must re-register theirs after this call.
|
|
111
|
+
* @param {{epoch: number}} state
|
|
112
|
+
*/
|
|
113
|
+
restoreState(state) {
|
|
114
|
+
this.cancelAll();
|
|
115
|
+
this.epoch = state.epoch;
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
/**
|
|
91
119
|
* Create a new task.
|
|
92
120
|
* @param {function(): void} onExpire function to call when the task expires
|
package/src/snapshot.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { typedArrayToBase64, base64ToTypedArray } from "./state-utils.js";
|
|
4
|
+
import { findModel } from "./models.js";
|
|
5
|
+
|
|
6
|
+
const SnapshotFormat = "jsbeeb-snapshot";
|
|
7
|
+
const SnapshotVersion = 2;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if two model names are compatible for state restore.
|
|
11
|
+
* Resolves synonyms via findModel, then compares by stripping any
|
|
12
|
+
* trailing parenthesised suffix (e.g. treating "BBC Master 128 (DFS)"
|
|
13
|
+
* and "BBC Master 128 (ADFS)" as compatible). Does not treat
|
|
14
|
+
* differently-named models like "BBC B with DFS 0.9" vs "BBC B with DFS 1.2"
|
|
15
|
+
* as compatible — those are distinct models.
|
|
16
|
+
*/
|
|
17
|
+
export function modelsCompatible(snapshotModel, currentModel) {
|
|
18
|
+
if (snapshotModel === currentModel) return true;
|
|
19
|
+
const resolvedSnapshot = findModel(snapshotModel);
|
|
20
|
+
const resolvedCurrent = findModel(currentModel);
|
|
21
|
+
if (resolvedSnapshot && resolvedCurrent) {
|
|
22
|
+
// Same model object, or same base machine (strip filesystem suffix)
|
|
23
|
+
if (resolvedSnapshot === resolvedCurrent) return true;
|
|
24
|
+
const base = (name) => name.replace(/\s*\(.*\)$/, "");
|
|
25
|
+
return base(resolvedSnapshot.name) === base(resolvedCurrent.name);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Map of TypedArray constructor names for deserialization
|
|
31
|
+
const TypedArrayConstructors = {
|
|
32
|
+
Uint8Array,
|
|
33
|
+
Uint16Array,
|
|
34
|
+
Uint32Array,
|
|
35
|
+
Int32Array,
|
|
36
|
+
Float32Array,
|
|
37
|
+
Float64Array,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a snapshot of the emulator state for save-to-file.
|
|
42
|
+
* Disc track pulse data is stripped — on restore, the discs are reloaded
|
|
43
|
+
* from the source references in the `media` field. (The in-memory rewind
|
|
44
|
+
* path uses cpu.snapshotState() directly, which retains full disc data.)
|
|
45
|
+
* @param {import('./6502.js').Cpu6502} cpu
|
|
46
|
+
* @param {object} model - the model definition object
|
|
47
|
+
* @param {object} [media] - optional media source references (disc1, disc2)
|
|
48
|
+
* @returns {object} snapshot object
|
|
49
|
+
*/
|
|
50
|
+
export function createSnapshot(cpu, model, media) {
|
|
51
|
+
const state = cpu.snapshotState();
|
|
52
|
+
// Strip clean disc track data from the save-to-file snapshot.
|
|
53
|
+
// The FDC/drive mechanical state is kept; only clean tracks
|
|
54
|
+
// (which can be reloaded from the disc image) are removed.
|
|
55
|
+
// Dirty tracks (written since disc load) are kept as an overlay.
|
|
56
|
+
if (state.fdc && state.fdc.drives) {
|
|
57
|
+
for (const drive of state.fdc.drives) {
|
|
58
|
+
if (drive.disc) {
|
|
59
|
+
const dirtyTracks = {};
|
|
60
|
+
for (const key of Object.keys(drive.disc.tracks)) {
|
|
61
|
+
const [sideStr, trackNumStr] = key.split(":");
|
|
62
|
+
const isSideUpper = sideStr === "true";
|
|
63
|
+
const trackNum = parseInt(trackNumStr, 10);
|
|
64
|
+
const dirtyKey = trackNum | (isSideUpper ? 0x100 : 0);
|
|
65
|
+
if (drive.disc._everDirtyTracks && drive.disc._everDirtyTracks.has(dirtyKey)) {
|
|
66
|
+
dirtyTracks[key] = drive.disc.tracks[key];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
drive.disc.tracks = {};
|
|
70
|
+
drive.disc.dirtyTracks = dirtyTracks;
|
|
71
|
+
// Clean up internal-only fields not needed in serialized state
|
|
72
|
+
delete drive.disc._everDirtyTracks;
|
|
73
|
+
delete drive.disc._originalImageData;
|
|
74
|
+
delete drive.disc._originalImageCrc32;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const snapshot = {
|
|
79
|
+
format: SnapshotFormat,
|
|
80
|
+
version: SnapshotVersion,
|
|
81
|
+
model: model.name,
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
state,
|
|
84
|
+
};
|
|
85
|
+
if (media) snapshot.media = media;
|
|
86
|
+
return snapshot;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Restore emulator state from a snapshot.
|
|
91
|
+
* @param {import('./6502.js').Cpu6502} cpu
|
|
92
|
+
* @param {object} model - the current model definition
|
|
93
|
+
* @param {object} snapshot
|
|
94
|
+
* @throws {Error} if the model doesn't match
|
|
95
|
+
*/
|
|
96
|
+
export function restoreSnapshot(cpu, model, snapshot) {
|
|
97
|
+
if (snapshot.format !== SnapshotFormat) {
|
|
98
|
+
throw new Error(`Unknown snapshot format: ${snapshot.format}`);
|
|
99
|
+
}
|
|
100
|
+
if (snapshot.version > SnapshotVersion) {
|
|
101
|
+
throw new Error(`Snapshot version ${snapshot.version} is newer than supported version ${SnapshotVersion}`);
|
|
102
|
+
}
|
|
103
|
+
if (!modelsCompatible(snapshot.model, model.name)) {
|
|
104
|
+
throw new Error(`Model mismatch: snapshot is for "${snapshot.model}" but current model is "${model.name}"`);
|
|
105
|
+
}
|
|
106
|
+
cpu.restoreState(snapshot.state);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Serialize a snapshot to a JSON string, converting TypedArrays to base64.
|
|
111
|
+
* @param {object} snapshot
|
|
112
|
+
* @returns {string} JSON string
|
|
113
|
+
*/
|
|
114
|
+
export function snapshotToJSON(snapshot) {
|
|
115
|
+
return JSON.stringify(snapshot, (key, value) => {
|
|
116
|
+
if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
|
|
117
|
+
return {
|
|
118
|
+
__typedArray: true,
|
|
119
|
+
type: value.constructor.name,
|
|
120
|
+
data: typedArrayToBase64(value),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Deserialize a snapshot from a JSON string, converting base64 back to TypedArrays.
|
|
129
|
+
* @param {string} json
|
|
130
|
+
* @returns {object} snapshot object
|
|
131
|
+
*/
|
|
132
|
+
export function snapshotFromJSON(json) {
|
|
133
|
+
return JSON.parse(json, (key, value) => {
|
|
134
|
+
if (value && value.__typedArray) {
|
|
135
|
+
const Constructor = TypedArrayConstructors[value.type];
|
|
136
|
+
if (!Constructor) {
|
|
137
|
+
throw new Error(`Unknown TypedArray type: ${value.type}`);
|
|
138
|
+
}
|
|
139
|
+
return base64ToTypedArray(value.data, Constructor);
|
|
140
|
+
}
|
|
141
|
+
return value;
|
|
142
|
+
});
|
|
143
|
+
}
|
package/src/soundchip.js
CHANGED
|
@@ -263,6 +263,45 @@ export class SoundChip {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
snapshotState() {
|
|
267
|
+
return {
|
|
268
|
+
registers: this.registers.slice(),
|
|
269
|
+
counter: this.counter.slice(),
|
|
270
|
+
outputBit: [...this.outputBit],
|
|
271
|
+
volume: this.volume.slice(),
|
|
272
|
+
lfsr: this.lfsr,
|
|
273
|
+
latchedRegister: this.latchedRegister,
|
|
274
|
+
residual: this.residual,
|
|
275
|
+
sineOn: this.sineOn,
|
|
276
|
+
sineStep: this.sineStep,
|
|
277
|
+
sineTime: this.sineTime,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
restoreState(state) {
|
|
282
|
+
this.registers.set(state.registers);
|
|
283
|
+
this.counter.set(state.counter);
|
|
284
|
+
this.outputBit[0] = state.outputBit[0];
|
|
285
|
+
this.outputBit[1] = state.outputBit[1];
|
|
286
|
+
this.outputBit[2] = state.outputBit[2];
|
|
287
|
+
this.outputBit[3] = state.outputBit[3];
|
|
288
|
+
this.volume.set(state.volume);
|
|
289
|
+
this.lfsr = state.lfsr;
|
|
290
|
+
this.latchedRegister = state.latchedRegister;
|
|
291
|
+
// Sync to current scheduler epoch to avoid a catch-up burst
|
|
292
|
+
this.lastRunEpoch = this.scheduler.epoch;
|
|
293
|
+
this.residual = state.residual;
|
|
294
|
+
this.sineOn = state.sineOn;
|
|
295
|
+
this.sineStep = state.sineStep;
|
|
296
|
+
this.sineTime = state.sineTime;
|
|
297
|
+
// Rebind the LFSR function based on noise register
|
|
298
|
+
this.shiftLfsr =
|
|
299
|
+
this.registers[3] & 4 ? this.shiftLfsrWhiteNoise.bind(this) : this.shiftLfsrPeriodicNoise.bind(this);
|
|
300
|
+
// Reset output buffer
|
|
301
|
+
this.position = 0;
|
|
302
|
+
this.buffer.fill(0);
|
|
303
|
+
}
|
|
304
|
+
|
|
266
305
|
reset(hard) {
|
|
267
306
|
if (!hard) return;
|
|
268
307
|
for (let i = 0; i < 4; ++i) {
|
|
@@ -338,11 +377,7 @@ export class InstrumentedSoundChip extends SoundChip {
|
|
|
338
377
|
/** Read current SN76489 register state in a friendly format. */
|
|
339
378
|
getState() {
|
|
340
379
|
return {
|
|
341
|
-
tone: [
|
|
342
|
-
this.registers[0],
|
|
343
|
-
this.registers[1],
|
|
344
|
-
this.registers[2],
|
|
345
|
-
],
|
|
380
|
+
tone: [this.registers[0], this.registers[1], this.registers[2]],
|
|
346
381
|
noise: this.registers[3],
|
|
347
382
|
volume: [
|
|
348
383
|
this._attenuationFromVolume(0),
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert a TypedArray to a base64 string for JSON serialization.
|
|
5
|
+
* @param {ArrayBufferView} typedArray
|
|
6
|
+
* @returns {string} base64-encoded string
|
|
7
|
+
*/
|
|
8
|
+
export function typedArrayToBase64(typedArray) {
|
|
9
|
+
const bytes = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength);
|
|
10
|
+
// Build binary string in chunks to avoid excessive string concatenation
|
|
11
|
+
const chunkSize = 8192;
|
|
12
|
+
const parts = [];
|
|
13
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
14
|
+
const end = Math.min(i + chunkSize, bytes.length);
|
|
15
|
+
parts.push(String.fromCharCode(...bytes.subarray(i, end)));
|
|
16
|
+
}
|
|
17
|
+
return btoa(parts.join(""));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert a base64 string back to a TypedArray.
|
|
22
|
+
* @param {string} base64 base64-encoded string
|
|
23
|
+
* @param {function} TypedArrayConstructor constructor for the desired type (e.g., Uint8Array)
|
|
24
|
+
* @returns {ArrayBufferView} the decoded typed array
|
|
25
|
+
*/
|
|
26
|
+
export function base64ToTypedArray(base64, TypedArrayConstructor) {
|
|
27
|
+
const binary = atob(base64);
|
|
28
|
+
const bytes = new Uint8Array(binary.length);
|
|
29
|
+
for (let i = 0; i < binary.length; i++) {
|
|
30
|
+
bytes[i] = binary.charCodeAt(i);
|
|
31
|
+
}
|
|
32
|
+
// Create a properly aligned typed array from the raw bytes
|
|
33
|
+
const elementSize = TypedArrayConstructor.BYTES_PER_ELEMENT;
|
|
34
|
+
const length = bytes.length / elementSize;
|
|
35
|
+
if (!Number.isInteger(length)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Base64 data length (${bytes.length} bytes) is not a multiple of ${TypedArrayConstructor.name} element size (${elementSize})`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const result = new TypedArrayConstructor(length);
|
|
41
|
+
new Uint8Array(result.buffer).set(bytes);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Deep copy a snapshot object, cloning any TypedArrays found within.
|
|
47
|
+
* This ensures rewind buffer snapshots are fully isolated from live state.
|
|
48
|
+
* @param {object} obj snapshot object to copy
|
|
49
|
+
* @returns {object} a deep copy with all TypedArrays cloned
|
|
50
|
+
*/
|
|
51
|
+
export function deepCopySnapshot(obj) {
|
|
52
|
+
if (obj === null || typeof obj !== "object") {
|
|
53
|
+
return obj;
|
|
54
|
+
}
|
|
55
|
+
if (ArrayBuffer.isView(obj)) {
|
|
56
|
+
return obj.slice();
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(obj)) {
|
|
59
|
+
return obj.map((item) => deepCopySnapshot(item));
|
|
60
|
+
}
|
|
61
|
+
const copy = {};
|
|
62
|
+
for (const key of Object.keys(obj)) {
|
|
63
|
+
copy[key] = deepCopySnapshot(obj[key]);
|
|
64
|
+
}
|
|
65
|
+
return copy;
|
|
66
|
+
}
|
package/src/teletext.js
CHANGED
|
@@ -150,6 +150,75 @@ export class Teletext {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
snapshotState() {
|
|
154
|
+
// Encode glyph table references as strings for serialization
|
|
155
|
+
let nextGlyphsName, curGlyphsName, heldGlyphsName;
|
|
156
|
+
for (const [name, table] of [
|
|
157
|
+
["normal", this.normalGlyphs],
|
|
158
|
+
["graphics", this.graphicsGlyphs],
|
|
159
|
+
["separated", this.separatedGlyphs],
|
|
160
|
+
]) {
|
|
161
|
+
if (this.nextGlyphs === table) nextGlyphsName = name;
|
|
162
|
+
if (this.curGlyphs === table) curGlyphsName = name;
|
|
163
|
+
if (this.heldGlyphs === table) heldGlyphsName = name;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
prevCol: this.prevCol,
|
|
167
|
+
col: this.col,
|
|
168
|
+
bg: this.bg,
|
|
169
|
+
sep: this.sep,
|
|
170
|
+
dbl: this.dbl,
|
|
171
|
+
oldDbl: this.oldDbl,
|
|
172
|
+
secondHalfOfDouble: this.secondHalfOfDouble,
|
|
173
|
+
wasDbl: this.wasDbl,
|
|
174
|
+
gfx: this.gfx,
|
|
175
|
+
flash: this.flash,
|
|
176
|
+
flashOn: this.flashOn,
|
|
177
|
+
flashTime: this.flashTime,
|
|
178
|
+
heldChar: this.heldChar,
|
|
179
|
+
holdChar: this.holdChar,
|
|
180
|
+
dataQueue: [...this.dataQueue],
|
|
181
|
+
scanlineCounter: this.scanlineCounter,
|
|
182
|
+
levelDEW: this.levelDEW,
|
|
183
|
+
levelDISPTMG: this.levelDISPTMG,
|
|
184
|
+
levelRA0: this.levelRA0,
|
|
185
|
+
nextGlyphs: nextGlyphsName,
|
|
186
|
+
curGlyphs: curGlyphsName,
|
|
187
|
+
heldGlyphs: heldGlyphsName,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
restoreState(state) {
|
|
192
|
+
this.prevCol = state.prevCol;
|
|
193
|
+
this.col = state.col;
|
|
194
|
+
this.bg = state.bg;
|
|
195
|
+
this.sep = state.sep;
|
|
196
|
+
this.dbl = state.dbl;
|
|
197
|
+
this.oldDbl = state.oldDbl;
|
|
198
|
+
this.secondHalfOfDouble = state.secondHalfOfDouble;
|
|
199
|
+
this.wasDbl = state.wasDbl;
|
|
200
|
+
this.gfx = state.gfx;
|
|
201
|
+
this.flash = state.flash;
|
|
202
|
+
this.flashOn = state.flashOn;
|
|
203
|
+
this.flashTime = state.flashTime;
|
|
204
|
+
this.heldChar = state.heldChar;
|
|
205
|
+
this.holdChar = state.holdChar;
|
|
206
|
+
this.dataQueue = [...state.dataQueue];
|
|
207
|
+
this.scanlineCounter = state.scanlineCounter;
|
|
208
|
+
this.levelDEW = state.levelDEW;
|
|
209
|
+
this.levelDISPTMG = state.levelDISPTMG;
|
|
210
|
+
this.levelRA0 = state.levelRA0;
|
|
211
|
+
|
|
212
|
+
const glyphMap = {
|
|
213
|
+
normal: this.normalGlyphs,
|
|
214
|
+
graphics: this.graphicsGlyphs,
|
|
215
|
+
separated: this.separatedGlyphs,
|
|
216
|
+
};
|
|
217
|
+
this.nextGlyphs = glyphMap[state.nextGlyphs] || this.normalGlyphs;
|
|
218
|
+
this.curGlyphs = glyphMap[state.curGlyphs] || this.normalGlyphs;
|
|
219
|
+
this.heldGlyphs = glyphMap[state.heldGlyphs] || this.normalGlyphs;
|
|
220
|
+
}
|
|
221
|
+
|
|
153
222
|
setNextChars() {
|
|
154
223
|
if (this.gfx) {
|
|
155
224
|
if (this.sep) {
|
package/src/via.js
CHANGED
|
@@ -402,6 +402,73 @@ class Via {
|
|
|
402
402
|
this.portBUpdated();
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
snapshotState() {
|
|
406
|
+
return {
|
|
407
|
+
ora: this.ora,
|
|
408
|
+
orb: this.orb,
|
|
409
|
+
ira: this.ira,
|
|
410
|
+
irb: this.irb,
|
|
411
|
+
ddra: this.ddra,
|
|
412
|
+
ddrb: this.ddrb,
|
|
413
|
+
sr: this.sr,
|
|
414
|
+
t1l: this.t1l,
|
|
415
|
+
t2l: this.t2l,
|
|
416
|
+
t1c: this.t1c,
|
|
417
|
+
t2c: this.t2c,
|
|
418
|
+
acr: this.acr,
|
|
419
|
+
pcr: this.pcr,
|
|
420
|
+
ifr: this.ifr,
|
|
421
|
+
ier: this.ier,
|
|
422
|
+
t1hit: this.t1hit,
|
|
423
|
+
t2hit: this.t2hit,
|
|
424
|
+
portapins: this.portapins,
|
|
425
|
+
portbpins: this.portbpins,
|
|
426
|
+
ca1: this.ca1,
|
|
427
|
+
ca2: this.ca2,
|
|
428
|
+
cb1: this.cb1,
|
|
429
|
+
cb2: this.cb2,
|
|
430
|
+
justhit: this.justhit,
|
|
431
|
+
t1_pb7: this.t1_pb7,
|
|
432
|
+
lastPolltime: this.lastPolltime,
|
|
433
|
+
taskOffset: this.task.scheduled() ? this.task.expireEpoch - this.scheduler.epoch : null,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
restoreState(state) {
|
|
438
|
+
this.ora = state.ora;
|
|
439
|
+
this.orb = state.orb;
|
|
440
|
+
this.ira = state.ira;
|
|
441
|
+
this.irb = state.irb;
|
|
442
|
+
this.ddra = state.ddra;
|
|
443
|
+
this.ddrb = state.ddrb;
|
|
444
|
+
this.sr = state.sr;
|
|
445
|
+
this.t1l = state.t1l;
|
|
446
|
+
this.t2l = state.t2l;
|
|
447
|
+
this.t1c = state.t1c;
|
|
448
|
+
this.t2c = state.t2c;
|
|
449
|
+
this.acr = state.acr;
|
|
450
|
+
this.pcr = state.pcr;
|
|
451
|
+
this.ifr = state.ifr;
|
|
452
|
+
this.ier = state.ier;
|
|
453
|
+
this.t1hit = state.t1hit;
|
|
454
|
+
this.t2hit = state.t2hit;
|
|
455
|
+
this.portapins = state.portapins;
|
|
456
|
+
this.portbpins = state.portbpins;
|
|
457
|
+
this.ca1 = state.ca1;
|
|
458
|
+
this.ca2 = state.ca2;
|
|
459
|
+
this.cb1 = state.cb1;
|
|
460
|
+
this.cb2 = state.cb2;
|
|
461
|
+
this.justhit = state.justhit;
|
|
462
|
+
this.t1_pb7 = state.t1_pb7;
|
|
463
|
+
this.lastPolltime = state.lastPolltime;
|
|
464
|
+
this.updateIFR();
|
|
465
|
+
if (state.taskOffset !== null) {
|
|
466
|
+
this.task.reschedule(state.taskOffset);
|
|
467
|
+
} else {
|
|
468
|
+
this.task.cancel();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
405
472
|
setca1(level) {
|
|
406
473
|
if (level === this.ca1) return;
|
|
407
474
|
const pcrSet = !!(this.pcr & 1);
|
|
@@ -484,6 +551,30 @@ export class SysVia extends Via {
|
|
|
484
551
|
this.reset();
|
|
485
552
|
}
|
|
486
553
|
|
|
554
|
+
snapshotState() {
|
|
555
|
+
return {
|
|
556
|
+
...super.snapshotState(),
|
|
557
|
+
IC32: this.IC32,
|
|
558
|
+
capsLockLight: this.capsLockLight,
|
|
559
|
+
shiftLockLight: this.shiftLockLight,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
restoreState(state) {
|
|
564
|
+
super.restoreState(state);
|
|
565
|
+
this.IC32 = state.IC32;
|
|
566
|
+
this.capsLockLight = state.capsLockLight;
|
|
567
|
+
this.shiftLockLight = state.shiftLockLight;
|
|
568
|
+
// Re-apply IC32 side effects (sound chip data bus, CMOS, port A)
|
|
569
|
+
// Must call portBUpdated directly (not recalculatePortBPins which
|
|
570
|
+
// overwrites IC32 based on portbpins) and then re-set IC32.
|
|
571
|
+
this.portBUpdated();
|
|
572
|
+
this.IC32 = state.IC32;
|
|
573
|
+
this.capsLockLight = state.capsLockLight;
|
|
574
|
+
this.shiftLockLight = state.shiftLockLight;
|
|
575
|
+
this.recalculatePortAPins();
|
|
576
|
+
}
|
|
577
|
+
|
|
487
578
|
setKeyLayout(map) {
|
|
488
579
|
this.keycodeToRowCol = utils.getKeyMap(map);
|
|
489
580
|
}
|