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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compile and link a shader program, throwing with the driver's own message if
|
|
5
|
+
* either step fails.
|
|
6
|
+
*
|
|
7
|
+
* @param {WebGLRenderingContext} gl
|
|
8
|
+
* @param {string} vertexSource
|
|
9
|
+
* @param {string} fragmentSource
|
|
10
|
+
* @param {string} name used in error messages, e.g. "xBR"
|
|
11
|
+
* @returns {WebGLProgram}
|
|
12
|
+
*/
|
|
13
|
+
export function compileProgram(gl, vertexSource, fragmentSource, name) {
|
|
14
|
+
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexSource, name);
|
|
15
|
+
let fragmentShader = null;
|
|
16
|
+
try {
|
|
17
|
+
fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource, name);
|
|
18
|
+
|
|
19
|
+
const program = gl.createProgram();
|
|
20
|
+
gl.attachShader(program, vertexShader);
|
|
21
|
+
gl.attachShader(program, fragmentShader);
|
|
22
|
+
gl.linkProgram(program);
|
|
23
|
+
|
|
24
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
25
|
+
const info = gl.getProgramInfoLog(program);
|
|
26
|
+
gl.deleteProgram(program);
|
|
27
|
+
throw new Error(`Failed to link ${name} shader program: ${info}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return program;
|
|
31
|
+
} finally {
|
|
32
|
+
// Once linked the program holds its own reference, so releasing ours here means
|
|
33
|
+
// deleting the program later frees the shaders too. If we never got that far, ours
|
|
34
|
+
// was the only reference and they go now.
|
|
35
|
+
gl.deleteShader(vertexShader);
|
|
36
|
+
gl.deleteShader(fragmentShader);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compileShader(gl, type, source, name) {
|
|
41
|
+
const shader = gl.createShader(type);
|
|
42
|
+
gl.shaderSource(shader, source);
|
|
43
|
+
gl.compileShader(shader);
|
|
44
|
+
|
|
45
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
46
|
+
const info = gl.getShaderInfoLog(shader);
|
|
47
|
+
const typeName = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
|
|
48
|
+
gl.deleteShader(shader);
|
|
49
|
+
throw new Error(`Failed to compile ${name} ${typeName} shader: ${info}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return shader;
|
|
53
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// xBR-lv2 edge-directed upscaling, applied to the BBC's *logical* pixels.
|
|
2
|
+
//
|
|
3
|
+
// Ported from Hyllian's xbr-lv2-standalone.slang (MIT, Copyright (C) 2011-2022
|
|
4
|
+
// Hyllian <sergiogdb@gmail.com>); see that for the original. tests/shader runs
|
|
5
|
+
// this file in headless Chrome and asserts on the pixels it produces.
|
|
6
|
+
//
|
|
7
|
+
// The one thing this does that a stock xBR shader does not: jsbeeb's
|
|
8
|
+
// framebuffer is a 1024-wide raster in which one BBC pixel spans up to eight
|
|
9
|
+
// texels horizontally and two vertically. Sampling raw texel neighbours would
|
|
10
|
+
// find nine copies of the same pixel and do nothing at all, so every sample
|
|
11
|
+
// here is taken on the logical grid, whose size for each row comes from the
|
|
12
|
+
// lineGrid texture that Video fills in (see video-filters/pixel-grid.js).
|
|
13
|
+
|
|
14
|
+
// Texel coordinates run to 1024 and are floored to find the pixel grid, which
|
|
15
|
+
// mediump's ~10 bits of mantissa cannot resolve — the grid would land on the
|
|
16
|
+
// wrong texel and the picture would break up. XbrFilter refuses to build on
|
|
17
|
+
// hardware without high precision, so this shader never runs at mediump; the
|
|
18
|
+
// fallback is here only so it still compiles if something else includes it.
|
|
19
|
+
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
|
20
|
+
precision highp float;
|
|
21
|
+
#else
|
|
22
|
+
precision mediump float;
|
|
23
|
+
#endif
|
|
24
|
+
|
|
25
|
+
uniform sampler2D tex; // the framebuffer
|
|
26
|
+
uniform sampler2D lineGrid; // one descriptor byte per framebuffer row
|
|
27
|
+
uniform vec2 uTextureSize; // framebuffer texture size, in texels
|
|
28
|
+
uniform vec2 uTexelSize; // 1.0 / uTextureSize
|
|
29
|
+
uniform float uTexelsPerOutputPixel; // horizontal, for the antialiasing ramp
|
|
30
|
+
|
|
31
|
+
varying vec2 uv;
|
|
32
|
+
|
|
33
|
+
// Colour distance below which two pixels count as "equal".
|
|
34
|
+
const float EqThreshold = 0.32;
|
|
35
|
+
// Used as the threshold of a step(), so larger values make the shallow 30/60
|
|
36
|
+
// degree rules harder to satisfy and fewer shallow edges get smoothed.
|
|
37
|
+
const float Lv2Coefficient = 0.3;
|
|
38
|
+
const float Lv2Cf = Lv2Coefficient + 2.0;
|
|
39
|
+
|
|
40
|
+
// Coefficients of the straight lines that bound each interpolation region.
|
|
41
|
+
const vec4 Ao = vec4(1.0, -1.0, -1.0, 1.0);
|
|
42
|
+
const vec4 Bo = vec4(1.0, 1.0, -1.0, -1.0);
|
|
43
|
+
const vec4 Co = vec4(1.5, 0.5, -0.5, 0.5);
|
|
44
|
+
const vec4 Ax = vec4(1.0, -1.0, -1.0, 1.0);
|
|
45
|
+
const vec4 Bx = vec4(0.5, 2.0, -0.5, -2.0);
|
|
46
|
+
const vec4 Cx = vec4(1.0, 1.0, -0.5, 0.0);
|
|
47
|
+
const vec4 Ay = vec4(1.0, -1.0, -1.0, 1.0);
|
|
48
|
+
const vec4 By = vec4(2.0, 0.5, -2.0, -0.5);
|
|
49
|
+
const vec4 Cy = vec4(2.0, 0.0, -1.0, 0.5);
|
|
50
|
+
const vec4 Ci = vec4(0.25, 0.25, 0.25, 0.25);
|
|
51
|
+
|
|
52
|
+
// BT.2020 luma weights, used to weight the per-channel colour distance.
|
|
53
|
+
const vec3 Y = vec3(0.2627, 0.678, 0.0593);
|
|
54
|
+
|
|
55
|
+
// Line grid descriptor bits; see video-filters/pixel-grid.js.
|
|
56
|
+
const float GridRendered = 128.0;
|
|
57
|
+
const float GridVerticalDouble = 8.0;
|
|
58
|
+
|
|
59
|
+
// Four colours held channel-wise, standing in for the reference shader's
|
|
60
|
+
// mat4x3. The four lanes are the same rule applied to the four rotations of
|
|
61
|
+
// the neighbourhood, so nearly every term is just a relabelling.
|
|
62
|
+
struct Lane4 {
|
|
63
|
+
vec4 r;
|
|
64
|
+
vec4 g;
|
|
65
|
+
vec4 b;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
Lane4 lane(vec3 x, vec3 y, vec3 z, vec3 w) {
|
|
69
|
+
return Lane4(vec4(x.r, y.r, z.r, w.r), vec4(x.g, y.g, z.g, w.g), vec4(x.b, y.b, z.b, w.b));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Perceptually weighted colour distance, per lane. */
|
|
73
|
+
vec4 dist4(Lane4 a, Lane4 b) {
|
|
74
|
+
return abs(a.r - b.r) * Y.r + abs(a.g - b.g) * Y.g + abs(a.b - b.b) * Y.b;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
vec4 eq4(Lane4 a, Lane4 b) {
|
|
78
|
+
return step(dist4(a, b), vec4(EqThreshold));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
vec4 neq4(Lane4 a, Lane4 b) {
|
|
82
|
+
return vec4(1.0) - eq4(a, b);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Distance among pixels in some direction; the 4x term dominates. */
|
|
86
|
+
vec4 weightedDistance(Lane4 a, Lane4 b, Lane4 c, Lane4 d, Lane4 e, Lane4 f, Lane4 g, Lane4 h) {
|
|
87
|
+
return dist4(a, b) + dist4(a, c) + dist4(d, e) + dist4(d, f) + 4.0 * dist4(g, h);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pack a colour so exact equality is one comparison rather than three, matching
|
|
92
|
+
* the reference shader's `v2f` trick.
|
|
93
|
+
*/
|
|
94
|
+
vec4 pack4(Lane4 a) {
|
|
95
|
+
return a.r * 65536.0 + a.g * 256.0 + a.b;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
float pack(vec3 c) {
|
|
99
|
+
return c.r * 65536.0 + c.g * 256.0 + c.b;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Exact (not thresholded) inequality. */
|
|
103
|
+
vec4 diff4(vec4 a, vec4 b) {
|
|
104
|
+
return vec4(notEqual(a, b));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
float colourDistance(vec3 a, vec3 b) {
|
|
108
|
+
return dot(abs(a - b), Y);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Pick whichever candidate is further from the centre pixel. */
|
|
112
|
+
vec3 furtherFrom(vec3 e, vec3 res1, vec3 res2) {
|
|
113
|
+
return mix(res1, res2, step(colourDistance(e, res1), colourDistance(e, res2)));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
void main() {
|
|
117
|
+
// Absolute texel coordinates within the framebuffer.
|
|
118
|
+
vec2 fbCoord = uv * uTextureSize;
|
|
119
|
+
|
|
120
|
+
// This row's logical pixel size. The descriptor is a byte, so recover it by
|
|
121
|
+
// scaling and rounding rather than trusting the sampled float directly.
|
|
122
|
+
float row = floor(fbCoord.y);
|
|
123
|
+
float descriptor = floor(texture2D(lineGrid, vec2((row + 0.5) * uTexelSize.x, 0.5)).r * 255.0 + 0.5);
|
|
124
|
+
descriptor -= step(GridRendered, descriptor) * GridRendered;
|
|
125
|
+
float verticalDouble = step(GridVerticalDouble, descriptor);
|
|
126
|
+
descriptor -= verticalDouble * GridVerticalDouble;
|
|
127
|
+
// What is left is the pixel's width in texels, less one.
|
|
128
|
+
vec2 pixelSize = vec2(descriptor + 1.0, 1.0 + verticalDouble);
|
|
129
|
+
|
|
130
|
+
// Everything below works in logical pixels, as a stock xBR shader would.
|
|
131
|
+
vec2 logical = fbCoord / pixelSize;
|
|
132
|
+
vec2 centre = floor(logical);
|
|
133
|
+
vec2 fp = logical - centre;
|
|
134
|
+
|
|
135
|
+
// Sample the centre of the first texel of each logical pixel. Every texel
|
|
136
|
+
// within a logical pixel holds the same colour — the ULA writes them from
|
|
137
|
+
// one table entry — so which one is arbitrary; the first avoids landing on
|
|
138
|
+
// a boundary and rounding the wrong way.
|
|
139
|
+
vec2 base = centre * pixelSize + 0.5;
|
|
140
|
+
#define AT(dx, dy) texture2D(tex, (base + vec2(dx, dy) * pixelSize) * uTexelSize).rgb
|
|
141
|
+
|
|
142
|
+
// A1 B1 C1
|
|
143
|
+
// A0 A B C C4
|
|
144
|
+
// D0 D E F F4
|
|
145
|
+
// G0 G H I I4
|
|
146
|
+
// G5 H5 I5
|
|
147
|
+
vec3 E = AT(0.0, 0.0);
|
|
148
|
+
vec3 B = AT(0.0, -1.0);
|
|
149
|
+
vec3 D = AT(-1.0, 0.0);
|
|
150
|
+
vec3 F = AT(1.0, 0.0);
|
|
151
|
+
vec3 H = AT(0.0, 1.0);
|
|
152
|
+
|
|
153
|
+
// Level 0: the centre differs from both its right and lower neighbours,
|
|
154
|
+
// i.e. there is an edge here at all. Every rule below is gated on it, so
|
|
155
|
+
// where it is zero in all four rotations the answer is exactly E — and
|
|
156
|
+
// most of a BBC screen is flat colour. Taking that exit early skips
|
|
157
|
+
// sixteen texture fetches and the whole of the algorithm.
|
|
158
|
+
//
|
|
159
|
+
// Correctness rests on this being the same irlv0 the full path computes.
|
|
160
|
+
// tests/shader asserts what the shader is for — flat areas untouched, hard
|
|
161
|
+
// edges left hard, diagonals smoothed — so a shortcut that took this exit
|
|
162
|
+
// where the algorithm would have blended shows up there.
|
|
163
|
+
//
|
|
164
|
+
// Returning here also puts every texture2D below inside non-uniform
|
|
165
|
+
// control flow, which GLSL ES 1.0 section 8.7 leaves undefined for lookups
|
|
166
|
+
// needing implicit derivatives. It is defined here only because there is
|
|
167
|
+
// no level of detail to choose: XbrFilter asks for `nearestSampling`, so
|
|
168
|
+
// canvas.js gives this texture NEAREST for both filters, and it has no
|
|
169
|
+
// mipmaps. Move this filter back to LINEAR, or give the texture mipmaps,
|
|
170
|
+
// and this shortcut stops being merely faster and starts being undefined.
|
|
171
|
+
vec4 eP = vec4(pack(E));
|
|
172
|
+
vec4 fP = vec4(pack(F), pack(B), pack(D), pack(H));
|
|
173
|
+
vec4 hP = fP.wxyz;
|
|
174
|
+
vec4 irlv0 = vec4(notEqual(eP, fP)) * vec4(notEqual(eP, hP));
|
|
175
|
+
if (all(equal(irlv0, vec4(0.0)))) {
|
|
176
|
+
gl_FragColor = vec4(E, 1.0);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
vec3 A1 = AT(-1.0, -2.0);
|
|
181
|
+
vec3 B1 = AT(0.0, -2.0);
|
|
182
|
+
vec3 C1 = AT(1.0, -2.0);
|
|
183
|
+
vec3 A = AT(-1.0, -1.0);
|
|
184
|
+
vec3 C = AT(1.0, -1.0);
|
|
185
|
+
vec3 G = AT(-1.0, 1.0);
|
|
186
|
+
vec3 I = AT(1.0, 1.0);
|
|
187
|
+
vec3 G5 = AT(-1.0, 2.0);
|
|
188
|
+
vec3 H5 = AT(0.0, 2.0);
|
|
189
|
+
vec3 I5 = AT(1.0, 2.0);
|
|
190
|
+
vec3 A0 = AT(-2.0, -1.0);
|
|
191
|
+
vec3 D0 = AT(-2.0, 0.0);
|
|
192
|
+
vec3 G0 = AT(-2.0, 1.0);
|
|
193
|
+
vec3 C4 = AT(2.0, -1.0);
|
|
194
|
+
vec3 F4 = AT(2.0, 0.0);
|
|
195
|
+
vec3 I4 = AT(2.0, 1.0);
|
|
196
|
+
#undef AT
|
|
197
|
+
|
|
198
|
+
Lane4 b = lane(B, D, H, F);
|
|
199
|
+
Lane4 c = lane(C, A, G, I);
|
|
200
|
+
Lane4 d = lane(D, H, F, B);
|
|
201
|
+
Lane4 e = lane(E, E, E, E);
|
|
202
|
+
Lane4 f = lane(F, B, D, H);
|
|
203
|
+
Lane4 g = lane(G, I, C, A);
|
|
204
|
+
Lane4 h = lane(H, F, B, D);
|
|
205
|
+
Lane4 i = lane(I, C, A, G);
|
|
206
|
+
Lane4 i4 = lane(I4, C1, A0, G5);
|
|
207
|
+
Lane4 i5 = lane(I5, C4, A1, G0);
|
|
208
|
+
Lane4 h5 = lane(H5, F4, B1, D0);
|
|
209
|
+
Lane4 f4 = lane(F4, B1, D0, H5);
|
|
210
|
+
|
|
211
|
+
// Packed forms, for the exact-inequality tests. `fP` and `hP` are already in
|
|
212
|
+
// hand from the early-out above, and `b` is `f` rotated, so `bP` is too —
|
|
213
|
+
// packing it again would cost eight multiplies for the same bits.
|
|
214
|
+
vec4 bP = fP.yzwx;
|
|
215
|
+
vec4 cP = pack4(c);
|
|
216
|
+
vec4 dP = bP.yzwx;
|
|
217
|
+
vec4 gP = cP.zwxy;
|
|
218
|
+
|
|
219
|
+
// These inequations define the line below which interpolation occurs.
|
|
220
|
+
vec4 fx = Ao * fp.y + Bo * fp.x;
|
|
221
|
+
vec4 fxL = Ax * fp.y + Bx * fp.x;
|
|
222
|
+
vec4 fxU = Ay * fp.y + By * fp.x;
|
|
223
|
+
|
|
224
|
+
// Corner detection variant C: also require the edge to be part of a longer
|
|
225
|
+
// run, so isolated single pixels are not rounded away.
|
|
226
|
+
vec4 eqEC = eq4(e, c);
|
|
227
|
+
vec4 eqEG = eq4(e, g);
|
|
228
|
+
vec4 irlv1 = clamp(
|
|
229
|
+
irlv0 *
|
|
230
|
+
(neq4(f, b) * neq4(f, c) + neq4(h, d) * neq4(h, g) +
|
|
231
|
+
eq4(e, i) * (neq4(f, f4) * neq4(f, i4) + neq4(h, h5) * neq4(h, i5)) + eqEG + eqEC),
|
|
232
|
+
0.0,
|
|
233
|
+
1.0);
|
|
234
|
+
|
|
235
|
+
// Level 2 restrictions gate the shallower 30 and 60 degree edges.
|
|
236
|
+
vec4 irlv2l = diff4(eP, gP) * diff4(dP, gP);
|
|
237
|
+
vec4 irlv2u = diff4(eP, cP) * diff4(bP, cP);
|
|
238
|
+
|
|
239
|
+
// How far across the blend ramp this fragment sits, for each edge angle.
|
|
240
|
+
// Two output pixels' worth of source, matching the reference's aa_factor.
|
|
241
|
+
float aaFactor = 2.0 * uTexelsPerOutputPixel / pixelSize.x;
|
|
242
|
+
vec4 delta = vec4(aaFactor);
|
|
243
|
+
vec4 deltaL = vec4(0.5, 1.0, 0.5, 1.0) * aaFactor;
|
|
244
|
+
vec4 deltaU = deltaL.yxwz;
|
|
245
|
+
|
|
246
|
+
vec4 fx45i = clamp(0.5 + (fx - Co - Ci) / delta, 0.0, 1.0);
|
|
247
|
+
vec4 fx45 = clamp(0.5 + (fx - Co) / delta, 0.0, 1.0);
|
|
248
|
+
vec4 fx30 = clamp(0.5 + (fxL - Cx) / deltaL, 0.0, 1.0);
|
|
249
|
+
vec4 fx60 = clamp(0.5 + (fxU - Cy) / deltaU, 0.0, 1.0);
|
|
250
|
+
|
|
251
|
+
// Which way does the edge run? wd1 small means the main diagonal through E,
|
|
252
|
+
// wd2 small means the anti-diagonal.
|
|
253
|
+
vec4 wd1 = weightedDistance(e, c, g, i, h5, f4, h, f);
|
|
254
|
+
vec4 wd2 = weightedDistance(h, d, i5, f, i4, b, e, i);
|
|
255
|
+
|
|
256
|
+
vec4 dFG = dist4(f, g);
|
|
257
|
+
vec4 dHC = dist4(h, c);
|
|
258
|
+
|
|
259
|
+
vec4 edri = step(wd1, wd2) * irlv0;
|
|
260
|
+
vec4 edr = vec4(lessThan(wd1, wd2)) * irlv1 * (vec4(1.0) - edri.yzwx * edri.wxyz);
|
|
261
|
+
vec4 edrL = step(Lv2Cf * dFG, dHC) * irlv2l * edr * ((vec4(1.0) - edri.yzwx) * eqEC);
|
|
262
|
+
vec4 edrU = step(Lv2Cf * dHC, dFG) * irlv2u * edr * ((vec4(1.0) - edri.wxyz) * eqEG);
|
|
263
|
+
|
|
264
|
+
fx45i *= edri;
|
|
265
|
+
fx45 *= edr;
|
|
266
|
+
fx30 *= edrL;
|
|
267
|
+
fx60 *= edrU;
|
|
268
|
+
|
|
269
|
+
// Of the two pixels either side of the edge, blend towards the nearer one.
|
|
270
|
+
vec4 px = step(dist4(e, f), dist4(e, h));
|
|
271
|
+
|
|
272
|
+
vec4 maximos = max(max(fx30, fx60), max(fx45, fx45i));
|
|
273
|
+
|
|
274
|
+
vec3 resA = furtherFrom(E, mix(E, mix(H, F, px.x), maximos.x), mix(E, mix(B, D, px.z), maximos.z));
|
|
275
|
+
vec3 resB = furtherFrom(E, mix(E, mix(F, B, px.y), maximos.y), mix(E, mix(D, H, px.w), maximos.w));
|
|
276
|
+
|
|
277
|
+
gl_FragColor = vec4(furtherFrom(E, resA, resB), 1.0);
|
|
278
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// The xBR-lv2 display mode. See shaders/xbr.frag.glsl for the algorithm, and
|
|
4
|
+
// tests/shader, which runs it in a browser and asserts on what it draws.
|
|
5
|
+
|
|
6
|
+
import VERT_SHADER from "./shaders/xbr.vert.glsl?raw";
|
|
7
|
+
import FRAG_SHADER from "./shaders/xbr.frag.glsl?raw";
|
|
8
|
+
import { compileProgram } from "./shader-program.js";
|
|
9
|
+
import { LineGridRows } from "./pixel-grid.js";
|
|
10
|
+
|
|
11
|
+
// The framebuffer texture GlCanvas hands us is square and fixed at this size.
|
|
12
|
+
const TextureSize = 1024;
|
|
13
|
+
|
|
14
|
+
export class XbrFilter {
|
|
15
|
+
static getDisplayConfig() {
|
|
16
|
+
return {
|
|
17
|
+
name: "Smoothed (xBR)",
|
|
18
|
+
image: "images/cub-monitor.png",
|
|
19
|
+
imageAlt: "A fake CUB computer monitor",
|
|
20
|
+
imageWidth: 896,
|
|
21
|
+
imageHeight: 648,
|
|
22
|
+
canvasLeft: 0,
|
|
23
|
+
canvasTop: 8,
|
|
24
|
+
visibleWidth: 896,
|
|
25
|
+
visibleHeight: 600,
|
|
26
|
+
canvasWidth: 896,
|
|
27
|
+
canvasHeight: 600,
|
|
28
|
+
// Reconstructed detail needs somewhere to go, so this mode will
|
|
29
|
+
// draw into up to twice the usual canvas — but only as far as the
|
|
30
|
+
// display can actually show. Rendering 1792x1200 into a window
|
|
31
|
+
// showing 900 pixels costs four times the fragments for nothing,
|
|
32
|
+
// and this shader is expensive per fragment.
|
|
33
|
+
maxCanvasScale: 2,
|
|
34
|
+
// The shader picks its own samples on the logical pixel grid, so
|
|
35
|
+
// hardware interpolation would only blur what it reads. It is also
|
|
36
|
+
// what makes the shader's early return legal: with no LOD to
|
|
37
|
+
// choose, its texture lookups need no implicit derivatives. See
|
|
38
|
+
// the note beside that return in xbr.frag.glsl before changing it.
|
|
39
|
+
nearestSampling: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
constructor(gl) {
|
|
44
|
+
this.gl = gl;
|
|
45
|
+
|
|
46
|
+
// The shader floors framebuffer coordinates that run to 1024 to find
|
|
47
|
+
// the pixel grid. mediump cannot resolve one texel from the next up
|
|
48
|
+
// there, so the grid would land on the wrong texel and the picture
|
|
49
|
+
// would come apart. Better to fall back to an unfiltered display than
|
|
50
|
+
// to render a broken one.
|
|
51
|
+
const highp = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT);
|
|
52
|
+
if (!highp || highp.precision === 0)
|
|
53
|
+
throw new Error("xBR needs high fragment shader precision, which this device does not offer");
|
|
54
|
+
|
|
55
|
+
this.program = compileProgram(gl, VERT_SHADER, FRAG_SHADER, "xBR");
|
|
56
|
+
this.locations = {
|
|
57
|
+
tex: gl.getUniformLocation(this.program, "tex"),
|
|
58
|
+
lineGrid: gl.getUniformLocation(this.program, "lineGrid"),
|
|
59
|
+
textureSize: gl.getUniformLocation(this.program, "uTextureSize"),
|
|
60
|
+
texelSize: gl.getUniformLocation(this.program, "uTexelSize"),
|
|
61
|
+
texelsPerOutputPixel: gl.getUniformLocation(this.program, "uTexelsPerOutputPixel"),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// One row holding the logical pixel size of each framebuffer row, laid
|
|
65
|
+
// out along x rather than y: 1024 contiguous bytes upload as a single
|
|
66
|
+
// row, where a one-texel-wide column would be 1024 rows of one byte and
|
|
67
|
+
// would need the unpack alignment changed around every upload.
|
|
68
|
+
this.lineGridTexture = gl.createTexture();
|
|
69
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
70
|
+
gl.bindTexture(gl.TEXTURE_2D, this.lineGridTexture);
|
|
71
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
72
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
73
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
74
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
75
|
+
// Its size is fixed, so allocate now and only ever refill it.
|
|
76
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, LineGridRows, 1, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, null);
|
|
77
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
78
|
+
|
|
79
|
+
// Everything but the line grid and the output scale is fixed for the
|
|
80
|
+
// life of the program, so set it once. GlCanvas has not called
|
|
81
|
+
// useProgram yet, and does so again straight after building us.
|
|
82
|
+
gl.useProgram(this.program);
|
|
83
|
+
gl.uniform1i(this.locations.tex, 0);
|
|
84
|
+
gl.uniform1i(this.locations.lineGrid, 1);
|
|
85
|
+
gl.uniform2f(this.locations.textureSize, TextureSize, TextureSize);
|
|
86
|
+
gl.uniform2f(this.locations.texelSize, 1.0 / TextureSize, 1.0 / TextureSize);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Release the GL objects this filter owns, the extra texture included. */
|
|
90
|
+
dispose() {
|
|
91
|
+
this.gl.deleteProgram(this.program);
|
|
92
|
+
this.gl.deleteTexture(this.lineGridTexture);
|
|
93
|
+
this.program = this.lineGridTexture = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setUniforms(params) {
|
|
97
|
+
const gl = this.gl;
|
|
98
|
+
gl.uniform1f(this.locations.texelsPerOutputPixel, params.texelsPerOutputPixel);
|
|
99
|
+
|
|
100
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
101
|
+
gl.bindTexture(gl.TEXTURE_2D, this.lineGridTexture);
|
|
102
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, LineGridRows, 1, gl.LUMINANCE, gl.UNSIGNED_BYTE, params.lineGrid);
|
|
103
|
+
// Leave unit 0 active and the framebuffer bound: the canvas uploads the
|
|
104
|
+
// next frame's pixels through whatever is current.
|
|
105
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
106
|
+
}
|
|
107
|
+
}
|