jsbeeb 1.12.0 → 1.13.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 +16 -2
- package/package.json +8 -8
- package/public/roms/atom/ATMMC3E.rom +0 -0
- package/public/roms/atom/Atom_Basic.rom +0 -0
- package/public/roms/atom/Atom_DOS.rom +0 -0
- package/public/roms/atom/Atom_FloatingPoint.rom +0 -0
- package/public/roms/atom/Atom_Kernel.rom +0 -0
- package/public/roms/atom/Atom_Kernel_E.rom +0 -0
- package/public/roms/atom/PCHARME.ROM +0 -0
- package/public/roms/atom/gags.rom +0 -0
- package/public/roms/atom/werom.rom +0 -0
- package/src/6502.js +344 -44
- package/src/6847.js +724 -0
- package/src/6847_fontdata.js +124 -0
- package/src/disc.js +2 -20
- package/src/fake6502.js +3 -2
- package/src/jsbeeb.css +23 -0
- package/src/keyboard.js +45 -23
- package/src/machine-session.js +85 -59
- package/src/main.js +142 -41
- package/src/mmc.js +1053 -0
- package/src/models.js +42 -1
- package/src/ppia.js +477 -0
- package/src/soundchip.js +99 -1
- package/src/tapes.js +73 -16
- package/src/url-params.js +7 -2
- package/src/utils.js +74 -1
- package/src/utils_atom.js +508 -0
- package/src/video.js +12 -1
- package/src/web/audio-handler.js +8 -3
- package/tests/test-machine.js +133 -8
package/src/main.js
CHANGED
|
@@ -7,12 +7,14 @@ import "./jsbeeb.css";
|
|
|
7
7
|
import * as utils from "./utils.js";
|
|
8
8
|
import { FakeVideo, Video } from "./video.js";
|
|
9
9
|
import { Debugger } from "./web/debug.js";
|
|
10
|
-
import { Cpu6502 } from "./6502.js";
|
|
10
|
+
import { Cpu6502, AtomCpu6502 } from "./6502.js";
|
|
11
|
+
import * as utils_atom from "./utils_atom.js";
|
|
12
|
+
import { LoadSD } from "./mmc.js";
|
|
11
13
|
import { Cmos } from "./cmos.js";
|
|
12
14
|
import { StairwayToHell } from "./sth.js";
|
|
13
15
|
import { GamePad } from "./gamepads.js";
|
|
14
16
|
import * as disc from "./fdc.js";
|
|
15
|
-
import {
|
|
17
|
+
import { loadTapeFromData } from "./tapes.js";
|
|
16
18
|
import { GoogleDriveLoader } from "./google-drive.js";
|
|
17
19
|
import * as tokeniser from "./basic-tokenise.js";
|
|
18
20
|
import * as canvasLib from "./canvas.js";
|
|
@@ -54,6 +56,21 @@ let discSth;
|
|
|
54
56
|
let tapeSth;
|
|
55
57
|
let running;
|
|
56
58
|
let model;
|
|
59
|
+
|
|
60
|
+
// Route tape to the correct interface (ACIA for BBC, PPIA for Atom)
|
|
61
|
+
function setProcessorTape(tape) {
|
|
62
|
+
if (model.isAtom) {
|
|
63
|
+
processor.atomppia.setTape(tape);
|
|
64
|
+
} else {
|
|
65
|
+
processor.acia.setTape(tape);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Convert text to machine-appropriate key sequences (BBC or Atom)
|
|
70
|
+
function stringToMachineKeys(text) {
|
|
71
|
+
return model.isAtom ? utils_atom.stringToATOMKeys(text) : utils.stringToBBCKeys(text);
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
const gamepad = new GamePad();
|
|
58
75
|
const availableImages = [
|
|
59
76
|
{
|
|
@@ -121,6 +138,7 @@ const paramTypes = {
|
|
|
121
138
|
disc1: ParamTypes.STRING,
|
|
122
139
|
disc2: ParamTypes.STRING,
|
|
123
140
|
tape: ParamTypes.STRING,
|
|
141
|
+
mmc: ParamTypes.STRING,
|
|
124
142
|
keyLayout: ParamTypes.STRING,
|
|
125
143
|
autotype: ParamTypes.STRING,
|
|
126
144
|
displayMode: ParamTypes.STRING,
|
|
@@ -143,7 +161,7 @@ let stationId = 101;
|
|
|
143
161
|
let econet = null;
|
|
144
162
|
|
|
145
163
|
// Parse disc and tape images from query parameters
|
|
146
|
-
const { discImage: queryDiscImage, secondDiscImage: querySecondDisc } = parseMediaParams(parsedQuery);
|
|
164
|
+
const { discImage: queryDiscImage, secondDiscImage: querySecondDisc, mmcImage } = parseMediaParams(parsedQuery);
|
|
147
165
|
|
|
148
166
|
// Only assign if values are provided
|
|
149
167
|
if (queryDiscImage) discImage = queryDiscImage;
|
|
@@ -321,7 +339,8 @@ if (parsedQuery.cpuMultiplier !== undefined) {
|
|
|
321
339
|
cpuMultiplier = parsedQuery.cpuMultiplier;
|
|
322
340
|
console.log("CPU multiplier set to " + cpuMultiplier);
|
|
323
341
|
}
|
|
324
|
-
const
|
|
342
|
+
const cpuSpeed = model.isAtom ? 1 * 1000 * 1000 : 2 * 1000 * 1000;
|
|
343
|
+
const clocksPerSecond = (cpuMultiplier * cpuSpeed) | 0;
|
|
325
344
|
const MaxCyclesPerFrame = clocksPerSecond / 10;
|
|
326
345
|
|
|
327
346
|
let tryGl = true;
|
|
@@ -377,12 +396,17 @@ function swapCanvas(newFilterClass) {
|
|
|
377
396
|
|
|
378
397
|
let canvas = createCanvasForFilter(displayModeFilter);
|
|
379
398
|
|
|
380
|
-
video = new Video(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
399
|
+
video = new Video(
|
|
400
|
+
model.isMaster,
|
|
401
|
+
canvas.fb32,
|
|
402
|
+
function paint(minx, miny, maxx, maxy) {
|
|
403
|
+
frames++;
|
|
404
|
+
if (frames < frameSkip) return;
|
|
405
|
+
frames = 0;
|
|
406
|
+
canvas.paint(minx, miny, maxx, maxy, this.frameCount);
|
|
407
|
+
},
|
|
408
|
+
{ isAtom: model.isAtom },
|
|
409
|
+
);
|
|
386
410
|
if (parsedQuery.fakeVideo !== undefined) video = new FakeVideo();
|
|
387
411
|
|
|
388
412
|
const audioStatsEl = document.getElementById("audio-stats");
|
|
@@ -394,6 +418,8 @@ const audioHandler = new AudioHandler({
|
|
|
394
418
|
audioFilterFreq,
|
|
395
419
|
audioFilterQ,
|
|
396
420
|
noSeek,
|
|
421
|
+
cpuSpeed,
|
|
422
|
+
isAtom: model.isAtom,
|
|
397
423
|
});
|
|
398
424
|
// Firefox will report that audio is suspended even when it will
|
|
399
425
|
// start playing without user interaction, so we need to delay a
|
|
@@ -485,7 +511,7 @@ const pastetext = document.getElementById("paste-text");
|
|
|
485
511
|
pastetext.closest("form").addEventListener("submit", (event) => event.preventDefault());
|
|
486
512
|
pastetext.addEventListener("paste", function (event) {
|
|
487
513
|
const text = event.clipboardData.getData("text/plain");
|
|
488
|
-
|
|
514
|
+
sendRawKeyboard(stringToMachineKeys(text), true);
|
|
489
515
|
});
|
|
490
516
|
pastetext.addEventListener("dragover", function (event) {
|
|
491
517
|
event.preventDefault();
|
|
@@ -500,7 +526,7 @@ pastetext.addEventListener("drop", async function (event) {
|
|
|
500
526
|
await loadStateFromFile(file, arrayBuffer);
|
|
501
527
|
} else if (file.name.toLowerCase().endsWith(".uef")) {
|
|
502
528
|
// Regular UEF tape image (not a BeebEm save state)
|
|
503
|
-
|
|
529
|
+
setProcessorTape(await loadTapeFromData(file.name, new Uint8Array(arrayBuffer), model.isAtom));
|
|
504
530
|
} else {
|
|
505
531
|
await loadHTMLFile(file);
|
|
506
532
|
}
|
|
@@ -624,7 +650,8 @@ function checkPrinterWindow() {
|
|
|
624
650
|
processor.uservia.setca1(true);
|
|
625
651
|
}
|
|
626
652
|
|
|
627
|
-
|
|
653
|
+
const CpuClass = model.isAtom ? AtomCpu6502 : Cpu6502;
|
|
654
|
+
processor = new CpuClass(model, {
|
|
628
655
|
dbgr,
|
|
629
656
|
video,
|
|
630
657
|
soundChip: audioHandler.soundChip,
|
|
@@ -920,7 +947,7 @@ async function tapeSthClick(item) {
|
|
|
920
947
|
popupLoading("Loading " + item);
|
|
921
948
|
try {
|
|
922
949
|
const tape = await loadTapeImage(parsedQuery.tape);
|
|
923
|
-
|
|
950
|
+
setProcessorTape(tape);
|
|
924
951
|
loadingFinished();
|
|
925
952
|
} catch (err) {
|
|
926
953
|
console.error("Error loading tape image:", err);
|
|
@@ -1008,9 +1035,9 @@ const sthFilter = document.getElementById("sth-filter");
|
|
|
1008
1035
|
sthFilter.addEventListener("change", () => setSthFilter(sthFilter.value));
|
|
1009
1036
|
sthFilter.addEventListener("keyup", () => setSthFilter(sthFilter.value));
|
|
1010
1037
|
|
|
1011
|
-
function
|
|
1038
|
+
function sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {
|
|
1012
1039
|
if (keyboard) {
|
|
1013
|
-
keyboard.
|
|
1040
|
+
keyboard.sendRawKeyboard(keysToSend, checkCapsAndShiftLocks);
|
|
1014
1041
|
} else {
|
|
1015
1042
|
console.warn("Tried to send keys before keyboard was initialised");
|
|
1016
1043
|
}
|
|
@@ -1023,39 +1050,39 @@ function autoboot(image) {
|
|
|
1023
1050
|
utils.noteEvent("init", "autoboot", image);
|
|
1024
1051
|
|
|
1025
1052
|
// Shift-break simulation, hold SHIFT for 1000ms.
|
|
1026
|
-
|
|
1053
|
+
sendRawKeyboard([BBC.SHIFT, 1000], false);
|
|
1027
1054
|
}
|
|
1028
1055
|
|
|
1029
1056
|
function autoBootType(keys) {
|
|
1030
1057
|
console.log("Auto typing '" + keys + "'");
|
|
1031
1058
|
utils.noteEvent("init", "autochain");
|
|
1032
1059
|
|
|
1033
|
-
const bbcKeys =
|
|
1034
|
-
|
|
1060
|
+
const bbcKeys = stringToMachineKeys(keys);
|
|
1061
|
+
sendRawKeyboard([1000].concat(bbcKeys), false);
|
|
1035
1062
|
}
|
|
1036
1063
|
|
|
1037
1064
|
function autoChainTape() {
|
|
1038
1065
|
console.log("Auto Chaining Tape");
|
|
1039
1066
|
utils.noteEvent("init", "autochain");
|
|
1040
1067
|
|
|
1041
|
-
const bbcKeys =
|
|
1042
|
-
|
|
1068
|
+
const bbcKeys = stringToMachineKeys('*TAPE\nCH.""\n');
|
|
1069
|
+
sendRawKeyboard([1000].concat(bbcKeys), false);
|
|
1043
1070
|
}
|
|
1044
1071
|
|
|
1045
1072
|
function autoRunTape() {
|
|
1046
1073
|
console.log("Auto Running Tape");
|
|
1047
1074
|
utils.noteEvent("init", "autorun");
|
|
1048
1075
|
|
|
1049
|
-
const bbcKeys =
|
|
1050
|
-
|
|
1076
|
+
const bbcKeys = stringToMachineKeys("*TAPE\n*/\n");
|
|
1077
|
+
sendRawKeyboard([1000].concat(bbcKeys), false);
|
|
1051
1078
|
}
|
|
1052
1079
|
|
|
1053
1080
|
function autoRunBasic() {
|
|
1054
1081
|
console.log("Auto Running basic");
|
|
1055
1082
|
utils.noteEvent("init", "autorunbasic");
|
|
1056
1083
|
|
|
1057
|
-
const bbcKeys =
|
|
1058
|
-
|
|
1084
|
+
const bbcKeys = stringToMachineKeys("RUN\n");
|
|
1085
|
+
sendRawKeyboard([1000].concat(bbcKeys), false);
|
|
1059
1086
|
}
|
|
1060
1087
|
|
|
1061
1088
|
function updateUrl() {
|
|
@@ -1175,16 +1202,17 @@ async function loadTapeImage(tapeImage) {
|
|
|
1175
1202
|
const split = splitImage(tapeImage);
|
|
1176
1203
|
tapeImage = split.image;
|
|
1177
1204
|
const schema = split.schema;
|
|
1205
|
+
const isAtom = model.isAtom;
|
|
1178
1206
|
|
|
1179
1207
|
switch (schema) {
|
|
1180
1208
|
case "|":
|
|
1181
1209
|
case "sth":
|
|
1182
|
-
return await loadTapeFromData(tapeImage, await tapeSth.fetch(tapeImage));
|
|
1210
|
+
return await loadTapeFromData(tapeImage, await tapeSth.fetch(tapeImage), isAtom);
|
|
1183
1211
|
|
|
1184
1212
|
case "data": {
|
|
1185
1213
|
const arr = Array.prototype.map.call(atob(tapeImage), (x) => x.charCodeAt(0));
|
|
1186
1214
|
const { name, data } = await utils.unzipDiscImage(arr);
|
|
1187
|
-
return await loadTapeFromData(name, data);
|
|
1215
|
+
return await loadTapeFromData(name, data, isAtom);
|
|
1188
1216
|
}
|
|
1189
1217
|
|
|
1190
1218
|
case "http":
|
|
@@ -1199,11 +1227,20 @@ async function loadTapeImage(tapeImage) {
|
|
|
1199
1227
|
tapeData = unzipped.data;
|
|
1200
1228
|
tapeImage = unzipped.name;
|
|
1201
1229
|
}
|
|
1202
|
-
return await loadTapeFromData(tapeImage, tapeData);
|
|
1230
|
+
return await loadTapeFromData(tapeImage, tapeData, isAtom);
|
|
1203
1231
|
}
|
|
1204
1232
|
|
|
1205
|
-
default:
|
|
1206
|
-
|
|
1233
|
+
default: {
|
|
1234
|
+
const tapePath = "tapes/" + tapeImage;
|
|
1235
|
+
let tapeData = await utils.loadData(tapePath);
|
|
1236
|
+
let tapeName = tapeImage;
|
|
1237
|
+
if (/\.zip/i.test(tapeName)) {
|
|
1238
|
+
const unzipped = await utils.unzipDiscImage(tapeData);
|
|
1239
|
+
tapeData = unzipped.data;
|
|
1240
|
+
tapeName = unzipped.name;
|
|
1241
|
+
}
|
|
1242
|
+
return await loadTapeFromData(tapeName, tapeData, isAtom);
|
|
1243
|
+
}
|
|
1207
1244
|
}
|
|
1208
1245
|
}
|
|
1209
1246
|
|
|
@@ -1228,8 +1265,14 @@ document.getElementById("tape_load").addEventListener("change", async function (
|
|
|
1228
1265
|
const file = evt.target.files[0];
|
|
1229
1266
|
utils.noteEvent("local", "clickTape"); // NB no filename here
|
|
1230
1267
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1268
|
+
let tapeData = await readFileAsBinaryString(file);
|
|
1269
|
+
let tapeName = file.name;
|
|
1270
|
+
if (/\.zip/i.test(tapeName)) {
|
|
1271
|
+
const unzipped = await utils.unzipDiscImage(utils.stringToUint8Array(tapeData));
|
|
1272
|
+
tapeData = unzipped.data;
|
|
1273
|
+
tapeName = unzipped.name;
|
|
1274
|
+
}
|
|
1275
|
+
setProcessorTape(await loadTapeFromData(tapeName, tapeData, model.isAtom));
|
|
1233
1276
|
delete parsedQuery.tape;
|
|
1234
1277
|
updateUrl();
|
|
1235
1278
|
bootstrap.Modal.getInstance(document.getElementById("tapes"))?.hide();
|
|
@@ -1570,13 +1613,58 @@ for (const link of document.querySelectorAll("#tape-menu a")) {
|
|
|
1570
1613
|
|
|
1571
1614
|
if (type === "rewind") {
|
|
1572
1615
|
console.log("Rewinding tape to the start");
|
|
1573
|
-
|
|
1616
|
+
if (model.isAtom) {
|
|
1617
|
+
processor.atomppia.stopTape();
|
|
1618
|
+
processor.atomppia.rewindTape();
|
|
1619
|
+
updateTapeButton();
|
|
1620
|
+
} else {
|
|
1621
|
+
processor.acia.rewindTape();
|
|
1622
|
+
}
|
|
1574
1623
|
} else {
|
|
1575
1624
|
console.log("unknown type", type);
|
|
1576
1625
|
}
|
|
1577
1626
|
});
|
|
1578
1627
|
}
|
|
1579
1628
|
|
|
1629
|
+
const tapePlayStopBtn = document.getElementById("tape-play-stop");
|
|
1630
|
+
const tapeControlHeader = document.getElementById("tape-control-header");
|
|
1631
|
+
const tapeControlCell = document.getElementById("tape-control-cell");
|
|
1632
|
+
|
|
1633
|
+
function updateTapeButton() {
|
|
1634
|
+
if (!model.isAtom) return;
|
|
1635
|
+
const playing = processor.atomppia.motorOn;
|
|
1636
|
+
const label = playing ? "Stop cassette" : "Play cassette";
|
|
1637
|
+
tapePlayStopBtn.textContent = playing ? "\u25A0" : "\u25B6";
|
|
1638
|
+
tapePlayStopBtn.title = label;
|
|
1639
|
+
tapePlayStopBtn.setAttribute("aria-label", label);
|
|
1640
|
+
tapePlayStopBtn.classList.toggle("playing", playing);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function showTapeControl(visible) {
|
|
1644
|
+
const display = visible ? "" : "none";
|
|
1645
|
+
tapeControlHeader.style.display = display;
|
|
1646
|
+
tapeControlCell.style.display = display;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function updateLedVisibility() {
|
|
1650
|
+
const bbcDisplay = model.isAtom ? "none" : "";
|
|
1651
|
+
for (const el of document.querySelectorAll(".bbc-only")) {
|
|
1652
|
+
el.style.display = bbcDisplay;
|
|
1653
|
+
}
|
|
1654
|
+
showTapeControl(model.isAtom);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
updateLedVisibility();
|
|
1658
|
+
|
|
1659
|
+
tapePlayStopBtn.addEventListener("click", () => {
|
|
1660
|
+
if (processor.atomppia.motorOn) {
|
|
1661
|
+
processor.atomppia.stopTape();
|
|
1662
|
+
} else {
|
|
1663
|
+
processor.atomppia.playTape();
|
|
1664
|
+
}
|
|
1665
|
+
updateTapeButton();
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1580
1668
|
function Light(name) {
|
|
1581
1669
|
const dom = document.getElementById(name);
|
|
1582
1670
|
let on = false;
|
|
@@ -1595,13 +1683,17 @@ const drive1 = new Light("drive1");
|
|
|
1595
1683
|
const network = new Light("networklight");
|
|
1596
1684
|
|
|
1597
1685
|
syncLights = function () {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1686
|
+
if (model.isAtom) {
|
|
1687
|
+
cassette.update(processor.atomppia.motorOn);
|
|
1688
|
+
} else {
|
|
1689
|
+
caps.update(processor.sysvia.capsLockLight);
|
|
1690
|
+
shift.update(processor.sysvia.shiftLockLight);
|
|
1691
|
+
drive0.update(processor.fdc.motorOn[0]);
|
|
1692
|
+
drive1.update(processor.fdc.motorOn[1]);
|
|
1693
|
+
cassette.update(processor.acia.motorOn);
|
|
1694
|
+
if (model.hasEconet) {
|
|
1695
|
+
network.update(processor.econet.activityLight());
|
|
1696
|
+
}
|
|
1605
1697
|
}
|
|
1606
1698
|
};
|
|
1607
1699
|
|
|
@@ -1636,7 +1728,16 @@ const startPromise = (async () => {
|
|
|
1636
1728
|
imageLoads.push(
|
|
1637
1729
|
(async () => {
|
|
1638
1730
|
const tape = await loadTapeImage(parsedQuery.tape);
|
|
1639
|
-
|
|
1731
|
+
setProcessorTape(tape);
|
|
1732
|
+
})(),
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
if (mmcImage && model.isAtom) {
|
|
1737
|
+
imageLoads.push(
|
|
1738
|
+
(async () => {
|
|
1739
|
+
const files = await LoadSD(mmcImage);
|
|
1740
|
+
processor.atommc.SetMMCData(files);
|
|
1640
1741
|
})(),
|
|
1641
1742
|
);
|
|
1642
1743
|
}
|