jsbeeb 1.22.4 → 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 +16 -11
- package/src/6502.js +39 -42
- package/src/6502.opcodes.js +198 -123
- 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/jsbeeb.css +33 -0
- 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 +69 -184
- package/src/media-resolver.js +68 -0
- package/src/mmc.js +3 -2
- package/src/models.js +40 -4
- 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 +42 -72
- package/src/web/console-surface.js +25 -0
- package/src/web/debug.js +8 -49
- 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/icons.js +42 -0
- package/src/web/keyboard-setup.js +29 -40
- 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/web/config.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
import { allModels, findModel, tubeModelFor } from "../models.js";
|
|
3
|
-
import { getFilterForMode } from "
|
|
2
|
+
import { getFilterForMode } from "./canvas.js";
|
|
4
3
|
import { AudioOutputs } from "../audio-output.js";
|
|
5
4
|
|
|
6
5
|
const round = (value) => Number(value.toFixed(2));
|
|
@@ -50,45 +49,45 @@ export function restartPending(settings, running) {
|
|
|
50
49
|
return RestartRequiredFields.some((field) => settings[field] !== running[field]);
|
|
51
50
|
}
|
|
52
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The Settings dialog: shows the settings, applies the ones the machine can
|
|
54
|
+
* follow live as they are picked, and saves the rest when it closes. Dispatches
|
|
55
|
+
* "restart-required" after saving a change the running machine cannot follow.
|
|
56
|
+
*/
|
|
53
57
|
export class Config extends EventTarget {
|
|
54
|
-
|
|
55
|
-
* @param {function(object)} onChange called as soon as a setting the emulator can follow live changes
|
|
56
|
-
* @param {function(object)} onClose called with the settings to apply and persist
|
|
57
|
-
* @param {function()} onRestartRequired called after the settings have been saved, when some of them
|
|
58
|
-
* will only take effect once the machine is rebuilt
|
|
59
|
-
*/
|
|
60
|
-
constructor(onChange, onClose, onRestartRequired) {
|
|
58
|
+
constructor(settings) {
|
|
61
59
|
super();
|
|
62
|
-
this.
|
|
63
|
-
this.onClose = onClose;
|
|
64
|
-
this.onRestartRequired = onRestartRequired;
|
|
60
|
+
this.settings = settings;
|
|
65
61
|
this.changed = {};
|
|
66
|
-
this
|
|
67
|
-
|
|
68
|
-
this.
|
|
62
|
+
// Built before anything can change, so this is what the running machine was built with.
|
|
63
|
+
this.runningSettings = this.proposedSettings();
|
|
64
|
+
this.setModel(settings.model);
|
|
65
|
+
this.setKeyLayout(settings.keyLayout);
|
|
66
|
+
this.setMicrophoneChannel(settings.microphoneChannel);
|
|
67
|
+
this.setDisplayMode(settings.displayMode);
|
|
68
|
+
this.setAudioOutput(settings.audioOutput);
|
|
69
|
+
this.setSpeakerAmount(settings.speakerAmount);
|
|
70
|
+
settings.on("audioOutput", (audioOutput) => this.setAudioOutput(audioOutput));
|
|
71
|
+
settings.on("speakerAmount", (speakerAmount) => this.setSpeakerAmount(speakerAmount));
|
|
72
|
+
settings.on("displayMode", (displayMode) => this.setDisplayMode(displayMode));
|
|
73
|
+
|
|
69
74
|
const configuration = document.getElementById("configuration");
|
|
70
75
|
configuration.addEventListener("show.bs.modal", () => {
|
|
71
76
|
this.changed = {};
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.setDropdownText(this.model.name);
|
|
76
|
-
this.setTubeCpuMultiplier(this.tubeCpuMultiplier);
|
|
77
|
-
this.setCheckboxes(this);
|
|
77
|
+
this.setDropdownText(settings.model.name);
|
|
78
|
+
this.setTubeCpuMultiplier(settings.tubeCpuMultiplier);
|
|
79
|
+
this.setCheckboxes(settings);
|
|
78
80
|
this.showRestartPending();
|
|
79
81
|
});
|
|
80
82
|
|
|
81
83
|
configuration.addEventListener("hide.bs.modal", () => {
|
|
82
84
|
const changed = this.changed;
|
|
83
|
-
|
|
84
|
-
if (changed.model !== undefined) this.model = findModel(changed.model);
|
|
85
|
-
this.setCheckboxes(changed);
|
|
86
|
-
this.onClose(changed);
|
|
85
|
+
this.changed = {};
|
|
87
86
|
if (Object.keys(changed).length === 0) return;
|
|
88
|
-
|
|
87
|
+
settings.set(changed);
|
|
89
88
|
// changed records which controls were touched, so a value in it can be what is already running.
|
|
90
89
|
if (needsRestart(changed) && restartPending(this.proposedSettings(), this.runningSettings))
|
|
91
|
-
this.
|
|
90
|
+
this.dispatchEvent(new Event("restart-required"));
|
|
92
91
|
});
|
|
93
92
|
|
|
94
93
|
const modelMenu = document.querySelector(".model-menu");
|
|
@@ -108,7 +107,10 @@ export class Config extends EventTarget {
|
|
|
108
107
|
if (!link) return;
|
|
109
108
|
this.changed.model = link.dataset.target;
|
|
110
109
|
this.setDropdownText(link.textContent);
|
|
111
|
-
this.showTubeCpuMultiplier(
|
|
110
|
+
this.showTubeCpuMultiplier(
|
|
111
|
+
this.changed.tubeCpuMultiplier ?? this.settings.tubeCpuMultiplier,
|
|
112
|
+
findModel(link.dataset.target),
|
|
113
|
+
);
|
|
112
114
|
this.showRestartPending();
|
|
113
115
|
});
|
|
114
116
|
|
|
@@ -149,23 +151,18 @@ export class Config extends EventTarget {
|
|
|
149
151
|
for (const option of document.querySelectorAll(".audio-output-option")) {
|
|
150
152
|
option.addEventListener("click", (e) => {
|
|
151
153
|
e.preventDefault();
|
|
152
|
-
|
|
153
|
-
this.changed.audioOutput = audioOutput;
|
|
154
|
-
this.onChange({ audioOutput });
|
|
154
|
+
settings.set({ audioOutput: e.currentTarget.dataset.output });
|
|
155
155
|
});
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
document.getElementById("speakerAmountSetting").addEventListener("input", (e) => {
|
|
159
|
-
|
|
159
|
+
settings.set({ speakerAmount: parseFloat(e.currentTarget.value) });
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
for (const option of document.querySelectorAll(".display-mode-option")) {
|
|
163
163
|
option.addEventListener("click", (e) => {
|
|
164
164
|
e.preventDefault();
|
|
165
|
-
|
|
166
|
-
this.changed.displayMode = mode;
|
|
167
|
-
this.setDisplayMode(mode);
|
|
168
|
-
this.onChange({ displayMode: mode });
|
|
165
|
+
settings.set({ displayMode: e.currentTarget.dataset.mode });
|
|
169
166
|
});
|
|
170
167
|
}
|
|
171
168
|
}
|
|
@@ -175,7 +172,7 @@ export class Config extends EventTarget {
|
|
|
175
172
|
* menu and the URL use: the model by synonym rather than resolved, the fittings as booleans.
|
|
176
173
|
*/
|
|
177
174
|
proposedSettings() {
|
|
178
|
-
const saved = (field) => (field === "model" ? this.model.synonyms[0] : this[field]);
|
|
175
|
+
const saved = (field) => (field === "model" ? this.settings.model.synonyms[0] : this.settings[field]);
|
|
179
176
|
return Object.fromEntries(RestartRequiredFields.map((field) => [field, this.changed[field] ?? saved(field)]));
|
|
180
177
|
}
|
|
181
178
|
|
|
@@ -184,13 +181,12 @@ export class Config extends EventTarget {
|
|
|
184
181
|
document.getElementById("restart-pending").classList.toggle("d-none", !pending);
|
|
185
182
|
}
|
|
186
183
|
|
|
187
|
-
/** Ticks the boxes named in `values
|
|
184
|
+
/** Ticks the boxes named in `values`, leaving any the object does not mention alone. */
|
|
188
185
|
setCheckboxes(values) {
|
|
189
186
|
for (const { id, field, enables } of CheckboxSettings) {
|
|
190
187
|
if (values[field] === undefined) continue;
|
|
191
188
|
const checked = !!values[field];
|
|
192
189
|
document.getElementById(id).checked = checked;
|
|
193
|
-
this[field] = checked;
|
|
194
190
|
if (enables) document.getElementById(enables).disabled = !checked;
|
|
195
191
|
}
|
|
196
192
|
}
|
|
@@ -213,9 +209,12 @@ export class Config extends EventTarget {
|
|
|
213
209
|
for (const el of document.querySelectorAll(".display-mode-text")) el.textContent = config.name;
|
|
214
210
|
}
|
|
215
211
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
for (const el of document.querySelectorAll(".bbc-model")) el.textContent =
|
|
212
|
+
/** Names the running machine everywhere the page shows it. */
|
|
213
|
+
setModel(model) {
|
|
214
|
+
for (const el of document.querySelectorAll(".bbc-model")) el.textContent = model.name;
|
|
215
|
+
for (const el of document.querySelectorAll(".bbc-model-short")) el.textContent = model.shortName;
|
|
216
|
+
const settingsLink = document.getElementById("model-settings");
|
|
217
|
+
if (settingsLink) settingsLink.title = `${model.name}: emulation settings`;
|
|
219
218
|
}
|
|
220
219
|
|
|
221
220
|
setKeyLayout(keyLayout) {
|
|
@@ -224,12 +223,11 @@ export class Config extends EventTarget {
|
|
|
224
223
|
}
|
|
225
224
|
|
|
226
225
|
setTubeCpuMultiplier(value) {
|
|
227
|
-
this.tubeCpuMultiplier = value;
|
|
228
226
|
document.getElementById("tubeCpuMultiplier").value = value;
|
|
229
227
|
this.showTubeCpuMultiplier(value);
|
|
230
228
|
}
|
|
231
229
|
|
|
232
|
-
showTubeCpuMultiplier(value, model = this.model) {
|
|
230
|
+
showTubeCpuMultiplier(value, model = this.settings.model) {
|
|
233
231
|
document.getElementById("tubeCpuMultiplierValue").textContent = tubeCpuSpeedLabel(value, model);
|
|
234
232
|
}
|
|
235
233
|
|
|
@@ -237,32 +235,4 @@ export class Config extends EventTarget {
|
|
|
237
235
|
const el = document.querySelector("#bbc-model-dropdown .bbc-model");
|
|
238
236
|
if (el) el.textContent = modelName;
|
|
239
237
|
}
|
|
240
|
-
|
|
241
|
-
get extraRoms() {
|
|
242
|
-
return fittedRoms(this);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
mapLegacyModels(parsedQuery) {
|
|
246
|
-
if (!parsedQuery.model) {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// "MasterTurbo" = Master + 6502 second processor
|
|
251
|
-
if (parsedQuery.model.toLowerCase() === "masterturbo") {
|
|
252
|
-
parsedQuery.model = "Master";
|
|
253
|
-
parsedQuery.coProcessor = true;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// "BMusic5000" = BBC DFS 1.2 + Music 5000
|
|
257
|
-
if (parsedQuery.model.toLowerCase() === "bmusic5000") {
|
|
258
|
-
parsedQuery.model = "B-DFS1.2";
|
|
259
|
-
parsedQuery.hasMusic5000 = true;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// "BTeletext" = BBC DFS 1.2 + Teletext adaptor
|
|
263
|
-
if (parsedQuery.model.toLowerCase() === "bteletext") {
|
|
264
|
-
parsedQuery.model = "B-DFS1.2";
|
|
265
|
-
parsedQuery.hasTeletextAdaptor = true;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
238
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { hd } from "../hex.js";
|
|
2
|
+
import { debounce } from "../debounce.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The debugging surface the wiki documents, put on `target` (window in the
|
|
6
|
+
* app). The bench/profile calls are debounced so that they can be safely run
|
|
7
|
+
* from the JS console in firefox.
|
|
8
|
+
*/
|
|
9
|
+
export function exposeConsoleSurface(target, { loop, processor, video, audioHandler }) {
|
|
10
|
+
target.benchmarkCpu = debounce((numCycles) => loop.benchmarkCpu(numCycles), 1);
|
|
11
|
+
target.profileCpu = debounce((arg) => loop.profileCpu(arg), 1);
|
|
12
|
+
target.benchmarkVideo = debounce((numCycles) => loop.benchmarkVideo(numCycles), 1);
|
|
13
|
+
target.profileVideo = debounce((arg) => loop.profileVideo(arg), 1);
|
|
14
|
+
target.go = () => loop.go();
|
|
15
|
+
target.stop = (debug) => loop.stop(debug);
|
|
16
|
+
target.soundChip = audioHandler.soundChip;
|
|
17
|
+
target.processor = processor;
|
|
18
|
+
target.video = video;
|
|
19
|
+
target.hd = (start, end) => {
|
|
20
|
+
console.log(hd((x) => processor.readmem(x), start, end));
|
|
21
|
+
};
|
|
22
|
+
target.m7dump = () => {
|
|
23
|
+
console.log(hd((x) => processor.readmem(x) & 0x7f, 0x7c00, 0x7fe8, { width: 40, gap: false }));
|
|
24
|
+
};
|
|
25
|
+
}
|
package/src/web/debug.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
import { hexbyte, hexword,
|
|
3
|
-
|
|
1
|
+
import { toggle } from "./dom-utils.js";
|
|
2
|
+
import { hexbyte, hexword, parseAddr } from "../hex.js";
|
|
3
|
+
|
|
4
|
+
const noop = () => {};
|
|
4
5
|
|
|
5
6
|
const numToShow = 16;
|
|
6
7
|
|
|
@@ -60,7 +61,7 @@ class MemoryView {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
//
|
|
64
|
+
// TODO(#1062) extract memory part from view part and reuse in disassembler etc. also peekmem can go then
|
|
64
65
|
dump(from, to) {
|
|
65
66
|
const hex = [];
|
|
66
67
|
const asc = [];
|
|
@@ -89,48 +90,6 @@ class MemoryView {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
/**
|
|
93
|
-
* Guesses the address of the instruction before the given one by scoring each
|
|
94
|
-
* possible run of instructions leading up to it: a point per "common"
|
|
95
|
-
* instruction (loads, stores, branches, compares, arithmetic and
|
|
96
|
-
* carry-set/clear that avoid unusual indexing modes) and a large boost for a
|
|
97
|
-
* run that passes through the current program counter. The highest-scoring
|
|
98
|
-
* run wins and its last instruction is the answer.
|
|
99
|
-
* Good test cases:
|
|
100
|
-
* Repton 2 @ 2cbb
|
|
101
|
-
* MOS @ cfc8
|
|
102
|
-
* also, just starting from the back of ROM and going up...
|
|
103
|
-
*/
|
|
104
|
-
export function prevInstruction(disassemble, pc, address) {
|
|
105
|
-
const commonInstructions =
|
|
106
|
-
/(RTS|B..|JMP|JSR|LD[AXY]|ST[AXY]|TA[XY]|T[XY]A|AD[DC]|SUB|SBC|CLC|SEC|CMP|EOR|ORR|AND|INC|DEC).*/;
|
|
107
|
-
const uncommonInstrucions = /.*,\s*([XY]|X\))$/;
|
|
108
|
-
|
|
109
|
-
address &= 0xffff;
|
|
110
|
-
let bestAddr = address - 1;
|
|
111
|
-
let bestScore = 0;
|
|
112
|
-
for (let startingPoint = address - 20; startingPoint !== address; startingPoint++) {
|
|
113
|
-
let score = 0;
|
|
114
|
-
let addr = startingPoint & 0xffff;
|
|
115
|
-
while (addr < address) {
|
|
116
|
-
const result = disassemble(addr);
|
|
117
|
-
if (addr === pc) score += 10;
|
|
118
|
-
if (result[0].match(commonInstructions) && !result[0].match(uncommonInstrucions)) {
|
|
119
|
-
score++;
|
|
120
|
-
}
|
|
121
|
-
if (result[1] === address) {
|
|
122
|
-
if (score > bestScore) {
|
|
123
|
-
bestScore = score;
|
|
124
|
-
bestAddr = addr;
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
addr = result[1];
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return bestAddr;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
93
|
export class Debugger {
|
|
135
94
|
constructor() {
|
|
136
95
|
this.patchInstructions = new Map();
|
|
@@ -424,7 +383,7 @@ export class Debugger {
|
|
|
424
383
|
}
|
|
425
384
|
|
|
426
385
|
prevInstruction(address) {
|
|
427
|
-
return
|
|
386
|
+
return this.cpu.disassembler.prevInstruction(address, this.cpu.pc);
|
|
428
387
|
}
|
|
429
388
|
|
|
430
389
|
updatePrevMem() {
|
|
@@ -436,7 +395,7 @@ export class Debugger {
|
|
|
436
395
|
}
|
|
437
396
|
|
|
438
397
|
stepUntil(f) {
|
|
439
|
-
this.cpu.targetCycles = this.cpu.currentCycles; // TODO
|
|
398
|
+
this.cpu.targetCycles = this.cpu.currentCycles; // TODO(#1062) this prevents the cpu from running any residual cycles. look into a better solution
|
|
440
399
|
for (let i = 0; i < 65536; i++) {
|
|
441
400
|
this.cpu.execute(1);
|
|
442
401
|
if (f()) break;
|
|
@@ -483,7 +442,7 @@ export class Debugger {
|
|
|
483
442
|
}
|
|
484
443
|
|
|
485
444
|
nextInstruction(address) {
|
|
486
|
-
return this.
|
|
445
|
+
return this.cpu.disassembler.nextInstruction(address);
|
|
487
446
|
}
|
|
488
447
|
|
|
489
448
|
instrClick(e) {
|
package/src/web/display.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import * as canvasLib from "
|
|
1
|
+
import * as canvasLib from "./canvas.js";
|
|
2
2
|
import { FakeVideo, Video } from "../video.js";
|
|
3
3
|
import { toast } from "./toast.js";
|
|
4
4
|
|
|
5
|
+
// While running fast the state machines still run accurately, but painting is
|
|
6
|
+
// the most expensive part of jsbeeb, so most frames are skipped. Odd, so that
|
|
7
|
+
// interlaced modes (MODE 7) alternate fields across the frames that do paint.
|
|
8
|
+
const SpeedyFrameSkip = 9;
|
|
9
|
+
|
|
5
10
|
/**
|
|
6
11
|
* The picture: the canvas and its filter, the video chip that paints into a
|
|
7
12
|
* framebuffer of our own, and the animation frame that presents it. A stalled
|
|
@@ -64,10 +69,22 @@ export class Display {
|
|
|
64
69
|
this.setCrtPic();
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
/**
|
|
73
|
+
* While running fast the skip moves into the video chip, which skips the
|
|
74
|
+
* render as well as the present; the deeper of the chip's skip and any
|
|
75
|
+
* configured frameSkip wins, so the two never multiply together.
|
|
76
|
+
*/
|
|
77
|
+
setSpeedy(speedy) {
|
|
78
|
+
const skip = Math.max(SpeedyFrameSkip, this.frameSkip);
|
|
79
|
+
this.video.frameSkipCount = speedy ? (skip % 2 ? skip : skip + 1) : 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
onPaint(video, minx, miny, maxx, maxy) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
if (!video.frameSkipCount) {
|
|
84
|
+
this.frames++;
|
|
85
|
+
if (this.frames < this.frameSkip) return;
|
|
86
|
+
this.frames = 0;
|
|
87
|
+
}
|
|
71
88
|
const start = performance.now();
|
|
72
89
|
this.canvas.fb32.set(this.videoFb32.subarray(miny * 1024, maxy * 1024), miny * 1024);
|
|
73
90
|
if (this.pendingFrame.lineGrid.length !== video.lineGrid.length)
|
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { replaceOrAddExtension } from "./utils.js";
|
|
1
|
+
import { replaceOrAddExtension } from "../archive.js";
|
|
4
2
|
|
|
5
3
|
// Minimal DOM helpers to replace jQuery usage.
|
|
6
4
|
|
|
7
|
-
export function show(el) {
|
|
8
|
-
el.style.display = "";
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function hide(el) {
|
|
12
|
-
el.style.display = "none";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
5
|
export function toggle(el, visible) {
|
|
16
6
|
el.style.display = visible ? "" : "none";
|
|
17
7
|
}
|
package/src/web/drives.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { toast } from "./toast.js";
|
|
2
2
|
import { DiscLayout, toSsdOrDsd } from "../disc.js";
|
|
3
3
|
import { toHfe } from "../disc-hfe.js";
|
|
4
|
-
import { downloadDriveData } from "
|
|
4
|
+
import { downloadDriveData } from "./dom-utils.js";
|
|
5
5
|
import { DriveTracks } from "../url-params.js";
|
|
6
6
|
|
|
7
7
|
const tracksPerStepFor = (tracks) => (tracks === "40" ? 2 : 1);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { noteEvent } from "./analytics.js";
|
|
2
2
|
|
|
3
3
|
// A timer, not requestAnimationFrame: a display presentation stall withholds
|
|
4
4
|
// animation frames, and with them the sound chip's samples (issue #885).
|
|
@@ -49,9 +49,14 @@ class VirtualSpeedUpdater {
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Runs the machine in real time: the tick that turns wall-clock time into
|
|
52
|
-
* cycles, starting and stopping, the audio lead, fast-forward
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
* cycles, starting and stopping, the audio lead, fast-forward and the speed
|
|
53
|
+
* readout. Owns `running`, and dispatches a "running" event whenever it
|
|
54
|
+
* changes hands, "tick" on every tick while running (the first only takes the
|
|
55
|
+
* time), and "rewind-capture" every RewindCaptureInterval frames for whoever
|
|
56
|
+
* keeps the rewind history. Anything that needs the machine held still while
|
|
57
|
+
* it works (a dialog, a snapshot, the rewind panel, a hidden tab) takes a
|
|
58
|
+
* `pause()`; the loop runs again once every hold has let go, provided the user
|
|
59
|
+
* still wants it running.
|
|
55
60
|
*/
|
|
56
61
|
export class EmulationLoop extends EventTarget {
|
|
57
62
|
constructor({
|
|
@@ -61,9 +66,6 @@ export class EmulationLoop extends EventTarget {
|
|
|
61
66
|
dbgr,
|
|
62
67
|
gamepad,
|
|
63
68
|
keyboard,
|
|
64
|
-
syncLights,
|
|
65
|
-
rewindBuffer,
|
|
66
|
-
onRewindCaptured,
|
|
67
69
|
clocksPerSecond,
|
|
68
70
|
cpuSpeed,
|
|
69
71
|
fastTape,
|
|
@@ -76,9 +78,6 @@ export class EmulationLoop extends EventTarget {
|
|
|
76
78
|
this.dbgr = dbgr;
|
|
77
79
|
this.gamepad = gamepad;
|
|
78
80
|
this.keyboard = keyboard;
|
|
79
|
-
this.syncLights = syncLights;
|
|
80
|
-
this.rewindBuffer = rewindBuffer;
|
|
81
|
-
this.onRewindCaptured = onRewindCaptured;
|
|
82
81
|
this.clocksPerSecond = clocksPerSecond;
|
|
83
82
|
this.maxCyclesPerTick = clocksPerSecond / 10;
|
|
84
83
|
this.rewindCaptureCycles = (RewindCaptureInterval * clocksPerSecond) / 50;
|
|
@@ -92,7 +91,9 @@ export class EmulationLoop extends EventTarget {
|
|
|
92
91
|
this.tickToken = null;
|
|
93
92
|
this.emulationLeadMs = 0;
|
|
94
93
|
this.rewindCycleCounter = 0;
|
|
95
|
-
this.
|
|
94
|
+
this.wanted = false;
|
|
95
|
+
this.holds = new Set();
|
|
96
|
+
this.resumeOnVisible = null;
|
|
96
97
|
|
|
97
98
|
this.virtualSpeedUpdater = new VirtualSpeedUpdater(cpuSpeed);
|
|
98
99
|
this.audioDebugLog = { start: 0, ticks: 0, cycles: 0, maxIdle: 0, maxExecute: 0, maxPaint: 0, maxSnapshot: 0 };
|
|
@@ -105,17 +106,47 @@ export class EmulationLoop extends EventTarget {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
go() {
|
|
109
|
+
this.wanted = true;
|
|
110
|
+
if (this.holds.size === 0) this.start();
|
|
111
|
+
else
|
|
112
|
+
console.warn(
|
|
113
|
+
`Emulator held by ${[...this.holds].map((hold) => hold.reason).join(", ")}; it runs when they let go`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
stop(debug) {
|
|
118
|
+
this.wanted = false;
|
|
119
|
+
this.halt();
|
|
120
|
+
if (debug) this.dbgr.debug(this.processor.pc);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Holds the machine stopped until the returned function is called. Holds
|
|
125
|
+
* nest, and letting go twice counts once. `reason` names the holder when a
|
|
126
|
+
* go() has to wait, so a hold that is never released can be found.
|
|
127
|
+
*/
|
|
128
|
+
pause(reason) {
|
|
129
|
+
if (this.holds.size === 0 && this.running) this.halt();
|
|
130
|
+
const hold = { reason };
|
|
131
|
+
this.holds.add(hold);
|
|
132
|
+
return () => {
|
|
133
|
+
if (!this.holds.delete(hold)) return;
|
|
134
|
+
if (this.holds.size === 0 && this.wanted) this.start();
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
start() {
|
|
139
|
+
if (this.running) return;
|
|
108
140
|
this.audioHandler.unmute();
|
|
109
141
|
this.running = true;
|
|
110
142
|
this.dispatchEvent(new Event("running"));
|
|
111
143
|
this.run();
|
|
112
144
|
}
|
|
113
145
|
|
|
114
|
-
|
|
146
|
+
halt() {
|
|
115
147
|
this.running = false;
|
|
116
148
|
this.dispatchEvent(new Event("running"));
|
|
117
149
|
this.processor.stop();
|
|
118
|
-
if (debug) this.dbgr.debug(this.processor.pc);
|
|
119
150
|
this.audioHandler.mute();
|
|
120
151
|
}
|
|
121
152
|
|
|
@@ -146,20 +177,15 @@ export class EmulationLoop extends EventTarget {
|
|
|
146
177
|
const now = performance.now();
|
|
147
178
|
|
|
148
179
|
const { processor, display, audioHandler } = this;
|
|
149
|
-
const motorOn = processor.
|
|
180
|
+
const motorOn = processor.tapeInterface.motorOn;
|
|
150
181
|
const speedy = this.fastAsPossible || (this.fastTape && motorOn);
|
|
151
182
|
|
|
152
|
-
|
|
153
|
-
// but we paint less often because painting is the most expensive
|
|
154
|
-
// part of jsbeeb at this time.
|
|
155
|
-
// We need need to paint per odd number of frames so that interlace
|
|
156
|
-
// modes, i.e. MODE 7, still look ok.
|
|
157
|
-
display.video.frameSkipCount = speedy ? 9 : 0;
|
|
183
|
+
display.setSpeedy(speedy);
|
|
158
184
|
|
|
159
185
|
this.scheduleTick(speedy ? 0 : TickMs);
|
|
160
186
|
|
|
161
187
|
this.gamepad.update(processor.sysvia);
|
|
162
|
-
this.
|
|
188
|
+
this.dispatchEvent(new Event("tick"));
|
|
163
189
|
if (this.last !== 0) {
|
|
164
190
|
let cycles;
|
|
165
191
|
if (!speedy) {
|
|
@@ -182,8 +208,7 @@ export class EmulationLoop extends EventTarget {
|
|
|
182
208
|
this.rewindCycleCounter += cycles;
|
|
183
209
|
if (this.rewindCycleCounter >= this.rewindCaptureCycles) {
|
|
184
210
|
this.rewindCycleCounter -= this.rewindCaptureCycles;
|
|
185
|
-
this.
|
|
186
|
-
this.onRewindCaptured();
|
|
211
|
+
this.dispatchEvent(new Event("rewind-capture"));
|
|
187
212
|
snapshotMs = performance.now() - end;
|
|
188
213
|
}
|
|
189
214
|
if (this.audioStatsNode)
|
|
@@ -197,7 +222,7 @@ export class EmulationLoop extends EventTarget {
|
|
|
197
222
|
);
|
|
198
223
|
} catch (e) {
|
|
199
224
|
this.running = false;
|
|
200
|
-
|
|
225
|
+
noteEvent("exception", "thrown", e.stack);
|
|
201
226
|
this.dbgr.debug(processor.pc);
|
|
202
227
|
throw e;
|
|
203
228
|
}
|
|
@@ -227,16 +252,12 @@ export class EmulationLoop extends EventTarget {
|
|
|
227
252
|
handleVisibilityChange() {
|
|
228
253
|
const { processor } = this;
|
|
229
254
|
if (document.visibilityState === "hidden") {
|
|
230
|
-
this.wasPreviouslyRunning = this.running;
|
|
231
255
|
const keepRunningWhenHidden =
|
|
232
|
-
processor.
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (this.wasPreviouslyRunning) {
|
|
238
|
-
this.go();
|
|
239
|
-
}
|
|
256
|
+
processor.tapeInterface.motorOn || processor.fdc.motorOn[0] || processor.fdc.motorOn[1];
|
|
257
|
+
if (!keepRunningWhenHidden && !this.resumeOnVisible) this.resumeOnVisible = this.pause("the hidden tab");
|
|
258
|
+
} else if (this.resumeOnVisible) {
|
|
259
|
+
this.resumeOnVisible();
|
|
260
|
+
this.resumeOnVisible = null;
|
|
240
261
|
}
|
|
241
262
|
}
|
|
242
263
|
|
package/src/web/front-panel.js
CHANGED
|
@@ -18,12 +18,20 @@ class Light {
|
|
|
18
18
|
* controls, and the pop-up window the printer prints into.
|
|
19
19
|
*/
|
|
20
20
|
export class FrontPanel {
|
|
21
|
-
constructor({ processor, model, printer }) {
|
|
21
|
+
constructor({ processor, model, printer, loop }) {
|
|
22
22
|
this.processor = processor;
|
|
23
23
|
this.model = model;
|
|
24
24
|
this.printer = printer;
|
|
25
25
|
this.printerWindow = null;
|
|
26
26
|
this.printerTextArea = null;
|
|
27
|
+
loop.addEventListener("tick", () => this.syncLights());
|
|
28
|
+
printer.addEventListener("output", (event) => this.printChar(event.detail));
|
|
29
|
+
printer.addEventListener("first-output", () =>
|
|
30
|
+
toast("Printer output is being kept. Press Ctrl-B to open the printer window.", {
|
|
31
|
+
title: "Printer",
|
|
32
|
+
quietKey: "quietPrinterOutput",
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
27
35
|
|
|
28
36
|
for (const link of document.querySelectorAll("#tape-menu a")) {
|
|
29
37
|
link.addEventListener("click", (e) => {
|
|
@@ -32,13 +40,8 @@ export class FrontPanel {
|
|
|
32
40
|
|
|
33
41
|
if (type === "rewind") {
|
|
34
42
|
console.log("Rewinding tape to the start");
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
processor.atomppia.rewindTape();
|
|
38
|
-
this.updateTapeButton();
|
|
39
|
-
} else {
|
|
40
|
-
processor.acia.rewindTape();
|
|
41
|
-
}
|
|
43
|
+
processor.tapeInterface.rewindTape();
|
|
44
|
+
this.updateTapeButton();
|
|
42
45
|
}
|
|
43
46
|
});
|
|
44
47
|
}
|
|
@@ -92,14 +95,12 @@ export class FrontPanel {
|
|
|
92
95
|
|
|
93
96
|
syncLights() {
|
|
94
97
|
const { processor } = this;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
} else {
|
|
98
|
+
this.cassette.update(processor.tapeInterface.motorOn);
|
|
99
|
+
if (!this.model.isAtom) {
|
|
98
100
|
this.caps.update(processor.sysvia.capsLockLight);
|
|
99
101
|
this.shift.update(processor.sysvia.shiftLockLight);
|
|
100
102
|
this.drive0.update(processor.fdc.motorOn[0]);
|
|
101
103
|
this.drive1.update(processor.fdc.motorOn[1]);
|
|
102
|
-
this.cassette.update(processor.acia.motorOn);
|
|
103
104
|
if (processor.econet) {
|
|
104
105
|
this.network.update(processor.econet.activityLight());
|
|
105
106
|
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import * as utils from "./utils.js";
|
|
3
|
-
|
|
4
|
-
const BBC = utils.BBC;
|
|
1
|
+
import { BBC, isFirefox } from "../keymap.js";
|
|
5
2
|
|
|
6
3
|
export class GamePad {
|
|
7
4
|
constructor() {
|
|
@@ -201,8 +198,7 @@ export class GamePad {
|
|
|
201
198
|
// process gamepad buttons
|
|
202
199
|
if (this.gamepad0) {
|
|
203
200
|
// these two lines needed in Chrome to update state, not Firefox
|
|
204
|
-
|
|
205
|
-
if (!utils.isFirefox()) {
|
|
201
|
+
if (!isFirefox()) {
|
|
206
202
|
this.gamepad0 = navigator.getGamepads()[0];
|
|
207
203
|
}
|
|
208
204
|
|