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/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"name": "jsbeeb",
|
|
8
8
|
"description": "Emulate a BBC Micro",
|
|
9
9
|
"repository": "git@github.com:mattgodbolt/jsbeeb.git",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.16.0",
|
|
11
11
|
"//engines": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=24.15.0"
|
|
@@ -54,10 +54,6 @@
|
|
|
54
54
|
"electron-builder": "^26.15.3",
|
|
55
55
|
"electron-store": "^11.0.2"
|
|
56
56
|
},
|
|
57
|
-
"//overrides": "brace-expansion forced to the patched version for GHSA-mh99-v99m-4gvg; the electron-builder dependency chain still pins vulnerable versions. Remove once `npm audit` is clean without it.",
|
|
58
|
-
"overrides": {
|
|
59
|
-
"brace-expansion": "^5.0.8"
|
|
60
|
-
},
|
|
61
57
|
"license": "GPL-3.0-or-later",
|
|
62
58
|
"build": {
|
|
63
59
|
"appId": "org.godbolt.bbc",
|
|
@@ -99,15 +95,21 @@
|
|
|
99
95
|
"test:unit": "vitest run --silent=true tests/unit",
|
|
100
96
|
"test:integration": "vitest run --silent=true tests/integration",
|
|
101
97
|
"test:cpu": "node tests/test-suite.js",
|
|
98
|
+
"test:shader": "vitest run --silent=true tests/shader",
|
|
102
99
|
"coverage:unit": "vitest run tests/unit --coverage",
|
|
103
100
|
"coverage:all-tests": "vitest run --coverage",
|
|
104
101
|
"benchmark": "node src/app-bench.js",
|
|
102
|
+
"mirror-sth": "node tools/mirror-sth.js --out .sth-mirror",
|
|
103
|
+
"mirror-sth:check": "node tools/mirror-sth.js --index-only",
|
|
104
|
+
"mirror-sth:upload": "npm-run-all mirror-sth:upload:*",
|
|
105
|
+
"mirror-sth:upload:blobs": "aws s3 sync .sth-mirror s3://bbc.xania.org/archive/sth/ --no-progress --exclude '*manifest.json' --exclude 'meta/*' --cache-control 'public, max-age=31536000, immutable'",
|
|
106
|
+
"mirror-sth:upload:index": "aws s3 sync .sth-mirror s3://bbc.xania.org/archive/sth/ --no-progress --exclude '*' --include '*manifest.json' --include 'meta/*' --cache-control 'public, max-age=300'",
|
|
105
107
|
"electron": "npm run build && ELECTRON_DISABLE_SANDBOX=1 electron .",
|
|
106
108
|
"electron:build": "electron-builder"
|
|
107
109
|
},
|
|
108
110
|
"lint-staged": {
|
|
109
111
|
"*.js": [
|
|
110
|
-
"vitest related --run --exclude tests/integration --no-file-parallelism",
|
|
112
|
+
"vitest related --run --exclude tests/integration --exclude tests/shader --no-file-parallelism",
|
|
111
113
|
"eslint --cache --fix",
|
|
112
114
|
"prettier --write"
|
|
113
115
|
],
|
package/src/6502.js
CHANGED
|
@@ -213,6 +213,9 @@ class Base6502 {
|
|
|
213
213
|
if (this._nmiLevel && !prevLevel) this._nmiEdge = true;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
/** Called as the NMI vector is taken, for devices that withdraw their request at that point. */
|
|
217
|
+
nmiAcknowledged() {}
|
|
218
|
+
|
|
216
219
|
polltime() {
|
|
217
220
|
throw new Error("Must be overridden");
|
|
218
221
|
}
|
|
@@ -239,6 +242,7 @@ class Base6502 {
|
|
|
239
242
|
if ((this.model.nmos || isIrq) && this._nmiEdge) {
|
|
240
243
|
vector = 0xfffa;
|
|
241
244
|
this._nmiEdge = false;
|
|
245
|
+
this.nmiAcknowledged();
|
|
242
246
|
}
|
|
243
247
|
this.takeInt = false;
|
|
244
248
|
this.pc = this.readmem(vector) | (this.readmem(vector + 1) << 8);
|
|
@@ -451,6 +455,10 @@ class Tube6502 extends Base6502 {
|
|
|
451
455
|
return this.memory[offset & 0xffff];
|
|
452
456
|
}
|
|
453
457
|
|
|
458
|
+
nmiAcknowledged() {
|
|
459
|
+
this.tube.acknowledgeNmi();
|
|
460
|
+
}
|
|
461
|
+
|
|
454
462
|
writemem(addr, b) {
|
|
455
463
|
if ((addr & 0xfff8) === 0xfef8) {
|
|
456
464
|
return this.tube.parasiteWrite(addr, b);
|
|
@@ -666,7 +674,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
666
674
|
this.resetLine = true;
|
|
667
675
|
this.cpuMultiplier = this.config.cpuMultiplier;
|
|
668
676
|
this.videoCyclesBatch = this.config.videoCyclesBatch | 0;
|
|
669
|
-
this.peripheralCyclesPerSecond =
|
|
677
|
+
this.peripheralCyclesPerSecond = model.cyclesPerSecond;
|
|
670
678
|
this.hasTube = !!this.config.tube;
|
|
671
679
|
this.hasMusic5000 = !!this.config.hasMusic5000;
|
|
672
680
|
this.hasTeletextAdaptor = !!this.config.hasTeletextAdaptor;
|
|
@@ -1361,7 +1369,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
1361
1369
|
this.fdc.powerOnReset();
|
|
1362
1370
|
this.adconverter.reset();
|
|
1363
1371
|
|
|
1364
|
-
this.touchScreen = new TouchScreen(this.scheduler);
|
|
1372
|
+
this.touchScreen = new TouchScreen(this.scheduler, this.model.cyclesPerSecond);
|
|
1365
1373
|
if (this.econet) this.filestore = new Filestore(this, this.econet);
|
|
1366
1374
|
}
|
|
1367
1375
|
|
|
@@ -1478,9 +1486,10 @@ export class Cpu6502 extends Base6502 {
|
|
|
1478
1486
|
// that from both, to keep the domain low (while accumulating seconds). Take care to preserve the bottom
|
|
1479
1487
|
// bit though; as that encodes whether we're on an even or odd bus cycle.
|
|
1480
1488
|
const smaller = Math.min(this.targetCycles, this.currentCycles) & 0xfffffffe;
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
this.
|
|
1489
|
+
const cyclesPerSecond = this.model.cyclesPerSecond;
|
|
1490
|
+
if (smaller >= cyclesPerSecond) {
|
|
1491
|
+
this.targetCycles -= cyclesPerSecond;
|
|
1492
|
+
this.currentCycles -= cyclesPerSecond;
|
|
1484
1493
|
this.cycleSeconds++;
|
|
1485
1494
|
}
|
|
1486
1495
|
// Any tracing or debugging means we need to run the potentially slower version: the debug read or
|
|
@@ -1614,8 +1623,6 @@ export class AtomCpu6502 extends Cpu6502 {
|
|
|
1614
1623
|
this.ramRomOs = expanded;
|
|
1615
1624
|
}
|
|
1616
1625
|
|
|
1617
|
-
// Atom runs at 1 MHz
|
|
1618
|
-
this.peripheralCyclesPerSecond = 1 * 1000 * 1000;
|
|
1619
1626
|
// reset() and debugger.setCpu() are called by initialise() after loadOs().
|
|
1620
1627
|
}
|
|
1621
1628
|
|
package/src/6847.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
import * as utils from "./utils.js";
|
|
3
3
|
import * as fontData from "./6847_fontdata.js";
|
|
4
|
+
import { encodeLineGrid } from "./video-filters/pixel-grid.js";
|
|
4
5
|
|
|
5
6
|
const VDISPENABLE = 1 << 0,
|
|
6
7
|
HDISPENABLE = 1 << 1,
|
|
@@ -111,6 +112,11 @@ only 1 bit is used of SG6 - to get yellow/red, cyan/orange
|
|
|
111
112
|
// constant for the graphics mode
|
|
112
113
|
const MODE_AG = 0x10; // graphics mode
|
|
113
114
|
|
|
115
|
+
// The MC6847 datasheet specifies 3.579545 MHz, but 3.638004 is used here as an
|
|
116
|
+
// empirical correction to align VSync/interrupt timing with the CPU clock.
|
|
117
|
+
// TODO: revisit once full integration is testable.
|
|
118
|
+
const VdgClockMhz = 3.638004;
|
|
119
|
+
|
|
114
120
|
export class Video6847 {
|
|
115
121
|
constructor(video) {
|
|
116
122
|
this.video = video;
|
|
@@ -175,6 +181,7 @@ export class Video6847 {
|
|
|
175
181
|
this.bitmapPxPerPixel = 2; // each pixel is 2 bitmap pixels wide and high
|
|
176
182
|
this.pixelsPerBit = this.bitmapPxPerPixel;
|
|
177
183
|
this.bpp = 1;
|
|
184
|
+
this.updateLineGrid();
|
|
178
185
|
|
|
179
186
|
this.cpuAddr = 0;
|
|
180
187
|
this.dispEnabled = 0;
|
|
@@ -225,6 +232,9 @@ export class Video6847 {
|
|
|
225
232
|
reset(cpu, ppia) {
|
|
226
233
|
this.cpu = cpu;
|
|
227
234
|
this.ppia = ppia;
|
|
235
|
+
// polltime() is handed CPU cycles, so how far the VDG advances per
|
|
236
|
+
// cycle depends on how fast the CPU it's attached to runs.
|
|
237
|
+
this.vdgCyclesPerCpuCycle = cpu ? VdgClockMhz / cpu.model.clockMhz : 0;
|
|
228
238
|
}
|
|
229
239
|
|
|
230
240
|
// USE PAINT from VIDEO
|
|
@@ -315,6 +325,7 @@ export class Video6847 {
|
|
|
315
325
|
this.pixelsPerBit = this.bitmapPxPerPixel * this.modes[mode][1];
|
|
316
326
|
let linesPerRow = this.modes[mode][2]; // move to reg9
|
|
317
327
|
this.bpp = this.modes[mode][3];
|
|
328
|
+
this.updateLineGrid();
|
|
318
329
|
|
|
319
330
|
this.charLinesreg9 = linesPerRow - 1; //2 - scanlines per char
|
|
320
331
|
|
|
@@ -344,11 +355,7 @@ export class Video6847 {
|
|
|
344
355
|
// it doesn't draw the whole frame. It regularly gives control back to the CPU.
|
|
345
356
|
|
|
346
357
|
const vdgcharclock = this.pixelsPerChar / 2; // 4 or 8
|
|
347
|
-
|
|
348
|
-
// as an empirical correction to align VSync/interrupt timing with the Atom's
|
|
349
|
-
// 1 MHz CPU clock. TODO: revisit once full integration is testable.
|
|
350
|
-
const vdgclock = 3.638004;
|
|
351
|
-
this.vdg_cycles += clocks * vdgclock;
|
|
358
|
+
this.vdg_cycles += clocks * this.vdgCyclesPerCpuCycle;
|
|
352
359
|
|
|
353
360
|
const vdgframelines = 262; // 312 PAL (but no pal on standard atom) 262; // NTSC
|
|
354
361
|
const vdglinetime = 228; // vdg cycles to do a line; not 227.5
|
|
@@ -491,9 +498,9 @@ export class Video6847 {
|
|
|
491
498
|
{
|
|
492
499
|
// TODO: Add in the INTEXT modifiers to mode (if necessary)
|
|
493
500
|
// blit into the fb32 buffer which is painted by VIDEO
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
501
|
+
const textMode = (mode & MODE_AG) === 0; // 0x10 is the AG bit
|
|
502
|
+
this.recordLineGrid(textMode);
|
|
503
|
+
if (textMode) this.blitChar(this.video.fb32, dat, offset, this.pixelsPerChar, css);
|
|
497
504
|
else this.blitPixels(this.video.fb32, dat, offset, css);
|
|
498
505
|
}
|
|
499
506
|
}
|
|
@@ -619,6 +626,41 @@ export class Video6847 {
|
|
|
619
626
|
}
|
|
620
627
|
}
|
|
621
628
|
|
|
629
|
+
/**
|
|
630
|
+
* Precompute this mode's grid descriptors, one per blitter.
|
|
631
|
+
*
|
|
632
|
+
* The two blitters derive their widths differently and cannot be unified:
|
|
633
|
+
* `blitChar` splits the character's texels across eight glyph bits, while
|
|
634
|
+
* `blitPixels` takes the bit width from the mode table — which for text
|
|
635
|
+
* mode holds -1, since it does not blit pixels at all.
|
|
636
|
+
*
|
|
637
|
+
* `pixelsPerBit` is right for graphics at 2bpp as well as 1bpp: the blitter
|
|
638
|
+
* steps its bit groups by `pixelsPerBit / bpp`, but reads the colour with
|
|
639
|
+
* `j & 0xe`, so pairs of groups share one and each pixel is twice as wide.
|
|
640
|
+
*/
|
|
641
|
+
updateLineGrid() {
|
|
642
|
+
// Every 6847 blitter writes each pixel row into two framebuffer lines.
|
|
643
|
+
// Text mode's entry is only reachable through blitChar, which is just
|
|
644
|
+
// as well: the mode table holds -1 for it and would not encode.
|
|
645
|
+
this.lineGridText = encodeLineGrid((this.pixelsPerChar * this.bitmapPxPerPixel) / 8, true);
|
|
646
|
+
this.lineGridGraphics = this.pixelsPerBit > 0 ? encodeLineGrid(this.pixelsPerBit, true) : this.lineGridText;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Note the logical pixel size of the two rows about to be written, so
|
|
651
|
+
* display filters can see the picture as pixels rather than as raster
|
|
652
|
+
* samples (see video-filters/pixel-grid.js). Only the picture records a
|
|
653
|
+
* grid: the border is a solid colour, and letting it write here would
|
|
654
|
+
* leave the row describing the border rather than the picture on it.
|
|
655
|
+
*
|
|
656
|
+
* @param {boolean} textMode whether the character blitter is about to run
|
|
657
|
+
*/
|
|
658
|
+
recordLineGrid(textMode) {
|
|
659
|
+
const grid = textMode ? this.lineGridText : this.lineGridGraphics;
|
|
660
|
+
this.video.lineGrid[this.bitmapY] = grid;
|
|
661
|
+
this.video.lineGrid[this.bitmapY + 1] = grid;
|
|
662
|
+
}
|
|
663
|
+
|
|
622
664
|
blitChar(buf, data, destOffset, numPixels, css) {
|
|
623
665
|
const scanline = this.scanlineCounter;
|
|
624
666
|
const chr = data & 0x7f;
|
package/src/acia.js
CHANGED
|
@@ -265,8 +265,9 @@ export class Acia {
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
// Byte times are held in CPU cycles because that's what the scheduler counts.
|
|
268
269
|
secondsToCycles(sec) {
|
|
269
|
-
return Math.floor(
|
|
270
|
+
return Math.floor(this.cpu.model.cyclesPerSecond * sec) | 0;
|
|
270
271
|
}
|
|
271
272
|
|
|
272
273
|
numBitsPerByte() {
|
package/src/canvas.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
import webglDebug from "./lib/webgl-debug.js";
|
|
3
3
|
import { PALCompositeFilter } from "./video-filters/pal-composite.js";
|
|
4
4
|
import { PassthroughFilter } from "./video-filters/passthrough-filter.js";
|
|
5
|
+
import { XbrFilter } from "./video-filters/xbr-filter.js";
|
|
5
6
|
|
|
6
7
|
const DISPLAY_MODE_FILTERS = {
|
|
7
8
|
pal: PALCompositeFilter,
|
|
8
9
|
rgb: PassthroughFilter,
|
|
10
|
+
xbr: XbrFilter,
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
export function getFilterForMode(mode) {
|
|
@@ -13,8 +15,9 @@ export function getFilterForMode(mode) {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export class Canvas {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
/** The 2D canvas draws the framebuffer as-is, which is what this filter is. */
|
|
19
|
+
get filterClass() {
|
|
20
|
+
return PassthroughFilter;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
constructor(canvas) {
|
|
@@ -27,24 +30,29 @@ export class Canvas {
|
|
|
27
30
|
this.backBuffer.height = 625;
|
|
28
31
|
this.backCtx = this.backBuffer.getContext("2d", { alpha: false });
|
|
29
32
|
this.imageData = this.backCtx.createImageData(this.backBuffer.width, this.backBuffer.height);
|
|
30
|
-
this.
|
|
31
|
-
this.canvasHeight = canvas.height;
|
|
33
|
+
this.canvas = canvas;
|
|
32
34
|
|
|
33
35
|
this.fb32 = new Uint32Array(this.imageData.data.buffer);
|
|
34
36
|
}
|
|
35
|
-
|
|
37
|
+
|
|
38
|
+
/** Nothing to release: the 2D context owns no objects of ours. */
|
|
39
|
+
dispose() {}
|
|
40
|
+
|
|
41
|
+
paint(minx, miny, maxx, maxy, _frame) {
|
|
36
42
|
const width = maxx - minx;
|
|
37
43
|
const height = maxy - miny;
|
|
38
44
|
this.backCtx.putImageData(this.imageData, 0, 0, minx, miny, width, height);
|
|
39
|
-
|
|
45
|
+
// Read the size each time: it can change when the window is resized.
|
|
46
|
+
this.ctx.drawImage(this.backBuffer, minx, miny, width, height, 0, 0, this.canvas.width, this.canvas.height);
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
const width = 1024;
|
|
44
51
|
const height = 1024;
|
|
45
52
|
export class GlCanvas {
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
/** The filter actually built, which may not be the one that was asked for. */
|
|
54
|
+
get filterClass() {
|
|
55
|
+
return this.filter.constructor;
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
constructor(canvas, filterClass) {
|
|
@@ -74,6 +82,10 @@ export class GlCanvas {
|
|
|
74
82
|
const program = this.filter.program;
|
|
75
83
|
checkedGl.useProgram(program);
|
|
76
84
|
|
|
85
|
+
// Filters that pick their own samples want the texels they asked for,
|
|
86
|
+
// not a hardware blend of the ones either side.
|
|
87
|
+
const sampling = filterClass.getDisplayConfig().nearestSampling ? checkedGl.NEAREST : checkedGl.LINEAR;
|
|
88
|
+
|
|
77
89
|
this.fb8 = new Uint8Array(width * height * 4);
|
|
78
90
|
this.fb32 = new Uint32Array(this.fb8.buffer);
|
|
79
91
|
this.texture = checkedGl.createTexture();
|
|
@@ -81,8 +93,8 @@ export class GlCanvas {
|
|
|
81
93
|
checkedGl.pixelStorei(checkedGl.UNPACK_ALIGNMENT, 4);
|
|
82
94
|
checkedGl.texParameteri(checkedGl.TEXTURE_2D, checkedGl.TEXTURE_WRAP_S, checkedGl.CLAMP_TO_EDGE);
|
|
83
95
|
checkedGl.texParameteri(checkedGl.TEXTURE_2D, checkedGl.TEXTURE_WRAP_T, checkedGl.CLAMP_TO_EDGE);
|
|
84
|
-
checkedGl.texParameteri(checkedGl.TEXTURE_2D, checkedGl.TEXTURE_MAG_FILTER,
|
|
85
|
-
checkedGl.texParameteri(checkedGl.TEXTURE_2D, checkedGl.TEXTURE_MIN_FILTER,
|
|
96
|
+
checkedGl.texParameteri(checkedGl.TEXTURE_2D, checkedGl.TEXTURE_MAG_FILTER, sampling);
|
|
97
|
+
checkedGl.texParameteri(checkedGl.TEXTURE_2D, checkedGl.TEXTURE_MIN_FILTER, sampling);
|
|
86
98
|
checkedGl.texImage2D(
|
|
87
99
|
checkedGl.TEXTURE_2D,
|
|
88
100
|
0,
|
|
@@ -98,8 +110,8 @@ export class GlCanvas {
|
|
|
98
110
|
|
|
99
111
|
const vertexPositionAttrLoc = checkedGl.getAttribLocation(program, "pos");
|
|
100
112
|
checkedGl.enableVertexAttribArray(vertexPositionAttrLoc);
|
|
101
|
-
|
|
102
|
-
checkedGl.bindBuffer(checkedGl.ARRAY_BUFFER, vertexPositionBuffer);
|
|
113
|
+
this.vertexPositionBuffer = checkedGl.createBuffer();
|
|
114
|
+
checkedGl.bindBuffer(checkedGl.ARRAY_BUFFER, this.vertexPositionBuffer);
|
|
103
115
|
checkedGl.bufferData(checkedGl.ARRAY_BUFFER, new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]), checkedGl.STATIC_DRAW);
|
|
104
116
|
checkedGl.vertexAttribPointer(vertexPositionAttrLoc, 2, checkedGl.FLOAT, false, 0, 0);
|
|
105
117
|
|
|
@@ -112,14 +124,42 @@ export class GlCanvas {
|
|
|
112
124
|
checkedGl.activeTexture(gl.TEXTURE0);
|
|
113
125
|
checkedGl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
114
126
|
|
|
127
|
+
this.checkedGl = checkedGl;
|
|
128
|
+
this.viewportWidth = this.viewportHeight = 0;
|
|
115
129
|
this.uvFloatArray = new Float32Array(8);
|
|
116
130
|
this.lastExtent = {};
|
|
117
131
|
|
|
118
132
|
console.log("GL Canvas set up");
|
|
119
133
|
}
|
|
120
134
|
|
|
121
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Release the GL objects this canvas owns.
|
|
137
|
+
*
|
|
138
|
+
* Switching display mode builds a new canvas over the same element, and a
|
|
139
|
+
* canvas only ever hands out one WebGL context — so the new one inherits
|
|
140
|
+
* the old one's context and the old one's objects stay resident unless
|
|
141
|
+
* they are deleted here. That is a megabytes-per-switch leak: the
|
|
142
|
+
* framebuffer texture alone is 1024x1024 RGBA.
|
|
143
|
+
*/
|
|
144
|
+
dispose() {
|
|
145
|
+
const gl = this.checkedGl;
|
|
146
|
+
this.filter.dispose();
|
|
147
|
+
gl.deleteTexture(this.texture);
|
|
148
|
+
gl.deleteBuffer(this.vertexPositionBuffer);
|
|
149
|
+
gl.deleteBuffer(this.uvBuffer);
|
|
150
|
+
this.texture = this.vertexPositionBuffer = this.uvBuffer = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
paint(minx, miny, maxx, maxy, frame) {
|
|
122
154
|
const gl = this.gl;
|
|
155
|
+
// The drawing buffer can be resized under us — modes that scale to the
|
|
156
|
+
// display do it on every window resize — and the viewport does not
|
|
157
|
+
// follow it.
|
|
158
|
+
if (gl.drawingBufferWidth !== this.viewportWidth || gl.drawingBufferHeight !== this.viewportHeight) {
|
|
159
|
+
this.viewportWidth = gl.drawingBufferWidth;
|
|
160
|
+
this.viewportHeight = gl.drawingBufferHeight;
|
|
161
|
+
gl.viewport(0, 0, this.viewportWidth, this.viewportHeight);
|
|
162
|
+
}
|
|
123
163
|
// We can't specify a stride for the source, so have to use the full width.
|
|
124
164
|
gl.texSubImage2D(
|
|
125
165
|
gl.TEXTURE_2D,
|
|
@@ -157,7 +197,17 @@ export class GlCanvas {
|
|
|
157
197
|
gl.bufferData(gl.ARRAY_BUFFER, this.uvFloatArray, gl.DYNAMIC_DRAW);
|
|
158
198
|
}
|
|
159
199
|
|
|
160
|
-
this.filter.setUniforms({
|
|
200
|
+
this.filter.setUniforms({
|
|
201
|
+
width,
|
|
202
|
+
height,
|
|
203
|
+
frameCount: frame.frameCount,
|
|
204
|
+
lineGrid: frame.lineGrid,
|
|
205
|
+
// How much of the framebuffer each output pixel covers, which sets
|
|
206
|
+
// how wide an edge-smoothing ramp should be. `extent` holds texel
|
|
207
|
+
// counts; the scaling into texture coordinates above applies only
|
|
208
|
+
// to the local copies that go into the UV buffer.
|
|
209
|
+
texelsPerOutputPixel: (extent.maxx - extent.minx) / gl.drawingBufferWidth,
|
|
210
|
+
});
|
|
161
211
|
|
|
162
212
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
163
213
|
}
|
|
@@ -167,11 +217,20 @@ export function bestCanvas(canvas, filterClass) {
|
|
|
167
217
|
try {
|
|
168
218
|
return new GlCanvas(canvas, filterClass);
|
|
169
219
|
} catch (e) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
220
|
+
// Either WebGL is unavailable or this particular filter declined it.
|
|
221
|
+
console.log(`Unable to use ${filterClass.getDisplayConfig().name} with WebGL: ${e}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// A canvas that has handed out a WebGL context can never hand out a 2D one,
|
|
225
|
+
// so if the failure came from the filter rather than from WebGL itself, the
|
|
226
|
+
// 2D fallback below would throw and take the emulator with it.
|
|
227
|
+
if (filterClass !== PassthroughFilter) {
|
|
228
|
+
try {
|
|
229
|
+
return new GlCanvas(canvas, PassthroughFilter);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
console.log("Unable to fall back to the passthrough filter: " + e);
|
|
174
232
|
}
|
|
175
233
|
}
|
|
234
|
+
|
|
176
235
|
return new Canvas(canvas);
|
|
177
236
|
}
|
package/src/disc-drive.js
CHANGED
|
@@ -33,6 +33,11 @@ export class BaseDiscDrive extends EventTarget {
|
|
|
33
33
|
throw new Error("Not implemented: headPosition getter");
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** @returns {boolean} */
|
|
37
|
+
get isSideUpper() {
|
|
38
|
+
throw new Error("Not implemented: isSideUpper getter");
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
/** @returns {boolean} */
|
|
37
42
|
get indexPulse() {
|
|
38
43
|
throw new Error("Not implemented: indexPulse getter");
|
|
@@ -240,6 +245,10 @@ export class DiscDrive extends BaseDiscDrive {
|
|
|
240
245
|
return this._track;
|
|
241
246
|
}
|
|
242
247
|
|
|
248
|
+
get isSideUpper() {
|
|
249
|
+
return this._isSideUpper;
|
|
250
|
+
}
|
|
251
|
+
|
|
243
252
|
get positionFraction() {
|
|
244
253
|
return (this._headPosition + this._pulsePosition / 32) / this.trackLength;
|
|
245
254
|
}
|
package/src/disc-hfe.js
CHANGED
|
@@ -190,9 +190,8 @@ export function loadHfe(disc, data, onChange) {
|
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
// Set up write track callback if onChange is provided
|
|
194
193
|
if (onChange) {
|
|
195
|
-
disc.
|
|
194
|
+
disc.addTrackWriteListener((_side, _trackNum, _trackObj) => {
|
|
196
195
|
// Generate a complete HFE image from the current disc state
|
|
197
196
|
const hfeData = toHfe(disc);
|
|
198
197
|
// Call the onChange handler with the updated HFE data
|