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/app/electron.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
1
|
// Electron integration for jsbeeb desktop application.
|
|
4
2
|
// Handles IPC communication for loading disc/tape images and showing modals from Electron's main process.
|
|
5
3
|
|
|
6
4
|
function init(args) {
|
|
7
|
-
const { loadDiscImage, loadTapeImage, loadStateFile, processor, modals, actions,
|
|
5
|
+
const { loadDiscImage, loadTapeImage, loadStateFile, processor, modals, actions, settings, media } = args;
|
|
8
6
|
const api = window.electronAPI;
|
|
9
7
|
|
|
10
8
|
api.onLoadDisc(async (message) => {
|
|
@@ -16,7 +14,7 @@ function init(args) {
|
|
|
16
14
|
api.onLoadTape(async (message) => {
|
|
17
15
|
const { path } = message;
|
|
18
16
|
const tape = await loadTapeImage(path);
|
|
19
|
-
processor.
|
|
17
|
+
processor.tapeInterface.setTape(tape);
|
|
20
18
|
});
|
|
21
19
|
|
|
22
20
|
api.onShowModal((message) => {
|
|
@@ -54,8 +52,8 @@ function init(args) {
|
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
// Save settings when they change
|
|
57
|
-
if (
|
|
58
|
-
|
|
55
|
+
if (settings) {
|
|
56
|
+
settings.addEventListener("change", (e) => {
|
|
59
57
|
api.saveSettings(e.detail);
|
|
60
58
|
});
|
|
61
59
|
}
|
package/src/app-bench.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
1
|
import { fake6502 } from "./fake6502.js";
|
|
4
2
|
import * as models from "./models.js";
|
|
5
3
|
import * as disc from "./fdc.js";
|
|
@@ -23,7 +21,7 @@ async function main() {
|
|
|
23
21
|
const cpu = fake6502(models.findModel("B"));
|
|
24
22
|
await cpu.initialise();
|
|
25
23
|
const data = await disc.load("discs/" + discName + ".ssd");
|
|
26
|
-
cpu.fdc.loadDisc(0, disc.discFor(
|
|
24
|
+
cpu.fdc.loadDisc(0, disc.discFor(discName, data));
|
|
27
25
|
cpu.sysvia.keyDown(16);
|
|
28
26
|
cpu.execute(10 * 1000 * 1000);
|
|
29
27
|
cpu.sysvia.keyUp(16);
|
package/src/archive.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Minimal ZIP extractor. Supports methods 0 (stored) and 8 (deflate); other
|
|
2
|
+
// methods (bzip2, lzma, etc.) will throw an error with the method number.
|
|
3
|
+
|
|
4
|
+
import { inflate as pakoInflate, inflateRaw as pakoInflateRaw, ungzip as pakoUngzip } from "pako";
|
|
5
|
+
|
|
6
|
+
const ZipLocalHeaderSig = 0x04034b50;
|
|
7
|
+
const ZipCentralDirSig = 0x02014b50;
|
|
8
|
+
const ZipEocdSig = 0x06054b50;
|
|
9
|
+
const ZipMethodStored = 0;
|
|
10
|
+
const ZipMethodDeflate = 8;
|
|
11
|
+
|
|
12
|
+
function readU16(buf, off) {
|
|
13
|
+
return buf[off] | (buf[off + 1] << 8);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readU32(buf, off) {
|
|
17
|
+
return (buf[off] | (buf[off + 1] << 8) | (buf[off + 2] << 16) | (buf[off + 3] << 24)) >>> 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isValidEocd(buf, off) {
|
|
21
|
+
const commentLen = readU16(buf, off + 20);
|
|
22
|
+
if (off + 22 + commentLen !== buf.length) return false;
|
|
23
|
+
const cdOff = readU32(buf, off + 16);
|
|
24
|
+
const cdSize = readU32(buf, off + 12);
|
|
25
|
+
return cdOff <= off && cdSize <= off - cdOff;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Find the End of Central Directory record by scanning backwards.
|
|
29
|
+
// EOCD is in the last 65557 bytes (22-byte fixed record + up to 65535-byte comment).
|
|
30
|
+
function findEocd(buf) {
|
|
31
|
+
const searchStart = Math.max(0, buf.length - 65557);
|
|
32
|
+
for (let i = buf.length - 22; i >= searchStart; i--) {
|
|
33
|
+
if (readU32(buf, i) === ZipEocdSig && isValidEocd(buf, i)) return i;
|
|
34
|
+
}
|
|
35
|
+
throw new Error("Not a ZIP file: EOCD not found");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function inflateWith(inflater, data, context) {
|
|
39
|
+
if (!(data instanceof Uint8Array)) data = new Uint8Array(data);
|
|
40
|
+
try {
|
|
41
|
+
return inflater(data);
|
|
42
|
+
} catch (cause) {
|
|
43
|
+
throw new Error(`${context}: ${cause.message || cause}`, { cause });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function inflate(data) {
|
|
48
|
+
return inflateWith(pakoInflate, data, "Unable to inflate");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Extract all files from a ZIP archive. Returns {filename: Uint8Array}.
|
|
52
|
+
export async function unzip(buf) {
|
|
53
|
+
if (!(buf instanceof Uint8Array)) buf = new Uint8Array(buf);
|
|
54
|
+
const eocdOff = findEocd(buf);
|
|
55
|
+
const cdOff = readU32(buf, eocdOff + 16);
|
|
56
|
+
const cdCount = readU16(buf, eocdOff + 10);
|
|
57
|
+
const decoder = new TextDecoder();
|
|
58
|
+
|
|
59
|
+
const files = Object.create(null);
|
|
60
|
+
let pos = cdOff;
|
|
61
|
+
for (let i = 0; i < cdCount; i++) {
|
|
62
|
+
if (pos + 46 > buf.length || readU32(buf, pos) !== ZipCentralDirSig)
|
|
63
|
+
throw new Error("Bad central directory entry");
|
|
64
|
+
const flags = readU16(buf, pos + 8);
|
|
65
|
+
if (flags & 0x0001) throw new Error("Encrypted ZIP entries are not supported");
|
|
66
|
+
const method = readU16(buf, pos + 10);
|
|
67
|
+
const expectedCrc = readU32(buf, pos + 16);
|
|
68
|
+
const compressedSize = readU32(buf, pos + 20);
|
|
69
|
+
const nameLen = readU16(buf, pos + 28);
|
|
70
|
+
const extraLen = readU16(buf, pos + 30);
|
|
71
|
+
const commentLen = readU16(buf, pos + 32);
|
|
72
|
+
const localHeaderOff = readU32(buf, pos + 42);
|
|
73
|
+
const name = decoder.decode(buf.subarray(pos + 46, pos + 46 + nameLen));
|
|
74
|
+
pos += 46 + nameLen + extraLen + commentLen;
|
|
75
|
+
|
|
76
|
+
// Read local file header to find actual data offset.
|
|
77
|
+
if (readU32(buf, localHeaderOff) !== ZipLocalHeaderSig) throw new Error(`Bad local file header for ${name}`);
|
|
78
|
+
const localNameLen = readU16(buf, localHeaderOff + 26);
|
|
79
|
+
const localExtraLen = readU16(buf, localHeaderOff + 28);
|
|
80
|
+
const dataOff = localHeaderOff + 30 + localNameLen + localExtraLen;
|
|
81
|
+
const dataEnd = dataOff + compressedSize;
|
|
82
|
+
if (dataEnd > buf.length) throw new Error(`Truncated ZIP entry data for ${name}`);
|
|
83
|
+
const raw = buf.subarray(dataOff, dataEnd);
|
|
84
|
+
|
|
85
|
+
if (method === ZipMethodStored) {
|
|
86
|
+
files[name] = raw.slice();
|
|
87
|
+
} else if (method === ZipMethodDeflate) {
|
|
88
|
+
files[name] = inflateWith(pakoInflateRaw, raw, `Unable to inflate ZIP entry ${name}`);
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error(`Unsupported ZIP compression method ${method} for ${name}`);
|
|
91
|
+
}
|
|
92
|
+
if (crc32(files[name]) >>> 0 !== expectedCrc) throw new Error(`Corrupt ZIP entry ${name}: CRC32 mismatch`);
|
|
93
|
+
}
|
|
94
|
+
return files;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Standard CRC-32/ISO-HDLC.
|
|
98
|
+
const Crc32Table = new Uint32Array(256).map((_, index) => {
|
|
99
|
+
let crc = index;
|
|
100
|
+
for (let bit = 0; bit < 8; ++bit) crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
|
|
101
|
+
return crc;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export function crc32(data) {
|
|
105
|
+
let crc = 0xffffffff;
|
|
106
|
+
for (let i = 0; i < data.length; ++i) crc = (crc >>> 8) ^ Crc32Table[(crc ^ data[i]) & 0xff];
|
|
107
|
+
return ~crc;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Create a ZIP blob from an array of {name: string, data: Uint8Array} entries.
|
|
111
|
+
// Uses stored method (no compression) with CRC-32 for maximum compatibility.
|
|
112
|
+
export function createZipBlob(files) {
|
|
113
|
+
const encoder = new TextEncoder();
|
|
114
|
+
const localHeaders = [];
|
|
115
|
+
const centralHeaders = [];
|
|
116
|
+
let offset = 0;
|
|
117
|
+
|
|
118
|
+
for (const { name, data } of files) {
|
|
119
|
+
const nameBytes = encoder.encode(name);
|
|
120
|
+
const crc = crc32(data);
|
|
121
|
+
// Local file header (30 + nameLen + data)
|
|
122
|
+
const local = new Uint8Array(30 + nameBytes.length + data.length);
|
|
123
|
+
const lv = new DataView(local.buffer);
|
|
124
|
+
lv.setUint32(0, 0x04034b50, true); // signature
|
|
125
|
+
lv.setUint16(4, 20, true); // version needed
|
|
126
|
+
lv.setUint16(8, 0, true); // method: stored
|
|
127
|
+
lv.setUint32(14, crc, true); // CRC-32
|
|
128
|
+
lv.setUint32(18, data.length, true); // compressed size
|
|
129
|
+
lv.setUint32(22, data.length, true); // uncompressed size
|
|
130
|
+
lv.setUint16(26, nameBytes.length, true); // name length
|
|
131
|
+
local.set(nameBytes, 30);
|
|
132
|
+
local.set(data, 30 + nameBytes.length);
|
|
133
|
+
localHeaders.push(local);
|
|
134
|
+
|
|
135
|
+
// Central directory entry (46 + nameLen)
|
|
136
|
+
const central = new Uint8Array(46 + nameBytes.length);
|
|
137
|
+
const cv = new DataView(central.buffer);
|
|
138
|
+
cv.setUint32(0, 0x02014b50, true); // signature
|
|
139
|
+
cv.setUint16(4, 20, true); // version made by
|
|
140
|
+
cv.setUint16(6, 20, true); // version needed
|
|
141
|
+
cv.setUint16(10, 0, true); // method: stored
|
|
142
|
+
cv.setUint32(16, crc, true); // CRC-32
|
|
143
|
+
cv.setUint32(20, data.length, true); // compressed size
|
|
144
|
+
cv.setUint32(24, data.length, true); // uncompressed size
|
|
145
|
+
cv.setUint16(28, nameBytes.length, true); // name length
|
|
146
|
+
cv.setUint32(42, offset, true); // local header offset
|
|
147
|
+
central.set(nameBytes, 46);
|
|
148
|
+
centralHeaders.push(central);
|
|
149
|
+
|
|
150
|
+
offset += local.length;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const cdOffset = offset;
|
|
154
|
+
let cdSize = 0;
|
|
155
|
+
for (const c of centralHeaders) cdSize += c.length;
|
|
156
|
+
|
|
157
|
+
// End of central directory (22 bytes)
|
|
158
|
+
const eocd = new Uint8Array(22);
|
|
159
|
+
const ev = new DataView(eocd.buffer);
|
|
160
|
+
ev.setUint32(0, 0x06054b50, true); // signature
|
|
161
|
+
ev.setUint16(8, files.length, true); // entries on this disc
|
|
162
|
+
ev.setUint16(10, files.length, true); // total entries
|
|
163
|
+
ev.setUint32(12, cdSize, true); // central directory size
|
|
164
|
+
ev.setUint32(16, cdOffset, true); // central directory offset
|
|
165
|
+
|
|
166
|
+
return new Blob([...localHeaders, ...centralHeaders, eocd], { type: "application/zip" });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function replaceOrAddExtension(name, newExt) {
|
|
170
|
+
const lastDot = name.lastIndexOf(".");
|
|
171
|
+
if (lastDot === -1) {
|
|
172
|
+
return name + newExt;
|
|
173
|
+
}
|
|
174
|
+
return name.substring(0, lastDot) + newExt;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function ungzip(data) {
|
|
178
|
+
return inflateWith(pakoUngzip, data, "Unable to ungzip");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const knownDiscExtensions = {
|
|
182
|
+
uef: true,
|
|
183
|
+
ssd: true,
|
|
184
|
+
dsd: true,
|
|
185
|
+
adf: true,
|
|
186
|
+
adm: true,
|
|
187
|
+
adl: true,
|
|
188
|
+
hfe: true,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const knownRomExtensions = {
|
|
192
|
+
rom: true,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
async function unzipImage(data, knownExtensions) {
|
|
196
|
+
console.log("Attempting to unzip");
|
|
197
|
+
|
|
198
|
+
let files;
|
|
199
|
+
try {
|
|
200
|
+
files = await unzip(data);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
throw new Error("Error unzipping " + e.message, { cause: e });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let uncompressed = null;
|
|
206
|
+
let loadedFile;
|
|
207
|
+
const ignored = [];
|
|
208
|
+
|
|
209
|
+
for (const [filename, fileData] of Object.entries(files)) {
|
|
210
|
+
const match = filename.match(/.*\.([a-z]+)/i);
|
|
211
|
+
if (!match || !knownExtensions[match[1].toLowerCase()]) {
|
|
212
|
+
console.log("Skipping file", filename);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (uncompressed) {
|
|
216
|
+
console.log("Ignoring", filename, "as already found a file");
|
|
217
|
+
ignored.push(filename);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
loadedFile = filename;
|
|
221
|
+
uncompressed = fileData;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!uncompressed) {
|
|
225
|
+
throw new Error("Couldn't find any compatible files in the archive");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log("Unzipped '" + loadedFile + "'");
|
|
229
|
+
return { data: uncompressed, name: loadedFile, ignored };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @param {Uint8Array|number[]} data a ZIP archive
|
|
234
|
+
* @returns {Promise<{data: Uint8Array, name: string, ignored: string[]}>} the one file loaded, and the
|
|
235
|
+
* names of any other loadable files the archive held
|
|
236
|
+
*/
|
|
237
|
+
export async function unzipDiscImage(data) {
|
|
238
|
+
return unzipImage(data, knownDiscExtensions);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function unzipRomImage(data) {
|
|
242
|
+
return unzipImage(data, knownRomExtensions);
|
|
243
|
+
}
|
package/src/basic-loader.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/** Where the OS sits waiting for a keypress, per machine: the place to interrupt with a program. */
|
|
4
|
-
export function basicIdleAddr(model) {
|
|
5
|
-
return model.isMaster ? 0xe7e6 : 0xe581;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
1
|
/**
|
|
9
2
|
* Pokes a tokenised BASIC program into memory at PAGE, and sets TOP and
|
|
10
3
|
* VARTOP after it, exactly as if it had just been typed.
|
package/src/basic-tokenise.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
import * as utils from "./utils.js";
|
|
3
1
|
import * as models from "./models.js";
|
|
4
2
|
import { fake6502 } from "./fake6502.js";
|
|
3
|
+
import { hexword } from "./hex.js";
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
async function makeTokeniser() {
|
|
7
6
|
const cpu = fake6502(models.basicOnly);
|
|
8
7
|
const callTokeniser = function (line) {
|
|
9
8
|
// Address of the tokenisation subroutine in the Master's BASIC ROM.
|
|
@@ -68,9 +67,7 @@ export async function create() {
|
|
|
68
67
|
result += String.fromCharCode(cpu.readmem(i));
|
|
69
68
|
}
|
|
70
69
|
if (safety === 0) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
"Unable to tokenize '" + line + "' - got as far as '" + result + "' pc=" + utils.hexword(cpu.pc),
|
|
73
|
-
);
|
|
70
|
+
throw new Error("Unable to tokenize '" + line + "' - got as far as '" + result + "' pc=" + hexword(cpu.pc));
|
|
74
71
|
}
|
|
75
72
|
return result;
|
|
76
73
|
};
|
|
@@ -102,3 +99,19 @@ export async function create() {
|
|
|
102
99
|
await cpu.initialise();
|
|
103
100
|
return { tokenise };
|
|
104
101
|
}
|
|
102
|
+
|
|
103
|
+
let tokeniser = null;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Building the tokeniser stands up a whole Master CPU and loads its ROMs, so
|
|
107
|
+
* the one instance is shared by every caller; each call to the tokeniser
|
|
108
|
+
* resets all the machine state it uses.
|
|
109
|
+
*/
|
|
110
|
+
export function create() {
|
|
111
|
+
if (!tokeniser)
|
|
112
|
+
tokeniser = makeTokeniser().catch((error) => {
|
|
113
|
+
tokeniser = null;
|
|
114
|
+
throw error;
|
|
115
|
+
});
|
|
116
|
+
return tokeniser;
|
|
117
|
+
}
|
package/src/bbc-palette.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
1
|
// The 16 standard BBC Micro colours in ABGR format (0xffBBGGRR, little-endian canvas RGBA).
|
|
4
2
|
// Colours 0-7 are the primary palette; 8-15 duplicate them as the default solid/non-flash set.
|
|
5
3
|
// Imported by both video.js (NulaDefaultPalette) and teletext.js (BbcDefaultCollook) to avoid
|
package/src/bbcdiscs.js
CHANGED
package/src/bem-snapshot.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
import { inflate } from "./utils.js";
|
|
3
|
-
|
|
4
1
|
// B-em snapshot format parser (versions 1 and 3).
|
|
5
2
|
// v1 (BEMSNAP1): Fixed-size 327,885 byte packed struct. Reference: beebjit state.c
|
|
6
3
|
// v3 (BEMSNAP3): Section-based with key+size headers, zlib-compressed memory. Reference: b-em savestate.c
|
|
7
4
|
|
|
8
5
|
import { volumeTable, buildVideoState, buildSnapshot } from "./snapshot-helpers.js";
|
|
6
|
+
import { inflate } from "./archive.js";
|
|
9
7
|
|
|
10
8
|
const BemV1Size = 327885;
|
|
11
9
|
|
package/src/binary.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
export function signExtend(val) {
|
|
2
|
+
return val >= 128 ? val - 256 : val;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function uint8ArrayToString(array) {
|
|
6
|
+
let str = "";
|
|
7
|
+
for (let i = 0; i < array.length; ++i) str += String.fromCharCode(array[i]);
|
|
8
|
+
return str;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function stringToUint8Array(str) {
|
|
12
|
+
if (str instanceof Uint8Array) return str;
|
|
13
|
+
const len = str.length;
|
|
14
|
+
const array = new Uint8Array(len);
|
|
15
|
+
for (let i = 0; i < len; ++i) array[i] = str.charCodeAt(i) & 0xff;
|
|
16
|
+
return array;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readInt32(data, offset) {
|
|
20
|
+
return (data[offset + 3] << 24) | (data[offset + 2] << 16) | (data[offset + 1] << 8) | data[offset + 0];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function readInt16(data, offset) {
|
|
24
|
+
return (data[offset + 1] << 8) | data[offset + 0];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const tempBuf = new ArrayBuffer(4);
|
|
28
|
+
const tempBuf8 = new Uint8Array(tempBuf);
|
|
29
|
+
const tempBufF32 = new Float32Array(tempBuf);
|
|
30
|
+
|
|
31
|
+
export function readFloat32(data, offset) {
|
|
32
|
+
tempBuf8[0] = data[offset];
|
|
33
|
+
tempBuf8[1] = data[offset + 1];
|
|
34
|
+
tempBuf8[2] = data[offset + 2];
|
|
35
|
+
tempBuf8[3] = data[offset + 3];
|
|
36
|
+
return tempBufF32[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class DataStream {
|
|
40
|
+
constructor(name, data) {
|
|
41
|
+
this.name = name;
|
|
42
|
+
this.pos = 0;
|
|
43
|
+
this.data = stringToUint8Array(data);
|
|
44
|
+
if (!this.data) {
|
|
45
|
+
throw new Error("No data in " + name);
|
|
46
|
+
}
|
|
47
|
+
this.end = this.data.length;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
bytesLeft() {
|
|
51
|
+
return this.end - this.pos;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
eof() {
|
|
55
|
+
return this.bytesLeft() === 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
advance(distance) {
|
|
59
|
+
if (this.bytesLeft() < distance) throw new RangeError("EOF in " + this.name);
|
|
60
|
+
this.pos += distance;
|
|
61
|
+
return this.pos - distance;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
readFloat32(pos) {
|
|
65
|
+
if (pos === undefined) pos = this.advance(4);
|
|
66
|
+
return readFloat32(this.data, pos);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
readInt32(pos) {
|
|
70
|
+
if (pos === undefined) pos = this.advance(4);
|
|
71
|
+
return readInt32(this.data, pos);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
readInt16(pos) {
|
|
75
|
+
if (pos === undefined) pos = this.advance(2);
|
|
76
|
+
return readInt16(this.data, pos);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
readByte(pos) {
|
|
80
|
+
if (pos === undefined) pos = this.advance(1);
|
|
81
|
+
return this.data[pos];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
readNulString(pos, maxLength) {
|
|
85
|
+
if (!maxLength) maxLength = 1024;
|
|
86
|
+
let posToUse = pos === undefined ? this.pos : pos;
|
|
87
|
+
let result = "";
|
|
88
|
+
let c;
|
|
89
|
+
while ((c = this.readByte(posToUse++)) !== 0 && --maxLength) {
|
|
90
|
+
result += String.fromCharCode(c);
|
|
91
|
+
}
|
|
92
|
+
if (maxLength === 0) return "";
|
|
93
|
+
if (pos === undefined) this.pos = posToUse;
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
substream(posOrLength, length) {
|
|
98
|
+
let pos;
|
|
99
|
+
if (length === undefined) {
|
|
100
|
+
length = posOrLength;
|
|
101
|
+
pos = this.advance(length);
|
|
102
|
+
} else {
|
|
103
|
+
pos = posOrLength;
|
|
104
|
+
if (pos + length >= this.end) throw new RangeError("EOF in " + this.name);
|
|
105
|
+
}
|
|
106
|
+
return new DataStream(this.name + ".sub", this.data.subarray(pos, pos + length));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
seek(to) {
|
|
110
|
+
if (to >= this.end) throw new RangeError("Seek out of range in " + this.name);
|
|
111
|
+
this.pos = to;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function makeFast32(u32) {
|
|
116
|
+
// Firefox is ~5% faster with signed 32-bit arrays. Chrome is the same speed
|
|
117
|
+
// either way, so here we unconditionally wrap all u32 buffers as i32.
|
|
118
|
+
// Having a function do this makes it easy to test u32 vs i32, and means we
|
|
119
|
+
// keep the rest of the code using u32 (which makes more sense to me).
|
|
120
|
+
return new Int32Array(u32.buffer);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class Fifo {
|
|
124
|
+
constructor(capacity) {
|
|
125
|
+
this._buffer = new Uint8Array(capacity);
|
|
126
|
+
this._size = 0;
|
|
127
|
+
this._wPtr = 0;
|
|
128
|
+
this._rPtr = 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @returns {number} */
|
|
132
|
+
get size() {
|
|
133
|
+
return this._size;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @returns {boolean} */
|
|
137
|
+
get full() {
|
|
138
|
+
return this._size === this._buffer.length;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @returns {boolean} */
|
|
142
|
+
get empty() {
|
|
143
|
+
return this._size === 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
clear() {
|
|
147
|
+
this._size = 0;
|
|
148
|
+
this._wPtr = 0;
|
|
149
|
+
this._rPtr = 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** @type {Number} b */
|
|
153
|
+
put(b) {
|
|
154
|
+
if (this.full) return;
|
|
155
|
+
this._buffer[this._wPtr % this._buffer.length] = b;
|
|
156
|
+
this._wPtr++;
|
|
157
|
+
this._size++;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** @returns {Number} */
|
|
161
|
+
get() {
|
|
162
|
+
if (this.empty) return;
|
|
163
|
+
const res = this._buffer[this._rPtr % this._buffer.length];
|
|
164
|
+
this._rPtr++;
|
|
165
|
+
this._size--;
|
|
166
|
+
return res;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @returns {number[]} pending bytes, oldest first */
|
|
170
|
+
toArray() {
|
|
171
|
+
const result = [];
|
|
172
|
+
for (let i = 0; i < this._size; ++i) result.push(this._buffer[(this._rPtr + i) % this._buffer.length]);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
}
|
package/src/cmos.js
CHANGED
package/src/ddnoise.js
CHANGED
package/src/debounce.js
ADDED
package/src/disc-surface.js
CHANGED
package/src/disc.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Translated from beebjit by Chris Evans.
|
|
2
2
|
// https://github.com/scarybeasts/beebjit
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { crc32 } from "./archive.js";
|
|
5
|
+
import { hexbyte } from "./hex.js";
|
|
5
6
|
class TrackBuilder {
|
|
6
7
|
/**
|
|
7
8
|
* @param {Track} track
|
|
@@ -69,7 +70,7 @@ class TrackBuilder {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
appendCrc(isMfm) {
|
|
72
|
-
// TODO consider remembering
|
|
73
|
+
// TODO(#1061) consider remembering isMfm if nothing else needs to know;
|
|
73
74
|
// could then break this into MFM and FM builder
|
|
74
75
|
const firstByte = (this._crc >>> 8) & 0xff;
|
|
75
76
|
const secondByte = this._crc & 0xff;
|
|
@@ -304,7 +305,7 @@ class Sector {
|
|
|
304
305
|
* @param {Sector|undefined} nextSector
|
|
305
306
|
*/
|
|
306
307
|
read(nextSector) {
|
|
307
|
-
const pulsesPerByte = this.isMfm ? 16 : 32; //
|
|
308
|
+
const pulsesPerByte = this.isMfm ? 16 : 32; // TODO(#1061) put in reader
|
|
308
309
|
if (this.dataPosBitOffset === null) {
|
|
309
310
|
this._warn(`Sector header without data ${this.description}`);
|
|
310
311
|
return;
|
|
@@ -536,7 +537,7 @@ export const DiscLayout = Object.freeze({
|
|
|
536
537
|
|
|
537
538
|
export class DiscConfig {
|
|
538
539
|
constructor() {
|
|
539
|
-
// TODO is this even useful?
|
|
540
|
+
// TODO(#1061) is this even useful?
|
|
540
541
|
this.logProtection = false;
|
|
541
542
|
this.logIffyPulses = false;
|
|
542
543
|
this.expandTo80 = false;
|
|
@@ -707,8 +708,8 @@ export function loadSsd(disc, data, isDsd, onChange) {
|
|
|
707
708
|
}
|
|
708
709
|
|
|
709
710
|
if (onChange) {
|
|
710
|
-
// TODO
|
|
711
|
-
// TODO maybe change this entirely and make it lazy; and have the onChange "pull" the disc as the format it wants
|
|
711
|
+
// TODO(#822) maybe construct the disc directly with this stuff?
|
|
712
|
+
// TODO(#822) maybe change this entirely and make it lazy; and have the onChange "pull" the disc as the format it wants
|
|
712
713
|
// instead of doing this here. Most stuff doesn't care about changes and only needs the image on save.
|
|
713
714
|
// Create a dataCopy large enough for all the sectors and tracks.
|
|
714
715
|
const dataCopy = new Uint8Array(maxSize);
|
|
@@ -797,7 +798,7 @@ export function loadAdf(disc, data, isDsd) {
|
|
|
797
798
|
}
|
|
798
799
|
}
|
|
799
800
|
|
|
800
|
-
// TODO writeback
|
|
801
|
+
// TODO(#822) writeback
|
|
801
802
|
return disc;
|
|
802
803
|
}
|
|
803
804
|
|
|
@@ -1053,7 +1054,7 @@ export class Disc {
|
|
|
1053
1054
|
this._snapshotDirtyTracks.add(dirtyKey);
|
|
1054
1055
|
this._everDirtyTracks.add(dirtyKey);
|
|
1055
1056
|
trackObj.pulses2Us[position] = pulses;
|
|
1056
|
-
// TODO a debug log flag for this
|
|
1057
|
+
// TODO(#1061) a debug log flag for this
|
|
1057
1058
|
// console.log(`wrote to ${track}:${position * 32}`);
|
|
1058
1059
|
}
|
|
1059
1060
|
|
|
@@ -1224,7 +1225,7 @@ export class Disc {
|
|
|
1224
1225
|
console.log(`"Unformatted track ${track.description}"`);
|
|
1225
1226
|
}
|
|
1226
1227
|
}
|
|
1227
|
-
// TODO add
|
|
1228
|
+
// TODO(#1061) add fingerprinting, catalog etc
|
|
1228
1229
|
}
|
|
1229
1230
|
}
|
|
1230
1231
|
}
|