jsbeeb 1.13.0 → 1.14.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 +3 -2
- package/package.json +23 -16
- package/src/6502.js +74 -16
- package/src/app/app.js +2 -15
- package/src/app/args.js +26 -0
- package/src/bem-snapshot.js +154 -15
- package/src/config.js +100 -81
- package/src/fake6502.js +7 -2
- package/src/keyboard.js +3 -2
- package/src/machine-session.js +11 -2
- package/src/main.js +79 -67
- package/src/models.js +24 -4
- package/src/mouse-coordinates.js +16 -0
- package/src/snapshot-helpers.js +16 -2
- package/src/snapshot.js +19 -1
- package/src/soundchip.js +23 -5
- package/src/teletext_adaptor.js +2 -1
- package/src/tube.js +33 -1
- package/src/web/audio-handler.js +4 -1
- package/tests/test-machine.js +7 -5
package/src/snapshot-helpers.js
CHANGED
|
@@ -166,12 +166,25 @@ export function buildVideoState(ulaControl, ulaPalette, crtcRegs, nulaCollook, c
|
|
|
166
166
|
* @param {object} uservia - jsbeeb user VIA state
|
|
167
167
|
* @param {object} video - jsbeeb video state (from buildVideoState)
|
|
168
168
|
* @param {object} soundChip - jsbeeb sound chip state
|
|
169
|
+
* @param {object|null} [tube] - jsbeeb tube state, or null if no co-processor was fitted
|
|
169
170
|
*/
|
|
170
|
-
export function buildSnapshot(
|
|
171
|
+
export function buildSnapshot(
|
|
172
|
+
importedFrom,
|
|
173
|
+
modelName,
|
|
174
|
+
cpuState,
|
|
175
|
+
ram,
|
|
176
|
+
roms,
|
|
177
|
+
sysvia,
|
|
178
|
+
uservia,
|
|
179
|
+
video,
|
|
180
|
+
soundChip,
|
|
181
|
+
tube = null,
|
|
182
|
+
) {
|
|
171
183
|
return {
|
|
172
184
|
format: "jsbeeb-snapshot",
|
|
173
|
-
version:
|
|
185
|
+
version: 3,
|
|
174
186
|
model: modelName,
|
|
187
|
+
coProcessor: !!tube,
|
|
175
188
|
timestamp: new Date().toISOString(),
|
|
176
189
|
importedFrom,
|
|
177
190
|
state: {
|
|
@@ -203,6 +216,7 @@ export function buildSnapshot(importedFrom, modelName, cpuState, ram, roms, sysv
|
|
|
203
216
|
soundChip,
|
|
204
217
|
acia: { ...DefaultAcia },
|
|
205
218
|
adc: { ...DefaultAdc },
|
|
219
|
+
...(tube ? { tube } : {}),
|
|
206
220
|
},
|
|
207
221
|
};
|
|
208
222
|
}
|
package/src/snapshot.js
CHANGED
|
@@ -4,7 +4,15 @@ import { typedArrayToBase64, base64ToTypedArray } from "./state-utils.js";
|
|
|
4
4
|
import { findModel } from "./models.js";
|
|
5
5
|
|
|
6
6
|
const SnapshotFormat = "jsbeeb-snapshot";
|
|
7
|
-
const SnapshotVersion =
|
|
7
|
+
const SnapshotVersion = 3;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Whether a snapshot was taken on a machine with a second processor fitted.
|
|
11
|
+
* Nothing before version 3 captured tube state, so those snapshots are always host-only.
|
|
12
|
+
*/
|
|
13
|
+
export function hasCoProcessor(snapshot) {
|
|
14
|
+
return !!snapshot.coProcessor;
|
|
15
|
+
}
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* Check if two model names resolve to the same model (accounting for
|
|
@@ -70,6 +78,9 @@ export function createSnapshot(cpu, model, media) {
|
|
|
70
78
|
format: SnapshotFormat,
|
|
71
79
|
version: SnapshotVersion,
|
|
72
80
|
model: model.name,
|
|
81
|
+
// The model name cannot distinguish a Turbo from a plain Master: the co-processor is
|
|
82
|
+
// emulation config rather than part of the model.
|
|
83
|
+
coProcessor: cpu.hasTube,
|
|
73
84
|
timestamp: new Date().toISOString(),
|
|
74
85
|
state,
|
|
75
86
|
};
|
|
@@ -94,6 +105,13 @@ export function restoreSnapshot(cpu, model, snapshot) {
|
|
|
94
105
|
if (!isSameModel(snapshot.model, model.name)) {
|
|
95
106
|
throw new Error(`Model mismatch: snapshot is for "${snapshot.model}" but current model is "${model.name}"`);
|
|
96
107
|
}
|
|
108
|
+
if (hasCoProcessor(snapshot) !== cpu.hasTube) {
|
|
109
|
+
const fitted = (yes) => (yes ? "with a second processor" : "without a second processor");
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Co-processor mismatch: snapshot was taken ${fitted(hasCoProcessor(snapshot))} ` +
|
|
112
|
+
`but this machine is ${fitted(cpu.hasTube)}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
97
115
|
cpu.restoreState(snapshot.state);
|
|
98
116
|
}
|
|
99
117
|
|
package/src/soundchip.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// The BBC volumeTable[0] (loudest) is 0.25 (1.0 / 4 channels).
|
|
3
3
|
const speakerVolume = 0.5;
|
|
4
4
|
|
|
5
|
+
// Samples per output chunk handed to the onBuffer callback.
|
|
6
|
+
export const SoundBufferSamples = 512;
|
|
7
|
+
|
|
5
8
|
const volumeTable = new Float32Array(16);
|
|
6
9
|
(() => {
|
|
7
10
|
let f = 1.0;
|
|
@@ -21,6 +24,11 @@ function makeSineTable(attenuation) {
|
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export class SoundChip {
|
|
27
|
+
/**
|
|
28
|
+
* @param {function(Float32Array): void} onBuffer called with each full
|
|
29
|
+
* SoundBufferSamples-sized buffer of output. Receives the same buffer
|
|
30
|
+
* every call, overwritten afterwards: copy the contents if they are kept.
|
|
31
|
+
*/
|
|
24
32
|
constructor(onBuffer) {
|
|
25
33
|
this._onBuffer = onBuffer;
|
|
26
34
|
// 4MHz input signal. Internal divide-by-8
|
|
@@ -62,7 +70,7 @@ export class SoundChip {
|
|
|
62
70
|
|
|
63
71
|
this.residual = 0;
|
|
64
72
|
this.position = 0;
|
|
65
|
-
this.buffer = new Float32Array(
|
|
73
|
+
this.buffer = new Float32Array(SoundBufferSamples);
|
|
66
74
|
|
|
67
75
|
this.latchedRegister = 0;
|
|
68
76
|
this.slowDataBus = 0;
|
|
@@ -211,17 +219,27 @@ export class SoundChip {
|
|
|
211
219
|
const num = cycles * this.samplesPerCycle + this.residual;
|
|
212
220
|
let rounded = num | 0;
|
|
213
221
|
this.residual = num - rounded;
|
|
214
|
-
|
|
222
|
+
// The buffer is deliberately reused for the chip's whole life: the
|
|
223
|
+
// previous transfer-then-reallocate pattern is miscompiled by a V8
|
|
224
|
+
// optimiser bug (Chrome 150, crbug.com/537801199) which allocates the
|
|
225
|
+
// replacement with length 0, even when the reallocation is reordered
|
|
226
|
+
// before the transfer, wedging this loop forever. Reuse is also
|
|
227
|
+
// cheaper, so this needn't be reverted once the crbug is fixed. The
|
|
228
|
+
// guard fails loudly if the buffer is ever detached or the accounting
|
|
229
|
+
// goes bad.
|
|
215
230
|
while (rounded > 0) {
|
|
216
|
-
const leftInBuffer =
|
|
231
|
+
const leftInBuffer = SoundBufferSamples - this.position;
|
|
217
232
|
const numSamplesToGenerate = Math.min(rounded, leftInBuffer);
|
|
233
|
+
if (numSamplesToGenerate <= 0 || this.buffer.length !== SoundBufferSamples)
|
|
234
|
+
throw new Error(
|
|
235
|
+
`Sound buffer accounting error (buffer=${this.buffer.length}, position=${this.position}, rounded=${rounded})`,
|
|
236
|
+
);
|
|
218
237
|
this.generate(this.buffer, this.position, numSamplesToGenerate);
|
|
219
238
|
this.position += numSamplesToGenerate;
|
|
220
239
|
rounded -= numSamplesToGenerate;
|
|
221
240
|
|
|
222
|
-
if (this.position ===
|
|
241
|
+
if (this.position === SoundBufferSamples) {
|
|
223
242
|
this._onBuffer(this.buffer);
|
|
224
|
-
this.buffer = new Float32Array(bufferLength);
|
|
225
243
|
this.position = 0;
|
|
226
244
|
}
|
|
227
245
|
}
|
package/src/teletext_adaptor.js
CHANGED
|
@@ -146,7 +146,8 @@ export class TeletextAdaptor {
|
|
|
146
146
|
this.teletextStatus &= 0x0f;
|
|
147
147
|
this.teletextStatus |= 0xd0; // data ready so latch INT, DOR, and FSYN
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
// The stream arrives asynchronously, so software can enable us before there is anything to copy.
|
|
150
|
+
if (this.teletextEnable && this.streamData) {
|
|
150
151
|
// Copy current stream position into the frame buffer
|
|
151
152
|
for (let i = 0; i < 16; ++i) {
|
|
152
153
|
if (this.streamData[offset + i * 43] !== 0) {
|
package/src/tube.js
CHANGED
|
@@ -219,7 +219,7 @@ export class Tube {
|
|
|
219
219
|
if (this.hostStatus[TUBE_ULA_R3] & TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL) {
|
|
220
220
|
if (this.internalStatusRegister & TUBE_ULA_FLAG_STATUS_ENABLE_2_BYTE_R3_DATA) {
|
|
221
221
|
if (this.hostToParasiteFifoByteCount3 < 2) {
|
|
222
|
-
this.hostToParasiteData[this.hostToParasiteFifoByteCount3++] = value;
|
|
222
|
+
this.hostToParasiteData[TUBE_ULA_R3][this.hostToParasiteFifoByteCount3++] = value;
|
|
223
223
|
}
|
|
224
224
|
if (this.hostToParasiteFifoByteCount3 === 2) {
|
|
225
225
|
this.parasiteStatus[TUBE_ULA_R3] |= TUBE_ULA_FLAG_DATA_AVAILABLE;
|
|
@@ -294,6 +294,38 @@ export class Tube {
|
|
|
294
294
|
}
|
|
295
295
|
return result;
|
|
296
296
|
}
|
|
297
|
+
snapshotState() {
|
|
298
|
+
return {
|
|
299
|
+
internalStatusRegister: this.internalStatusRegister,
|
|
300
|
+
hostStatus: this.hostStatus.slice(),
|
|
301
|
+
parasiteStatus: this.parasiteStatus.slice(),
|
|
302
|
+
parasiteToHostData: this.parasiteToHostData.map((fifo) => fifo.slice()),
|
|
303
|
+
hostToParasiteData: this.hostToParasiteData.map((fifo) => fifo.slice()),
|
|
304
|
+
parasiteToHostFifoByteCount1: this.parasiteToHostFifoByteCount1,
|
|
305
|
+
parasiteToHostFifoByteCount3: this.parasiteToHostFifoByteCount3,
|
|
306
|
+
hostToParasiteFifoByteCount3: this.hostToParasiteFifoByteCount3,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The interrupt and reset lines are not saved: they are derived from the status registers
|
|
312
|
+
* and FIFO counts, in the same way the host's `interrupt` is rebuilt by the VIA and ACIA
|
|
313
|
+
* restores.
|
|
314
|
+
*/
|
|
315
|
+
restoreState(state) {
|
|
316
|
+
this.internalStatusRegister = state.internalStatusRegister;
|
|
317
|
+
this.hostStatus.set(state.hostStatus);
|
|
318
|
+
this.parasiteStatus.set(state.parasiteStatus);
|
|
319
|
+
for (let i = 0; i < 4; i++) {
|
|
320
|
+
this.parasiteToHostData[i].set(state.parasiteToHostData[i]);
|
|
321
|
+
this.hostToParasiteData[i].set(state.hostToParasiteData[i]);
|
|
322
|
+
}
|
|
323
|
+
this.parasiteToHostFifoByteCount1 = state.parasiteToHostFifoByteCount1;
|
|
324
|
+
this.parasiteToHostFifoByteCount3 = state.parasiteToHostFifoByteCount3;
|
|
325
|
+
this.hostToParasiteFifoByteCount3 = state.hostToParasiteFifoByteCount3;
|
|
326
|
+
this.updateInterrupts();
|
|
327
|
+
}
|
|
328
|
+
|
|
297
329
|
parasiteWrite(address, value) {
|
|
298
330
|
// Not implemented - needs to be integrated with the parasite CPU code:
|
|
299
331
|
// Boot mode is terminated by the software when it selects any one of the Tube addresses.
|
package/src/web/audio-handler.js
CHANGED
|
@@ -123,7 +123,10 @@ export class AudioHandler {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
_onBuffer(buffer) {
|
|
126
|
-
|
|
126
|
+
// No transfer list, deliberately: the chip reuses this buffer, and
|
|
127
|
+
// transferring would detach it and trip crbug.com/537801199. The clone
|
|
128
|
+
// costs little (512 floats per 1.024ms of chip output, ~2MB/s).
|
|
129
|
+
if (this._jsAudioNode) this._jsAudioNode.port.postMessage({ time: Date.now(), buffer });
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
// Recent browsers, particularly Safari and Chrome, require a user interaction in order to enable sound playback.
|
package/tests/test-machine.js
CHANGED
|
@@ -11,9 +11,9 @@ const MaxCyclesPerIter = 100 * 1000;
|
|
|
11
11
|
export class TestMachine {
|
|
12
12
|
constructor(model, opts) {
|
|
13
13
|
model = model || "B-DFS1.2";
|
|
14
|
-
|
|
15
|
-
this.model
|
|
16
|
-
this.processor = fake6502(
|
|
14
|
+
this.model = findModel(model);
|
|
15
|
+
if (!this.model) throw new Error(`Unknown model "${model}"`);
|
|
16
|
+
this.processor = fake6502(this.model, opts || {});
|
|
17
17
|
this._capturedChars = [];
|
|
18
18
|
this._captureHookInstalled = false;
|
|
19
19
|
}
|
|
@@ -362,7 +362,9 @@ export class TestMachine {
|
|
|
362
362
|
}
|
|
363
363
|
const fullText = text + "\n"; // append RETURN
|
|
364
364
|
const keys = fullText.split("").map((ch) => this._charToKey(ch));
|
|
365
|
-
|
|
365
|
+
// Key hold is counted in CPU cycles, so scale it to keep the hold constant in
|
|
366
|
+
// real time: the OS scans the keyboard on a peripheral-rate interrupt.
|
|
367
|
+
const holdCycles = (40000 * this.processor.cpuMultiplier) | 0;
|
|
366
368
|
let index = 0;
|
|
367
369
|
let phase = "idle"; // "idle" → "down" → "idle"
|
|
368
370
|
let nextEventCycle = 0;
|
|
@@ -414,7 +416,7 @@ export class TestMachine {
|
|
|
414
416
|
// toggle the ROM's internal caps lock state.
|
|
415
417
|
const keySequence = utils_atom.stringToATOMKeys(text + "\n");
|
|
416
418
|
const ppia = this.processor.atomppia;
|
|
417
|
-
const holdCycles = 80000; // Atom at 1 MHz needs longer hold than BBC at 2 MHz
|
|
419
|
+
const holdCycles = (80000 * this.processor.cpuMultiplier) | 0; // Atom at 1 MHz needs longer hold than BBC at 2 MHz
|
|
418
420
|
const SHIFT = utils_atom.ATOM.SHIFT;
|
|
419
421
|
|
|
420
422
|
let index = 0;
|