jsbeeb 1.17.1 → 1.19.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/src/utils.js CHANGED
@@ -1013,7 +1013,10 @@ function loadDataHttp(url) {
1013
1013
  request.open("GET", baseUrl + url, true);
1014
1014
  request.overrideMimeType("text/plain; charset=x-user-defined");
1015
1015
  request.onload = function () {
1016
- if (request.status !== 200) reject(new Error("Unable to load " + url + ", http code " + request.status));
1016
+ if (request.status !== 200) {
1017
+ reject(new Error("Unable to load " + url + ", http code " + request.status));
1018
+ return;
1019
+ }
1017
1020
  if (typeof request.response !== "string") {
1018
1021
  resolve(request.response);
1019
1022
  } else {
@@ -1208,6 +1211,7 @@ async function unzipImage(data, knownExtensions) {
1208
1211
 
1209
1212
  let uncompressed = null;
1210
1213
  let loadedFile;
1214
+ const ignored = [];
1211
1215
 
1212
1216
  for (const [filename, fileData] of Object.entries(files)) {
1213
1217
  const match = filename.match(/.*\.([a-z]+)/i);
@@ -1217,6 +1221,7 @@ async function unzipImage(data, knownExtensions) {
1217
1221
  }
1218
1222
  if (uncompressed) {
1219
1223
  console.log("Ignoring", filename, "as already found a file");
1224
+ ignored.push(filename);
1220
1225
  continue;
1221
1226
  }
1222
1227
  loadedFile = filename;
@@ -1228,9 +1233,14 @@ async function unzipImage(data, knownExtensions) {
1228
1233
  }
1229
1234
 
1230
1235
  console.log("Unzipped '" + loadedFile + "'");
1231
- return { data: uncompressed, name: loadedFile };
1236
+ return { data: uncompressed, name: loadedFile, ignored };
1232
1237
  }
1233
1238
 
1239
+ /**
1240
+ * @param {Uint8Array|number[]} data a ZIP archive
1241
+ * @returns {Promise<{data: Uint8Array, name: string, ignored: string[]}>} the one file loaded, and the
1242
+ * names of any other loadable files the archive held
1243
+ */
1234
1244
  export async function unzipDiscImage(data) {
1235
1245
  return unzipImage(data, knownDiscExtensions);
1236
1246
  }
@@ -1290,4 +1300,11 @@ export class Fifo {
1290
1300
  this._size--;
1291
1301
  return res;
1292
1302
  }
1303
+
1304
+ /** @returns {number[]} pending bytes, oldest first */
1305
+ toArray() {
1306
+ const result = [];
1307
+ for (let i = 0; i < this._size; ++i) result.push(this._buffer[(this._rPtr + i) % this._buffer.length]);
1308
+ return result;
1309
+ }
1293
1310
  }
package/src/via.js CHANGED
@@ -24,6 +24,10 @@ const ORB = 0x0,
24
24
  INT_CB1 = 0x10,
25
25
  INT_CB2 = 0x08;
26
26
 
27
+ // Pulse mode holds CA2/CB2 low for one 1MHz VIA cycle, here in the scheduler's 2MHz ticks.
28
+ // Figures 3-4 and 3-6 of https://6502.org/documents/datasheets/wdc/wdc_w65c22s_mar_2004.pdf
29
+ const PulseWidthCycles = 2;
30
+
27
31
  class Via {
28
32
  constructor(cpu, scheduler, irq) {
29
33
  this.cpu = cpu;
@@ -59,6 +63,8 @@ class Via {
59
63
  this.t1_pb7 = 0;
60
64
 
61
65
  this.task = this.scheduler.newTask(() => this._onTimeout());
66
+ this.ca2PulseTask = this.scheduler.newTask(() => this.setca2(true));
67
+ this.cb2PulseTask = this.scheduler.newTask(() => this.setcb2(true));
62
68
  this.lastPolltime = 0;
63
69
  }
64
70
 
@@ -72,6 +78,8 @@ class Via {
72
78
  this.t1hit = this.t2hit = true;
73
79
  this.acr = this.pcr = 0;
74
80
  this.t1_pb7 = 1;
81
+ this.ca2PulseTask.cancel();
82
+ this.cb2PulseTask.cancel();
75
83
  this.updateNextTime();
76
84
  }
77
85
 
@@ -166,7 +174,7 @@ class Via {
166
174
  } else if (mode === 0x0a) {
167
175
  // Pulse mode
168
176
  this.setca2(false);
169
- this.setca2(true);
177
+ this.ca2PulseTask.reschedule(PulseWidthCycles);
170
178
  }
171
179
  break;
172
180
 
@@ -193,7 +201,7 @@ class Via {
193
201
  } else if (mode === 0x0a) {
194
202
  // Pulse mode
195
203
  this.setcb2(false);
196
- this.setcb2(true);
204
+ this.cb2PulseTask.reschedule(PulseWidthCycles);
197
205
  }
198
206
  break;
199
207
 
@@ -408,6 +416,15 @@ class Via {
408
416
  this.portBUpdated();
409
417
  }
410
418
 
419
+ _taskOffset(task) {
420
+ return task.scheduled() ? task.expireEpoch - this.scheduler.epoch : null;
421
+ }
422
+
423
+ _restoreTask(task, offset) {
424
+ if (offset === null || offset === undefined) task.cancel();
425
+ else task.reschedule(offset);
426
+ }
427
+
411
428
  snapshotState() {
412
429
  return {
413
430
  ora: this.ora,
@@ -436,7 +453,9 @@ class Via {
436
453
  justhit: this.justhit,
437
454
  t1_pb7: this.t1_pb7,
438
455
  lastPolltime: this.lastPolltime,
439
- taskOffset: this.task.scheduled() ? this.task.expireEpoch - this.scheduler.epoch : null,
456
+ taskOffset: this._taskOffset(this.task),
457
+ ca2PulseTaskOffset: this._taskOffset(this.ca2PulseTask),
458
+ cb2PulseTaskOffset: this._taskOffset(this.cb2PulseTask),
440
459
  };
441
460
  }
442
461
 
@@ -468,11 +487,9 @@ class Via {
468
487
  this.t1_pb7 = state.t1_pb7;
469
488
  this.lastPolltime = state.lastPolltime;
470
489
  this.updateIFR();
471
- if (state.taskOffset !== null) {
472
- this.task.reschedule(state.taskOffset);
473
- } else {
474
- this.task.cancel();
475
- }
490
+ this._restoreTask(this.task, state.taskOffset);
491
+ this._restoreTask(this.ca2PulseTask, state.ca2PulseTaskOffset);
492
+ this._restoreTask(this.cb2PulseTask, state.cb2PulseTaskOffset);
476
493
  }
477
494
 
478
495
  setca1(level) {
@@ -3,21 +3,26 @@
3
3
  const lowPassFilterFreq = sampleRate / 2;
4
4
  const RC = 1 / (2 * Math.PI * lowPassFilterFreq);
5
5
 
6
+ const InputSampleRate = 4000000.0 / 8;
7
+ const MaxQueuedMs = 250;
8
+
9
+ const samplesFor = (ms) => (InputSampleRate * ms) / 1000;
10
+
6
11
  class SoundChipProcessor extends AudioWorkletProcessor {
7
12
  constructor(...args) {
8
13
  super(...args);
9
14
 
10
- this.inputSampleRate = 4000000.0 / 8;
15
+ this.inputSampleRate = InputSampleRate;
11
16
  this._lastSample = 0;
12
17
  this._lastFilteredOutput = 0;
13
18
  this.queue = [];
14
- this._queueSizeBytes = 0;
19
+ this._queueSizeSamples = 0;
15
20
  this.dropped = 0;
16
21
  this.underruns = 0;
17
22
  this.targetLatencyMs = 1000 * (1 / 50); // One frame
18
- this.startQueueSizeBytes = this.inputSampleRate / this.targetLatencyMs / 2;
23
+ this.startQueueSizeSamples = samplesFor(this.targetLatencyMs);
19
24
  this.running = false;
20
- this.maxQueueSizeBytes = this.inputSampleRate * 0.25;
25
+ this.maxQueueSizeSamples = samplesFor(MaxQueuedMs);
21
26
  this.port.onmessage = (event) => {
22
27
  // TODO: even better than this, send over register settings/catch up and run the audio work _here_
23
28
  this.onBuffer(event.data.time, event.data.buffer);
@@ -47,19 +52,19 @@ class SoundChipProcessor extends AudioWorkletProcessor {
47
52
 
48
53
  onBuffer(time, buffer) {
49
54
  this.queue.push({ offset: 0, time, buffer });
50
- this._queueSizeBytes += buffer.length;
55
+ this._queueSizeSamples += buffer.length;
51
56
  this.cleanQueue();
52
- if (!this.running && this._queueSizeBytes >= this.startQueueSizeBytes) this.running = true;
57
+ if (!this.running && this._queueSizeSamples >= this.startQueueSizeSamples) this.running = true;
53
58
  }
54
59
 
55
60
  _shift() {
56
61
  const dropped = this.queue.shift();
57
- this._queueSizeBytes -= dropped.buffer.length;
62
+ this._queueSizeSamples -= dropped.buffer.length;
58
63
  }
59
64
 
60
65
  cleanQueue() {
61
66
  const maxLatency = this.targetLatencyMs * 2;
62
- while (this._queueSizeBytes > this.maxQueueSizeBytes || this._queueAge() > maxLatency) {
67
+ while (this._queueSizeSamples > this.maxQueueSizeSamples || this._queueAge() > maxLatency) {
63
68
  this._shift();
64
69
  this.dropped++;
65
70
  }
@@ -111,7 +111,9 @@ export class TestMachine {
111
111
  stopped = !this.processor.execute(todo);
112
112
  left -= todo;
113
113
  }
114
- if (left && !stopped) {
114
+ // Not truthiness: a negative or NaN request clamps todo to zero,
115
+ // so left would never move and the loop never end.
116
+ if (left > 0 && !stopped) {
115
117
  setTimeout(runAnIter, 0);
116
118
  } else {
117
119
  resolve(stopped);
@@ -136,17 +138,13 @@ export class TestMachine {
136
138
  throw new Error(`Cursor did not reach state ${on} in time (cursorOnThisFrame=${video.cursorOnThisFrame})`);
137
139
  }
138
140
 
139
- /**
140
- * Run until the teletext flash state reaches the desired phase.
141
- * @param {boolean} on - true for flash-on (flashing cells blanked), false for flash-off
142
- */
143
- async runToFlashState(on) {
141
+ async runUntilFlashHidden() {
144
142
  const teletext = this.processor.video.teletext;
145
143
  for (let i = 0; i < 100; i++) {
146
- if (teletext.flashOn === on) return;
144
+ if (teletext.hideFlashing) return;
147
145
  await this.runFor(40000);
148
146
  }
149
- throw new Error(`Flash did not reach state ${on} in time (flashOn=${teletext.flashOn})`);
147
+ throw new Error("Flashing text did not reach its hidden phase in time");
150
148
  }
151
149
 
152
150
  async runUntilVblank() {