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
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// B-em snapshot format parser (versions 1 and 3).
|
|
4
|
+
// v1 (BEMSNAP1): Fixed-size 327,885 byte packed struct. Reference: beebjit state.c
|
|
5
|
+
// v3 (BEMSNAP3): Section-based with key+size headers, zlib-compressed memory. Reference: b-em savestate.c
|
|
6
|
+
|
|
7
|
+
const BemV1Size = 327885;
|
|
8
|
+
|
|
9
|
+
// v1 struct offsets
|
|
10
|
+
const V1Off = {
|
|
11
|
+
signature: 0,
|
|
12
|
+
model: 8,
|
|
13
|
+
a: 9,
|
|
14
|
+
x: 10,
|
|
15
|
+
y: 11,
|
|
16
|
+
flags: 12,
|
|
17
|
+
s: 13,
|
|
18
|
+
pc: 14,
|
|
19
|
+
nmi: 16,
|
|
20
|
+
interrupt: 17,
|
|
21
|
+
cycles: 18,
|
|
22
|
+
fe30: 22,
|
|
23
|
+
fe34: 23,
|
|
24
|
+
ram: 24,
|
|
25
|
+
rom: 24 + 65536,
|
|
26
|
+
sysvia: 24 + 65536 + 262144,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if an ArrayBuffer looks like a b-em snapshot (any version).
|
|
31
|
+
* @param {ArrayBuffer} buffer
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
export function isBemSnapshot(buffer) {
|
|
35
|
+
if (buffer.byteLength < 8) return false;
|
|
36
|
+
const sig = String.fromCharCode(...new Uint8Array(buffer, 0, 7));
|
|
37
|
+
return sig === "BEMSNAP";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a b-em snapshot (v1 or v3) into a jsbeeb snapshot object.
|
|
42
|
+
* @param {ArrayBuffer} buffer
|
|
43
|
+
* @returns {object} jsbeeb snapshot
|
|
44
|
+
*/
|
|
45
|
+
export async function parseBemSnapshot(buffer) {
|
|
46
|
+
if (buffer.byteLength < 8) throw new Error("File too small to be a b-em snapshot");
|
|
47
|
+
const bytes = new Uint8Array(buffer);
|
|
48
|
+
const sig = String.fromCharCode(...bytes.slice(0, 8));
|
|
49
|
+
|
|
50
|
+
if (sig === "BEMSNAP1") return parseBemV1(buffer);
|
|
51
|
+
if (sig === "BEMSNAP3") return parseBemV3(buffer);
|
|
52
|
+
throw new Error(`Unsupported b-em snapshot version: "${sig}"`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Shared helpers ──────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function readViaFromBytes(data, offset) {
|
|
58
|
+
const view = new DataView(data.buffer, data.byteOffset + offset);
|
|
59
|
+
return {
|
|
60
|
+
ora: data[offset],
|
|
61
|
+
orb: data[offset + 1],
|
|
62
|
+
ira: data[offset + 2],
|
|
63
|
+
irb: data[offset + 3],
|
|
64
|
+
// +4, +5 are port read values (ignored on load, matching b-em via_loadstate)
|
|
65
|
+
ddra: data[offset + 6],
|
|
66
|
+
ddrb: data[offset + 7],
|
|
67
|
+
sr: data[offset + 8],
|
|
68
|
+
acr: data[offset + 9],
|
|
69
|
+
pcr: data[offset + 10],
|
|
70
|
+
ifr: data[offset + 11],
|
|
71
|
+
ier: data[offset + 12],
|
|
72
|
+
t1l: view.getInt32(13, true),
|
|
73
|
+
t2l: view.getInt32(17, true),
|
|
74
|
+
t1c: view.getInt32(21, true),
|
|
75
|
+
t2c: view.getInt32(25, true),
|
|
76
|
+
t1hit: data[offset + 29],
|
|
77
|
+
t2hit: data[offset + 30],
|
|
78
|
+
ca1: data[offset + 31],
|
|
79
|
+
ca2: data[offset + 32],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ViaDataSize = 33; // 13 bytes + 4*4 timer ints + 4 booleans
|
|
84
|
+
|
|
85
|
+
function readCpuFromBytes(data) {
|
|
86
|
+
return {
|
|
87
|
+
a: data[0],
|
|
88
|
+
x: data[1],
|
|
89
|
+
y: data[2],
|
|
90
|
+
flags: data[3],
|
|
91
|
+
s: data[4],
|
|
92
|
+
pc: data[5] | (data[6] << 8),
|
|
93
|
+
nmi: data[7],
|
|
94
|
+
interrupt: data[8],
|
|
95
|
+
cycles: data[9] | (data[10] << 8) | (data[11] << 16) | (data[12] << 24),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function convertViaState(bemVia, ic32) {
|
|
100
|
+
const result = {
|
|
101
|
+
ora: bemVia.ora,
|
|
102
|
+
orb: bemVia.orb,
|
|
103
|
+
ira: bemVia.ira,
|
|
104
|
+
irb: bemVia.irb,
|
|
105
|
+
ddra: bemVia.ddra,
|
|
106
|
+
ddrb: bemVia.ddrb,
|
|
107
|
+
sr: bemVia.sr,
|
|
108
|
+
acr: bemVia.acr,
|
|
109
|
+
pcr: bemVia.pcr,
|
|
110
|
+
ifr: bemVia.ifr,
|
|
111
|
+
ier: bemVia.ier,
|
|
112
|
+
t1l: bemVia.t1l,
|
|
113
|
+
t2l: bemVia.t2l,
|
|
114
|
+
t1c: bemVia.t1c,
|
|
115
|
+
t2c: bemVia.t2c,
|
|
116
|
+
t1hit: !!bemVia.t1hit,
|
|
117
|
+
t2hit: !!bemVia.t2hit,
|
|
118
|
+
portapins: 0xff,
|
|
119
|
+
portbpins: 0xff,
|
|
120
|
+
ca1: !!bemVia.ca1,
|
|
121
|
+
ca2: !!bemVia.ca2,
|
|
122
|
+
cb1: false,
|
|
123
|
+
cb2: false,
|
|
124
|
+
justhit: 0,
|
|
125
|
+
t1_pb7: (bemVia.orb >> 7) & 1,
|
|
126
|
+
lastPolltime: 0,
|
|
127
|
+
taskOffset: 1,
|
|
128
|
+
};
|
|
129
|
+
if (ic32 !== undefined) {
|
|
130
|
+
result.IC32 = ic32;
|
|
131
|
+
result.capsLockLight = !(ic32 & 0x40);
|
|
132
|
+
result.shiftLockLight = !(ic32 & 0x80);
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Volume lookup table matching soundchip.js
|
|
138
|
+
const volumeTable = new Float32Array(16);
|
|
139
|
+
(() => {
|
|
140
|
+
let f = 1.0;
|
|
141
|
+
for (let i = 0; i < 15; ++i) {
|
|
142
|
+
volumeTable[i] = f / 4;
|
|
143
|
+
f *= Math.pow(10, -0.1);
|
|
144
|
+
}
|
|
145
|
+
volumeTable[15] = 0;
|
|
146
|
+
})();
|
|
147
|
+
|
|
148
|
+
function convertSoundState(snLatch, snCount, snStat, snVol, snNoise, snShift) {
|
|
149
|
+
const registers = new Uint16Array(4);
|
|
150
|
+
const counter = new Float32Array(4);
|
|
151
|
+
const outputBit = [false, false, false, false];
|
|
152
|
+
const volume = new Float32Array(4);
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < 4; ++i) {
|
|
155
|
+
const snChannel = 3 - i;
|
|
156
|
+
let period = snLatch[snChannel] >> 6;
|
|
157
|
+
let count = snCount[snChannel] >> 6;
|
|
158
|
+
if (i === 3) {
|
|
159
|
+
period >>= 1;
|
|
160
|
+
count >>= 1;
|
|
161
|
+
}
|
|
162
|
+
registers[i] = period;
|
|
163
|
+
counter[i] = count;
|
|
164
|
+
outputBit[i] = snStat[snChannel] < 16;
|
|
165
|
+
volume[i] = volumeTable[snVol[snChannel] & 0x0f];
|
|
166
|
+
}
|
|
167
|
+
registers[3] = snNoise & 0x07;
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
registers,
|
|
171
|
+
counter,
|
|
172
|
+
outputBit,
|
|
173
|
+
volume,
|
|
174
|
+
lfsr: snShift,
|
|
175
|
+
latchedRegister: 0,
|
|
176
|
+
residual: 0,
|
|
177
|
+
sineOn: false,
|
|
178
|
+
sineStep: 0,
|
|
179
|
+
sineTime: 0,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildVideoState(ulaControl, ulaPalette, crtcRegs, nulaCollook, crtcCounters) {
|
|
184
|
+
const regs = new Uint8Array(32);
|
|
185
|
+
regs.set(crtcRegs.slice(0, 18));
|
|
186
|
+
const actualPal = new Uint8Array(16);
|
|
187
|
+
for (let i = 0; i < 16; i++) actualPal[i] = ulaPalette[i] & 0x0f;
|
|
188
|
+
|
|
189
|
+
// Use NULA collook if provided, otherwise use default BBC palette
|
|
190
|
+
const collook =
|
|
191
|
+
nulaCollook ||
|
|
192
|
+
new Int32Array([
|
|
193
|
+
0xff000000, 0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff, 0xff000000,
|
|
194
|
+
0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff,
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
// Compute ulaPal from actualPal + collook, matching jsbeeb's Ula._recomputeUlaPal
|
|
198
|
+
const flashEnabled = !!(ulaControl & 1);
|
|
199
|
+
const defaultFlash = new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1]);
|
|
200
|
+
const flash = defaultFlash;
|
|
201
|
+
const ulaPal = new Int32Array(16);
|
|
202
|
+
for (let i = 0; i < 16; i++) {
|
|
203
|
+
const palVal = actualPal[i];
|
|
204
|
+
let colour = collook[(palVal & 0xf) ^ 7];
|
|
205
|
+
if (palVal & 8 && flashEnabled && flash[(palVal & 7) ^ 7]) {
|
|
206
|
+
colour = collook[palVal & 0xf];
|
|
207
|
+
}
|
|
208
|
+
ulaPal[i] = colour;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
regs,
|
|
213
|
+
bitmapX: 0,
|
|
214
|
+
bitmapY: 0,
|
|
215
|
+
oddClock: false,
|
|
216
|
+
frameCount: 0,
|
|
217
|
+
doEvenFrameLogic: false,
|
|
218
|
+
isEvenRender: true,
|
|
219
|
+
lastRenderWasEven: false,
|
|
220
|
+
firstScanline: true,
|
|
221
|
+
inHSync: false,
|
|
222
|
+
inVSync: false,
|
|
223
|
+
hadVSyncThisRow: false,
|
|
224
|
+
checkVertAdjust: false,
|
|
225
|
+
endOfMainLatched: false,
|
|
226
|
+
endOfVertAdjustLatched: false,
|
|
227
|
+
endOfFrameLatched: false,
|
|
228
|
+
inVertAdjust: false,
|
|
229
|
+
inDummyRaster: false,
|
|
230
|
+
hpulseWidth: regs[3] & 0x0f,
|
|
231
|
+
vpulseWidth: (regs[3] & 0xf0) >>> 4,
|
|
232
|
+
hpulseCounter: 0,
|
|
233
|
+
vpulseCounter: 0,
|
|
234
|
+
dispEnabled: 0x3f,
|
|
235
|
+
horizCounter: crtcCounters ? crtcCounters.hc : 0,
|
|
236
|
+
vertCounter: crtcCounters ? crtcCounters.vc : 0,
|
|
237
|
+
scanlineCounter: crtcCounters ? crtcCounters.sc : 0,
|
|
238
|
+
vertAdjustCounter: 0,
|
|
239
|
+
addr: crtcCounters ? crtcCounters.ma : (regs[13] | (regs[12] << 8)) & 0x3fff,
|
|
240
|
+
lineStartAddr: crtcCounters ? crtcCounters.maback : (regs[13] | (regs[12] << 8)) & 0x3fff,
|
|
241
|
+
nextLineStartAddr: crtcCounters ? crtcCounters.maback : (regs[13] | (regs[12] << 8)) & 0x3fff,
|
|
242
|
+
ulactrl: ulaControl,
|
|
243
|
+
pixelsPerChar: ulaControl & 0x10 ? 8 : 16,
|
|
244
|
+
halfClock: !(ulaControl & 0x10),
|
|
245
|
+
ulaMode: (ulaControl >>> 2) & 3,
|
|
246
|
+
teletextMode: !!(ulaControl & 2),
|
|
247
|
+
displayEnableSkew: Math.min((regs[8] & 0x30) >>> 4, 2),
|
|
248
|
+
ulaPal,
|
|
249
|
+
actualPal,
|
|
250
|
+
cursorOn: false,
|
|
251
|
+
cursorOff: false,
|
|
252
|
+
cursorOnThisFrame: false,
|
|
253
|
+
cursorDrawIndex: 0,
|
|
254
|
+
cursorPos: (regs[15] | (regs[14] << 8)) & 0x3fff,
|
|
255
|
+
interlacedSyncAndVideo: (regs[8] & 3) === 3,
|
|
256
|
+
screenSubtract: 0,
|
|
257
|
+
ula: {
|
|
258
|
+
collook: collook.slice(),
|
|
259
|
+
flash: new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1]),
|
|
260
|
+
paletteWriteFlag: false,
|
|
261
|
+
paletteFirstByte: 0,
|
|
262
|
+
paletteMode: 0,
|
|
263
|
+
horizontalOffset: 0,
|
|
264
|
+
leftBlank: 0,
|
|
265
|
+
disabled: false,
|
|
266
|
+
attributeMode: 0,
|
|
267
|
+
attributeText: 0,
|
|
268
|
+
},
|
|
269
|
+
crtc: { curReg: 0 },
|
|
270
|
+
teletext: {
|
|
271
|
+
prevCol: 0,
|
|
272
|
+
col: 7,
|
|
273
|
+
bg: 0,
|
|
274
|
+
sep: false,
|
|
275
|
+
dbl: false,
|
|
276
|
+
oldDbl: false,
|
|
277
|
+
secondHalfOfDouble: false,
|
|
278
|
+
wasDbl: false,
|
|
279
|
+
gfx: false,
|
|
280
|
+
flash: false,
|
|
281
|
+
flashOn: false,
|
|
282
|
+
flashTime: 0,
|
|
283
|
+
heldChar: 0,
|
|
284
|
+
holdChar: false,
|
|
285
|
+
dataQueue: [0, 0, 0, 0],
|
|
286
|
+
scanlineCounter: 0,
|
|
287
|
+
levelDEW: false,
|
|
288
|
+
levelDISPTMG: false,
|
|
289
|
+
levelRA0: false,
|
|
290
|
+
nextGlyphs: "normal",
|
|
291
|
+
curGlyphs: "normal",
|
|
292
|
+
heldGlyphs: "normal",
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const DefaultAcia = {
|
|
298
|
+
sr: 0x02,
|
|
299
|
+
cr: 0x00,
|
|
300
|
+
dr: 0x00,
|
|
301
|
+
rs423Selected: false,
|
|
302
|
+
motorOn: false,
|
|
303
|
+
tapeCarrierCount: 0,
|
|
304
|
+
tapeDcdLineLevel: false,
|
|
305
|
+
hadDcdHigh: false,
|
|
306
|
+
serialReceiveRate: 19200,
|
|
307
|
+
serialReceiveCyclesPerByte: 0,
|
|
308
|
+
txCompleteTaskOffset: null,
|
|
309
|
+
runTapeTaskOffset: null,
|
|
310
|
+
runRs423TaskOffset: null,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const DefaultAdc = { status: 0x40, low: 0x00, high: 0x00, taskOffset: null };
|
|
314
|
+
|
|
315
|
+
function buildSnapshot(modelName, cpuState, ram, roms, sysvia, uservia, video, soundChip) {
|
|
316
|
+
return {
|
|
317
|
+
format: "jsbeeb-snapshot",
|
|
318
|
+
version: 1,
|
|
319
|
+
model: modelName,
|
|
320
|
+
timestamp: new Date().toISOString(),
|
|
321
|
+
importedFrom: "b-em",
|
|
322
|
+
state: {
|
|
323
|
+
a: cpuState.a,
|
|
324
|
+
x: cpuState.x,
|
|
325
|
+
y: cpuState.y,
|
|
326
|
+
s: cpuState.s,
|
|
327
|
+
pc: cpuState.pc,
|
|
328
|
+
p: cpuState.flags | 0x30,
|
|
329
|
+
nmiLevel: !!cpuState.nmi,
|
|
330
|
+
nmiEdge: false,
|
|
331
|
+
halted: false,
|
|
332
|
+
takeInt: false,
|
|
333
|
+
romsel: cpuState.fe30 ?? 0,
|
|
334
|
+
acccon: cpuState.fe34 ?? 0,
|
|
335
|
+
videoDisplayPage: 0,
|
|
336
|
+
currentCycles: 0,
|
|
337
|
+
targetCycles: 0,
|
|
338
|
+
cycleSeconds: 0,
|
|
339
|
+
peripheralCycles: 0,
|
|
340
|
+
videoCycles: 0,
|
|
341
|
+
music5000PageSel: 0,
|
|
342
|
+
ram,
|
|
343
|
+
roms,
|
|
344
|
+
scheduler: { epoch: 0 },
|
|
345
|
+
sysvia,
|
|
346
|
+
uservia,
|
|
347
|
+
video,
|
|
348
|
+
soundChip,
|
|
349
|
+
acia: { ...DefaultAcia },
|
|
350
|
+
adc: { ...DefaultAdc },
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ── V1 parser (BEMSNAP1, fixed struct) ──────────────────────────────
|
|
356
|
+
|
|
357
|
+
function parseBemV1(buffer) {
|
|
358
|
+
if (buffer.byteLength !== BemV1Size) {
|
|
359
|
+
throw new Error(`Invalid BEM v1 snapshot size: expected ${BemV1Size}, got ${buffer.byteLength}`);
|
|
360
|
+
}
|
|
361
|
+
const bytes = new Uint8Array(buffer);
|
|
362
|
+
const view = new DataView(buffer);
|
|
363
|
+
|
|
364
|
+
const bemModel = bytes[V1Off.model];
|
|
365
|
+
if (bemModel !== 3 && bemModel !== 4) {
|
|
366
|
+
throw new Error(`Unsupported BEM v1 model: ${bemModel} (only BBC Model B supported)`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const cpuState = {
|
|
370
|
+
...readCpuFromBytes(bytes.slice(V1Off.a)),
|
|
371
|
+
fe30: bytes[V1Off.fe30],
|
|
372
|
+
fe34: bytes[V1Off.fe34],
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const ram = new Uint8Array(128 * 1024);
|
|
376
|
+
ram.set(bytes.slice(V1Off.ram, V1Off.ram + 65536));
|
|
377
|
+
const roms = bytes.slice(V1Off.rom, V1Off.rom + 262144);
|
|
378
|
+
|
|
379
|
+
const sysVia = readViaFromBytes(bytes, V1Off.sysvia);
|
|
380
|
+
const sysViaIC32 = bytes[V1Off.sysvia + ViaDataSize];
|
|
381
|
+
const userVia = readViaFromBytes(bytes, V1Off.sysvia + ViaDataSize + 1);
|
|
382
|
+
|
|
383
|
+
const ulaOff = V1Off.sysvia + ViaDataSize + 1 + ViaDataSize;
|
|
384
|
+
const ulaControl = bytes[ulaOff];
|
|
385
|
+
const ulaPalette = bytes.slice(ulaOff + 1, ulaOff + 17);
|
|
386
|
+
const crtcOff = ulaOff + 17;
|
|
387
|
+
const crtcRegs = bytes.slice(crtcOff, crtcOff + 18);
|
|
388
|
+
|
|
389
|
+
const soundOff = crtcOff + 18 + 7 + 4 + 1 + 4;
|
|
390
|
+
const snLatch = [],
|
|
391
|
+
snCount = [],
|
|
392
|
+
snStat = [],
|
|
393
|
+
snVol = [];
|
|
394
|
+
for (let i = 0; i < 4; i++) {
|
|
395
|
+
snLatch.push(view.getUint32(soundOff + i * 4, true));
|
|
396
|
+
snCount.push(view.getUint32(soundOff + 16 + i * 4, true));
|
|
397
|
+
snStat.push(view.getUint32(soundOff + 32 + i * 4, true));
|
|
398
|
+
snVol.push(bytes[soundOff + 48 + i]);
|
|
399
|
+
}
|
|
400
|
+
const snNoise = bytes[soundOff + 52];
|
|
401
|
+
const snShift = view.getUint16(soundOff + 53, true);
|
|
402
|
+
|
|
403
|
+
return buildSnapshot(
|
|
404
|
+
"B",
|
|
405
|
+
cpuState,
|
|
406
|
+
ram,
|
|
407
|
+
roms,
|
|
408
|
+
convertViaState(sysVia, sysViaIC32),
|
|
409
|
+
convertViaState(userVia),
|
|
410
|
+
buildVideoState(ulaControl, ulaPalette, crtcRegs),
|
|
411
|
+
convertSoundState(snLatch, snCount, snStat, snVol, snNoise, snShift),
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── V3 parser (BEMSNAP3, section-based) ─────────────────────────────
|
|
416
|
+
|
|
417
|
+
// Variable-length integer encoding used by b-em v3
|
|
418
|
+
function readVar(data, pos) {
|
|
419
|
+
let value = 0;
|
|
420
|
+
let shift = 0;
|
|
421
|
+
while (pos.offset < data.length) {
|
|
422
|
+
const byte = data[pos.offset++];
|
|
423
|
+
value |= (byte & 0x7f) << shift;
|
|
424
|
+
if (byte & 0x80) break;
|
|
425
|
+
shift += 7;
|
|
426
|
+
}
|
|
427
|
+
return value;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function readString(data, pos) {
|
|
431
|
+
const len = readVar(data, pos);
|
|
432
|
+
const str = String.fromCharCode(...data.slice(pos.offset, pos.offset + len));
|
|
433
|
+
pos.offset += len;
|
|
434
|
+
return str;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function inflateRaw(compressedData) {
|
|
438
|
+
// Use DecompressionStream (available in modern browsers and Node 18+)
|
|
439
|
+
if (typeof DecompressionStream !== "undefined") {
|
|
440
|
+
const ds = new DecompressionStream("deflate");
|
|
441
|
+
const writer = ds.writable.getWriter();
|
|
442
|
+
const reader = ds.readable.getReader();
|
|
443
|
+
const chunks = [];
|
|
444
|
+
|
|
445
|
+
// Start reading before writing to avoid backpressure deadlock
|
|
446
|
+
const readPromise = (async () => {
|
|
447
|
+
for (;;) {
|
|
448
|
+
const { done, value } = await reader.read();
|
|
449
|
+
if (done) break;
|
|
450
|
+
chunks.push(value);
|
|
451
|
+
}
|
|
452
|
+
})();
|
|
453
|
+
|
|
454
|
+
await writer.write(compressedData);
|
|
455
|
+
await writer.close();
|
|
456
|
+
await readPromise;
|
|
457
|
+
|
|
458
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
459
|
+
const result = new Uint8Array(totalLength);
|
|
460
|
+
let offset = 0;
|
|
461
|
+
for (const chunk of chunks) {
|
|
462
|
+
result.set(chunk, offset);
|
|
463
|
+
offset += chunk.length;
|
|
464
|
+
}
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
throw new Error("Zlib decompression not available (need DecompressionStream API)");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function parseBemV3(buffer) {
|
|
471
|
+
const bytes = new Uint8Array(buffer);
|
|
472
|
+
let offset = 8; // Skip "BEMSNAP3" signature
|
|
473
|
+
|
|
474
|
+
const sections = {};
|
|
475
|
+
while (offset < bytes.length) {
|
|
476
|
+
let key = bytes[offset];
|
|
477
|
+
let size = bytes[offset + 1] | (bytes[offset + 2] << 8);
|
|
478
|
+
let headerSize = 3;
|
|
479
|
+
|
|
480
|
+
if (key & 0x80) {
|
|
481
|
+
// Extended size (4 bytes) for compressed sections
|
|
482
|
+
key &= 0x7f;
|
|
483
|
+
size |= (bytes[offset + 3] << 16) | (bytes[offset + 4] << 24);
|
|
484
|
+
headerSize = 5;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const sectionData = bytes.slice(offset + headerSize, offset + headerSize + size);
|
|
488
|
+
const keyChar = String.fromCharCode(key);
|
|
489
|
+
sections[keyChar] = { data: sectionData, compressed: headerSize === 5 };
|
|
490
|
+
offset += headerSize + size;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Parse model section to determine jsbeeb model name.
|
|
494
|
+
// Use jsbeeb synonyms (from models.js) so findModel() resolves them.
|
|
495
|
+
let modelName = "B";
|
|
496
|
+
if (sections["m"]) {
|
|
497
|
+
const pos = { offset: 0 };
|
|
498
|
+
const data = sections["m"].data;
|
|
499
|
+
readVar(data, pos); // curmodel index (skip)
|
|
500
|
+
const name = readString(data, pos);
|
|
501
|
+
if (name.includes("Master")) {
|
|
502
|
+
if (name.includes("ADFS")) modelName = "MasterADFS";
|
|
503
|
+
else if (name.includes("ANFS")) modelName = "MasterANFS";
|
|
504
|
+
else modelName = "Master";
|
|
505
|
+
} else if (name.includes("1770")) {
|
|
506
|
+
if (name.includes("ADFS")) modelName = "B1770A";
|
|
507
|
+
else modelName = "B1770";
|
|
508
|
+
} else {
|
|
509
|
+
modelName = "B";
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Parse CPU
|
|
514
|
+
let cpuState = { a: 0, x: 0, y: 0, flags: 0x30, s: 0xff, pc: 0, nmi: 0, interrupt: 0, cycles: 0 };
|
|
515
|
+
if (sections["6"]) {
|
|
516
|
+
cpuState = readCpuFromBytes(sections["6"].data);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Parse memory (zlib-compressed in v3)
|
|
520
|
+
const ram = new Uint8Array(128 * 1024);
|
|
521
|
+
let roms = null;
|
|
522
|
+
const memSection = sections["M"];
|
|
523
|
+
if (memSection) {
|
|
524
|
+
// Memory is zlib-compressed; decompression is async.
|
|
525
|
+
// Decompressed layout: 2 bytes (fe30, fe34) + 64KB RAM + 256KB ROM
|
|
526
|
+
return inflateRaw(memSection.data).then((memData) => {
|
|
527
|
+
cpuState.fe30 = memData[0];
|
|
528
|
+
cpuState.fe34 = memData[1];
|
|
529
|
+
const ramStart = 2;
|
|
530
|
+
const ramSize = 64 * 1024;
|
|
531
|
+
ram.set(memData.slice(ramStart, ramStart + ramSize));
|
|
532
|
+
const romStart = ramStart + ramSize;
|
|
533
|
+
if (memData.length > romStart) {
|
|
534
|
+
roms = memData.slice(romStart, romStart + 262144);
|
|
535
|
+
}
|
|
536
|
+
return finishV3Parse(modelName, cpuState, ram, roms, sections);
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return finishV3Parse(modelName, cpuState, ram, roms, sections);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function finishV3Parse(modelName, cpuState, ram, roms, sections) {
|
|
543
|
+
// Parse system VIA
|
|
544
|
+
let sysvia = convertViaState(
|
|
545
|
+
{
|
|
546
|
+
ora: 0,
|
|
547
|
+
orb: 0,
|
|
548
|
+
ira: 0,
|
|
549
|
+
irb: 0,
|
|
550
|
+
ddra: 0,
|
|
551
|
+
ddrb: 0,
|
|
552
|
+
sr: 0,
|
|
553
|
+
acr: 0,
|
|
554
|
+
pcr: 0,
|
|
555
|
+
ifr: 0,
|
|
556
|
+
ier: 0,
|
|
557
|
+
t1l: 0x1fffe,
|
|
558
|
+
t2l: 0x1fffe,
|
|
559
|
+
t1c: 0x1fffe,
|
|
560
|
+
t2c: 0x1fffe,
|
|
561
|
+
t1hit: 1,
|
|
562
|
+
t2hit: 1,
|
|
563
|
+
ca1: 0,
|
|
564
|
+
ca2: 0,
|
|
565
|
+
},
|
|
566
|
+
0,
|
|
567
|
+
);
|
|
568
|
+
if (sections["S"]) {
|
|
569
|
+
const data = sections["S"].data;
|
|
570
|
+
const via = readViaFromBytes(data, 0);
|
|
571
|
+
const ic32 = data.length > ViaDataSize ? data[ViaDataSize] : 0;
|
|
572
|
+
sysvia = convertViaState(via, ic32);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Parse user VIA
|
|
576
|
+
let uservia = convertViaState({
|
|
577
|
+
ora: 0,
|
|
578
|
+
orb: 0,
|
|
579
|
+
ira: 0,
|
|
580
|
+
irb: 0,
|
|
581
|
+
ddra: 0,
|
|
582
|
+
ddrb: 0,
|
|
583
|
+
sr: 0,
|
|
584
|
+
acr: 0,
|
|
585
|
+
pcr: 0,
|
|
586
|
+
ifr: 0,
|
|
587
|
+
ier: 0,
|
|
588
|
+
t1l: 0x1fffe,
|
|
589
|
+
t2l: 0x1fffe,
|
|
590
|
+
t1c: 0x1fffe,
|
|
591
|
+
t2c: 0x1fffe,
|
|
592
|
+
t1hit: 1,
|
|
593
|
+
t2hit: 1,
|
|
594
|
+
ca1: 0,
|
|
595
|
+
ca2: 0,
|
|
596
|
+
});
|
|
597
|
+
if (sections["U"]) {
|
|
598
|
+
uservia = convertViaState(readViaFromBytes(sections["U"].data, 0));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Parse Video ULA
|
|
602
|
+
// v3 section layout (97 bytes):
|
|
603
|
+
// 1: ula_ctrl
|
|
604
|
+
// 16: ula_palbak[16] (raw palette register values)
|
|
605
|
+
// 64: nula_collook[16] (4 bytes each: R, G, B, A in Allegro RGBA format)
|
|
606
|
+
// 1: nula_pal_write_flag
|
|
607
|
+
// 1: nula_pal_first_byte
|
|
608
|
+
// 8: nula_flash[8]
|
|
609
|
+
// 1: nula_palette_mode
|
|
610
|
+
// ... (more NULA state follows)
|
|
611
|
+
let ulaControl = 0;
|
|
612
|
+
let ulaPalette = new Uint8Array(16);
|
|
613
|
+
let nulaCollook = null;
|
|
614
|
+
if (sections["V"]) {
|
|
615
|
+
const data = sections["V"].data;
|
|
616
|
+
ulaControl = data[0];
|
|
617
|
+
ulaPalette = data.slice(1, 17);
|
|
618
|
+
// Parse NULA collook if section is large enough (v3 has 97 bytes)
|
|
619
|
+
if (data.length >= 81) {
|
|
620
|
+
nulaCollook = new Int32Array(16);
|
|
621
|
+
for (let c = 0; c < 16; c++) {
|
|
622
|
+
const off = 17 + c * 4;
|
|
623
|
+
// b-em stores as R, G, B, A (Allegro format)
|
|
624
|
+
// jsbeeb uses ABGR format (Uint32 on little-endian = canvas RGBA)
|
|
625
|
+
const r = data[off];
|
|
626
|
+
const g = data[off + 1];
|
|
627
|
+
const b = data[off + 2];
|
|
628
|
+
const a = data[off + 3];
|
|
629
|
+
nulaCollook[c] = (a << 24) | (b << 16) | (g << 8) | r;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Parse CRTC (18 regs + optional 7 counter bytes: vc, sc, hc, ma_lo, ma_hi, maback_lo, maback_hi)
|
|
635
|
+
let crtcRegs = new Uint8Array(18);
|
|
636
|
+
let crtcCounters = null;
|
|
637
|
+
if (sections["C"]) {
|
|
638
|
+
const data = sections["C"].data;
|
|
639
|
+
crtcRegs = data.slice(0, 18);
|
|
640
|
+
if (data.length >= 25) {
|
|
641
|
+
crtcCounters = {
|
|
642
|
+
vc: data[18],
|
|
643
|
+
sc: data[19],
|
|
644
|
+
hc: data[20],
|
|
645
|
+
ma: data[21] | (data[22] << 8),
|
|
646
|
+
maback: data[23] | (data[24] << 8),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Parse sound
|
|
652
|
+
let soundChip = convertSoundState([0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], 0, 1 << 14);
|
|
653
|
+
if (sections["s"]) {
|
|
654
|
+
const data = sections["s"].data;
|
|
655
|
+
const view = new DataView(data.buffer, data.byteOffset);
|
|
656
|
+
const snLatch = [],
|
|
657
|
+
snCount = [],
|
|
658
|
+
snStat = [],
|
|
659
|
+
snVol = [];
|
|
660
|
+
for (let i = 0; i < 4; i++) {
|
|
661
|
+
snLatch.push(view.getUint32(i * 4, true));
|
|
662
|
+
snCount.push(view.getUint32(16 + i * 4, true));
|
|
663
|
+
snStat.push(view.getUint32(32 + i * 4, true));
|
|
664
|
+
snVol.push(data[48 + i]);
|
|
665
|
+
}
|
|
666
|
+
const snNoise = data[52];
|
|
667
|
+
const snShift = view.getUint16(53, true);
|
|
668
|
+
soundChip = convertSoundState(snLatch, snCount, snStat, snVol, snNoise, snShift);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return buildSnapshot(
|
|
672
|
+
modelName,
|
|
673
|
+
cpuState,
|
|
674
|
+
ram,
|
|
675
|
+
roms,
|
|
676
|
+
sysvia,
|
|
677
|
+
uservia,
|
|
678
|
+
buildVideoState(ulaControl, ulaPalette, crtcRegs, nulaCollook, crtcCounters),
|
|
679
|
+
soundChip,
|
|
680
|
+
);
|
|
681
|
+
}
|
package/src/cmos.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
// CMOS layout: bytes 0-13 are RTC internal registers; bytes 14-63 are the 50
|
|
4
|
+
// bytes of user storage. Storage byte 11 (= CMOS address 25 = 0x19) holds
|
|
5
|
+
// the BBC Master DFS configuration byte whose bits [1:0] are the WD1770 step
|
|
6
|
+
// rate (*CONFIGURE FDRIVE): 0=6ms 1=12ms 2=20ms 3=30ms.
|
|
7
|
+
// The correct default is FDRIVE 0 (6ms, value 0xc8). The previous default
|
|
8
|
+
// 0xca had bits[1:0]=0b10 (20ms), which caused disc-streaming demos such as
|
|
9
|
+
// STNICC-beeb to hang on the Master 128 because the WD1770 seek overran the
|
|
10
|
+
// vsync window. Confirmed by @tom-seddon (b2 emulator):
|
|
11
|
+
// https://github.com/mattgodbolt/jsbeeb/issues/576
|
|
3
12
|
const defaultCmos = [
|
|
4
13
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0xeb, 0x00,
|
|
5
|
-
0xc9, 0xff, 0xff, 0x12, 0x00, 0x17,
|
|
14
|
+
0xc9, 0xff, 0xff, 0x12, 0x00, 0x17, 0xc8, 0x1e, 0x05, 0x00, 0x35, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
6
15
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
7
16
|
];
|
|
8
17
|
|
|
@@ -22,6 +31,8 @@ function fromBcd(value) {
|
|
|
22
31
|
return parseInt(value.toString(16), 10);
|
|
23
32
|
}
|
|
24
33
|
|
|
34
|
+
export { defaultCmos };
|
|
35
|
+
|
|
25
36
|
export class Cmos {
|
|
26
37
|
constructor(persistence, cmosOverride, econet) {
|
|
27
38
|
this.store = persistence ? persistence.load() : null;
|