jsbeeb 1.13.1 → 1.15.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 +97 -2
- package/package.json +23 -16
- package/public/roms/tube/65C102Tube.rom +0 -0
- package/src/6502.js +74 -17
- package/src/acia.js +29 -11
- package/src/bem-snapshot.js +154 -15
- package/src/config.js +114 -83
- package/src/disc.js +47 -5
- package/src/dom-utils.js +16 -0
- package/src/fake6502.js +7 -2
- package/src/gamepads.js +11 -4
- package/src/keyboard.js +3 -2
- package/src/machine-session.js +11 -2
- package/src/main.js +117 -100
- package/src/models.js +57 -5
- package/src/mouse-coordinates.js +16 -0
- package/src/serial.js +1 -0
- package/src/snapshot-helpers.js +17 -3
- package/src/snapshot.js +19 -1
- package/src/soundchip.js +23 -5
- package/src/teletext_adaptor.js +40 -14
- package/src/tube.js +33 -1
- package/src/url-params.js +23 -19
- package/src/utils.js +10 -5
- package/src/utils_atom.js +9 -5
- package/src/web/audio-handler.js +4 -1
- package/tests/test-machine.js +7 -5
package/src/main.js
CHANGED
|
@@ -19,6 +19,7 @@ import { GoogleDriveLoader } from "./google-drive.js";
|
|
|
19
19
|
import * as tokeniser from "./basic-tokenise.js";
|
|
20
20
|
import * as canvasLib from "./canvas.js";
|
|
21
21
|
import { Config } from "./config.js";
|
|
22
|
+
import { tubeModelFor } from "./models.js";
|
|
22
23
|
import { initialise as electron } from "./app/electron.js";
|
|
23
24
|
import { AudioHandler } from "./web/audio-handler.js";
|
|
24
25
|
import { Econet } from "./econet.js";
|
|
@@ -29,12 +30,21 @@ import { GamepadSource } from "./gamepad-source.js";
|
|
|
29
30
|
import { MicrophoneInput } from "./microphone-input.js";
|
|
30
31
|
import { SpeechOutput } from "./speech-output.js";
|
|
31
32
|
import { MouseJoystickSource } from "./mouse-joystick-source.js";
|
|
33
|
+
import { calculateMouseCoordinates } from "./mouse-coordinates.js";
|
|
32
34
|
import { getFilterForMode } from "./canvas.js";
|
|
33
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
createSnapshot,
|
|
37
|
+
restoreSnapshot,
|
|
38
|
+
snapshotToJSON,
|
|
39
|
+
snapshotFromJSON,
|
|
40
|
+
isSameModel,
|
|
41
|
+
hasCoProcessor,
|
|
42
|
+
} from "./snapshot.js";
|
|
34
43
|
import { isBemSnapshot, parseBemSnapshot } from "./bem-snapshot.js";
|
|
35
44
|
import { isUefSnapshot, parseUefSnapshot } from "./uef-snapshot.js";
|
|
36
45
|
import { RewindBuffer } from "./rewind.js";
|
|
37
46
|
import { RewindUI } from "./rewind-ui.js";
|
|
47
|
+
import { downloadBlob } from "./dom-utils.js";
|
|
38
48
|
import {
|
|
39
49
|
buildUrlFromParams,
|
|
40
50
|
guessModelFromHostname,
|
|
@@ -42,7 +52,7 @@ import {
|
|
|
42
52
|
parseMediaParams,
|
|
43
53
|
parseQueryString,
|
|
44
54
|
processAutobootParams,
|
|
45
|
-
|
|
55
|
+
processInputParams,
|
|
46
56
|
} from "./url-params.js";
|
|
47
57
|
|
|
48
58
|
let processor;
|
|
@@ -129,7 +139,7 @@ const paramTypes = {
|
|
|
129
139
|
audiofilterfreq: ParamTypes.FLOAT,
|
|
130
140
|
audiofilterq: ParamTypes.FLOAT,
|
|
131
141
|
cpuMultiplier: ParamTypes.FLOAT,
|
|
132
|
-
tubeCpuMultiplier: ParamTypes.
|
|
142
|
+
tubeCpuMultiplier: ParamTypes.FLOAT,
|
|
133
143
|
microphoneChannel: ParamTypes.INT,
|
|
134
144
|
|
|
135
145
|
// String parameters (these are the default but listed for clarity)
|
|
@@ -151,7 +161,7 @@ let keyLayout = window.localStorage.keyLayout || "physical";
|
|
|
151
161
|
|
|
152
162
|
const BBC = utils.BBC;
|
|
153
163
|
const keyCodes = utils.keyCodes;
|
|
154
|
-
|
|
164
|
+
const cpuMultiplier = parsedQuery.cpuMultiplier ?? 1;
|
|
155
165
|
let fastAsPossible = false;
|
|
156
166
|
let fastTape = false;
|
|
157
167
|
let noSeek;
|
|
@@ -167,9 +177,6 @@ const { discImage: queryDiscImage, secondDiscImage: querySecondDisc, mmcImage }
|
|
|
167
177
|
if (queryDiscImage) discImage = queryDiscImage;
|
|
168
178
|
if (querySecondDisc) secondDiscImage = querySecondDisc;
|
|
169
179
|
|
|
170
|
-
// Process keyboard mappings
|
|
171
|
-
parsedQuery = processKeyboardParams(parsedQuery, BBC, keyCodes, utils.userKeymap, gamepad);
|
|
172
|
-
|
|
173
180
|
// Handle specific query parameters
|
|
174
181
|
if (Array.isArray(parsedQuery.rom)) {
|
|
175
182
|
parsedQuery.rom.forEach((romPath) => {
|
|
@@ -217,27 +224,8 @@ const userPort = {
|
|
|
217
224
|
},
|
|
218
225
|
};
|
|
219
226
|
|
|
220
|
-
const emulationConfig = {
|
|
221
|
-
keyLayout: keyLayout,
|
|
222
|
-
coProcessor: parsedQuery.coProcessor,
|
|
223
|
-
cpuMultiplier: cpuMultiplier,
|
|
224
|
-
tubeCpuMultiplier: parsedQuery.tubeCpuMultiplier || 2,
|
|
225
|
-
videoCyclesBatch: parsedQuery.videoCyclesBatch,
|
|
226
|
-
extraRoms: extraRoms,
|
|
227
|
-
userPort: userPort,
|
|
228
|
-
printerPort: printerPort,
|
|
229
|
-
getGamepads: function () {
|
|
230
|
-
// Gamepads are only available in secure contexts. If e.g. loading from http:// urls they aren't there.
|
|
231
|
-
return navigator.getGamepads ? navigator.getGamepads() : [];
|
|
232
|
-
},
|
|
233
|
-
debugFlags: {
|
|
234
|
-
logFdcCommands: parsedQuery.logFdcCommands !== undefined,
|
|
235
|
-
logFdcStateChanges: parsedQuery.logFdcStateChanges !== undefined,
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
|
|
239
227
|
// Speech output: initialised from URL param; can be toggled at runtime via the Settings panel.
|
|
240
|
-
// Must be created before Config so the onClose callback and
|
|
228
|
+
// Must be created before Config so the onClose callback and the initial checkbox state can reference it.
|
|
241
229
|
const speechOutput = new SpeechOutput();
|
|
242
230
|
speechOutput.enabled = !!parsedQuery.speechOutput;
|
|
243
231
|
|
|
@@ -253,23 +241,6 @@ const config = new Config(
|
|
|
253
241
|
},
|
|
254
242
|
function onClose(changed) {
|
|
255
243
|
parsedQuery = Object.assign(parsedQuery, changed);
|
|
256
|
-
if (
|
|
257
|
-
changed.model ||
|
|
258
|
-
changed.coProcessor !== undefined ||
|
|
259
|
-
changed.hasMusic5000 !== undefined ||
|
|
260
|
-
changed.hasTeletextAdaptor !== undefined ||
|
|
261
|
-
changed.hasEconet !== undefined
|
|
262
|
-
) {
|
|
263
|
-
areYouSure(
|
|
264
|
-
"Changing model requires a restart of the emulator. Restart now?",
|
|
265
|
-
"Yes, restart now",
|
|
266
|
-
"No, thanks",
|
|
267
|
-
function () {
|
|
268
|
-
updateUrl();
|
|
269
|
-
window.location.reload();
|
|
270
|
-
},
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
244
|
if (changed.keyLayout) {
|
|
274
245
|
window.localStorage.keyLayout = changed.keyLayout;
|
|
275
246
|
emulationConfig.keyLayout = changed.keyLayout;
|
|
@@ -288,12 +259,22 @@ const config = new Config(
|
|
|
288
259
|
if (changed.tubeCpuMultiplier !== undefined) {
|
|
289
260
|
emulationConfig.tubeCpuMultiplier = changed.tubeCpuMultiplier;
|
|
290
261
|
config.setTubeCpuMultiplier(changed.tubeCpuMultiplier);
|
|
291
|
-
if (processor.
|
|
262
|
+
if (processor.hasTube) {
|
|
292
263
|
processor.tube.cpuMultiplier = changed.tubeCpuMultiplier;
|
|
293
264
|
}
|
|
294
265
|
}
|
|
295
266
|
updateUrl();
|
|
296
267
|
},
|
|
268
|
+
function onRestartRequired() {
|
|
269
|
+
areYouSure(
|
|
270
|
+
"Your change is saved, but only takes effect when the emulator restarts. Restart now?",
|
|
271
|
+
"Restart now",
|
|
272
|
+
"Later",
|
|
273
|
+
function () {
|
|
274
|
+
window.location.reload();
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
},
|
|
297
278
|
);
|
|
298
279
|
|
|
299
280
|
// Perform mapping of legacy models to the new format
|
|
@@ -301,19 +282,54 @@ config.mapLegacyModels(parsedQuery);
|
|
|
301
282
|
|
|
302
283
|
config.setModel(parsedQuery.model || guessModelFromHostname(window.location.hostname));
|
|
303
284
|
config.setKeyLayout(keyLayout);
|
|
304
|
-
config.
|
|
305
|
-
config.setTubeCpuMultiplier(parsedQuery.tubeCpuMultiplier || 2);
|
|
306
|
-
config.setEconet(parsedQuery.hasEconet);
|
|
307
|
-
config.setMusic5000(parsedQuery.hasMusic5000);
|
|
308
|
-
config.setTeletext(parsedQuery.hasTeletextAdaptor);
|
|
285
|
+
config.setTubeCpuMultiplier(parsedQuery.tubeCpuMultiplier || 1);
|
|
309
286
|
config.setMicrophoneChannel(parsedQuery.microphoneChannel);
|
|
310
|
-
config.
|
|
311
|
-
|
|
287
|
+
config.setCheckboxes({
|
|
288
|
+
coProcessor: !!parsedQuery.coProcessor,
|
|
289
|
+
hasEconet: !!parsedQuery.hasEconet,
|
|
290
|
+
hasMusic5000: !!parsedQuery.hasMusic5000,
|
|
291
|
+
hasTeletextAdaptor: !!parsedQuery.hasTeletextAdaptor,
|
|
292
|
+
mouseJoystickEnabled: !!parsedQuery.mouseJoystickEnabled,
|
|
293
|
+
speechOutput: speechOutput.enabled,
|
|
294
|
+
});
|
|
312
295
|
let displayMode = parsedQuery.displayMode || "rgb";
|
|
313
296
|
config.setDisplayMode(displayMode);
|
|
314
297
|
|
|
315
298
|
model = config.model;
|
|
316
299
|
|
|
300
|
+
// Must come after we know the model, to validate names against those of the hardware.
|
|
301
|
+
const keyMappingWarnings = processInputParams(
|
|
302
|
+
parsedQuery,
|
|
303
|
+
model.isAtom ? utils_atom.ATOM : BBC,
|
|
304
|
+
keyCodes,
|
|
305
|
+
utils.userKeymap,
|
|
306
|
+
gamepad,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Depends on the config.setX calls above having applied the URL parameters.
|
|
310
|
+
const emulationConfig = {
|
|
311
|
+
keyLayout,
|
|
312
|
+
cpuMultiplier,
|
|
313
|
+
tubeCpuMultiplier: config.tubeCpuMultiplier,
|
|
314
|
+
videoCyclesBatch: parsedQuery.videoCyclesBatch,
|
|
315
|
+
tube: config.coProcessor ? tubeModelFor(config.model) : null,
|
|
316
|
+
hasMusic5000: config.hasMusic5000,
|
|
317
|
+
hasTeletextAdaptor: config.hasTeletextAdaptor,
|
|
318
|
+
// ROM order determines sideways bank allocation, and the fittings' ROMs claim banks
|
|
319
|
+
// before any the user asked for with ?rom=.
|
|
320
|
+
extraRoms: [...config.extraRoms, ...extraRoms],
|
|
321
|
+
userPort,
|
|
322
|
+
printerPort,
|
|
323
|
+
getGamepads: function () {
|
|
324
|
+
// Gamepads are only available in secure contexts. If e.g. loading from http:// urls they aren't there.
|
|
325
|
+
return navigator.getGamepads ? navigator.getGamepads() : [];
|
|
326
|
+
},
|
|
327
|
+
debugFlags: {
|
|
328
|
+
logFdcCommands: parsedQuery.logFdcCommands !== undefined,
|
|
329
|
+
logFdcStateChanges: parsedQuery.logFdcStateChanges !== undefined,
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
|
|
317
333
|
function sbBind(div, url, onload) {
|
|
318
334
|
const img = div.querySelector("img");
|
|
319
335
|
img.style.display = "none";
|
|
@@ -335,11 +351,8 @@ sbBind(document.querySelector(".sidebar.bottom"), parsedQuery.sbBottom, function
|
|
|
335
351
|
div.style.bottom = -img.naturalHeight + "px";
|
|
336
352
|
});
|
|
337
353
|
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
console.log("CPU multiplier set to " + cpuMultiplier);
|
|
341
|
-
}
|
|
342
|
-
const cpuSpeed = model.isAtom ? 1 * 1000 * 1000 : 2 * 1000 * 1000;
|
|
354
|
+
if (cpuMultiplier !== 1) console.log(`CPU multiplier set to ${cpuMultiplier}`);
|
|
355
|
+
const cpuSpeed = model.clockMhz * 1000 * 1000;
|
|
343
356
|
const clocksPerSecond = (cpuMultiplier * cpuSpeed) | 0;
|
|
344
357
|
const MaxCyclesPerFrame = clocksPerSecond / 10;
|
|
345
358
|
|
|
@@ -368,6 +381,10 @@ function showError(context, error) {
|
|
|
368
381
|
errorDialogModal.show();
|
|
369
382
|
}
|
|
370
383
|
|
|
384
|
+
if (keyMappingWarnings.length) {
|
|
385
|
+
showError("applying the key mappings in the URL", keyMappingWarnings.join(" "));
|
|
386
|
+
}
|
|
387
|
+
|
|
371
388
|
function createCanvasForFilter(filterClass) {
|
|
372
389
|
const newCanvas = tryGl ? canvasLib.bestCanvas(screenCanvas, filterClass) : new canvasLib.Canvas(screenCanvas);
|
|
373
390
|
|
|
@@ -465,18 +482,8 @@ function replaceOrAddExtension(name, newExt) {
|
|
|
465
482
|
* @param {string} extension - The file extension to use
|
|
466
483
|
*/
|
|
467
484
|
function downloadDriveData(data, name, extension) {
|
|
468
|
-
const a = document.createElement("a");
|
|
469
|
-
document.body.appendChild(a);
|
|
470
|
-
a.style = "display: none";
|
|
471
|
-
|
|
472
|
-
const fileName = replaceOrAddExtension(name, extension);
|
|
473
485
|
const blob = new Blob([data], { type: "application/octet-stream" });
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
a.href = url;
|
|
477
|
-
a.download = fileName;
|
|
478
|
-
a.click();
|
|
479
|
-
window.URL.revokeObjectURL(url);
|
|
486
|
+
downloadBlob(blob, replaceOrAddExtension(name, extension));
|
|
480
487
|
}
|
|
481
488
|
|
|
482
489
|
async function loadHTMLFile(file) {
|
|
@@ -536,10 +543,8 @@ const cubMonitor = document.getElementById("cub-monitor");
|
|
|
536
543
|
function onCubMouseEvent(evt) {
|
|
537
544
|
audioHandler.tryResume();
|
|
538
545
|
if (document.activeElement !== document.body) document.activeElement.blur();
|
|
539
|
-
const cubRect = cubMonitor.getBoundingClientRect();
|
|
540
546
|
const screenRect = screenCanvas.getBoundingClientRect();
|
|
541
|
-
const x = (evt
|
|
542
|
-
const y = (evt.offsetY - cubRect.top + screenRect.top) / screenCanvas.offsetHeight;
|
|
547
|
+
const { x, y } = calculateMouseCoordinates(evt, screenRect);
|
|
543
548
|
|
|
544
549
|
// Handle touchscreen
|
|
545
550
|
if (processor.touchScreen) processor.touchScreen.onMouse(x, y, evt.buttons);
|
|
@@ -613,7 +618,7 @@ window.addEventListener("beforeunload", function (event) {
|
|
|
613
618
|
}
|
|
614
619
|
});
|
|
615
620
|
|
|
616
|
-
if (
|
|
621
|
+
if (config.hasEconet) {
|
|
617
622
|
econet = new Econet(stationId);
|
|
618
623
|
} else {
|
|
619
624
|
document.getElementById("fsmenuitem").style.display = "none";
|
|
@@ -657,12 +662,14 @@ processor = new CpuClass(model, {
|
|
|
657
662
|
soundChip: audioHandler.soundChip,
|
|
658
663
|
ddNoise: audioHandler.ddNoise,
|
|
659
664
|
relayNoise: audioHandler.relayNoise,
|
|
660
|
-
music5000:
|
|
665
|
+
music5000: config.hasMusic5000 ? audioHandler.music5000 : null,
|
|
661
666
|
cmos,
|
|
662
667
|
config: emulationConfig,
|
|
663
668
|
econet,
|
|
664
669
|
});
|
|
665
670
|
|
|
671
|
+
processor.teletextAdaptor?.addEventListener("showError", (e) => showError(e.detail.context, e.detail.error));
|
|
672
|
+
|
|
666
673
|
// Create input sources
|
|
667
674
|
const gamepadSource = new GamepadSource(emulationConfig.getGamepads);
|
|
668
675
|
// Create MicrophoneInput but don't enable by default
|
|
@@ -677,18 +684,16 @@ const mouseJoystickSource = new MouseJoystickSource(screenCanvas);
|
|
|
677
684
|
/**
|
|
678
685
|
* Attach an RS-423 composite handler to the ACIA that combines the touchscreen
|
|
679
686
|
* (which sends position data to the BBC) with the speech output (which speaks
|
|
680
|
-
* text the BBC sends out).
|
|
681
|
-
* again whenever speechOutput.enabled changes.
|
|
687
|
+
* text the BBC sends out).
|
|
682
688
|
*/
|
|
683
689
|
function setupRs423Handler() {
|
|
684
|
-
const touchScreen = processor.touchScreen;
|
|
685
690
|
processor.acia.setRs423Handler({
|
|
686
691
|
onTransmit(val) {
|
|
687
|
-
touchScreen.onTransmit(val);
|
|
692
|
+
processor.touchScreen.onTransmit(val);
|
|
688
693
|
speechOutput.onTransmit(val);
|
|
689
694
|
},
|
|
690
695
|
tryReceive(rts) {
|
|
691
|
-
return touchScreen.tryReceive(rts);
|
|
696
|
+
return processor.touchScreen.tryReceive(rts);
|
|
692
697
|
},
|
|
693
698
|
});
|
|
694
699
|
}
|
|
@@ -1441,7 +1446,12 @@ document.querySelector("#google-drive form").addEventListener("submit", async fu
|
|
|
1441
1446
|
let data;
|
|
1442
1447
|
if (document.querySelector("#google-drive .create-from-existing").checked) {
|
|
1443
1448
|
const discType = disc.guessDiscTypeFromName(name);
|
|
1444
|
-
|
|
1449
|
+
try {
|
|
1450
|
+
data = discType.saver(processor.fdc.drives[0].disc);
|
|
1451
|
+
} catch (e) {
|
|
1452
|
+
loadingFinished(`Create failed: ${e.message}`);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1445
1455
|
name = replaceOrAddExtension(name, discType.extension);
|
|
1446
1456
|
console.log(`Saving existing disc: ${name}`);
|
|
1447
1457
|
} else {
|
|
@@ -1470,11 +1480,15 @@ document.querySelector("#google-drive form").addEventListener("submit", async fu
|
|
|
1470
1480
|
|
|
1471
1481
|
document.getElementById("download-drive-link").addEventListener("click", function () {
|
|
1472
1482
|
const disc = processor.fdc.drives[0].disc;
|
|
1473
|
-
const
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1483
|
+
const save = (options) =>
|
|
1484
|
+
downloadDriveData(toSsdOrDsd(disc, options), disc.name, disc.isDoubleSided ? ".dsd" : ".ssd");
|
|
1485
|
+
try {
|
|
1486
|
+
save();
|
|
1487
|
+
} catch (e) {
|
|
1488
|
+
areYouSure(`${e.message} Save anyway, losing what will not fit?`, "Save anyway", "Cancel", () =>
|
|
1489
|
+
save({ force: true }),
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1478
1492
|
});
|
|
1479
1493
|
|
|
1480
1494
|
document.getElementById("download-drive-hfe-link").addEventListener("click", function () {
|
|
@@ -1535,13 +1549,8 @@ document.getElementById("save-state").addEventListener("click", async function (
|
|
|
1535
1549
|
const snapshot = createSnapshot(processor, model, Object.keys(media).length > 0 ? media : undefined);
|
|
1536
1550
|
const json = snapshotToJSON(snapshot);
|
|
1537
1551
|
const blob = await compressBlob(new Blob([json]));
|
|
1538
|
-
const url = URL.createObjectURL(blob);
|
|
1539
1552
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1540
|
-
|
|
1541
|
-
a.href = url;
|
|
1542
|
-
a.download = `jsbeeb-${model.name}-${timestamp}.json.gz`;
|
|
1543
|
-
a.click();
|
|
1544
|
-
URL.revokeObjectURL(url);
|
|
1553
|
+
downloadBlob(blob, `jsbeeb-${model.name}-${timestamp}.json.gz`);
|
|
1545
1554
|
} catch (e) {
|
|
1546
1555
|
showError("saving state", e);
|
|
1547
1556
|
}
|
|
@@ -1570,10 +1579,10 @@ async function loadStateFromFile(file, preReadBuffer) {
|
|
|
1570
1579
|
}
|
|
1571
1580
|
snapshot = snapshotFromJSON(text);
|
|
1572
1581
|
}
|
|
1573
|
-
if (!isSameModel(snapshot.model, model.name)) {
|
|
1574
|
-
// Model mismatch: stash state and reload with
|
|
1582
|
+
if (!isSameModel(snapshot.model, model.name) || hasCoProcessor(snapshot) !== processor.hasTube) {
|
|
1583
|
+
// Model or co-processor mismatch: stash state and reload with a matching machine
|
|
1575
1584
|
sessionStorage.setItem("jsbeeb-pending-state", snapshotToJSON(snapshot));
|
|
1576
|
-
const newQuery = { ...parsedQuery, model: snapshot.model };
|
|
1585
|
+
const newQuery = { ...parsedQuery, model: snapshot.model, coProcessor: hasCoProcessor(snapshot) };
|
|
1577
1586
|
const baseUrl = window.location.origin + window.location.pathname;
|
|
1578
1587
|
window.location.href = buildUrlFromParams(baseUrl, newQuery, paramTypes);
|
|
1579
1588
|
return;
|
|
@@ -1691,7 +1700,7 @@ syncLights = function () {
|
|
|
1691
1700
|
drive0.update(processor.fdc.motorOn[0]);
|
|
1692
1701
|
drive1.update(processor.fdc.motorOn[1]);
|
|
1693
1702
|
cassette.update(processor.acia.motorOn);
|
|
1694
|
-
if (
|
|
1703
|
+
if (processor.econet) {
|
|
1695
1704
|
network.update(processor.econet.activityLight());
|
|
1696
1705
|
}
|
|
1697
1706
|
}
|
|
@@ -1849,14 +1858,22 @@ const aysEl = document.getElementById("are-you-sure");
|
|
|
1849
1858
|
const aysModal = new bootstrap.Modal(aysEl);
|
|
1850
1859
|
|
|
1851
1860
|
function areYouSure(message, yesText, noText, yesFunc) {
|
|
1861
|
+
const yesButton = aysEl.querySelector(".ays-yes");
|
|
1852
1862
|
aysEl.querySelector(".context").textContent = message;
|
|
1853
|
-
aysEl.querySelector(".ays-yes").textContent = yesText;
|
|
1854
1863
|
aysEl.querySelector(".ays-no").textContent = noText;
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1864
|
+
yesButton.textContent = yesText;
|
|
1865
|
+
let confirmed = false;
|
|
1866
|
+
const onYes = () => {
|
|
1867
|
+
confirmed = true;
|
|
1868
|
+
aysModal.hide();
|
|
1869
|
+
};
|
|
1870
|
+
yesButton.addEventListener("click", onYes, { once: true });
|
|
1871
|
+
// The "no" button, Escape and a click outside raise no event of their own: they only hide the modal.
|
|
1872
|
+
aysEl.addEventListener(
|
|
1873
|
+
"hidden.bs.modal",
|
|
1874
|
+
() => {
|
|
1875
|
+
yesButton.removeEventListener("click", onYes);
|
|
1876
|
+
if (confirmed) yesFunc();
|
|
1860
1877
|
},
|
|
1861
1878
|
{ once: true },
|
|
1862
1879
|
);
|
package/src/models.js
CHANGED
|
@@ -10,11 +10,21 @@ const CpuModel = Object.freeze({
|
|
|
10
10
|
CMOS65C12: 2,
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Describes what a machine *is*: which CPU, which ROMs, which disc controller.
|
|
15
|
+
* Anything a user can turn on and off for a session (second processor, Econet,
|
|
16
|
+
* Music 5000, teletext adaptor) belongs in the emulation config passed to Cpu6502.
|
|
17
|
+
*
|
|
18
|
+
* Instances are shared process-wide via `allModels` and frozen, so they can be handed
|
|
19
|
+
* to any number of machines without one session's settings leaking into the next.
|
|
20
|
+
*/
|
|
13
21
|
class Model {
|
|
14
|
-
constructor({ name, synonyms, os, cpuModel, isMaster, isAtom, swram, fdc,
|
|
22
|
+
constructor({ name, synonyms, os, cpuModel, isMaster, isAtom, swram, fdc, cmosOverride, banks, clockMhz } = {}) {
|
|
23
|
+
if (!(clockMhz > 0)) throw new Error(`Model ${name} has no clock speed`);
|
|
15
24
|
this.name = name;
|
|
16
25
|
this.synonyms = synonyms;
|
|
17
26
|
this.os = os;
|
|
27
|
+
this.clockMhz = clockMhz;
|
|
18
28
|
this.banks = banks;
|
|
19
29
|
this._cpuModel = cpuModel;
|
|
20
30
|
this.isMaster = isMaster;
|
|
@@ -22,10 +32,7 @@ class Model {
|
|
|
22
32
|
this.Fdc = fdc;
|
|
23
33
|
this.swram = swram;
|
|
24
34
|
this.isTest = false;
|
|
25
|
-
this.tube = tube;
|
|
26
35
|
this.cmosOverride = cmosOverride;
|
|
27
|
-
this.hasEconet = false;
|
|
28
|
-
this.hasMusic5000 = false;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
get nmos() {
|
|
@@ -67,6 +74,7 @@ function atomModel({ name, synonyms, os, banks }) {
|
|
|
67
74
|
os,
|
|
68
75
|
cpuModel: CpuModel.MOS6502,
|
|
69
76
|
isMaster: false,
|
|
77
|
+
clockMhz: 1,
|
|
70
78
|
isAtom: true,
|
|
71
79
|
swram: beebSwram,
|
|
72
80
|
fdc: NoiseAwareIntelFdc,
|
|
@@ -119,6 +127,7 @@ export const allModels = [
|
|
|
119
127
|
os: ["os.rom", "BASIC.ROM", "b/DFS-1.2.rom"],
|
|
120
128
|
cpuModel: CpuModel.MOS6502,
|
|
121
129
|
isMaster: false,
|
|
130
|
+
clockMhz: 2,
|
|
122
131
|
swram: beebSwram,
|
|
123
132
|
fdc: NoiseAwareIntelFdc,
|
|
124
133
|
}),
|
|
@@ -128,6 +137,7 @@ export const allModels = [
|
|
|
128
137
|
os: ["os.rom", "BASIC.ROM", "b/DFS-0.9.rom"],
|
|
129
138
|
cpuModel: CpuModel.MOS6502,
|
|
130
139
|
isMaster: false,
|
|
140
|
+
clockMhz: 2,
|
|
131
141
|
swram: beebSwram,
|
|
132
142
|
fdc: NoiseAwareIntelFdc,
|
|
133
143
|
}),
|
|
@@ -137,6 +147,7 @@ export const allModels = [
|
|
|
137
147
|
os: ["os.rom", "BASIC.ROM", "b1770/dfs1770.rom", "b1770/zADFS.ROM"],
|
|
138
148
|
cpuModel: CpuModel.MOS6502,
|
|
139
149
|
isMaster: false,
|
|
150
|
+
clockMhz: 2,
|
|
140
151
|
swram: beebSwram,
|
|
141
152
|
fdc: NoiseAwareWdFdc,
|
|
142
153
|
}),
|
|
@@ -147,6 +158,7 @@ export const allModels = [
|
|
|
147
158
|
os: ["os.rom", "BASIC.ROM", "b1770/zADFS.ROM", "b1770/dfs1770.rom"],
|
|
148
159
|
cpuModel: CpuModel.MOS6502,
|
|
149
160
|
isMaster: false,
|
|
161
|
+
clockMhz: 2,
|
|
150
162
|
swram: beebSwram,
|
|
151
163
|
fdc: NoiseAwareWdFdc,
|
|
152
164
|
}),
|
|
@@ -156,6 +168,7 @@ export const allModels = [
|
|
|
156
168
|
os: ["master/mos3.20"],
|
|
157
169
|
cpuModel: CpuModel.CMOS65C12,
|
|
158
170
|
isMaster: true,
|
|
171
|
+
clockMhz: 2,
|
|
159
172
|
swram: masterSwram,
|
|
160
173
|
fdc: NoiseAwareWdFdc,
|
|
161
174
|
cmosOverride: pickDfs,
|
|
@@ -166,6 +179,7 @@ export const allModels = [
|
|
|
166
179
|
os: ["master/mos3.20"],
|
|
167
180
|
cpuModel: CpuModel.CMOS65C12,
|
|
168
181
|
isMaster: true,
|
|
182
|
+
clockMhz: 2,
|
|
169
183
|
swram: masterSwram,
|
|
170
184
|
fdc: NoiseAwareWdFdc,
|
|
171
185
|
cmosOverride: pickAdfs,
|
|
@@ -176,6 +190,7 @@ export const allModels = [
|
|
|
176
190
|
os: ["master/mos3.20"],
|
|
177
191
|
cpuModel: CpuModel.CMOS65C12,
|
|
178
192
|
isMaster: true,
|
|
193
|
+
clockMhz: 2,
|
|
179
194
|
swram: masterSwram,
|
|
180
195
|
fdc: NoiseAwareWdFdc,
|
|
181
196
|
cmosOverride: pickAnfs,
|
|
@@ -205,13 +220,24 @@ export const allModels = [
|
|
|
205
220
|
synonyms: ["Atom-DOS"],
|
|
206
221
|
os: ["atom/Atom_Kernel.rom", "atom/Atom_DOS.rom", "atom/Atom_FloatingPoint.rom", "atom/Atom_Basic.rom"],
|
|
207
222
|
}),
|
|
208
|
-
//
|
|
223
|
+
// Neither can be selected as a model: they are fitted to one, by the configuration builder later.
|
|
209
224
|
new Model({
|
|
210
225
|
name: "Tube65C02",
|
|
211
226
|
synonyms: [],
|
|
212
227
|
os: ["tube/6502Tube.rom"],
|
|
228
|
+
// TODO(#746): the external second processor was an NMOS 6502A.
|
|
213
229
|
cpuModel: CpuModel.CMOS65C02,
|
|
214
230
|
isMaster: false,
|
|
231
|
+
clockMhz: 3,
|
|
232
|
+
}),
|
|
233
|
+
new Model({
|
|
234
|
+
name: "Tube65C102",
|
|
235
|
+
synonyms: [],
|
|
236
|
+
os: ["tube/65C102Tube.rom"],
|
|
237
|
+
// TODO(#746): Acorn's 65C102 has no Rockwell bit instructions.
|
|
238
|
+
cpuModel: CpuModel.CMOS65C02,
|
|
239
|
+
isMaster: false,
|
|
240
|
+
clockMhz: 4,
|
|
215
241
|
}),
|
|
216
242
|
];
|
|
217
243
|
|
|
@@ -231,6 +257,7 @@ export const TEST_6502 = new Model({
|
|
|
231
257
|
name: "TEST",
|
|
232
258
|
synonyms: ["TEST"],
|
|
233
259
|
os: [],
|
|
260
|
+
clockMhz: 2,
|
|
234
261
|
cpuModel: CpuModel.MOS6502,
|
|
235
262
|
isMaster: false,
|
|
236
263
|
swram: beebSwram,
|
|
@@ -241,6 +268,7 @@ export const TEST_65C02 = new Model({
|
|
|
241
268
|
name: "TEST",
|
|
242
269
|
synonyms: ["TEST"],
|
|
243
270
|
os: [],
|
|
271
|
+
clockMhz: 2,
|
|
244
272
|
cpuModel: CpuModel.CMOS65C02,
|
|
245
273
|
isMaster: false,
|
|
246
274
|
swram: masterSwram,
|
|
@@ -251,6 +279,7 @@ export const TEST_65C12 = new Model({
|
|
|
251
279
|
name: "TEST",
|
|
252
280
|
synonyms: ["TEST"],
|
|
253
281
|
os: [],
|
|
282
|
+
clockMhz: 2,
|
|
254
283
|
cpuModel: CpuModel.CMOS65C12,
|
|
255
284
|
isMaster: false,
|
|
256
285
|
swram: masterSwram,
|
|
@@ -260,6 +289,7 @@ TEST_65C12.isTest = true;
|
|
|
260
289
|
|
|
261
290
|
export const basicOnly = new Model({
|
|
262
291
|
name: "Basic only",
|
|
292
|
+
clockMhz: 2,
|
|
263
293
|
synonyms: ["Basic only"],
|
|
264
294
|
os: ["master/mos3.20"],
|
|
265
295
|
cpuModel: CpuModel.CMOS65C12,
|
|
@@ -267,3 +297,25 @@ export const basicOnly = new Model({
|
|
|
267
297
|
swram: masterSwram,
|
|
268
298
|
fdc: NoiseAwareWdFdc,
|
|
269
299
|
});
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The second processors jsbeeb emulates: the external 3MHz box, and the Master Turbo's
|
|
303
|
+
* internal 4MHz board. Machine-building code passes one of these as the emulation config's
|
|
304
|
+
* `tube`, so that 6502.js needn't import this module and close an import cycle via the FDC
|
|
305
|
+
* modules.
|
|
306
|
+
*/
|
|
307
|
+
export const TubeModel = findModel("Tube65C02");
|
|
308
|
+
export const TurboTubeModel = findModel("Tube65C102");
|
|
309
|
+
|
|
310
|
+
/** @returns {Model} the second processor sold for this machine: the Turbo board for a Master. */
|
|
311
|
+
export function tubeModelFor(model) {
|
|
312
|
+
return model.isMaster ? TurboTubeModel : TubeModel;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// After the isTest assignments above, so those still apply.
|
|
316
|
+
for (const model of [...allModels, TEST_6502, TEST_65C02, TEST_65C12, basicOnly]) {
|
|
317
|
+
Object.freeze(model.os);
|
|
318
|
+
Object.freeze(model.synonyms);
|
|
319
|
+
Object.freeze(model);
|
|
320
|
+
}
|
|
321
|
+
Object.freeze(allModels);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculate normalized mouse coordinates relative to the screen canvas.
|
|
3
|
+
*
|
|
4
|
+
* Uses clientX/clientY against the canvas bounding rect so that coordinates
|
|
5
|
+
* are correct even when the canvas is inset within a monitor image (e.g.
|
|
6
|
+
* display filters with non-zero canvasLeft/canvasTop).
|
|
7
|
+
*
|
|
8
|
+
* @param {MouseEvent} evt - The mouse event
|
|
9
|
+
* @param {DOMRect} screenRect - Bounding client rect of the screen canvas
|
|
10
|
+
* @returns {{x: number, y: number}} Normalized coordinates in [0, 1]
|
|
11
|
+
*/
|
|
12
|
+
export function calculateMouseCoordinates(evt, screenRect) {
|
|
13
|
+
const x = (evt.clientX - screenRect.left) / screenRect.width;
|
|
14
|
+
const y = (evt.clientY - screenRect.top) / screenRect.height;
|
|
15
|
+
return { x, y };
|
|
16
|
+
}
|
package/src/serial.js
CHANGED
|
@@ -20,6 +20,7 @@ export class Serial {
|
|
|
20
20
|
this.transmitRate = val & 0x07;
|
|
21
21
|
this.receiveRate = (val >>> 3) & 0x07;
|
|
22
22
|
this.acia.setSerialReceive(table[this.receiveRate]);
|
|
23
|
+
this.acia.setSerialTransmit(table[this.transmitRate]);
|
|
23
24
|
this.acia.setMotor(!!(val & 0x80));
|
|
24
25
|
this.acia.selectRs423(!!(val & 0x40));
|
|
25
26
|
}
|
package/src/snapshot-helpers.js
CHANGED
|
@@ -25,7 +25,7 @@ export const DefaultAcia = {
|
|
|
25
25
|
tapeDcdLineLevel: false,
|
|
26
26
|
hadDcdHigh: false,
|
|
27
27
|
serialReceiveRate: 19200,
|
|
28
|
-
|
|
28
|
+
serialTransmitRate: 19200,
|
|
29
29
|
txCompleteTaskOffset: null,
|
|
30
30
|
runTapeTaskOffset: null,
|
|
31
31
|
runRs423TaskOffset: null,
|
|
@@ -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
|
}
|