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.
Files changed (110) hide show
  1. package/README.md +2 -2
  2. package/package.json +14 -10
  3. package/src/6502.js +39 -42
  4. package/src/6502.opcodes.js +7 -13
  5. package/src/6847.js +4 -5
  6. package/src/6847_fontdata.js +0 -2
  7. package/src/acia.js +2 -3
  8. package/src/app/app.js +0 -1
  9. package/src/app/args.js +0 -2
  10. package/src/app/electron.js +4 -6
  11. package/src/app-bench.js +1 -3
  12. package/src/archive.js +243 -0
  13. package/src/basic-loader.js +0 -7
  14. package/src/basic-tokenise.js +19 -6
  15. package/src/bbc-palette.js +0 -2
  16. package/src/bbcdiscs.js +0 -2
  17. package/src/bem-snapshot.js +1 -3
  18. package/src/binary.js +175 -0
  19. package/src/cmos.js +0 -2
  20. package/src/ddnoise.js +0 -1
  21. package/src/debounce.js +7 -0
  22. package/src/disc-surface.js +0 -2
  23. package/src/disc.js +10 -9
  24. package/src/fake6502.js +7 -23
  25. package/src/fdc.js +9 -51
  26. package/src/filestore.js +10 -17
  27. package/src/hex.js +38 -0
  28. package/src/index.js +11 -0
  29. package/src/intel-fdc.js +18 -20
  30. package/src/{utils_atom.js → keymap-atom.js} +2 -18
  31. package/src/{utils.js → keymap.js} +47 -610
  32. package/src/loader.js +63 -0
  33. package/src/machine-session.js +37 -177
  34. package/src/machine-spec.js +59 -0
  35. package/src/main.js +66 -180
  36. package/src/media-resolver.js +68 -0
  37. package/src/mmc.js +3 -2
  38. package/src/models.js +17 -3
  39. package/src/music5000.js +0 -2
  40. package/src/ppia.js +3 -4
  41. package/src/printer.js +7 -14
  42. package/src/relaynoise.js +0 -1
  43. package/src/rewind.js +0 -2
  44. package/src/sample-player.js +2 -3
  45. package/src/serial.js +0 -2
  46. package/src/snapshot-helpers.js +0 -2
  47. package/src/snapshot.js +0 -2
  48. package/src/soundchip.js +1 -1
  49. package/src/state-utils.js +0 -2
  50. package/src/sth.js +2 -4
  51. package/src/tapes.js +10 -4
  52. package/src/teletext.js +2 -3
  53. package/src/teletext_adaptor.js +2 -4
  54. package/src/teletext_data.js +0 -2
  55. package/src/test-machine.js +307 -0
  56. package/src/touchscreen.js +2 -4
  57. package/src/tube.js +5 -6
  58. package/src/typist.js +103 -0
  59. package/src/uef-snapshot.js +0 -2
  60. package/src/url-params.js +4 -5
  61. package/src/vdu-capture.js +113 -0
  62. package/src/via.js +3 -4
  63. package/src/video-filters/pal-composite.js +0 -2
  64. package/src/video-filters/passthrough-filter.js +0 -2
  65. package/src/video-filters/pixel-grid.js +0 -2
  66. package/src/video-filters/shader-program.js +0 -2
  67. package/src/video-filters/xbr-filter.js +0 -2
  68. package/src/video.js +12 -11
  69. package/src/wd-fdc.js +20 -20
  70. package/src/web/analogue-inputs.js +6 -8
  71. package/src/web/analytics.js +13 -0
  72. package/src/web/audio-handler.js +2 -2
  73. package/src/{audio-utils.js → web/audio-utils.js} +0 -2
  74. package/src/web/autoboot.js +10 -13
  75. package/src/{canvas.js → web/canvas.js} +4 -5
  76. package/src/web/config.js +41 -74
  77. package/src/web/console-surface.js +25 -0
  78. package/src/web/debug.js +6 -5
  79. package/src/web/disc-visualiser.js +0 -2
  80. package/src/web/display.js +21 -4
  81. package/src/{dom-utils.js → web/dom-utils.js} +1 -11
  82. package/src/web/drives.js +1 -1
  83. package/src/web/emulation-loop.js +54 -33
  84. package/src/web/front-panel.js +13 -12
  85. package/src/{gamepads.js → web/gamepads.js} +2 -6
  86. package/src/web/google-drive-picker.js +14 -9
  87. package/src/{google-drive.js → web/google-drive.js} +9 -10
  88. package/src/web/hfe-picker.js +5 -4
  89. package/src/web/keyboard-setup.js +18 -38
  90. package/src/{keyboard.js → web/keyboard.js} +18 -119
  91. package/src/web/layout.js +4 -0
  92. package/src/web/local-disc.js +44 -0
  93. package/src/web/machine.js +27 -20
  94. package/src/web/media-loader.js +38 -147
  95. package/src/{microphone-input.js → web/microphone-input.js} +1 -1
  96. package/src/web/modals.js +6 -9
  97. package/src/web/page-actions.js +88 -0
  98. package/src/web/quick-settings.js +16 -15
  99. package/src/web/reporting.js +0 -7
  100. package/src/{rewind-thumbnail.js → web/rewind-thumbnail.js} +0 -2
  101. package/src/web/rewind-ui.js +29 -29
  102. package/src/web/run-controls.js +39 -0
  103. package/src/web/settings.js +64 -135
  104. package/src/web/snapshot-ui.js +10 -16
  105. package/src/{speech-output.js → web/speech-output.js} +0 -2
  106. package/src/web/sth-picker.js +9 -7
  107. package/src/web/url-state.js +20 -6
  108. package/src/econet/L3FS.dat +0 -0
  109. package/src/econet/scsi.dat +0 -0
  110. package/tests/test-machine.js +0 -609
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
+ }
@@ -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.
@@ -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
- export async function create() {
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
+ }
@@ -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
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  const mirrorBase = "https://bbc.xania.org/archive/bbcdiscs";
4
2
 
5
3
  /** How a disc came to be an image, which is the difference between the archives it came from. */
@@ -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
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  // CMOS layout: bytes 0-13 are RTC internal registers; bytes 14-63 are the 50
4
2
  // bytes of user storage. Storage byte 11 (= CMOS address 25 = 0x19) holds
5
3
  // the BBC Master DFS configuration byte whose bits [1:0] are the WD1770 step
package/src/ddnoise.js CHANGED
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  import { SamplePlayer } from "./sample-player.js";
3
2
 
4
3
  const Idle = 0;
@@ -0,0 +1,7 @@
1
+ export function debounce(fn, wait) {
2
+ let timeout;
3
+ return function (...args) {
4
+ clearTimeout(timeout);
5
+ timeout = setTimeout(() => fn.apply(this, args), wait);
6
+ };
7
+ }
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  // Pure geometry and colour mapping, free of the DOM; disc-visualiser.js owns the canvases.
4
2
 
5
3
  import { IbmDiscFormat } from "./disc.js";
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 { hexbyte, crc32 } from "./utils.js";
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 isMfM if nothing else needs to know/
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; // todo put in reader
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, maybe construct the disc directly with this stuff?
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 fingerprintings, catalog etcetc
1228
+ // TODO(#1061) add fingerprinting, catalog etc
1228
1229
  }
1229
1230
  }
1230
1231
  }
package/src/fake6502.js CHANGED
@@ -1,40 +1,24 @@
1
1
  // Fakes out various 6502s for testing purposes.
2
- "use strict";
3
2
 
4
3
  import { FakeVideo } from "./video.js";
5
4
  import { FakeSoundChip } from "./soundchip.js";
6
5
  import { TEST_6502, TEST_65C02, TEST_65C12, tubeModelFor } from "./models.js";
7
- import { FakeDdNoise } from "./ddnoise.js";
8
- import { FakeRelayNoise } from "./relaynoise.js";
9
- import { Cpu6502, AtomCpu6502 } from "./6502.js";
10
- import { Cmos } from "./cmos.js";
11
- import { FakeMusic5000 } from "./music5000.js";
6
+ import { machineSpec, nullIo } from "./machine-spec.js";
12
7
 
13
8
  const fakeVideo = new FakeVideo();
14
9
  const soundChip = new FakeSoundChip();
15
- const dbgr = {
16
- setCpu: () => {},
17
- };
18
10
 
19
- export function fake6502(model, opts) {
20
- opts = opts || {};
11
+ export function fake6502(model, opts = {}) {
21
12
  model = model || TEST_6502;
22
- const CpuClass = model.isAtom ? AtomCpu6502 : Cpu6502;
23
- return new CpuClass(model, {
24
- dbgr,
25
- video: opts.video || fakeVideo,
26
- soundChip: opts.soundChip || soundChip,
27
- ddNoise: new FakeDdNoise(),
28
- relayNoise: new FakeRelayNoise(),
29
- music5000: new FakeMusic5000(),
30
- cmos: new Cmos(),
31
- cycleAccurate: opts.cycleAccurate,
32
- config: {
13
+ return new model.Cpu(model, {
14
+ ...nullIo({ video: opts.video ?? fakeVideo, soundChip: opts.soundChip ?? soundChip }),
15
+ config: machineSpec({
33
16
  tube: opts.tube ? tubeModelFor(model) : null,
34
17
  tubeCpuMultiplier: opts.tubeCpuMultiplier,
35
18
  cpuMultiplier: opts.cpuMultiplier,
36
19
  hasTeletextAdaptor: opts.hasTeletextAdaptor,
37
- },
20
+ }),
21
+ cycleAccurate: opts.cycleAccurate,
38
22
  });
39
23
  }
40
24