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.
@@ -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 = null;
40
- this.locations = {};
41
-
42
- this._init();
43
- }
44
-
45
- _init() {
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
- _compileShader(type, source) {
73
- const gl = this.gl;
74
- const shader = gl.createShader(type);
75
- gl.shaderSource(shader, source);
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 = null;
28
- this.locations = {};
29
-
30
- this._init();
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
- _compileShader(type, source) {
52
- const gl = this.gl;
53
- const shader = gl.createShader(type);
54
- gl.shaderSource(shader, source);
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,7 @@
1
+ attribute vec2 pos;
2
+ attribute vec2 uvIn;
3
+ varying vec2 uv;
4
+ void main() {
5
+ uv = uvIn;
6
+ gl_Position = vec4(2.0 * pos - 1.0, 0.0, 1.0);
7
+ }
@@ -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
+ }