jsbeeb 1.14.0 → 1.16.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/tapes.js CHANGED
@@ -1,9 +1,6 @@
1
1
  "use strict";
2
2
  import * as utils from "./utils.js";
3
3
 
4
- const BbcCpuSpeed = 2 * 1000 * 1000;
5
- const AtomCpuSpeed = 1 * 1000 * 1000;
6
-
7
4
  function secsToClocks(secs, cpuSpeed) {
8
5
  return (cpuSpeed * secs) | 0;
9
6
  }
@@ -25,11 +22,11 @@ function parityOf(curByte) {
25
22
  const ParityN = "N".charCodeAt(0);
26
23
 
27
24
  class UefTape {
28
- constructor(stream, isAtom = false) {
25
+ constructor(stream, model) {
29
26
  this.stream = stream;
30
27
  this.baseFrequency = 1200;
31
- this.isAtom = isAtom;
32
- this.cpuSpeed = isAtom ? AtomCpuSpeed : BbcCpuSpeed;
28
+ this.isAtom = model.isAtom;
29
+ this.cpuSpeed = model.cyclesPerSecond;
33
30
  this.rewind();
34
31
 
35
32
  this.curChunk = this.readChunk();
@@ -265,9 +262,10 @@ class UefTape {
265
262
  const dividerTable = [1, 16, 64, -1];
266
263
 
267
264
  class TapefileTape {
268
- constructor(stream) {
265
+ constructor(stream, model) {
269
266
  this.count = 0;
270
267
  this.stream = stream;
268
+ this.cpuSpeed = model.cyclesPerSecond;
271
269
  }
272
270
 
273
271
  rate(acia) {
@@ -278,7 +276,7 @@ class TapefileTape {
278
276
  const divider = dividerTable[acia.cr & 0x03];
279
277
  // http://beebwiki.mdfs.net/index.php/Serial_ULA says the serial rate is ignored
280
278
  // for cassette mode.
281
- const cpp = (2 * 1000 * 1000) / (19200 / divider);
279
+ const cpp = this.cpuSpeed / (19200 / divider);
282
280
  return Math.floor(bitsPerByte * cpp);
283
281
  }
284
282
 
@@ -297,7 +295,7 @@ class TapefileTape {
297
295
  } else if (byte === 0x04) {
298
296
  acia.setTapeCarrier(true);
299
297
  // Simulate 5 seconds of carrier.
300
- return 5 * 2 * 1000 * 1000;
298
+ return secsToClocks(5, this.cpuSpeed);
301
299
  } else if (byte !== 0xff) {
302
300
  throw "Got a weird byte in the tape";
303
301
  }
@@ -307,15 +305,15 @@ class TapefileTape {
307
305
  }
308
306
  }
309
307
 
310
- export async function loadTapeFromData(name, data, isAtom = false) {
308
+ export async function loadTapeFromData(name, data, model) {
311
309
  const stream = await utils.DataStream.create(name, data);
312
310
  if (stream.readByte(0) === 0xff && stream.readByte(1) === 0x04) {
313
311
  console.log("Detected a 'tapefile' tape");
314
- return new TapefileTape(stream);
312
+ return new TapefileTape(stream, model);
315
313
  }
316
314
  if (stream.readNulString(0) === "UEF File!") {
317
315
  console.log("Detected a UEF tape");
318
- return new UefTape(stream, isAtom);
316
+ return new UefTape(stream, model);
319
317
  }
320
318
  console.log("Unknown tape format");
321
319
  return null;
@@ -34,9 +34,20 @@ Status register:
34
34
 
35
35
  */
36
36
 
37
- export class TeletextAdaptor {
37
+ /**
38
+ * Emulates the Acorn teletext adaptor. Dispatches a `showError` CustomEvent, carrying
39
+ * `context` and `error` in its detail, when a channel's stream cannot be loaded.
40
+ */
41
+ export class TeletextAdaptor extends EventTarget {
38
42
  constructor(cpu) {
43
+ super();
39
44
  this.cpu = cpu;
45
+ // Not cleared by a reset, so a fetch still in flight across one is recognised as stale.
46
+ this.streamRequest = 0;
47
+ this.clearState();
48
+ }
49
+
50
+ clearState() {
40
51
  this.teletextStatus = 0x0f; /* low nibble comes from LK4-7 and mystery links which are left floating */
41
52
  this.teletextInts = false;
42
53
  this.teletextEnable = false;
@@ -45,26 +56,40 @@ export class TeletextAdaptor {
45
56
  this.totalFrames = 0;
46
57
  this.rowPtr = 0x00;
47
58
  this.colPtr = 0x00;
48
- this.frameBuffer = new Array(16).fill(0).map(() => new Array(64).fill(0));
49
59
  this.streamData = null;
50
60
  this.pollCount = 0;
61
+ this.frameBuffer = new Array(16).fill(0).map(() => new Array(64).fill(0));
62
+ // Only a register access clears our IRQ, so an interrupt latched before the reset would hang the machine.
63
+ this.cpu.interrupt &= ~(1 << TELETEXT_IRQ);
51
64
  }
52
65
 
53
66
  reset(hard) {
54
- if (hard) {
55
- console.log("Teletext adaptor: initialisation");
56
- this.loadChannelStream(this.channel);
57
- }
67
+ if (!hard) return;
68
+ this.clearState();
69
+ this.loadChannelStream(this.channel);
58
70
  }
59
71
 
60
- loadChannelStream(channel) {
72
+ async loadChannelStream(channel) {
61
73
  console.log("Teletext adaptor: switching to channel " + channel);
62
- const teletextRef = this;
63
- utils.loadData("teletext/txt" + channel + ".dat").then(function (data) {
64
- teletextRef.streamData = data;
65
- teletextRef.totalFrames = data.length / TELETEXT_FRAME_SIZE;
66
- teletextRef.currentFrame = 0;
67
- });
74
+ const request = ++this.streamRequest;
75
+ let data;
76
+ try {
77
+ data = await utils.loadData(`teletext/txt${channel}.dat`);
78
+ } catch (error) {
79
+ if (request !== this.streamRequest) return;
80
+ console.error(`Teletext adaptor: failed to load channel ${channel}`, error);
81
+ this.dispatchEvent(
82
+ new CustomEvent("showError", {
83
+ detail: { context: `loading teletext channel ${channel}`, error },
84
+ }),
85
+ );
86
+ return;
87
+ }
88
+ // Fetches can resolve out of order; only the newest request may apply its data.
89
+ if (request !== this.streamRequest) return;
90
+ this.streamData = data;
91
+ this.totalFrames = Math.floor(data.length / TELETEXT_FRAME_SIZE);
92
+ this.currentFrame = 0;
68
93
  }
69
94
 
70
95
  read(addr) {
@@ -3,7 +3,6 @@
3
3
  import * as utils from "./utils.js";
4
4
 
5
5
  const PollHz = 8; // Made up
6
- const PollCycles = (2 * 1000 * 1000) / PollHz;
7
6
 
8
7
  function doScale(val, scale, margin) {
9
8
  val = (val - margin) / (1 - 2 * margin);
@@ -11,13 +10,14 @@ function doScale(val, scale, margin) {
11
10
  }
12
11
 
13
12
  export class TouchScreen {
14
- constructor(scheduler) {
13
+ constructor(scheduler, cyclesPerSecond) {
15
14
  this.scheduler = scheduler;
16
- this.mouse = [];
15
+ this.pollCycles = cyclesPerSecond / PollHz;
16
+ this.mouse = { x: 0, y: 0, button: 0 };
17
17
  this.outBuffer = new utils.Fifo(16);
18
18
  this.delay = 0;
19
19
  this.mode = 0;
20
- this.pollTask = this.scheduler.newTask(this.poll);
20
+ this.pollTask = this.scheduler.newTask(() => this.poll());
21
21
  }
22
22
 
23
23
  tryReceive(rts) {
@@ -47,7 +47,7 @@ export class TouchScreen {
47
47
 
48
48
  poll() {
49
49
  this.doRead();
50
- this.pollTask.reschedule(PollCycles);
50
+ this.pollTask.reschedule(this.pollCycles);
51
51
  }
52
52
 
53
53
  store(byte) {
@@ -81,6 +81,6 @@ export class TouchScreen {
81
81
  if (this.mode === 1) this.doRead();
82
82
  break;
83
83
  }
84
- this.pollTask.ensureScheduled(this.mode === 129 || this.mode === 130, PollCycles);
84
+ this.pollTask.ensureScheduled(this.mode === 129 || this.mode === 130, this.pollCycles);
85
85
  }
86
86
  }
package/src/tube.js CHANGED
@@ -36,6 +36,11 @@ const TUBE_ULA_FLAG_STATUS_PARASITE_RESET_ACTIVE_LOW = TUBE_ULA_FLAG_STATUS_P;
36
36
  const TUBE_ULA_FLAG_STATUS_CLEAR_ALL_TUBE_REGISTERS = TUBE_ULA_FLAG_STATUS_T;
37
37
  const TUBE_ULA_FLAG_STATUS_SET_CONTROL_FLAGS = TUBE_ULA_FLAG_STATUS_S;
38
38
  const TUBE_ULA_R1_PARASITE_BYTE_COUNT = 24;
39
+ // the control flags live in the bottom six bits of the register 1 status registers, which both
40
+ // sides see merged with their own flow control bits
41
+ const TUBE_ULA_CONTROL_FLAG_MASK = 0x3f;
42
+ // every status register except register 1's reads its unused bits as ones
43
+ const TUBE_ULA_STATUS_UNUSED_BITS = 0x3f;
39
44
 
40
45
  export class Tube {
41
46
  constructor(hostCpu, parasiteCpu) {
@@ -54,6 +59,7 @@ export class Tube {
54
59
  this.parasiteToHostFifoByteCount1 = 0;
55
60
  this.parasiteToHostFifoByteCount3 = 0;
56
61
  this.hostToParasiteFifoByteCount3 = 0;
62
+ this.parasiteNmi = false;
57
63
  this.debug = false;
58
64
  }
59
65
  reset(updateInternalStatusRegister = true) {
@@ -61,8 +67,10 @@ export class Tube {
61
67
  this.internalStatusRegister = 0;
62
68
  }
63
69
  for (let i = 0; i < 4; i++) {
64
- this.hostStatus[i] = TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
65
- this.parasiteStatus[i] = TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
70
+ // register 1's spare bits carry the control flags instead, merged in when either side reads it
71
+ const unused = i === TUBE_ULA_R1 ? 0 : TUBE_ULA_STATUS_UNUSED_BITS;
72
+ this.hostStatus[i] = TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL | unused;
73
+ this.parasiteStatus[i] = TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL | unused;
66
74
  if (i === TUBE_ULA_R3) {
67
75
  // register 3 has one valid but insignificant byte in the parasite to host FIFO (this is to prevent an immediate PNMI state after PRST)
68
76
  this.hostStatus[i] |= TUBE_ULA_FLAG_DATA_AVAILABLE;
@@ -73,8 +81,39 @@ export class Tube {
73
81
  // see info in the loop above from Tube Application Note about R3
74
82
  this.parasiteToHostFifoByteCount3 = 1;
75
83
  this.hostToParasiteFifoByteCount3 = 0;
84
+ this.parasiteNmi = false;
76
85
  this.updateInterrupts();
77
86
  }
87
+
88
+ /**
89
+ * The register 3 condition the ULA raises an NMI from, before the M flag gates it: the host has
90
+ * queued a whole transfer for the parasite, or the parasite has nothing queued back to the host.
91
+ *
92
+ * @returns {boolean} whether the condition currently holds
93
+ */
94
+ r3NmiCondition() {
95
+ const bytesPerTransfer = this.internalStatusRegister & TUBE_ULA_FLAG_STATUS_ENABLE_2_BYTE_R3_DATA ? 2 : 1;
96
+ return this.hostToParasiteFifoByteCount3 >= bytesPerTransfer || this.parasiteToHostFifoByteCount3 === 0;
97
+ }
98
+
99
+ /** @returns {boolean} whether that condition is currently allowed to reach the parasite's NMI pin. */
100
+ parasiteNmiEnabled() {
101
+ return !!(this.internalStatusRegister & TUBE_ULA_FLAG_STATUS_ENABLE_PARASITE_NMI_FROM_R3_DATA);
102
+ }
103
+
104
+ /** Drops the NMI request once the parasite has vectored through it. */
105
+ acknowledgeNmi() {
106
+ this.parasiteNmi = false;
107
+ this.updateInterrupts();
108
+ }
109
+
110
+ /**
111
+ * Withdraws a pending NMI whose register 3 condition has since gone away. Only the parasite's own
112
+ * accesses can do this: a transfer it has satisfied no longer needs servicing.
113
+ */
114
+ clearNmiIfSatisfied() {
115
+ if (!this.r3NmiCondition()) this.parasiteNmi = false;
116
+ }
78
117
  updateInterrupts() {
79
118
  // host IRQ
80
119
  if (
@@ -97,21 +136,11 @@ export class Tube {
97
136
  this.parasiteCpu.interrupt = false;
98
137
  }
99
138
  // parasite NMI
100
- // (from Tube Application Note)
101
- // either: M = 1, V = 0, 1 or 2 bytes in host to parasite register 3 FIFO or 0 bytes in parasite
102
- // to host register 3 FIFO (this allows single byte transfers across
103
- // register 3)
104
- // or: M = 1, V = 1, 2 bytes in host to parasite register 3 FIFO or 0 bytes in parasite to host
105
- // register 3 FIFO. (this allows two byte transfers across register 3)
106
- const r3Size = this.internalStatusRegister & TUBE_ULA_FLAG_STATUS_ENABLE_2_BYTE_R3_DATA ? 2 : 1;
107
- if (
108
- this.internalStatusRegister & TUBE_ULA_FLAG_STATUS_ENABLE_PARASITE_NMI_FROM_R3_DATA &&
109
- (this.hostToParasiteFifoByteCount3 >= r3Size || this.parasiteToHostFifoByteCount3 === 0)
110
- ) {
111
- this.parasiteCpu.NMI(true);
112
- } else {
113
- this.parasiteCpu.NMI(false);
114
- }
139
+ // The ULA latches its NMI request rather than presenting the register 3 condition as a level:
140
+ // each host access to register 3 that gives the parasite something to do raises a request, and
141
+ // only the parasite can retire one, by servicing the transfer or by taking the NMI. Recomputing
142
+ // the condition here instead would lose every request raised while an earlier one still stood.
143
+ this.parasiteCpu.NMI(this.parasiteNmi && this.parasiteNmiEnabled());
115
144
  // parasite CPU RESET held low - not implemented in the CPU - the CPU should be frozen until this signal is released
116
145
  this.parasiteCpu.resetHeldLow = this.internalStatusRegister & TUBE_ULA_FLAG_STATUS_PARASITE_RESET_ACTIVE_LOW;
117
146
  }
@@ -122,8 +151,7 @@ export class Tube {
122
151
  result =
123
152
  (this.hostStatus[TUBE_ULA_R1] &
124
153
  (TUBE_ULA_FLAG_DATA_AVAILABLE | TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL)) |
125
- (this.internalStatusRegister &
126
- ~(TUBE_ULA_FLAG_DATA_AVAILABLE | TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL));
154
+ (this.internalStatusRegister & TUBE_ULA_CONTROL_FLAG_MASK);
127
155
  break;
128
156
  case TUBE_ULA_R1_DATA_ADDRESS:
129
157
  result = this.parasiteToHostData[TUBE_ULA_R1][0];
@@ -143,8 +171,10 @@ export class Tube {
143
171
  break;
144
172
  case TUBE_ULA_R2_DATA_ADDRESS:
145
173
  result = this.parasiteToHostData[TUBE_ULA_R2][0];
146
- this.parasiteStatus[TUBE_ULA_R2] |= TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
147
- this.hostStatus[TUBE_ULA_R2] &= ~TUBE_ULA_FLAG_DATA_AVAILABLE;
174
+ if (this.hostStatus[TUBE_ULA_R2] & TUBE_ULA_FLAG_DATA_AVAILABLE) {
175
+ this.parasiteStatus[TUBE_ULA_R2] |= TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
176
+ this.hostStatus[TUBE_ULA_R2] &= ~TUBE_ULA_FLAG_DATA_AVAILABLE;
177
+ }
148
178
  break;
149
179
  case TUBE_ULA_R3_STATUS_ADDRESS:
150
180
  result = this.hostStatus[TUBE_ULA_R3];
@@ -157,6 +187,8 @@ export class Tube {
157
187
  this.parasiteToHostFifoByteCount3--;
158
188
  if (this.parasiteToHostFifoByteCount3 === 0) {
159
189
  this.hostStatus[TUBE_ULA_R3] &= ~TUBE_ULA_FLAG_DATA_AVAILABLE;
190
+ // the parasite owes the host a byte, so ask it for one
191
+ this.parasiteNmi = true;
160
192
  }
161
193
  }
162
194
  break;
@@ -165,8 +197,10 @@ export class Tube {
165
197
  break;
166
198
  case TUBE_ULA_R4_DATA_ADDRESS:
167
199
  result = this.parasiteToHostData[TUBE_ULA_R4][0];
168
- this.parasiteStatus[TUBE_ULA_R4] |= TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
169
- this.hostStatus[TUBE_ULA_R4] &= ~TUBE_ULA_FLAG_DATA_AVAILABLE;
200
+ if (this.hostStatus[TUBE_ULA_R4] & TUBE_ULA_FLAG_DATA_AVAILABLE) {
201
+ this.parasiteStatus[TUBE_ULA_R4] |= TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
202
+ this.hostStatus[TUBE_ULA_R4] &= ~TUBE_ULA_FLAG_DATA_AVAILABLE;
203
+ }
170
204
  break;
171
205
  }
172
206
  this.updateInterrupts();
@@ -180,14 +214,13 @@ export class Tube {
180
214
  console.log("TUBE ULA: host write " + utils.hexword(address) + " = " + utils.hexbyte(value));
181
215
  }
182
216
  switch (address & 7) {
183
- case TUBE_ULA_R1_STATUS_ADDRESS:
217
+ case TUBE_ULA_R1_STATUS_ADDRESS: {
218
+ // M and V both feed the NMI, so a write here can raise or drop the request on its own
219
+ const nmiBefore = this.parasiteNmiEnabled() && this.r3NmiCondition();
184
220
  if (value & TUBE_ULA_FLAG_STATUS_SET_CONTROL_FLAGS) {
185
- this.internalStatusRegister |=
186
- value & ~(TUBE_ULA_FLAG_DATA_AVAILABLE | TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL);
221
+ this.internalStatusRegister |= value & TUBE_ULA_CONTROL_FLAG_MASK;
187
222
  } else {
188
- this.internalStatusRegister &= ~(
189
- value & ~(TUBE_ULA_FLAG_DATA_AVAILABLE | TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL)
190
- );
223
+ this.internalStatusRegister &= ~(value & TUBE_ULA_CONTROL_FLAG_MASK);
191
224
  }
192
225
  if (value & TUBE_ULA_FLAG_STATUS_CLEAR_ALL_TUBE_REGISTERS) {
193
226
  this.reset(false);
@@ -200,7 +233,11 @@ export class Tube {
200
233
  this.parasiteCpu.reset(true); // this in turn calls our this.reset(true)
201
234
  }
202
235
  }
236
+ const nmiAfter = this.parasiteNmiEnabled() && this.r3NmiCondition();
237
+ if (!nmiBefore && nmiAfter) this.parasiteNmi = true;
238
+ if (!nmiAfter) this.parasiteNmi = false;
203
239
  break;
240
+ }
204
241
  case TUBE_ULA_R1_DATA_ADDRESS:
205
242
  if (this.hostStatus[TUBE_ULA_R1] & TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL) {
206
243
  this.hostToParasiteData[TUBE_ULA_R1][0] = value;
@@ -224,12 +261,15 @@ export class Tube {
224
261
  if (this.hostToParasiteFifoByteCount3 === 2) {
225
262
  this.parasiteStatus[TUBE_ULA_R3] |= TUBE_ULA_FLAG_DATA_AVAILABLE;
226
263
  this.hostStatus[TUBE_ULA_R3] &= ~TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
264
+ // a whole transfer is waiting, so ask the parasite to take it
265
+ this.parasiteNmi = true;
227
266
  }
228
267
  } else {
229
268
  this.hostToParasiteData[TUBE_ULA_R3][0] = value;
230
269
  this.hostToParasiteFifoByteCount3 = 1;
231
270
  this.parasiteStatus[TUBE_ULA_R3] |= TUBE_ULA_FLAG_DATA_AVAILABLE;
232
271
  this.hostStatus[TUBE_ULA_R3] &= ~TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
272
+ this.parasiteNmi = true;
233
273
  }
234
274
  }
235
275
  break;
@@ -250,20 +290,24 @@ export class Tube {
250
290
  let result = 0;
251
291
  switch (address & 7) {
252
292
  case TUBE_ULA_R1_STATUS_ADDRESS:
253
- result = this.parasiteStatus[TUBE_ULA_R1];
293
+ result = this.parasiteStatus[TUBE_ULA_R1] | (this.internalStatusRegister & TUBE_ULA_CONTROL_FLAG_MASK);
254
294
  break;
255
295
  case TUBE_ULA_R1_DATA_ADDRESS:
256
296
  result = this.hostToParasiteData[TUBE_ULA_R1][0];
257
- this.hostStatus[TUBE_ULA_R1] |= TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
258
- this.parasiteStatus[TUBE_ULA_R1] &= ~TUBE_ULA_FLAG_DATA_AVAILABLE;
297
+ if (this.parasiteStatus[TUBE_ULA_R1] & TUBE_ULA_FLAG_DATA_AVAILABLE) {
298
+ this.hostStatus[TUBE_ULA_R1] |= TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
299
+ this.parasiteStatus[TUBE_ULA_R1] &= ~TUBE_ULA_FLAG_DATA_AVAILABLE;
300
+ }
259
301
  break;
260
302
  case TUBE_ULA_R2_STATUS_ADDRESS:
261
303
  result = this.parasiteStatus[TUBE_ULA_R2];
262
304
  break;
263
305
  case TUBE_ULA_R2_DATA_ADDRESS:
264
306
  result = this.hostToParasiteData[TUBE_ULA_R2][0];
265
- this.hostStatus[TUBE_ULA_R2] |= TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
266
- this.parasiteStatus[TUBE_ULA_R2] &= ~TUBE_ULA_FLAG_DATA_AVAILABLE;
307
+ if (this.parasiteStatus[TUBE_ULA_R2] & TUBE_ULA_FLAG_DATA_AVAILABLE) {
308
+ this.hostStatus[TUBE_ULA_R2] |= TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
309
+ this.parasiteStatus[TUBE_ULA_R2] &= ~TUBE_ULA_FLAG_DATA_AVAILABLE;
310
+ }
267
311
  break;
268
312
  case TUBE_ULA_R3_STATUS_ADDRESS:
269
313
  result = this.parasiteStatus[TUBE_ULA_R3];
@@ -277,6 +321,7 @@ export class Tube {
277
321
  this.hostStatus[TUBE_ULA_R3] |= TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
278
322
  this.parasiteStatus[TUBE_ULA_R3] &= ~TUBE_ULA_FLAG_DATA_AVAILABLE;
279
323
  }
324
+ this.clearNmiIfSatisfied();
280
325
  }
281
326
  break;
282
327
  case TUBE_ULA_R4_STATUS_ADDRESS:
@@ -284,8 +329,10 @@ export class Tube {
284
329
  break;
285
330
  case TUBE_ULA_R4_DATA_ADDRESS:
286
331
  result = this.hostToParasiteData[TUBE_ULA_R4][0];
287
- this.hostStatus[TUBE_ULA_R4] |= TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
288
- this.parasiteStatus[TUBE_ULA_R4] &= ~TUBE_ULA_FLAG_DATA_AVAILABLE;
332
+ if (this.parasiteStatus[TUBE_ULA_R4] & TUBE_ULA_FLAG_DATA_AVAILABLE) {
333
+ this.hostStatus[TUBE_ULA_R4] |= TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
334
+ this.parasiteStatus[TUBE_ULA_R4] &= ~TUBE_ULA_FLAG_DATA_AVAILABLE;
335
+ }
289
336
  break;
290
337
  }
291
338
  this.updateInterrupts();
@@ -304,13 +351,15 @@ export class Tube {
304
351
  parasiteToHostFifoByteCount1: this.parasiteToHostFifoByteCount1,
305
352
  parasiteToHostFifoByteCount3: this.parasiteToHostFifoByteCount3,
306
353
  hostToParasiteFifoByteCount3: this.hostToParasiteFifoByteCount3,
354
+ parasiteNmi: this.parasiteNmi,
307
355
  };
308
356
  }
309
357
 
310
358
  /**
311
359
  * The interrupt and reset lines are not saved: they are derived from the status registers
312
360
  * and FIFO counts, in the same way the host's `interrupt` is rebuilt by the VIA and ACIA
313
- * restores.
361
+ * restores. The latched NMI request is saved, as nothing else records whether the parasite
362
+ * still owes the host a transfer.
314
363
  */
315
364
  restoreState(state) {
316
365
  this.internalStatusRegister = state.internalStatusRegister;
@@ -323,6 +372,8 @@ export class Tube {
323
372
  this.parasiteToHostFifoByteCount1 = state.parasiteToHostFifoByteCount1;
324
373
  this.parasiteToHostFifoByteCount3 = state.parasiteToHostFifoByteCount3;
325
374
  this.hostToParasiteFifoByteCount3 = state.hostToParasiteFifoByteCount3;
375
+ // snapshots taken before the ULA latched its NMI have to fall back on the condition itself
376
+ this.parasiteNmi = state.parasiteNmi ?? this.r3NmiCondition();
326
377
  this.updateInterrupts();
327
378
  }
328
379
 
@@ -366,6 +417,7 @@ export class Tube {
366
417
  this.hostStatus[TUBE_ULA_R3] |= TUBE_ULA_FLAG_DATA_AVAILABLE;
367
418
  this.parasiteStatus[TUBE_ULA_R3] &= ~TUBE_ULA_FLAG_DATA_REGISTER_NOT_FULL;
368
419
  }
420
+ this.clearNmiIfSatisfied();
369
421
  }
370
422
  break;
371
423
  case TUBE_ULA_R4_DATA_ADDRESS:
package/src/url-params.js CHANGED
@@ -161,52 +161,56 @@ export function buildUrlFromParams(baseUrl, parsedQuery, paramTypes = {}) {
161
161
  }
162
162
 
163
163
  /**
164
- * Process keyboard mapping parameters from query string
164
+ * Process keyboard and gamepad mapping parameters from query string
165
165
  * @param {Object} parsedQuery - The parsed query parameters
166
- * @param {Object} BBC - BBC key constants
166
+ * @param {Object} machineKeys - Emulated machine's key constants (`BBC`, or `ATOM` for the Atom)
167
167
  * @param {Object} keyCodes - Key code constants
168
168
  * @param {Array} userKeymap - Array to store user key mappings
169
169
  * @param {Object} gamepad - Gamepad object for handling mapping
170
- * @returns {Object} Updated query parameters
170
+ * @returns {string[]} descriptions of any mappings that were skipped, for showing to the user
171
171
  */
172
- export function processKeyboardParams(parsedQuery, BBC, keyCodes, userKeymap, gamepad) {
172
+ export function processInputParams(parsedQuery, machineKeys, keyCodes, userKeymap, gamepad) {
173
+ const warnings = [];
174
+
173
175
  Object.entries(parsedQuery).forEach(([key, val]) => {
174
176
  if (!val) return;
175
177
 
176
- // eg KEY.CAPSLOCK=CTRL
178
+ // `KEY.<host key>=<machine key>`, eg `KEY.CAPSLOCK=CTRL`. Host names come from
179
+ // `keyCodes`, so the BBC's RETURN is ENTER here; both lists are in the README.
177
180
  if (key.toUpperCase().indexOf("KEY.") === 0) {
178
- const bbcKey = val.toUpperCase();
181
+ const machineKey = val.toUpperCase();
182
+ const nativeKey = key.substring(4).toUpperCase(); // remove KEY.
179
183
 
180
- if (BBC[bbcKey]) {
181
- const nativeKey = key.substring(4).toUpperCase(); // remove KEY.
182
- if (keyCodes[nativeKey]) {
183
- console.log("mapping " + nativeKey + " to " + bbcKey);
184
- userKeymap.push({ native: nativeKey, bbc: bbcKey });
185
- } else {
186
- console.log("unknown key: " + nativeKey);
187
- }
184
+ if (!machineKeys[machineKey]) {
185
+ warnings.push(`${key}=${val}: "${machineKey}" is not a key on the emulated machine.`);
186
+ } else if (!keyCodes[nativeKey]) {
187
+ warnings.push(`${key}=${val}: "${nativeKey}" is not a key on your keyboard.`);
188
188
  } else {
189
- console.log("unknown BBC key: " + val);
189
+ console.log("mapping " + nativeKey + " to " + machineKey);
190
+ userKeymap.push({ native: nativeKey, key: machineKey });
190
191
  }
191
192
  } else if (key.indexOf("GP.") === 0) {
192
193
  // gamepad mapping
193
194
  // eg ?GP.FIRE2=RETURN
194
195
  const gamepadKey = key.substring(3).toUpperCase(); // remove GP. prefix
195
- gamepad.remap(gamepadKey, val.toUpperCase());
196
+ const problem = gamepad.remap(gamepadKey, val.toUpperCase());
197
+ if (problem) warnings.push(`${key}=${val}: ${problem}`);
196
198
  } else {
197
199
  switch (key) {
198
200
  case "LEFT":
199
201
  case "RIGHT":
200
202
  case "UP":
201
203
  case "DOWN":
202
- case "FIRE":
203
- gamepad.remap(key, val.toUpperCase());
204
+ case "FIRE": {
205
+ const problem = gamepad.remap(key, val.toUpperCase());
206
+ if (problem) warnings.push(`${key}=${val}: ${problem}`);
204
207
  break;
208
+ }
205
209
  }
206
210
  }
207
211
  });
208
212
 
209
- return parsedQuery;
213
+ return warnings;
210
214
  }
211
215
 
212
216
  /**
package/src/utils.js CHANGED
@@ -657,6 +657,12 @@ export function getKeyMap(keyLayout) {
657
657
  keys2[shiftDown][s] = colRow;
658
658
  }
659
659
 
660
+ // Overriding a default is the point here, so unlike `map` this doesn't warn about the clash.
661
+ function remap(s, colRow) {
662
+ keys2[true][s] = colRow;
663
+ keys2[false][s] = colRow;
664
+ }
665
+
660
666
  // shiftDown undefined -> map both
661
667
  function map(s, colRow, shiftDown) {
662
668
  if ((!s && s !== 0) || !colRow) {
@@ -935,11 +941,10 @@ export function getKeyMap(keyLayout) {
935
941
  // eg Master Dunjunz needs # Del 3 , * Enter
936
942
  // https://web.archive.org/web/20080305042238/http://bbc.nvg.org/doc/games/Dunjunz-docs.txt
937
943
 
938
- // user keymapping
939
- // do last (to override defaults)
940
- while (userKeymap.length > 0) {
941
- const mapping = userKeymap.pop();
942
- map(keyCodes[mapping.native], BBC[mapping.bbc]);
944
+ // `KEY.` URL parameters, applied last so they win. Not consumed: this map is rebuilt on
945
+ // layout and model changes, and the user's mapping has to survive that.
946
+ for (const mapping of userKeymap) {
947
+ remap(keyCodes[mapping.native], BBC[mapping.key]);
943
948
  }
944
949
 
945
950
  return keys2;
package/src/utils_atom.js CHANGED
@@ -249,6 +249,12 @@ export function getKeyMapAtom(keyLayout) {
249
249
  keys2[shiftDown][s] = colRow;
250
250
  }
251
251
 
252
+ // Overriding a default is the point here, so unlike `map` this doesn't warn about the clash.
253
+ function remap(s, colRow) {
254
+ keys2[true][s] = colRow;
255
+ keys2[false][s] = colRow;
256
+ }
257
+
252
258
  // shiftDown undefined -> map both
253
259
  function map(s, colRow, shiftDown) {
254
260
  if ((!s && s !== 0) || !colRow) {
@@ -477,11 +483,9 @@ export function getKeyMapAtom(keyLayout) {
477
483
  // Z - M normal
478
484
  }
479
485
 
480
- // user keymapping
481
- // do last (to override defaults)
482
- while (userKeymap.length > 0) {
483
- const mapping = userKeymap.pop();
484
- map(keyCodes[mapping.native], ATOM[mapping.atom]);
486
+ // `KEY.` URL parameters, applied last so they win. See the equivalent in `getKeyMap`.
487
+ for (const mapping of userKeymap) {
488
+ remap(keyCodes[mapping.native], ATOM[mapping.key]);
485
489
  }
486
490
 
487
491
  return keys2;