jsbeeb 1.23.0 → 1.24.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 +2 -2
- package/package.json +14 -10
- package/src/6502.js +39 -42
- package/src/6502.opcodes.js +7 -13
- package/src/6847.js +4 -5
- package/src/6847_fontdata.js +0 -2
- package/src/acia.js +2 -3
- package/src/app/app.js +0 -1
- package/src/app/args.js +0 -2
- package/src/app/electron.js +4 -6
- package/src/app-bench.js +1 -3
- package/src/archive.js +243 -0
- package/src/basic-loader.js +0 -7
- package/src/basic-tokenise.js +19 -6
- package/src/bbc-palette.js +0 -2
- package/src/bbcdiscs.js +0 -2
- package/src/bem-snapshot.js +1 -3
- package/src/binary.js +175 -0
- package/src/cmos.js +0 -2
- package/src/ddnoise.js +0 -1
- package/src/debounce.js +7 -0
- package/src/disc-surface.js +0 -2
- package/src/disc.js +10 -9
- package/src/fake6502.js +7 -23
- package/src/fdc.js +9 -51
- package/src/filestore.js +10 -17
- package/src/hex.js +38 -0
- package/src/index.js +11 -0
- package/src/intel-fdc.js +18 -20
- package/src/{utils_atom.js → keymap-atom.js} +2 -18
- package/src/{utils.js → keymap.js} +47 -610
- package/src/loader.js +63 -0
- package/src/machine-session.js +37 -177
- package/src/machine-spec.js +59 -0
- package/src/main.js +66 -180
- package/src/media-resolver.js +68 -0
- package/src/mmc.js +3 -2
- package/src/models.js +17 -3
- package/src/music5000.js +0 -2
- package/src/ppia.js +3 -4
- package/src/printer.js +7 -14
- package/src/relaynoise.js +0 -1
- package/src/rewind.js +0 -2
- package/src/sample-player.js +2 -3
- package/src/serial.js +0 -2
- package/src/snapshot-helpers.js +0 -2
- package/src/snapshot.js +0 -2
- package/src/soundchip.js +1 -1
- package/src/state-utils.js +0 -2
- package/src/sth.js +2 -4
- package/src/tapes.js +10 -4
- package/src/teletext.js +2 -3
- package/src/teletext_adaptor.js +2 -4
- package/src/teletext_data.js +0 -2
- package/src/test-machine.js +307 -0
- package/src/touchscreen.js +2 -4
- package/src/tube.js +5 -6
- package/src/typist.js +103 -0
- package/src/uef-snapshot.js +0 -2
- package/src/url-params.js +4 -5
- package/src/vdu-capture.js +113 -0
- package/src/via.js +3 -4
- package/src/video-filters/pal-composite.js +0 -2
- package/src/video-filters/passthrough-filter.js +0 -2
- package/src/video-filters/pixel-grid.js +0 -2
- package/src/video-filters/shader-program.js +0 -2
- package/src/video-filters/xbr-filter.js +0 -2
- package/src/video.js +12 -11
- package/src/wd-fdc.js +20 -20
- package/src/web/analogue-inputs.js +6 -8
- package/src/web/analytics.js +13 -0
- package/src/web/audio-handler.js +2 -2
- package/src/{audio-utils.js → web/audio-utils.js} +0 -2
- package/src/web/autoboot.js +10 -13
- package/src/{canvas.js → web/canvas.js} +4 -5
- package/src/web/config.js +41 -74
- package/src/web/console-surface.js +25 -0
- package/src/web/debug.js +6 -5
- package/src/web/disc-visualiser.js +0 -2
- package/src/web/display.js +21 -4
- package/src/{dom-utils.js → web/dom-utils.js} +1 -11
- package/src/web/drives.js +1 -1
- package/src/web/emulation-loop.js +54 -33
- package/src/web/front-panel.js +13 -12
- package/src/{gamepads.js → web/gamepads.js} +2 -6
- package/src/web/google-drive-picker.js +14 -9
- package/src/{google-drive.js → web/google-drive.js} +9 -10
- package/src/web/hfe-picker.js +5 -4
- package/src/web/keyboard-setup.js +18 -38
- package/src/{keyboard.js → web/keyboard.js} +18 -119
- package/src/web/layout.js +4 -0
- package/src/web/local-disc.js +44 -0
- package/src/web/machine.js +27 -20
- package/src/web/media-loader.js +38 -147
- package/src/{microphone-input.js → web/microphone-input.js} +1 -1
- package/src/web/modals.js +6 -9
- package/src/web/page-actions.js +88 -0
- package/src/web/quick-settings.js +16 -15
- package/src/web/reporting.js +0 -7
- package/src/{rewind-thumbnail.js → web/rewind-thumbnail.js} +0 -2
- package/src/web/rewind-ui.js +29 -29
- package/src/web/run-controls.js +39 -0
- package/src/web/settings.js +64 -135
- package/src/web/snapshot-ui.js +10 -16
- package/src/{speech-output.js → web/speech-output.js} +0 -2
- package/src/web/sth-picker.js +9 -7
- package/src/web/url-state.js +20 -6
- package/src/econet/L3FS.dat +0 -0
- package/src/econet/scsi.dat +0 -0
- package/tests/test-machine.js +0 -609
package/src/main.js
CHANGED
|
@@ -3,10 +3,8 @@ import { version } from "../package.json";
|
|
|
3
3
|
import "bootswatch/dist/darkly/bootstrap.min.css";
|
|
4
4
|
import "./jsbeeb.css";
|
|
5
5
|
|
|
6
|
-
import * as utils from "./utils.js";
|
|
7
6
|
import { Debugger } from "./web/debug.js";
|
|
8
|
-
import
|
|
9
|
-
import { GamePad } from "./gamepads.js";
|
|
7
|
+
import { GamePad } from "./web/gamepads.js";
|
|
10
8
|
import { initialise as electron } from "./app/electron.js";
|
|
11
9
|
import { AudioHandler } from "./web/audio-handler.js";
|
|
12
10
|
import { installIcons } from "./web/icons.js";
|
|
@@ -21,7 +19,9 @@ import { isSnapshotFile, SnapshotUI } from "./web/snapshot-ui.js";
|
|
|
21
19
|
import { Autoboot } from "./web/autoboot.js";
|
|
22
20
|
import { Display } from "./web/display.js";
|
|
23
21
|
import { Layout } from "./web/layout.js";
|
|
24
|
-
import { EmulationLoop
|
|
22
|
+
import { EmulationLoop } from "./web/emulation-loop.js";
|
|
23
|
+
import { RunControls } from "./web/run-controls.js";
|
|
24
|
+
import { exposeConsoleSurface } from "./web/console-surface.js";
|
|
25
25
|
import { KeyboardSetup } from "./web/keyboard-setup.js";
|
|
26
26
|
import { AccessibilitySwitches } from "./web/accessibility-switches.js";
|
|
27
27
|
import { AnalogueInputs } from "./web/analogue-inputs.js";
|
|
@@ -31,14 +31,17 @@ import { Drives } from "./web/drives.js";
|
|
|
31
31
|
import { UrlState } from "./web/url-state.js";
|
|
32
32
|
import { Modals } from "./web/modals.js";
|
|
33
33
|
import { Settings } from "./web/settings.js";
|
|
34
|
+
import { Config } from "./web/config.js";
|
|
35
|
+
import { SpeechOutput } from "./web/speech-output.js";
|
|
34
36
|
import { Printer } from "./printer.js";
|
|
35
|
-
import { RewindBuffer } from "./rewind.js";
|
|
36
37
|
import { RewindUI } from "./web/rewind-ui.js";
|
|
37
38
|
import { DiscVisualiser } from "./web/disc-visualiser.js";
|
|
38
|
-
import {
|
|
39
|
+
import { PageActions } from "./web/page-actions.js";
|
|
39
40
|
import { parseMediaParams, processAutobootParams, processDriveTrackParams, processInputParams } from "./url-params.js";
|
|
41
|
+
import { adaptKeyCodesToBrowser, keyCodes, userKeymap } from "./keymap.js";
|
|
40
42
|
|
|
41
43
|
installIcons();
|
|
44
|
+
adaptKeyCodesToBrowser();
|
|
42
45
|
|
|
43
46
|
// ------------------------------------------------------------------------
|
|
44
47
|
// What the URL asked for.
|
|
@@ -70,11 +73,6 @@ const stationId = parsedQuery.stationId !== undefined ? parsedQuery.stationId :
|
|
|
70
73
|
const tryGl = parsedQuery.glEnabled ?? true;
|
|
71
74
|
const lowLatency = parsedQuery.lowLatency ?? true;
|
|
72
75
|
|
|
73
|
-
if (parsedQuery.embed) {
|
|
74
|
-
for (const el of document.querySelectorAll(".embed-hide")) el.style.display = "none";
|
|
75
|
-
document.body.style.backgroundColor = "transparent";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
76
|
// ------------------------------------------------------------------------
|
|
79
77
|
// The pieces that exist before the machine: settings, screen, sound.
|
|
80
78
|
// ------------------------------------------------------------------------
|
|
@@ -88,29 +86,26 @@ if (!window.isSecureContext)
|
|
|
88
86
|
quietKey: "quietInsecureGamepads",
|
|
89
87
|
});
|
|
90
88
|
|
|
91
|
-
const printer = new Printer(
|
|
92
|
-
onOutput: (char) => frontPanel.printChar(char),
|
|
93
|
-
onFirstOutput: () =>
|
|
94
|
-
toast("Printer output is being kept. Press Ctrl-B to open the printer window.", {
|
|
95
|
-
title: "Printer",
|
|
96
|
-
quietKey: "quietPrinterOutput",
|
|
97
|
-
}),
|
|
98
|
-
});
|
|
89
|
+
const printer = new Printer();
|
|
99
90
|
|
|
100
91
|
const accessibilitySwitches = new AccessibilitySwitches();
|
|
101
92
|
|
|
102
93
|
const settings = new Settings({ urlState });
|
|
103
|
-
const {
|
|
94
|
+
const { keyLayout, displayMode, audioOutput, speakerAmount } = settings;
|
|
104
95
|
const model = settings.model;
|
|
96
|
+
const config = new Config(settings);
|
|
97
|
+
|
|
98
|
+
const speechOutput = new SpeechOutput();
|
|
99
|
+
const speak = (enabled) => {
|
|
100
|
+
speechOutput.enabled = enabled;
|
|
101
|
+
if (enabled && typeof speechSynthesis === "undefined")
|
|
102
|
+
toast("This browser has no speech synthesis, so speech output has nothing to speak with.", { title: "Speech" });
|
|
103
|
+
};
|
|
104
|
+
speak(settings.speechOutput);
|
|
105
|
+
settings.on("speechOutput", speak);
|
|
105
106
|
|
|
106
107
|
// Must come after we know the model, to validate names against those of the hardware.
|
|
107
|
-
const keyMappingWarnings = processInputParams(
|
|
108
|
-
parsedQuery,
|
|
109
|
-
model.isAtom ? utils_atom.ATOM : utils.BBC,
|
|
110
|
-
utils.keyCodes,
|
|
111
|
-
utils.userKeymap,
|
|
112
|
-
gamepad,
|
|
113
|
-
);
|
|
108
|
+
const keyMappingWarnings = processInputParams(parsedQuery, model.keys, keyCodes, userKeymap, gamepad);
|
|
114
109
|
if (keyMappingWarnings.length) {
|
|
115
110
|
toast(`${keyMappingWarnings.join(" ")} The key names are listed in the README.`, {
|
|
116
111
|
title: "Mappings in the URL",
|
|
@@ -152,21 +147,14 @@ const audioHandler = new AudioHandler({
|
|
|
152
147
|
noSeek,
|
|
153
148
|
cpuSpeed,
|
|
154
149
|
isAtom: model.isAtom,
|
|
155
|
-
hasMusic5000:
|
|
150
|
+
hasMusic5000: settings.hasMusic5000,
|
|
156
151
|
});
|
|
157
152
|
// Firefox will report that audio is suspended even when it will
|
|
158
153
|
// start playing without user interaction, so we need to delay a
|
|
159
154
|
// little to get a reliable indication.
|
|
160
155
|
window.setTimeout(() => audioHandler.checkStatus(), 1000);
|
|
161
156
|
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
onAudioOutput: (output) => settings.applyAudioOutput(output),
|
|
165
|
-
onSpeakerAmount: (amount) => settings.applySpeakerAmount(amount),
|
|
166
|
-
onDisplayMode: (mode) => settings.applyDisplayMode(mode),
|
|
167
|
-
},
|
|
168
|
-
{ audioOutput, speakerAmount, displayMode },
|
|
169
|
-
);
|
|
157
|
+
new QuickSettings(settings);
|
|
170
158
|
|
|
171
159
|
// ------------------------------------------------------------------------
|
|
172
160
|
// The machine, the loop that runs it, and everything that feeds it.
|
|
@@ -175,7 +163,7 @@ const quickSettings = new QuickSettings(
|
|
|
175
163
|
// Depends on the settings having applied the URL parameters.
|
|
176
164
|
const machine = new Machine({
|
|
177
165
|
model,
|
|
178
|
-
|
|
166
|
+
settings,
|
|
179
167
|
parsedQuery,
|
|
180
168
|
keyLayout,
|
|
181
169
|
cpuMultiplier,
|
|
@@ -190,7 +178,7 @@ const machine = new Machine({
|
|
|
190
178
|
});
|
|
191
179
|
const processor = machine.processor;
|
|
192
180
|
|
|
193
|
-
const
|
|
181
|
+
const { keyboard } = new KeyboardSetup({
|
|
194
182
|
actions: {
|
|
195
183
|
enterDebugger: () => loop.stop(true),
|
|
196
184
|
reload: () => window.location.reload(),
|
|
@@ -199,7 +187,7 @@ const keys = new KeyboardSetup({
|
|
|
199
187
|
openPrinter: () => frontPanel.checkPrinterWindow(),
|
|
200
188
|
pause: () => loop.stop(false),
|
|
201
189
|
resume: () => loop.go(),
|
|
202
|
-
paste: (text) =>
|
|
190
|
+
paste: (text) => keyboard.sendRawKeyboard(autoBoot.stringToMachineKeys(text), true),
|
|
203
191
|
onAnyKeyDown: () => {
|
|
204
192
|
audioHandler.tryResume();
|
|
205
193
|
inputs.ensureMicrophoneRunning();
|
|
@@ -211,31 +199,20 @@ const keys = new KeyboardSetup({
|
|
|
211
199
|
keyLayout,
|
|
212
200
|
});
|
|
213
201
|
|
|
214
|
-
const rewindBuffer = new RewindBuffer(30);
|
|
215
202
|
const loop = new EmulationLoop({
|
|
216
203
|
processor,
|
|
217
204
|
display,
|
|
218
205
|
audioHandler,
|
|
219
206
|
dbgr,
|
|
220
207
|
gamepad,
|
|
221
|
-
keyboard
|
|
222
|
-
syncLights: () => frontPanel.syncLights(),
|
|
223
|
-
rewindBuffer,
|
|
224
|
-
onRewindCaptured: () => rewindUI.updateButtonState(),
|
|
208
|
+
keyboard,
|
|
225
209
|
clocksPerSecond,
|
|
226
210
|
cpuSpeed,
|
|
227
211
|
fastTape: !!parsedQuery.fasttape,
|
|
228
212
|
audioStatsNode,
|
|
229
213
|
});
|
|
230
214
|
|
|
231
|
-
const
|
|
232
|
-
const debugPlay = document.getElementById("debug-play");
|
|
233
|
-
loop.addEventListener("running", () => {
|
|
234
|
-
const running = loop.isRunning();
|
|
235
|
-
keys.setRunning(running);
|
|
236
|
-
debugPlay.disabled = running;
|
|
237
|
-
debugPause.disabled = !running;
|
|
238
|
-
});
|
|
215
|
+
const runControls = new RunControls({ loop, dbgr, keyboard });
|
|
239
216
|
|
|
240
217
|
const modals = new Modals({ loop });
|
|
241
218
|
|
|
@@ -252,7 +229,7 @@ const media = new MediaLoader({
|
|
|
252
229
|
const autoBoot = new Autoboot({
|
|
253
230
|
model,
|
|
254
231
|
processor,
|
|
255
|
-
sendKeys: (keysToSend, check) =>
|
|
232
|
+
sendKeys: (keysToSend, check) => keyboard.sendRawKeyboard(keysToSend, check),
|
|
256
233
|
});
|
|
257
234
|
const autobootTicks = new AutobootTicks({ urlState });
|
|
258
235
|
const sthPicker = new SthPicker({
|
|
@@ -287,12 +264,11 @@ const snapshots = new SnapshotUI({
|
|
|
287
264
|
const inputs = new AnalogueInputs({
|
|
288
265
|
processor,
|
|
289
266
|
screenCanvas,
|
|
290
|
-
getGamepads: machine.
|
|
291
|
-
|
|
292
|
-
config,
|
|
267
|
+
getGamepads: machine.spec.getGamepads,
|
|
268
|
+
settings,
|
|
293
269
|
audioHandler,
|
|
294
270
|
});
|
|
295
|
-
if (
|
|
271
|
+
if (settings.microphoneChannel !== undefined) {
|
|
296
272
|
// We need to use setTimeout to make sure this runs after the page has loaded
|
|
297
273
|
// This is needed because some browsers require user interaction for audio context
|
|
298
274
|
setTimeout(async () => {
|
|
@@ -300,105 +276,54 @@ if (parsedQuery.microphoneChannel !== undefined) {
|
|
|
300
276
|
}, 1000);
|
|
301
277
|
}
|
|
302
278
|
// Apply ADC source settings from URL parameters
|
|
303
|
-
inputs.updateAdcSources(
|
|
279
|
+
inputs.updateAdcSources(settings.mouseJoystickEnabled, settings.microphoneChannel);
|
|
304
280
|
|
|
305
|
-
const frontPanel = new FrontPanel({ processor, model, printer });
|
|
281
|
+
const frontPanel = new FrontPanel({ processor, model, printer, loop });
|
|
306
282
|
|
|
307
283
|
// ------------------------------------------------------------------------
|
|
308
284
|
// Rewind, the visualiser and the layout.
|
|
309
285
|
// ------------------------------------------------------------------------
|
|
310
286
|
|
|
311
|
-
const rewindUI = new RewindUI({
|
|
312
|
-
rewindBuffer,
|
|
313
|
-
processor,
|
|
314
|
-
video,
|
|
315
|
-
captureInterval: RewindCaptureInterval,
|
|
316
|
-
loop,
|
|
317
|
-
});
|
|
287
|
+
const rewindUI = new RewindUI({ processor, video, loop });
|
|
318
288
|
rewindUI.updateButtonState();
|
|
319
289
|
|
|
320
|
-
|
|
321
|
-
else document.getElementById("disc-visualiser-open").classList.add("disabled");
|
|
290
|
+
new DiscVisualiser({ fdc: processor.fdc });
|
|
322
291
|
|
|
323
292
|
const layout = new Layout({
|
|
324
293
|
screenCanvas,
|
|
325
294
|
display,
|
|
326
|
-
embed: parsedQuery.embed
|
|
295
|
+
embed: !!parsedQuery.embed,
|
|
327
296
|
sidebars: { left: parsedQuery.sbLeft, right: parsedQuery.sbRight, bottom: parsedQuery.sbBottom },
|
|
328
297
|
});
|
|
329
298
|
|
|
330
299
|
// Everything a setting can reach now exists.
|
|
331
|
-
settings.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
for (const el of document.querySelectorAll(".initially-hidden")) el.classList.remove("initially-hidden");
|
|
338
|
-
|
|
339
|
-
document.getElementById("paste-form").addEventListener("submit", (event) => event.preventDefault());
|
|
340
|
-
|
|
341
|
-
window.addEventListener("blur", function () {
|
|
342
|
-
keys.clearKeys();
|
|
343
|
-
loop.setEmulationLead(audioHandler.setWindowFocused(false));
|
|
300
|
+
settings.on("audioOutput", (output) => audioHandler.setAudioOutput(output));
|
|
301
|
+
settings.on("speakerAmount", (amount) => audioHandler.setSpeakerAmount(amount));
|
|
302
|
+
settings.on("displayMode", (mode) => {
|
|
303
|
+
display.setMode(mode);
|
|
304
|
+
// The monitor picture may have changed shape.
|
|
305
|
+
layout.resize();
|
|
344
306
|
});
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
loop.stop(true);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function resumeFromDebugger() {
|
|
352
|
-
dbgr.hide();
|
|
353
|
-
keys.resumeEmulation();
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
debugPause.addEventListener("click", pauseIntoDebugger);
|
|
357
|
-
debugPlay.addEventListener("click", resumeFromDebugger);
|
|
358
|
-
|
|
359
|
-
// To lower chance of data loss, only accept drop events in the drop
|
|
360
|
-
// zone in the menu bar.
|
|
361
|
-
document.addEventListener("dragover", function (event) {
|
|
362
|
-
event.preventDefault();
|
|
363
|
-
event.dataTransfer.dropEffect = "none";
|
|
364
|
-
});
|
|
365
|
-
document.addEventListener("drop", function (event) {
|
|
366
|
-
event.preventDefault();
|
|
307
|
+
settings.on("keyLayout", (chosen) => keyboard.setKeyLayout(chosen));
|
|
308
|
+
settings.on("tubeCpuMultiplier", (multiplier) => {
|
|
309
|
+
if (processor.hasTube) processor.tube.cpuMultiplier = multiplier;
|
|
367
310
|
});
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
"As a workarond, create an 'Application Shortcut' from the Tools menu. When jsbeeb runs as an application, it *can* prevent ctrl-W from closing the window.";
|
|
374
|
-
event.preventDefault();
|
|
375
|
-
event.returnValue = message;
|
|
376
|
-
return message;
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
function hardReset() {
|
|
381
|
-
rewindUI.reset();
|
|
382
|
-
processor.reset(true);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
document.getElementById("hard-reset").addEventListener("click", function (event) {
|
|
386
|
-
hardReset();
|
|
387
|
-
event.preventDefault();
|
|
311
|
+
const rerouteAnalogue = () => inputs.updateAdcSources(settings.mouseJoystickEnabled, settings.microphoneChannel);
|
|
312
|
+
settings.on("mouseJoystickEnabled", rerouteAnalogue);
|
|
313
|
+
settings.on("microphoneChannel", (channel) => {
|
|
314
|
+
rerouteAnalogue();
|
|
315
|
+
if (channel !== undefined) inputs.setupMicrophone();
|
|
388
316
|
});
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (!processor.filestore) return;
|
|
397
|
-
downloadDriveData(processor.filestore.scsi, "scsi", ".dat");
|
|
317
|
+
config.addEventListener("restart-required", async () => {
|
|
318
|
+
const restart = await modals.confirm(
|
|
319
|
+
"Your change is saved, but only takes effect when the emulator restarts. Restart now?",
|
|
320
|
+
"Restart now",
|
|
321
|
+
"Later",
|
|
322
|
+
);
|
|
323
|
+
if (restart) window.location.reload();
|
|
398
324
|
});
|
|
399
325
|
|
|
400
|
-
|
|
401
|
-
if (Object.hasOwn(parsedQuery, "pp-tos")) modals.show("pp-tos");
|
|
326
|
+
const page = new PageActions({ loop, processor, keyboard, audioHandler, rewindUI, modals, parsedQuery, version });
|
|
402
327
|
|
|
403
328
|
// ------------------------------------------------------------------------
|
|
404
329
|
// Start it up.
|
|
@@ -460,47 +385,14 @@ const startPromise = machine.start({
|
|
|
460
385
|
// The console surface the wiki documents, and the desktop app's hooks.
|
|
461
386
|
// ------------------------------------------------------------------------
|
|
462
387
|
|
|
463
|
-
|
|
464
|
-
// safely run from the JS console in firefox.
|
|
465
|
-
window.benchmarkCpu = utils.debounce((numCycles) => loop.benchmarkCpu(numCycles), 1);
|
|
466
|
-
window.profileCpu = utils.debounce((arg) => loop.profileCpu(arg), 1);
|
|
467
|
-
window.benchmarkVideo = utils.debounce((numCycles) => loop.benchmarkVideo(numCycles), 1);
|
|
468
|
-
window.profileVideo = utils.debounce((arg) => loop.profileVideo(arg), 1);
|
|
469
|
-
window.go = () => loop.go();
|
|
470
|
-
window.stop = (debug) => loop.stop(debug);
|
|
471
|
-
window.soundChip = audioHandler.soundChip;
|
|
472
|
-
window.processor = processor;
|
|
473
|
-
window.video = video;
|
|
474
|
-
window.hd = function (start, end) {
|
|
475
|
-
console.log(
|
|
476
|
-
utils.hd(
|
|
477
|
-
function (x) {
|
|
478
|
-
return processor.readmem(x);
|
|
479
|
-
},
|
|
480
|
-
start,
|
|
481
|
-
end,
|
|
482
|
-
),
|
|
483
|
-
);
|
|
484
|
-
};
|
|
485
|
-
window.m7dump = function () {
|
|
486
|
-
console.log(
|
|
487
|
-
utils.hd(
|
|
488
|
-
function (x) {
|
|
489
|
-
return processor.readmem(x) & 0x7f;
|
|
490
|
-
},
|
|
491
|
-
0x7c00,
|
|
492
|
-
0x7fe8,
|
|
493
|
-
{ width: 40, gap: false },
|
|
494
|
-
),
|
|
495
|
-
);
|
|
496
|
-
};
|
|
388
|
+
exposeConsoleSurface(window, { loop, processor, video, audioHandler });
|
|
497
389
|
|
|
498
390
|
// Hooks for electron.
|
|
499
391
|
electron({
|
|
500
392
|
loadDiscImage: media.loadDiscImage.bind(media),
|
|
501
393
|
loadTapeImage: media.loadTapeImage.bind(media),
|
|
502
394
|
processor,
|
|
503
|
-
|
|
395
|
+
settings,
|
|
504
396
|
media,
|
|
505
397
|
modals: {
|
|
506
398
|
show: (modalId, sthType) => {
|
|
@@ -513,17 +405,11 @@ electron({
|
|
|
513
405
|
},
|
|
514
406
|
loadStateFile: snapshots.loadStateFromFile.bind(snapshots),
|
|
515
407
|
actions: {
|
|
516
|
-
"soft-reset": () =>
|
|
517
|
-
"hard-reset": hardReset,
|
|
408
|
+
"soft-reset": () => page.softReset(),
|
|
409
|
+
"hard-reset": () => page.hardReset(),
|
|
518
410
|
"save-state": () => snapshots.saveState(),
|
|
519
411
|
rewind: () => rewindUI.open(),
|
|
520
|
-
pause:
|
|
521
|
-
resume:
|
|
412
|
+
pause: () => runControls.pause(),
|
|
413
|
+
resume: () => runControls.resume(),
|
|
522
414
|
},
|
|
523
415
|
});
|
|
524
|
-
|
|
525
|
-
// Display version in About dialog
|
|
526
|
-
const versionElement = document.getElementById("jsbeeb-version");
|
|
527
|
-
if (versionElement) {
|
|
528
|
-
versionElement.textContent = `Version ${version}`;
|
|
529
|
-
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { unzipDiscImage } from "./archive.js";
|
|
2
|
+
import { stringToUint8Array } from "./binary.js";
|
|
3
|
+
import { loadData } from "./loader.js";
|
|
4
|
+
|
|
5
|
+
/** Splits an image reference into its schema (empty for a bare name) and the image it names. */
|
|
6
|
+
export function splitImage(image) {
|
|
7
|
+
const match = image.match(/(([^:]+):\/?\/?|[!^|])?(.*)/);
|
|
8
|
+
return { schema: match[2] || match[1] || "", image: match[3] };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Where a bare name is looked for, and which registered source serves the archive.
|
|
12
|
+
const Kinds = {
|
|
13
|
+
disc: { folder: "discs", sth: "sth" },
|
|
14
|
+
tape: { folder: "tapes", sth: "tapeSth" },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** The image itself when `name` is a zip, else the bytes as given, either way as `{ name, data, ignored }`. */
|
|
18
|
+
export function openIfZip(name, data) {
|
|
19
|
+
return /\.zip/i.test(name) ? unzipDiscImage(data) : { name, data, ignored: [] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Turns any image reference the URL can name into bytes, `{ name, data, ignored }`,
|
|
24
|
+
* for a disc or a tape: the archives through the sources registered for them,
|
|
25
|
+
* `data:` and `b64data:` inline, `http:`, `https:` and `file:` by URL, and a
|
|
26
|
+
* bare name from the built-in folder. Zips are opened once, here. Nothing in
|
|
27
|
+
* it needs a page, so a headless machine can load the same references.
|
|
28
|
+
*/
|
|
29
|
+
export class MediaResolver {
|
|
30
|
+
constructor({ load = loadData } = {}) {
|
|
31
|
+
this.sources = {};
|
|
32
|
+
this.load = load;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Registers the fetcher behind an archive schema; each archive registers as it is constructed. */
|
|
36
|
+
addSource(schema, fetcher) {
|
|
37
|
+
this.sources[schema] = fetcher;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async resolve(kind, ref) {
|
|
41
|
+
const { schema, image } = splitImage(ref);
|
|
42
|
+
const { folder, sth } = Kinds[kind];
|
|
43
|
+
switch (schema) {
|
|
44
|
+
case "|":
|
|
45
|
+
case "sth":
|
|
46
|
+
return this.source(sth)(image);
|
|
47
|
+
case "hfe":
|
|
48
|
+
return { name: image, data: await this.source("hfe")(image), ignored: [] };
|
|
49
|
+
case "b64data":
|
|
50
|
+
return { name: "disk.ssd", data: stringToUint8Array(atob(image)), ignored: [] };
|
|
51
|
+
case "data":
|
|
52
|
+
return unzipDiscImage(stringToUint8Array(atob(image)));
|
|
53
|
+
case "http":
|
|
54
|
+
case "https":
|
|
55
|
+
case "file":
|
|
56
|
+
// The URL may end in query parameters, which would upset the extension check.
|
|
57
|
+
return openIfZip(new URL(ref).pathname, await this.load(ref));
|
|
58
|
+
default:
|
|
59
|
+
return openIfZip(image, await this.load(`${folder}/${image}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
source(schema) {
|
|
64
|
+
const fetcher = this.sources[schema];
|
|
65
|
+
if (!fetcher) throw new Error(`No ${schema} archive is available here`);
|
|
66
|
+
return fetcher;
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/mmc.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createZipBlob, unzip } from "./archive.js";
|
|
2
|
+
import { loadData } from "./loader.js";
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
highly adapted from:
|
|
@@ -617,7 +618,7 @@ class WFN {
|
|
|
617
618
|
let fname = this.trimToFilename(this.globalData);
|
|
618
619
|
|
|
619
620
|
if (this.filenum === 0) {
|
|
620
|
-
this.fildataIndex = 0; // FIXME
|
|
621
|
+
this.fildataIndex = 0; // FIXME(#1068) should be one for each fildata if going down this line!
|
|
621
622
|
// The scratch file is fixed, so we are backwards compatible with 2.9 firmware
|
|
622
623
|
let fopen = this.f_open(fname, mode);
|
|
623
624
|
if (fopen.error == FR_OK) this.fildata[0] = this.allfiles[fopen.fp];
|
package/src/models.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
1
|
import { NoiseAwareWdFdc } from "./wd-fdc.js";
|
|
4
2
|
import { NoiseAwareIntelFdc } from "./intel-fdc.js";
|
|
5
3
|
import * as opcodes from "./6502.opcodes.js";
|
|
4
|
+
import { AtomCpu6502, Cpu6502 } from "./6502.js";
|
|
5
|
+
import { BBC, stringToBBCKeys } from "./keymap.js";
|
|
6
|
+
import { ATOM, stringToATOMKeys } from "./keymap-atom.js";
|
|
6
7
|
|
|
7
8
|
const CpuModel = Object.freeze({
|
|
8
9
|
MOS6502: 0,
|
|
@@ -43,6 +44,19 @@ class Model {
|
|
|
43
44
|
this._cpuModel = cpuModel;
|
|
44
45
|
this.isMaster = isMaster;
|
|
45
46
|
this.isAtom = !!isAtom;
|
|
47
|
+
// The one place the two machine families part: everything else asks the model.
|
|
48
|
+
this.Cpu = this.isAtom ? AtomCpu6502 : Cpu6502;
|
|
49
|
+
this.keys = this.isAtom ? ATOM : BBC;
|
|
50
|
+
this.stringToKeys = this.isAtom ? stringToATOMKeys : stringToBBCKeys;
|
|
51
|
+
// The OS write-character vector, watched to capture what the machine prints.
|
|
52
|
+
this.wrchvAddress = this.isAtom ? 0x0208 : 0x020e;
|
|
53
|
+
// Where the machine sits waiting for a key: the Atom kernel's read loop, or BASIC's.
|
|
54
|
+
this.idleAddress = this.isAtom ? 0xfe94 : isMaster ? 0xe7e6 : 0xe581;
|
|
55
|
+
// The Atom ROM polls its keyboard once per VSync, so pasted keys need longer apart.
|
|
56
|
+
this.pasteKeyDelayMs = this.isAtom ? 80 : 50;
|
|
57
|
+
// Two of those 60 Hz scans, 33 ms, with margin: the ROM wants a key seen up
|
|
58
|
+
// twice before it takes the next.
|
|
59
|
+
this.pasteReleaseGapMs = this.isAtom ? 40 : 0;
|
|
46
60
|
this.Fdc = fdc;
|
|
47
61
|
this.swram = swram;
|
|
48
62
|
this.isTest = false;
|
|
@@ -106,7 +120,7 @@ function atomModel({ name, synonyms, os, banks }) {
|
|
|
106
120
|
});
|
|
107
121
|
}
|
|
108
122
|
|
|
109
|
-
// TODO
|
|
123
|
+
// TODO(#1066) semi-bplus-style to get swram for exile hardcoded here
|
|
110
124
|
const beebSwram = [
|
|
111
125
|
true,
|
|
112
126
|
true,
|
package/src/music5000.js
CHANGED
package/src/ppia.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getKeyMapAtom } from "./
|
|
1
|
+
import { getKeyMapAtom } from "./keymap-atom.js";
|
|
2
2
|
|
|
3
3
|
// 8255 Programmable Peripheral Interface Adapter for the Acorn Atom.
|
|
4
4
|
// Reference: http://mdfs.net/Docs/Comp/Acorn/Atom/atap25.htm
|
|
@@ -415,9 +415,8 @@ export class AtomPPIA extends PPIA {
|
|
|
415
415
|
}
|
|
416
416
|
|
|
417
417
|
rewindTape() {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
418
|
+
this.stopTape();
|
|
419
|
+
if (this.tape) this.tape.rewind();
|
|
421
420
|
}
|
|
422
421
|
|
|
423
422
|
playTape() {
|
package/src/printer.js
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Centronics printer attached to the user VIA: acknowledges each character and keeps the most
|
|
5
|
-
* recent output so it can be shown whenever a printer window is opened.
|
|
3
|
+
* recent output so it can be shown whenever a printer window is opened. Dispatches "output" with
|
|
4
|
+
* each character as its detail, and "first-output" once, when the machine first prints.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
export const MaxBufferedChars = 64 * 1024;
|
|
9
8
|
|
|
10
|
-
export class Printer {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* @param {function(string): void} [handlers.onOutput] called with each character as it is printed
|
|
14
|
-
* @param {function(): void} [handlers.onFirstOutput] called once, when the machine first prints
|
|
15
|
-
*/
|
|
16
|
-
constructor({ onOutput = () => {}, onFirstOutput = () => {} } = {}) {
|
|
17
|
-
this._onOutput = onOutput;
|
|
18
|
-
this._onFirstOutput = onFirstOutput;
|
|
9
|
+
export class Printer extends EventTarget {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
19
12
|
this._uservia = null;
|
|
20
13
|
this._olderText = "";
|
|
21
14
|
this._newerText = "";
|
|
@@ -52,8 +45,8 @@ export class Printer {
|
|
|
52
45
|
}
|
|
53
46
|
if (!this._hasPrinted) {
|
|
54
47
|
this._hasPrinted = true;
|
|
55
|
-
this.
|
|
48
|
+
this.dispatchEvent(new Event("first-output"));
|
|
56
49
|
}
|
|
57
|
-
this.
|
|
50
|
+
this.dispatchEvent(new CustomEvent("output", { detail: char }));
|
|
58
51
|
}
|
|
59
52
|
}
|
package/src/relaynoise.js
CHANGED
package/src/rewind.js
CHANGED
package/src/sample-player.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import * as utils from "./utils.js";
|
|
1
|
+
import { loadData } from "./loader.js";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Base class for audio components that load and play back sample buffers
|
|
@@ -27,7 +26,7 @@ export class SamplePlayer {
|
|
|
27
26
|
const entries = Object.entries(pathMap);
|
|
28
27
|
const decoded = await Promise.all(
|
|
29
28
|
entries.map(async ([, path]) => {
|
|
30
|
-
const data = await
|
|
29
|
+
const data = await loadData(path);
|
|
31
30
|
// Safari doesn't support the promise form of decodeAudioData.
|
|
32
31
|
return new Promise((resolve, reject) => {
|
|
33
32
|
this.context.decodeAudioData(
|
package/src/serial.js
CHANGED
package/src/snapshot-helpers.js
CHANGED
package/src/snapshot.js
CHANGED
package/src/soundchip.js
CHANGED
|
@@ -347,7 +347,7 @@ export class SoundChip {
|
|
|
347
347
|
updateSlowDataBus(slowDataBus, active) {
|
|
348
348
|
this.slowDataBus = slowDataBus;
|
|
349
349
|
this.active = active;
|
|
350
|
-
// TODO
|
|
350
|
+
// TODO(#835) this probably isn't modeled correctly. Currently the sound chip "notices" a new data bus value some
|
|
351
351
|
// fixed number of cycles after WE (write enable) is triggered. In reality, the sound chip likely pulls data off
|
|
352
352
|
// the bus at a fixed point in its cycle, iff WE is active.
|
|
353
353
|
if (active) {
|