jsbeeb 1.10.0 → 1.12.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 +1 -1
- package/package.json +2 -7
- package/src/6502.js +8 -1
- package/src/app/app.js +1 -1
- package/src/app/electron.js +8 -8
- package/src/bem-snapshot.js +7 -218
- package/src/config.js +79 -62
- package/src/dom-utils.js +32 -0
- package/src/google-drive.js +3 -4
- package/src/keyboard.js +17 -14
- package/src/main.js +267 -235
- package/src/models.js +4 -4
- package/src/snapshot-helpers.js +208 -0
- package/src/snapshot.js +9 -18
- package/src/sth.js +1 -1
- package/src/tapes.js +2 -2
- package/src/uef-snapshot.js +402 -0
- package/src/utils.js +146 -15
- package/src/web/audio-handler.js +41 -25
- package/src/web/debug.js +100 -71
package/src/models.js
CHANGED
|
@@ -98,8 +98,8 @@ const masterSwram = [
|
|
|
98
98
|
|
|
99
99
|
export const allModels = [
|
|
100
100
|
new Model({
|
|
101
|
-
name: "BBC B with DFS 1.2",
|
|
102
|
-
synonyms: ["B-DFS1.2"],
|
|
101
|
+
name: "BBC B with 8271 (DFS 1.2)",
|
|
102
|
+
synonyms: ["B-DFS1.2", "BBC B with DFS 1.2"],
|
|
103
103
|
os: ["os.rom", "BASIC.ROM", "b/DFS-1.2.rom"],
|
|
104
104
|
cpuModel: CpuModel.MOS6502,
|
|
105
105
|
isMaster: false,
|
|
@@ -107,8 +107,8 @@ export const allModels = [
|
|
|
107
107
|
fdc: NoiseAwareIntelFdc,
|
|
108
108
|
}),
|
|
109
109
|
new Model({
|
|
110
|
-
name: "BBC B with DFS 0.9",
|
|
111
|
-
synonyms: ["B-DFS0.9", "B"],
|
|
110
|
+
name: "BBC B with 8271 (DFS 0.9)",
|
|
111
|
+
synonyms: ["B-DFS0.9", "B", "BBC B with DFS 0.9"],
|
|
112
112
|
os: ["os.rom", "BASIC.ROM", "b/DFS-0.9.rom"],
|
|
113
113
|
cpuModel: CpuModel.MOS6502,
|
|
114
114
|
isMaster: false,
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Shared helpers for snapshot importers (bem-snapshot.js, uef-snapshot.js).
|
|
4
|
+
// Contains volume table, default peripheral state, video state builder,
|
|
5
|
+
// and the common snapshot envelope that wraps per-format parsed state.
|
|
6
|
+
|
|
7
|
+
// Volume lookup table matching soundchip.js
|
|
8
|
+
export const volumeTable = new Float32Array(16);
|
|
9
|
+
(() => {
|
|
10
|
+
let f = 1.0;
|
|
11
|
+
for (let i = 0; i < 15; ++i) {
|
|
12
|
+
volumeTable[i] = f / 4;
|
|
13
|
+
f *= Math.pow(10, -0.1);
|
|
14
|
+
}
|
|
15
|
+
volumeTable[15] = 0;
|
|
16
|
+
})();
|
|
17
|
+
|
|
18
|
+
export const DefaultAcia = {
|
|
19
|
+
sr: 0x02,
|
|
20
|
+
cr: 0x00,
|
|
21
|
+
dr: 0x00,
|
|
22
|
+
rs423Selected: false,
|
|
23
|
+
motorOn: false,
|
|
24
|
+
tapeCarrierCount: 0,
|
|
25
|
+
tapeDcdLineLevel: false,
|
|
26
|
+
hadDcdHigh: false,
|
|
27
|
+
serialReceiveRate: 19200,
|
|
28
|
+
serialReceiveCyclesPerByte: 0,
|
|
29
|
+
txCompleteTaskOffset: null,
|
|
30
|
+
runTapeTaskOffset: null,
|
|
31
|
+
runRs423TaskOffset: null,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const DefaultAdc = { status: 0x40, low: 0x00, high: 0x00, taskOffset: null };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build jsbeeb video state from parsed CRTC, ULA, and palette data.
|
|
38
|
+
* @param {number} ulaControl - VideoULA control register
|
|
39
|
+
* @param {Uint8Array} ulaPalette - 16-entry raw ULA palette register values (lower nibble of &FE21 writes)
|
|
40
|
+
* @param {Uint8Array} crtcRegs - CRTC registers (at least 18 bytes)
|
|
41
|
+
* @param {Int32Array|null} [nulaCollook] - NULA colour lookup (16 ABGR entries), or null for default BBC palette
|
|
42
|
+
* @param {object|null} [crtcCounters] - CRTC counter state {hc, vc, sc, ma, maback}, or null for defaults
|
|
43
|
+
*/
|
|
44
|
+
export function buildVideoState(ulaControl, ulaPalette, crtcRegs, nulaCollook, crtcCounters) {
|
|
45
|
+
const regs = new Uint8Array(32);
|
|
46
|
+
regs.set(crtcRegs.slice(0, 18));
|
|
47
|
+
const actualPal = new Uint8Array(16);
|
|
48
|
+
for (let i = 0; i < 16; i++) actualPal[i] = ulaPalette[i] & 0x0f;
|
|
49
|
+
|
|
50
|
+
// Use NULA collook if provided, otherwise use default BBC palette
|
|
51
|
+
const collook =
|
|
52
|
+
nulaCollook ||
|
|
53
|
+
new Int32Array([
|
|
54
|
+
0xff000000, 0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff, 0xff000000,
|
|
55
|
+
0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff,
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
// Compute ulaPal from actualPal + collook, matching jsbeeb's Ula._recomputeUlaPal
|
|
59
|
+
const flashEnabled = !!(ulaControl & 1);
|
|
60
|
+
const defaultFlash = new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1]);
|
|
61
|
+
const flash = defaultFlash;
|
|
62
|
+
const ulaPal = new Int32Array(16);
|
|
63
|
+
for (let i = 0; i < 16; i++) {
|
|
64
|
+
const palVal = actualPal[i];
|
|
65
|
+
let colour = collook[(palVal & 0xf) ^ 7];
|
|
66
|
+
if (palVal & 8 && flashEnabled && flash[(palVal & 7) ^ 7]) {
|
|
67
|
+
colour = collook[palVal & 0xf];
|
|
68
|
+
}
|
|
69
|
+
ulaPal[i] = colour;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
regs,
|
|
74
|
+
bitmapX: 0,
|
|
75
|
+
bitmapY: 0,
|
|
76
|
+
oddClock: false,
|
|
77
|
+
frameCount: 0,
|
|
78
|
+
doEvenFrameLogic: false,
|
|
79
|
+
isEvenRender: true,
|
|
80
|
+
lastRenderWasEven: false,
|
|
81
|
+
firstScanline: true,
|
|
82
|
+
inHSync: false,
|
|
83
|
+
inVSync: false,
|
|
84
|
+
hadVSyncThisRow: false,
|
|
85
|
+
checkVertAdjust: false,
|
|
86
|
+
endOfMainLatched: false,
|
|
87
|
+
endOfVertAdjustLatched: false,
|
|
88
|
+
endOfFrameLatched: false,
|
|
89
|
+
inVertAdjust: false,
|
|
90
|
+
inDummyRaster: false,
|
|
91
|
+
hpulseWidth: regs[3] & 0x0f,
|
|
92
|
+
vpulseWidth: (regs[3] & 0xf0) >>> 4,
|
|
93
|
+
hpulseCounter: 0,
|
|
94
|
+
vpulseCounter: 0,
|
|
95
|
+
dispEnabled: 0x3f,
|
|
96
|
+
horizCounter: crtcCounters ? crtcCounters.hc : 0,
|
|
97
|
+
vertCounter: crtcCounters ? crtcCounters.vc : 0,
|
|
98
|
+
scanlineCounter: crtcCounters ? crtcCounters.sc : 0,
|
|
99
|
+
vertAdjustCounter: 0,
|
|
100
|
+
addr: crtcCounters ? crtcCounters.ma : (regs[13] | (regs[12] << 8)) & 0x3fff,
|
|
101
|
+
lineStartAddr: crtcCounters ? crtcCounters.maback : (regs[13] | (regs[12] << 8)) & 0x3fff,
|
|
102
|
+
nextLineStartAddr: crtcCounters ? crtcCounters.maback : (regs[13] | (regs[12] << 8)) & 0x3fff,
|
|
103
|
+
ulactrl: ulaControl,
|
|
104
|
+
pixelsPerChar: ulaControl & 0x10 ? 8 : 16,
|
|
105
|
+
halfClock: !(ulaControl & 0x10),
|
|
106
|
+
ulaMode: (ulaControl >>> 2) & 3,
|
|
107
|
+
teletextMode: !!(ulaControl & 2),
|
|
108
|
+
displayEnableSkew: Math.min((regs[8] & 0x30) >>> 4, 2),
|
|
109
|
+
ulaPal,
|
|
110
|
+
actualPal,
|
|
111
|
+
cursorOn: false,
|
|
112
|
+
cursorOff: false,
|
|
113
|
+
cursorOnThisFrame: false,
|
|
114
|
+
cursorDrawIndex: 0,
|
|
115
|
+
cursorPos: (regs[15] | (regs[14] << 8)) & 0x3fff,
|
|
116
|
+
interlacedSyncAndVideo: (regs[8] & 3) === 3,
|
|
117
|
+
screenSubtract: 0,
|
|
118
|
+
ula: {
|
|
119
|
+
collook: collook.slice(),
|
|
120
|
+
flash: new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1]),
|
|
121
|
+
paletteWriteFlag: false,
|
|
122
|
+
paletteFirstByte: 0,
|
|
123
|
+
paletteMode: 0,
|
|
124
|
+
horizontalOffset: 0,
|
|
125
|
+
leftBlank: 0,
|
|
126
|
+
disabled: false,
|
|
127
|
+
attributeMode: 0,
|
|
128
|
+
attributeText: 0,
|
|
129
|
+
},
|
|
130
|
+
crtc: { curReg: 0 },
|
|
131
|
+
teletext: {
|
|
132
|
+
prevCol: 0,
|
|
133
|
+
col: 7,
|
|
134
|
+
bg: 0,
|
|
135
|
+
sep: false,
|
|
136
|
+
dbl: false,
|
|
137
|
+
oldDbl: false,
|
|
138
|
+
secondHalfOfDouble: false,
|
|
139
|
+
wasDbl: false,
|
|
140
|
+
gfx: false,
|
|
141
|
+
flash: false,
|
|
142
|
+
flashOn: false,
|
|
143
|
+
flashTime: 0,
|
|
144
|
+
heldChar: 0,
|
|
145
|
+
holdChar: false,
|
|
146
|
+
dataQueue: [0, 0, 0, 0],
|
|
147
|
+
scanlineCounter: 0,
|
|
148
|
+
levelDEW: false,
|
|
149
|
+
levelDISPTMG: false,
|
|
150
|
+
levelRA0: false,
|
|
151
|
+
nextGlyphs: "normal",
|
|
152
|
+
curGlyphs: "normal",
|
|
153
|
+
heldGlyphs: "normal",
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build a jsbeeb snapshot envelope from parsed emulator components.
|
|
160
|
+
* @param {string} importedFrom - source identifier (e.g. "b-em", "beebem-uef")
|
|
161
|
+
* @param {string} modelName - jsbeeb model name/synonym
|
|
162
|
+
* @param {object} cpuState - parsed CPU state {a, x, y, flags, s, pc, nmi, fe30, fe34}
|
|
163
|
+
* @param {Uint8Array} ram - 128KB RAM array
|
|
164
|
+
* @param {Uint8Array|null} roms - 256KB sideways ROM/RAM array, or null
|
|
165
|
+
* @param {object} sysvia - jsbeeb sys VIA state
|
|
166
|
+
* @param {object} uservia - jsbeeb user VIA state
|
|
167
|
+
* @param {object} video - jsbeeb video state (from buildVideoState)
|
|
168
|
+
* @param {object} soundChip - jsbeeb sound chip state
|
|
169
|
+
*/
|
|
170
|
+
export function buildSnapshot(importedFrom, modelName, cpuState, ram, roms, sysvia, uservia, video, soundChip) {
|
|
171
|
+
return {
|
|
172
|
+
format: "jsbeeb-snapshot",
|
|
173
|
+
version: 2,
|
|
174
|
+
model: modelName,
|
|
175
|
+
timestamp: new Date().toISOString(),
|
|
176
|
+
importedFrom,
|
|
177
|
+
state: {
|
|
178
|
+
a: cpuState.a,
|
|
179
|
+
x: cpuState.x,
|
|
180
|
+
y: cpuState.y,
|
|
181
|
+
s: cpuState.s,
|
|
182
|
+
pc: cpuState.pc,
|
|
183
|
+
p: cpuState.flags | 0x30,
|
|
184
|
+
nmiLevel: !!cpuState.nmi,
|
|
185
|
+
nmiEdge: false,
|
|
186
|
+
halted: false,
|
|
187
|
+
takeInt: false,
|
|
188
|
+
romsel: cpuState.fe30 ?? 0,
|
|
189
|
+
acccon: cpuState.fe34 ?? 0,
|
|
190
|
+
videoDisplayPage: 0,
|
|
191
|
+
currentCycles: 0,
|
|
192
|
+
targetCycles: 0,
|
|
193
|
+
cycleSeconds: 0,
|
|
194
|
+
peripheralCycles: 0,
|
|
195
|
+
videoCycles: 0,
|
|
196
|
+
music5000PageSel: 0,
|
|
197
|
+
ram,
|
|
198
|
+
...(roms instanceof Uint8Array ? { roms } : {}),
|
|
199
|
+
scheduler: { epoch: 0 },
|
|
200
|
+
sysvia,
|
|
201
|
+
uservia,
|
|
202
|
+
video,
|
|
203
|
+
soundChip,
|
|
204
|
+
acia: { ...DefaultAcia },
|
|
205
|
+
adc: { ...DefaultAdc },
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
package/src/snapshot.js
CHANGED
|
@@ -7,24 +7,15 @@ const SnapshotFormat = "jsbeeb-snapshot";
|
|
|
7
7
|
const SnapshotVersion = 2;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Check if two model names
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
10
|
+
* Check if two model names resolve to the same model (accounting for
|
|
11
|
+
* synonyms and old names). Used to decide whether a page reload is
|
|
12
|
+
* needed when loading a snapshot saved under a different model name.
|
|
16
13
|
*/
|
|
17
|
-
export function
|
|
18
|
-
if (
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
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;
|
|
14
|
+
export function isSameModel(nameA, nameB) {
|
|
15
|
+
if (nameA === nameB) return true;
|
|
16
|
+
const a = findModel(nameA);
|
|
17
|
+
const b = findModel(nameB);
|
|
18
|
+
return a !== null && a === b;
|
|
28
19
|
}
|
|
29
20
|
|
|
30
21
|
// Map of TypedArray constructor names for deserialization
|
|
@@ -100,7 +91,7 @@ export function restoreSnapshot(cpu, model, snapshot) {
|
|
|
100
91
|
if (snapshot.version > SnapshotVersion) {
|
|
101
92
|
throw new Error(`Snapshot version ${snapshot.version} is newer than supported version ${SnapshotVersion}`);
|
|
102
93
|
}
|
|
103
|
-
if (!
|
|
94
|
+
if (!isSameModel(snapshot.model, model.name)) {
|
|
104
95
|
throw new Error(`Model mismatch: snapshot is for "${snapshot.model}" but current model is "${model.name}"`);
|
|
105
96
|
}
|
|
106
97
|
cpu.restoreState(snapshot.state);
|
package/src/sth.js
CHANGED
|
@@ -52,7 +52,7 @@ export class StairwayToHell {
|
|
|
52
52
|
const response = await fetch(name);
|
|
53
53
|
if (!response.ok) throw new Error("Network response was not ok");
|
|
54
54
|
try {
|
|
55
|
-
return utils.unzipDiscImage(new Uint8Array(await response.arrayBuffer())).data;
|
|
55
|
+
return (await utils.unzipDiscImage(new Uint8Array(await response.arrayBuffer()))).data;
|
|
56
56
|
} catch (error) {
|
|
57
57
|
console.error("Failed to fetch file:", error);
|
|
58
58
|
throw error;
|
package/src/tapes.js
CHANGED
|
@@ -245,8 +245,8 @@ class TapefileTape {
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
export function loadTapeFromData(name, data) {
|
|
249
|
-
const stream =
|
|
248
|
+
export async function loadTapeFromData(name, data) {
|
|
249
|
+
const stream = await utils.DataStream.create(name, data);
|
|
250
250
|
if (stream.readByte(0) === 0xff && stream.readByte(1) === 0x04) {
|
|
251
251
|
console.log("Detected a 'tapefile' tape");
|
|
252
252
|
return new TapefileTape(stream);
|