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/README.md +94 -0
- package/package.json +8 -6
- package/public/roms/tube/65C102Tube.rom +0 -0
- package/src/6502.js +22 -16
- package/src/6847.js +50 -8
- package/src/acia.js +31 -12
- package/src/canvas.js +77 -18
- package/src/config.js +16 -4
- package/src/disc-drive.js +9 -0
- package/src/disc-hfe.js +1 -2
- package/src/disc-surface.js +350 -0
- package/src/disc-visualiser.js +569 -0
- package/src/disc.js +143 -49
- package/src/dom-utils.js +16 -0
- package/src/econet.js +6 -2
- package/src/fake6502.js +2 -2
- package/src/gamepads.js +11 -4
- package/src/jsbeeb.css +126 -0
- package/src/main.js +112 -55
- package/src/models.js +47 -5
- package/src/serial.js +1 -0
- package/src/snapshot-helpers.js +1 -1
- package/src/sth.js +21 -18
- package/src/tapes.js +10 -12
- package/src/teletext_adaptor.js +38 -13
- package/src/touchscreen.js +6 -6
- package/src/tube.js +89 -37
- package/src/url-params.js +23 -19
- package/src/utils.js +10 -5
- package/src/utils_atom.js +9 -5
- package/src/video-filters/pal-composite.js +14 -48
- package/src/video-filters/passthrough-filter.js +11 -39
- package/src/video-filters/pixel-grid.js +55 -0
- package/src/video-filters/shader-program.js +53 -0
- package/src/video-filters/shaders/xbr.frag.glsl +278 -0
- package/src/video-filters/shaders/xbr.vert.glsl +7 -0
- package/src/video-filters/xbr-filter.js +107 -0
- package/src/video.js +56 -16
- package/src/wd-fdc.js +17 -7
- package/tests/test-machine.js +7 -5
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,
|
|
25
|
+
constructor(stream, model) {
|
|
29
26
|
this.stream = stream;
|
|
30
27
|
this.baseFrequency = 1200;
|
|
31
|
-
this.isAtom = isAtom;
|
|
32
|
-
this.cpuSpeed =
|
|
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 =
|
|
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
|
|
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,
|
|
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,
|
|
316
|
+
return new UefTape(stream, model);
|
|
319
317
|
}
|
|
320
318
|
console.log("Unknown tape format");
|
|
321
319
|
return null;
|
package/src/teletext_adaptor.js
CHANGED
|
@@ -34,9 +34,20 @@ Status register:
|
|
|
34
34
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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) {
|
package/src/touchscreen.js
CHANGED
|
@@ -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.
|
|
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(
|
|
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,
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
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.
|
|
147
|
-
|
|
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.
|
|
169
|
-
|
|
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.
|
|
258
|
-
|
|
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.
|
|
266
|
-
|
|
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.
|
|
288
|
-
|
|
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}
|
|
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 {
|
|
170
|
+
* @returns {string[]} descriptions of any mappings that were skipped, for showing to the user
|
|
171
171
|
*/
|
|
172
|
-
export function
|
|
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
|
|
181
|
+
const machineKey = val.toUpperCase();
|
|
182
|
+
const nativeKey = key.substring(4).toUpperCase(); // remove KEY.
|
|
179
183
|
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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("
|
|
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
|
|
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
|
-
//
|
|
939
|
-
//
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
//
|
|
481
|
-
|
|
482
|
-
|
|
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;
|