jsbeeb 1.15.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/package.json +8 -6
- package/src/6502.js +14 -7
- package/src/6847.js +50 -8
- package/src/acia.js +2 -1
- package/src/canvas.js +77 -18
- 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 +96 -44
- package/src/econet.js +6 -2
- package/src/jsbeeb.css +126 -0
- package/src/main.js +70 -18
- package/src/models.js +13 -3
- package/src/sth.js +21 -18
- package/src/tapes.js +10 -12
- package/src/touchscreen.js +6 -6
- package/src/tube.js +89 -37
- 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/sth.js
CHANGED
|
@@ -2,30 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
import * as utils from "./utils.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// Always https, whatever the page was loaded over: the mirror redirects plain
|
|
6
|
+
// http, so following the page's protocol would cost a redirect on every request
|
|
7
|
+
// when developing over http, and Electron reports "file:" anyway.
|
|
8
|
+
const mirrorBase = "https://bbc.xania.org/archive/sth";
|
|
7
9
|
|
|
8
|
-
async function
|
|
10
|
+
async function _fetchManifest(url) {
|
|
9
11
|
const response = await fetch(url);
|
|
10
12
|
if (!response.ok) {
|
|
11
|
-
throw new Error(
|
|
13
|
+
throw new Error(`Network response was not ok (${response.status})`);
|
|
12
14
|
}
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
const data = await response.json();
|
|
16
|
+
if (!Array.isArray(data?.files)) {
|
|
17
|
+
throw new Error("Invalid manifest: missing files array");
|
|
18
|
+
}
|
|
19
|
+
return data.files.map((f) => f.path).sort();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Each path component is encoded individually so slashes survive but special
|
|
23
|
+
// characters in filenames (e.g. brackets in "Daxis[droids]-demo.zip") don't
|
|
24
|
+
// produce a malformed URL.
|
|
25
|
+
function encodePath(path) {
|
|
26
|
+
return path.split("/").map(encodeURIComponent).join("/");
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export class StairwayToHell {
|
|
25
30
|
constructor(onStart, onCat, onError, tape) {
|
|
26
|
-
|
|
27
|
-
const protocol = document.location.protocol === "file:" ? "https:" : document.location.protocol;
|
|
28
|
-
this._baseUrl = `${protocol}//${sthArchive}/${tape ? "tape" : "disk"}images/`;
|
|
31
|
+
this._baseUrl = `${mirrorBase}/${tape ? "tape" : "disk"}images/`;
|
|
29
32
|
this._catalog = [];
|
|
30
33
|
this._onStart = onStart;
|
|
31
34
|
this._onCat = onCat;
|
|
@@ -36,7 +39,7 @@ export class StairwayToHell {
|
|
|
36
39
|
this._onStart();
|
|
37
40
|
if (this._catalog.length === 0) {
|
|
38
41
|
try {
|
|
39
|
-
this._catalog = await
|
|
42
|
+
this._catalog = await _fetchManifest(this._baseUrl + "manifest.json");
|
|
40
43
|
} catch (error) {
|
|
41
44
|
console.error("Failed to fetch catalog:", error);
|
|
42
45
|
if (this._onError) this._onError();
|
|
@@ -47,7 +50,7 @@ export class StairwayToHell {
|
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
async fetch(file) {
|
|
50
|
-
const name = this._baseUrl + file;
|
|
53
|
+
const name = this._baseUrl + encodePath(file);
|
|
51
54
|
console.log("Loading ZIP from " + name);
|
|
52
55
|
const response = await fetch(name);
|
|
53
56
|
if (!response.ok) throw new Error("Network response was not ok");
|
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/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:
|
|
@@ -14,12 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
import VERT_SHADER from "./shaders/pal-composite.vert.glsl?raw";
|
|
16
16
|
import FRAG_SHADER from "./shaders/pal-composite.frag.glsl?raw";
|
|
17
|
+
import { compileProgram } from "./shader-program.js";
|
|
17
18
|
|
|
18
19
|
export class PALCompositeFilter {
|
|
19
|
-
static requiresGl() {
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
20
|
static getDisplayConfig() {
|
|
24
21
|
return {
|
|
25
22
|
name: "PAL TV",
|
|
@@ -31,57 +28,26 @@ export class PALCompositeFilter {
|
|
|
31
28
|
canvasTop: 70,
|
|
32
29
|
visibleWidth: 800,
|
|
33
30
|
visibleHeight: 600,
|
|
31
|
+
canvasWidth: 896,
|
|
32
|
+
canvasHeight: 600,
|
|
34
33
|
};
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
constructor(gl) {
|
|
38
37
|
this.gl = gl;
|
|
39
|
-
this.program =
|
|
40
|
-
this.locations = {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const gl = this.gl;
|
|
47
|
-
|
|
48
|
-
// Compile shaders
|
|
49
|
-
const vertShader = this._compileShader(gl.VERTEX_SHADER, VERT_SHADER);
|
|
50
|
-
const fragShader = this._compileShader(gl.FRAGMENT_SHADER, FRAG_SHADER);
|
|
51
|
-
|
|
52
|
-
// Link program
|
|
53
|
-
this.program = gl.createProgram();
|
|
54
|
-
gl.attachShader(this.program, vertShader);
|
|
55
|
-
gl.attachShader(this.program, fragShader);
|
|
56
|
-
gl.linkProgram(this.program);
|
|
57
|
-
|
|
58
|
-
if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
|
|
59
|
-
const info = gl.getProgramInfoLog(this.program);
|
|
60
|
-
throw new Error("Failed to link PAL shader program: " + info);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Get uniform locations
|
|
64
|
-
this.locations.uFramebuffer = gl.getUniformLocation(this.program, "uFramebuffer");
|
|
65
|
-
this.locations.uResolution = gl.getUniformLocation(this.program, "uResolution");
|
|
66
|
-
this.locations.uTexelSize = gl.getUniformLocation(this.program, "uTexelSize");
|
|
67
|
-
this.locations.uFrameCount = gl.getUniformLocation(this.program, "uFrameCount");
|
|
68
|
-
|
|
69
|
-
console.log("PAL composite filter initialized");
|
|
38
|
+
this.program = compileProgram(gl, VERT_SHADER, FRAG_SHADER, "PAL composite");
|
|
39
|
+
this.locations = {
|
|
40
|
+
uFramebuffer: gl.getUniformLocation(this.program, "uFramebuffer"),
|
|
41
|
+
uResolution: gl.getUniformLocation(this.program, "uResolution"),
|
|
42
|
+
uTexelSize: gl.getUniformLocation(this.program, "uTexelSize"),
|
|
43
|
+
uFrameCount: gl.getUniformLocation(this.program, "uFrameCount"),
|
|
44
|
+
};
|
|
70
45
|
}
|
|
71
46
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
gl.compileShader(shader);
|
|
77
|
-
|
|
78
|
-
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
79
|
-
const info = gl.getShaderInfoLog(shader);
|
|
80
|
-
const typeName = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
|
|
81
|
-
throw new Error(`Failed to compile ${typeName} shader: ${info}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return shader;
|
|
47
|
+
/** Release the GL objects this filter owns. */
|
|
48
|
+
dispose() {
|
|
49
|
+
this.gl.deleteProgram(this.program);
|
|
50
|
+
this.program = null;
|
|
85
51
|
}
|
|
86
52
|
|
|
87
53
|
setUniforms(params) {
|
|
@@ -2,12 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import VERT_SHADER from "./shaders/passthrough.vert.glsl?raw";
|
|
4
4
|
import FRAG_SHADER from "./shaders/passthrough.frag.glsl?raw";
|
|
5
|
+
import { compileProgram } from "./shader-program.js";
|
|
5
6
|
|
|
6
7
|
export class PassthroughFilter {
|
|
7
|
-
static requiresGl() {
|
|
8
|
-
return false;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
8
|
static getDisplayConfig() {
|
|
12
9
|
return {
|
|
13
10
|
name: "RGB Monitor",
|
|
@@ -19,48 +16,23 @@ export class PassthroughFilter {
|
|
|
19
16
|
canvasTop: 8,
|
|
20
17
|
visibleWidth: 896,
|
|
21
18
|
visibleHeight: 600,
|
|
19
|
+
canvasWidth: 896,
|
|
20
|
+
canvasHeight: 600,
|
|
22
21
|
};
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
constructor(gl) {
|
|
26
25
|
this.gl = gl;
|
|
27
|
-
this.program =
|
|
28
|
-
this.locations = {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
_init() {
|
|
34
|
-
const gl = this.gl;
|
|
35
|
-
|
|
36
|
-
const vertexShader = this._compileShader(gl.VERTEX_SHADER, VERT_SHADER);
|
|
37
|
-
const fragmentShader = this._compileShader(gl.FRAGMENT_SHADER, FRAG_SHADER);
|
|
38
|
-
|
|
39
|
-
this.program = gl.createProgram();
|
|
40
|
-
gl.attachShader(this.program, vertexShader);
|
|
41
|
-
gl.attachShader(this.program, fragmentShader);
|
|
42
|
-
gl.linkProgram(this.program);
|
|
43
|
-
|
|
44
|
-
if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
|
|
45
|
-
throw new Error("Failed to link passthrough shader program: " + gl.getProgramInfoLog(this.program));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
this.locations.tex = gl.getUniformLocation(this.program, "tex");
|
|
26
|
+
this.program = compileProgram(gl, VERT_SHADER, FRAG_SHADER, "passthrough");
|
|
27
|
+
this.locations = {
|
|
28
|
+
tex: gl.getUniformLocation(this.program, "tex"),
|
|
29
|
+
};
|
|
49
30
|
}
|
|
50
31
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
gl.compileShader(shader);
|
|
56
|
-
|
|
57
|
-
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
58
|
-
const error = gl.getShaderInfoLog(shader);
|
|
59
|
-
gl.deleteShader(shader);
|
|
60
|
-
throw new Error("Shader compilation failed: " + error);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return shader;
|
|
32
|
+
/** Release the GL objects this filter owns. */
|
|
33
|
+
dispose() {
|
|
34
|
+
this.gl.deleteProgram(this.program);
|
|
35
|
+
this.program = null;
|
|
64
36
|
}
|
|
65
37
|
|
|
66
38
|
setUniforms(_params) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Describing the logical pixel grid of jsbeeb's framebuffer, which is a
|
|
4
|
+
// 1024-wide *raster* rather than a grid of BBC pixels: one logical pixel covers
|
|
5
|
+
// up to eight texels across and two down. Filters need to know that, and only
|
|
6
|
+
// the video chips do, so they record one descriptor byte per row and this
|
|
7
|
+
// module owns the encoding.
|
|
8
|
+
//
|
|
9
|
+
// See docs/xbr-display-mode.md for why an upscaler that samples raw texels
|
|
10
|
+
// achieves nothing at all.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Rows in the line grid. The framebuffer is 625 rows, but the GL texture it is
|
|
14
|
+
* uploaded into is 1024 square, and the shader indexes the grid with the same
|
|
15
|
+
* coordinate it uses for the picture — so the two must agree.
|
|
16
|
+
*/
|
|
17
|
+
export const LineGridRows = 1024;
|
|
18
|
+
|
|
19
|
+
// Row descriptor bits, as stored in `Video.lineGrid`. The width is held less
|
|
20
|
+
// one, so it needs no logarithm to write and no exponential to read, and any
|
|
21
|
+
// width from one to eight can be described — the 6847 uses widths the BBC's ULA
|
|
22
|
+
// never selects.
|
|
23
|
+
const LineGridRendered = 0x80;
|
|
24
|
+
const LineGridVerticalDouble = 0x08;
|
|
25
|
+
const LineGridWidthMask = 0x07;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* How many framebuffer texels wide one logical pixel is, given the ULA's
|
|
29
|
+
* colour-select bits. The ULA writes 8 pixels per byte at `ulaMode` 3 and
|
|
30
|
+
* halves the count for each step down, which `Video.table4bpp` implements by
|
|
31
|
+
* indexing its 1bpp entries with `i >> (3 - ulaMode)`. This holds for the 1MHz
|
|
32
|
+
* modes too: `pixelsPerChar` is 16 there rather than 8, and the same shift over
|
|
33
|
+
* twice as many texels gives the same width — MODE 4 is two, MODE 5 is four.
|
|
34
|
+
*
|
|
35
|
+
* @param {number} ulaMode 0..3, the ULA control register's colour bits
|
|
36
|
+
* @returns {number} 1, 2, 4 or 8; the standard modes use 1, 2 and 4
|
|
37
|
+
*/
|
|
38
|
+
export function texelsPerPixel(ulaMode) {
|
|
39
|
+
return 8 >> ulaMode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pack a row's grid description into the byte the video chips store.
|
|
44
|
+
*
|
|
45
|
+
* @param {number} texelsWide 1 to 8 (see {@link texelsPerPixel})
|
|
46
|
+
* @param {boolean} doubledLines whether this scanline was written to two rows
|
|
47
|
+
*/
|
|
48
|
+
export function encodeLineGrid(texelsWide, doubledLines) {
|
|
49
|
+
// A width of 9 would set the doubling bit and read back as a doubled width
|
|
50
|
+
// of 1 — a silent lie rather than an error. The widths come from mode-table
|
|
51
|
+
// arithmetic, so check.
|
|
52
|
+
if (texelsWide < 1 || texelsWide > LineGridWidthMask + 1)
|
|
53
|
+
throw new Error(`Logical pixel width ${texelsWide} cannot be described in a line grid descriptor`);
|
|
54
|
+
return LineGridRendered | (texelsWide - 1) | (doubledLines ? LineGridVerticalDouble : 0);
|
|
55
|
+
}
|