@pixagram/renderart 1.0.1 → 1.0.2

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/src/xbrz-gpu.ts CHANGED
@@ -10,29 +10,22 @@
10
10
 
11
11
  import type { ImageInput, ImageOutput, Renderer, XbrzOptions } from './types.js';
12
12
 
13
- // Vertex shader
14
13
  const VERTEX_SHADER = `#version 300 es
15
14
  layout(location = 0) in vec2 position;
16
15
  out vec2 vUv;
17
-
18
16
  void main() {
19
17
  vUv = position * 0.5 + 0.5;
20
18
  gl_Position = vec4(position, 0.0, 1.0);
21
19
  }`;
22
20
 
23
- // Fragment shader with xBRZ algorithm
24
- const FRAGMENT_SHADER = `#version 300 es
21
+ const COMMON_HEADER = `#version 300 es
25
22
  precision highp float;
26
-
27
23
  uniform sampler2D uTex;
28
24
  uniform vec2 uInputRes;
29
- uniform vec2 uOutputRes;
30
- uniform int uScale;
31
25
  uniform float uLuminanceWeight;
32
26
  uniform float uEqualColorTolerance;
33
27
  uniform float uSteepDirectionThreshold;
34
28
  uniform float uDominantDirectionThreshold;
35
-
36
29
  in vec2 vUv;
37
30
  out vec4 outColor;
38
31
 
@@ -40,61 +33,24 @@ out vec4 outColor;
40
33
  #define BLEND_NORMAL 1
41
34
  #define BLEND_DOMINANT 2
42
35
 
43
- // Sample source texture with clamping
44
- vec4 tex(vec2 coord) {
45
- return texture(uTex, clamp(coord, vec2(0.0), vec2(1.0)));
46
- }
36
+ vec4 tex(vec2 coord) { return texture(uTex, clamp(coord, vec2(0.0), vec2(1.0))); }
37
+ float reduce(vec3 c) { return dot(c, vec3(65536.0, 256.0, 1.0)); }
47
38
 
48
- // Reduce color to single value for equality comparison
49
- float reduce(vec3 color) {
50
- return dot(color, vec3(65536.0, 256.0, 1.0));
51
- }
52
-
53
- // YCbCr color distance (perceptually weighted)
54
- float distYCbCr(vec3 pixA, vec3 pixB) {
39
+ float DistYCbCr(vec3 pixA, vec3 pixB) {
55
40
  const vec3 w = vec3(0.2627, 0.6780, 0.0593);
56
41
  const float scaleB = 0.5 / (1.0 - w.b);
57
42
  const float scaleR = 0.5 / (1.0 - w.r);
58
-
59
43
  vec3 diff = pixA - pixB;
60
44
  float Y = dot(diff, w);
61
45
  float Cb = scaleB * (diff.b - Y);
62
46
  float Cr = scaleR * (diff.r - Y);
63
-
64
47
  return sqrt((uLuminanceWeight * Y) * (uLuminanceWeight * Y) + Cb * Cb + Cr * Cr);
65
48
  }
66
49
 
67
- // Check if two pixels are equal within tolerance
68
- bool isPixEqual(vec3 pixA, vec3 pixB) {
69
- return distYCbCr(pixA, pixB) < uEqualColorTolerance;
70
- }
71
-
72
- // Check if any blending is needed
73
- bool isBlendingNeeded(ivec4 blend) {
74
- return blend.x != BLEND_NONE || blend.y != BLEND_NONE ||
75
- blend.z != BLEND_NONE || blend.w != BLEND_NONE;
76
- }
50
+ bool IsPixEqual(vec3 pixA, vec3 pixB) { return DistYCbCr(pixA, pixB) < uEqualColorTolerance; }
51
+ bool IsBlendingNeeded(ivec4 blend) { return blend.x != 0 || blend.y != 0 || blend.z != 0 || blend.w != 0; }
77
52
 
78
- void main() {
79
- vec2 texelSize = 1.0 / uInputRes;
80
-
81
- // Find which source pixel and sub-pixel position
82
- vec2 srcPos = vUv * uInputRes;
83
- vec2 srcPixel = floor(srcPos);
84
- vec2 f = fract(srcPos);
85
-
86
- // Sample 5x5 neighborhood
87
- // Pixel Mapping: 20|21|22|23|24
88
- // 19|06|07|08|09
89
- // 18|05|00|01|10
90
- // 17|04|03|02|11
91
- // 16|15|14|13|12
92
-
93
- vec2 tc = (srcPixel + 0.5) * texelSize;
94
- float dx = texelSize.x;
95
- float dy = texelSize.y;
96
-
97
- vec3 src[25];
53
+ void sampleNeighborhood(vec2 tc, float dx, float dy, out vec3 src[25], out float v[9]) {
98
54
  src[21] = tex(tc + vec2(-dx, -2.0*dy)).rgb;
99
55
  src[22] = tex(tc + vec2( 0, -2.0*dy)).rgb;
100
56
  src[23] = tex(tc + vec2( dx, -2.0*dy)).rgb;
@@ -116,373 +72,253 @@ void main() {
116
72
  src[ 9] = tex(tc + vec2( 2.0*dx, -dy)).rgb;
117
73
  src[10] = tex(tc + vec2( 2.0*dx, 0)).rgb;
118
74
  src[11] = tex(tc + vec2( 2.0*dx, dy)).rgb;
119
-
120
- // Reduce colors for comparison
121
- float v[9];
122
- v[0] = reduce(src[0]);
123
- v[1] = reduce(src[1]);
124
- v[2] = reduce(src[2]);
125
- v[3] = reduce(src[3]);
126
- v[4] = reduce(src[4]);
127
- v[5] = reduce(src[5]);
128
- v[6] = reduce(src[6]);
129
- v[7] = reduce(src[7]);
130
- v[8] = reduce(src[8]);
131
-
75
+ for (int i = 0; i < 9; i++) v[i] = reduce(src[i]);
76
+ }
77
+
78
+ ivec4 detectCorners(vec3 src[25], float v[9]) {
132
79
  ivec4 blendResult = ivec4(BLEND_NONE);
133
-
134
- // Preprocess corners - determine blend type for each
135
-
136
- // Corner (1, 1) - bottom-right
80
+ // Corner (1, 1)
137
81
  if (!((v[0] == v[1] && v[3] == v[2]) || (v[0] == v[3] && v[1] == v[2]))) {
138
- float dist_03_01 = distYCbCr(src[4], src[0]) + distYCbCr(src[0], src[8]) +
139
- distYCbCr(src[14], src[2]) + distYCbCr(src[2], src[10]) +
140
- 4.0 * distYCbCr(src[3], src[1]);
141
- float dist_00_02 = distYCbCr(src[5], src[3]) + distYCbCr(src[3], src[13]) +
142
- distYCbCr(src[7], src[1]) + distYCbCr(src[1], src[11]) +
143
- 4.0 * distYCbCr(src[0], src[2]);
144
- bool dominantGradient = (uDominantDirectionThreshold * dist_03_01) < dist_00_02;
145
- blendResult[2] = ((dist_03_01 < dist_00_02) && (v[0] != v[1]) && (v[0] != v[3]))
146
- ? (dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
82
+ float dist_03_01 = DistYCbCr(src[4], src[0]) + DistYCbCr(src[0], src[8]) + DistYCbCr(src[14], src[2]) + DistYCbCr(src[2], src[10]) + 4.0 * DistYCbCr(src[3], src[1]);
83
+ float dist_00_02 = DistYCbCr(src[5], src[3]) + DistYCbCr(src[3], src[13]) + DistYCbCr(src[7], src[1]) + DistYCbCr(src[1], src[11]) + 4.0 * DistYCbCr(src[0], src[2]);
84
+ bool dominant = (uDominantDirectionThreshold * dist_03_01) < dist_00_02;
85
+ blendResult[2] = ((dist_03_01 < dist_00_02) && (v[0] != v[1]) && (v[0] != v[3])) ? (dominant ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
147
86
  }
148
-
149
- // Corner (0, 1) - bottom-left
87
+ // Corner (0, 1)
150
88
  if (!((v[5] == v[0] && v[4] == v[3]) || (v[5] == v[4] && v[0] == v[3]))) {
151
- float dist_04_00 = distYCbCr(src[17], src[5]) + distYCbCr(src[5], src[7]) +
152
- distYCbCr(src[15], src[3]) + distYCbCr(src[3], src[1]) +
153
- 4.0 * distYCbCr(src[4], src[0]);
154
- float dist_05_03 = distYCbCr(src[18], src[4]) + distYCbCr(src[4], src[14]) +
155
- distYCbCr(src[6], src[0]) + distYCbCr(src[0], src[2]) +
156
- 4.0 * distYCbCr(src[5], src[3]);
157
- bool dominantGradient = (uDominantDirectionThreshold * dist_05_03) < dist_04_00;
158
- blendResult[3] = ((dist_04_00 > dist_05_03) && (v[0] != v[5]) && (v[0] != v[3]))
159
- ? (dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
89
+ float dist_04_00 = DistYCbCr(src[17], src[5]) + DistYCbCr(src[5], src[7]) + DistYCbCr(src[15], src[3]) + DistYCbCr(src[3], src[1]) + 4.0 * DistYCbCr(src[4], src[0]);
90
+ float dist_05_03 = DistYCbCr(src[18], src[4]) + DistYCbCr(src[4], src[14]) + DistYCbCr(src[6], src[0]) + DistYCbCr(src[0], src[2]) + 4.0 * DistYCbCr(src[5], src[3]);
91
+ bool dominant = (uDominantDirectionThreshold * dist_05_03) < dist_04_00;
92
+ blendResult[3] = ((dist_04_00 > dist_05_03) && (v[0] != v[5]) && (v[0] != v[3])) ? (dominant ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
160
93
  }
161
-
162
- // Corner (1, 0) - top-right
94
+ // Corner (1, 0)
163
95
  if (!((v[7] == v[8] && v[0] == v[1]) || (v[7] == v[0] && v[8] == v[1]))) {
164
- float dist_00_08 = distYCbCr(src[5], src[7]) + distYCbCr(src[7], src[23]) +
165
- distYCbCr(src[3], src[1]) + distYCbCr(src[1], src[9]) +
166
- 4.0 * distYCbCr(src[0], src[8]);
167
- float dist_07_01 = distYCbCr(src[6], src[0]) + distYCbCr(src[0], src[2]) +
168
- distYCbCr(src[22], src[8]) + distYCbCr(src[8], src[10]) +
169
- 4.0 * distYCbCr(src[7], src[1]);
170
- bool dominantGradient = (uDominantDirectionThreshold * dist_07_01) < dist_00_08;
171
- blendResult[1] = ((dist_00_08 > dist_07_01) && (v[0] != v[7]) && (v[0] != v[1]))
172
- ? (dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
96
+ float dist_00_08 = DistYCbCr(src[5], src[7]) + DistYCbCr(src[7], src[23]) + DistYCbCr(src[3], src[1]) + DistYCbCr(src[1], src[9]) + 4.0 * DistYCbCr(src[0], src[8]);
97
+ float dist_07_01 = DistYCbCr(src[6], src[0]) + DistYCbCr(src[0], src[2]) + DistYCbCr(src[22], src[8]) + DistYCbCr(src[8], src[10]) + 4.0 * DistYCbCr(src[7], src[1]);
98
+ bool dominant = (uDominantDirectionThreshold * dist_07_01) < dist_00_08;
99
+ blendResult[1] = ((dist_00_08 > dist_07_01) && (v[0] != v[7]) && (v[0] != v[1])) ? (dominant ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
173
100
  }
174
-
175
- // Corner (0, 0) - top-left
101
+ // Corner (0, 0)
176
102
  if (!((v[6] == v[7] && v[5] == v[0]) || (v[6] == v[5] && v[7] == v[0]))) {
177
- float dist_05_07 = distYCbCr(src[18], src[6]) + distYCbCr(src[6], src[22]) +
178
- distYCbCr(src[4], src[0]) + distYCbCr(src[0], src[8]) +
179
- 4.0 * distYCbCr(src[5], src[7]);
180
- float dist_06_00 = distYCbCr(src[19], src[5]) + distYCbCr(src[5], src[3]) +
181
- distYCbCr(src[21], src[7]) + distYCbCr(src[7], src[1]) +
182
- 4.0 * distYCbCr(src[6], src[0]);
183
- bool dominantGradient = (uDominantDirectionThreshold * dist_05_07) < dist_06_00;
184
- blendResult[0] = ((dist_05_07 < dist_06_00) && (v[0] != v[5]) && (v[0] != v[7]))
185
- ? (dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
103
+ float dist_05_07 = DistYCbCr(src[18], src[6]) + DistYCbCr(src[6], src[22]) + DistYCbCr(src[4], src[0]) + DistYCbCr(src[0], src[8]) + 4.0 * DistYCbCr(src[5], src[7]);
104
+ float dist_06_00 = DistYCbCr(src[19], src[5]) + DistYCbCr(src[5], src[3]) + DistYCbCr(src[21], src[7]) + DistYCbCr(src[7], src[1]) + 4.0 * DistYCbCr(src[6], src[0]);
105
+ bool dominant = (uDominantDirectionThreshold * dist_05_07) < dist_06_00;
106
+ blendResult[0] = ((dist_05_07 < dist_06_00) && (v[0] != v[5]) && (v[0] != v[7])) ? (dominant ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
186
107
  }
108
+ return blendResult;
109
+ }
110
+ `;
111
+
112
+ // 2x xBRZ fragment shader
113
+ const FRAG_2X = COMMON_HEADER + `
114
+ const float M_PI = 3.1415926535897932384626433832795;
115
+
116
+ void main() {
117
+ vec2 texelSize = 1.0 / uInputRes;
118
+ vec2 srcPos = vUv * uInputRes;
119
+ vec2 srcPixel = floor(srcPos);
120
+ vec2 f = fract(srcPos);
121
+ vec2 tc = (srcPixel + 0.5) * texelSize;
187
122
 
188
- // Start with center pixel for all output pixels
189
- vec3 result = src[0];
123
+ vec3 src[25]; float v[9];
124
+ sampleNeighborhood(tc, texelSize.x, texelSize.y, src, v);
125
+ ivec4 blendResult = detectCorners(src, v);
190
126
 
191
- if (isBlendingNeeded(blendResult)) {
192
- // Scale-dependent blending
193
- if (uScale == 2) {
194
- // 2x output: 4 pixels per source pixel
195
- // Output Mapping: 00|01
196
- // 03|02
197
-
198
- vec3 dst[4];
199
- dst[0] = src[0]; dst[1] = src[0];
200
- dst[2] = src[0]; dst[3] = src[0];
201
-
202
- // Process each corner with rotation
203
- for (int rot = 0; rot < 4; rot++) {
204
- // Rotated kernel indices
205
- int k1, k2, k3, k4, k5, k6, k7, k8;
206
- ivec4 blend;
207
-
208
- if (rot == 0) {
209
- k1 = 1; k2 = 2; k3 = 3; k4 = 4; k5 = 5; k6 = 6; k7 = 7; k8 = 8;
210
- blend = blendResult;
211
- } else if (rot == 1) {
212
- k1 = 7; k2 = 8; k3 = 1; k4 = 2; k5 = 3; k6 = 4; k7 = 5; k8 = 6;
213
- blend = blendResult.wxyz;
214
- } else if (rot == 2) {
215
- k1 = 5; k2 = 6; k3 = 7; k4 = 8; k5 = 1; k6 = 2; k7 = 3; k8 = 4;
216
- blend = blendResult.zwxy;
217
- } else {
218
- k1 = 3; k2 = 4; k3 = 5; k4 = 6; k5 = 7; k6 = 8; k7 = 1; k8 = 2;
219
- blend = blendResult.yzwx;
220
- }
221
-
222
- vec3 k[9];
223
- k[0] = src[0];
224
- k[1] = (rot == 0) ? src[1] : (rot == 1) ? src[7] : (rot == 2) ? src[5] : src[3];
225
- k[2] = (rot == 0) ? src[2] : (rot == 1) ? src[8] : (rot == 2) ? src[6] : src[4];
226
- k[3] = (rot == 0) ? src[3] : (rot == 1) ? src[1] : (rot == 2) ? src[7] : src[5];
227
- k[4] = (rot == 0) ? src[4] : (rot == 1) ? src[2] : (rot == 2) ? src[8] : src[6];
228
- k[5] = (rot == 0) ? src[5] : (rot == 1) ? src[3] : (rot == 2) ? src[1] : src[7];
229
- k[6] = (rot == 0) ? src[6] : (rot == 1) ? src[4] : (rot == 2) ? src[2] : src[8];
230
- k[7] = (rot == 0) ? src[7] : (rot == 1) ? src[5] : (rot == 2) ? src[3] : src[1];
231
- k[8] = (rot == 0) ? src[8] : (rot == 1) ? src[6] : (rot == 2) ? src[4] : src[2];
232
-
233
- float vk0 = reduce(k[0]);
234
- float vk4 = reduce(k[4]);
235
- float vk5 = reduce(k[5]);
236
- float vk7 = reduce(k[7]);
237
- float vk8 = reduce(k[8]);
238
-
239
- float dist_01_04 = distYCbCr(k[1], k[4]);
240
- float dist_03_08 = distYCbCr(k[3], k[8]);
241
- bool haveShallowLine = (uSteepDirectionThreshold * dist_01_04 <= dist_03_08) && (vk0 != vk4) && (vk5 != vk4);
242
- bool haveSteepLine = (uSteepDirectionThreshold * dist_03_08 <= dist_01_04) && (vk0 != vk8) && (vk7 != vk8);
243
- bool needBlend = (blend[2] != BLEND_NONE);
244
- bool doLineBlend = blend[2] >= BLEND_DOMINANT ||
245
- !((blend[1] != BLEND_NONE && !isPixEqual(k[0], k[4])) ||
246
- (blend[3] != BLEND_NONE && !isPixEqual(k[0], k[8])) ||
247
- (isPixEqual(k[4], k[3]) && isPixEqual(k[3], k[2]) &&
248
- isPixEqual(k[2], k[1]) && isPixEqual(k[1], k[8]) && !isPixEqual(k[0], k[2])));
249
-
250
- vec3 blendPix = (distYCbCr(k[0], k[1]) <= distYCbCr(k[0], k[3])) ? k[1] : k[3];
251
-
252
- const float PI = 3.14159265359;
253
-
254
- // Apply blends to rotated destination
255
- int d1 = (rot == 0) ? 1 : (rot == 1) ? 0 : (rot == 2) ? 3 : 2;
256
- int d2 = (rot == 0) ? 2 : (rot == 1) ? 1 : (rot == 2) ? 0 : 3;
257
- int d3 = (rot == 0) ? 3 : (rot == 1) ? 2 : (rot == 2) ? 1 : 0;
258
-
259
- float blend1 = (needBlend && doLineBlend && haveSteepLine) ? 0.25 : 0.0;
260
- float blend2 = needBlend ? (doLineBlend ?
261
- (haveShallowLine ? (haveSteepLine ? 5.0/6.0 : 0.75) : (haveSteepLine ? 0.75 : 0.5))
262
- : (1.0 - PI/4.0)) : 0.0;
263
- float blend3 = (needBlend && doLineBlend && haveShallowLine) ? 0.25 : 0.0;
264
-
265
- dst[d1] = mix(dst[d1], blendPix, blend1);
266
- dst[d2] = mix(dst[d2], blendPix, blend2);
267
- dst[d3] = mix(dst[d3], blendPix, blend3);
268
- }
269
-
270
- // Select output pixel based on sub-pixel position
271
- result = mix(mix(dst[0], dst[1], step(0.5, f.x)),
272
- mix(dst[3], dst[2], step(0.5, f.x)), step(0.5, f.y));
273
-
274
- } else if (uScale == 3) {
275
- // 3x output: 9 pixels per source pixel
276
- vec3 dst[9];
277
- for (int i = 0; i < 9; i++) dst[i] = src[0];
127
+ vec3 dst[4];
128
+ dst[0] = src[0]; dst[1] = src[0]; dst[2] = src[0]; dst[3] = src[0];
129
+
130
+ if (IsBlendingNeeded(blendResult)) {
131
+ // Process all 4 corners
132
+ for (int corner = 0; corner < 4; corner++) {
133
+ vec3 k1, k3, k4, k5, k7, k8;
134
+ int blendIdx;
135
+ if (corner == 0) { k1=src[1]; k3=src[3]; k4=src[4]; k5=src[5]; k7=src[7]; k8=src[8]; blendIdx=2; }
136
+ else if (corner == 1) { k1=src[7]; k3=src[1]; k4=src[2]; k5=src[3]; k7=src[5]; k8=src[6]; blendIdx=1; }
137
+ else if (corner == 2) { k1=src[5]; k3=src[7]; k4=src[8]; k5=src[1]; k7=src[3]; k8=src[4]; blendIdx=0; }
138
+ else { k1=src[3]; k3=src[5]; k4=src[6]; k5=src[7]; k7=src[1]; k8=src[2]; blendIdx=3; }
278
139
 
279
- // Process all 4 corners with rotation
280
- for (int rot = 0; rot < 4; rot++) {
281
- ivec4 blend = (rot == 0) ? blendResult :
282
- (rot == 1) ? blendResult.wxyz :
283
- (rot == 2) ? blendResult.zwxy : blendResult.yzwx;
284
-
285
- vec3 k[9];
286
- k[0] = src[0];
287
- k[1] = (rot == 0) ? src[1] : (rot == 1) ? src[7] : (rot == 2) ? src[5] : src[3];
288
- k[2] = (rot == 0) ? src[2] : (rot == 1) ? src[8] : (rot == 2) ? src[6] : src[4];
289
- k[3] = (rot == 0) ? src[3] : (rot == 1) ? src[1] : (rot == 2) ? src[7] : src[5];
290
- k[4] = (rot == 0) ? src[4] : (rot == 1) ? src[2] : (rot == 2) ? src[8] : src[6];
291
- k[5] = (rot == 0) ? src[5] : (rot == 1) ? src[3] : (rot == 2) ? src[1] : src[7];
292
- k[6] = (rot == 0) ? src[6] : (rot == 1) ? src[4] : (rot == 2) ? src[2] : src[8];
293
- k[7] = (rot == 0) ? src[7] : (rot == 1) ? src[5] : (rot == 2) ? src[3] : src[1];
294
- k[8] = (rot == 0) ? src[8] : (rot == 1) ? src[6] : (rot == 2) ? src[4] : src[2];
295
-
296
- float vk0 = reduce(k[0]);
297
- float vk4 = reduce(k[4]);
298
- float vk5 = reduce(k[5]);
299
- float vk7 = reduce(k[7]);
300
- float vk8 = reduce(k[8]);
301
-
302
- float dist_01_04 = distYCbCr(k[1], k[4]);
303
- float dist_03_08 = distYCbCr(k[3], k[8]);
304
- bool haveShallowLine = (uSteepDirectionThreshold * dist_01_04 <= dist_03_08) && (vk0 != vk4) && (vk5 != vk4);
305
- bool haveSteepLine = (uSteepDirectionThreshold * dist_03_08 <= dist_01_04) && (vk0 != vk8) && (vk7 != vk8);
306
- bool needBlend = (blend[2] != BLEND_NONE);
307
- bool doLineBlend = blend[2] >= BLEND_DOMINANT ||
308
- !((blend[1] != BLEND_NONE && !isPixEqual(k[0], k[4])) ||
309
- (blend[3] != BLEND_NONE && !isPixEqual(k[0], k[8])) ||
310
- (isPixEqual(k[4], k[3]) && isPixEqual(k[3], k[2]) &&
311
- isPixEqual(k[2], k[1]) && isPixEqual(k[1], k[8]) && !isPixEqual(k[0], k[2])));
312
-
313
- vec3 blendPix = (distYCbCr(k[0], k[1]) <= distYCbCr(k[0], k[3])) ? k[1] : k[3];
314
-
315
- // 3x specific blend weights
316
- // Output mapping for rot=0: 0|1|2
317
- // 3|4|5
318
- // 6|7|8
319
- // Corner is at position 8
320
-
321
- int idx[4];
322
- if (rot == 0) { idx[0] = 5; idx[1] = 8; idx[2] = 7; idx[3] = 2; }
323
- else if (rot == 1) { idx[0] = 1; idx[1] = 2; idx[2] = 5; idx[3] = 0; }
324
- else if (rot == 2) { idx[0] = 3; idx[1] = 0; idx[2] = 1; idx[3] = 6; }
325
- else { idx[0] = 7; idx[1] = 6; idx[2] = 3; idx[3] = 8; }
326
-
327
- dst[idx[0]] = mix(dst[idx[0]], blendPix, (needBlend && doLineBlend && haveSteepLine) ? 0.25 : 0.0);
328
- dst[idx[1]] = mix(dst[idx[1]], blendPix, needBlend ? (doLineBlend ?
329
- (haveShallowLine ? (haveSteepLine ? 2.0/3.0 : 0.75) : (haveSteepLine ? 0.75 : 0.5)) : 0.3) : 0.0);
330
- dst[idx[2]] = mix(dst[idx[2]], blendPix, (needBlend && doLineBlend && haveShallowLine) ? 0.25 : 0.0);
331
- }
140
+ float vk0 = v[0], vk4 = reduce(k4), vk5 = reduce(k5), vk7 = reduce(k7), vk8 = reduce(k8);
141
+ float dist_01_04 = DistYCbCr(k1, k4);
142
+ float dist_03_08 = DistYCbCr(k3, k8);
143
+ bool haveShallowLine = (uSteepDirectionThreshold * dist_01_04 <= dist_03_08) && (vk0 != vk4) && (vk5 != vk4);
144
+ bool haveSteepLine = (uSteepDirectionThreshold * dist_03_08 <= dist_01_04) && (vk0 != vk8) && (vk7 != vk8);
145
+ bool needBlend = (blendResult[blendIdx] != BLEND_NONE);
146
+ bool doLineBlend = blendResult[blendIdx] >= BLEND_DOMINANT ||
147
+ !((blendResult[(blendIdx+3)%4] != BLEND_NONE && !IsPixEqual(src[0], k4)) ||
148
+ (blendResult[(blendIdx+1)%4] != BLEND_NONE && !IsPixEqual(src[0], k8)) ||
149
+ (IsPixEqual(k4, k3) && IsPixEqual(k3, k8) && !IsPixEqual(src[0], k3)));
332
150
 
333
- // Select output based on 3x3 grid position
334
- float fx = f.x * 3.0;
335
- float fy = f.y * 3.0;
336
- int xi = int(fx);
337
- int yi = int(fy);
338
- int idx = yi * 3 + xi;
151
+ vec3 blendPix = (DistYCbCr(src[0], k1) <= DistYCbCr(src[0], k3)) ? k1 : k3;
152
+ float b1 = (needBlend && doLineBlend && haveSteepLine) ? 0.25 : 0.0;
153
+ float b2 = needBlend ? (doLineBlend ? (haveShallowLine ? (haveSteepLine ? 5.0/6.0 : 0.75) : (haveSteepLine ? 0.75 : 0.5)) : (1.0 - M_PI/4.0)) : 0.0;
154
+ float b3 = (needBlend && doLineBlend && haveShallowLine) ? 0.25 : 0.0;
339
155
 
340
- result = dst[clamp(idx, 0, 8)];
156
+ if (corner == 0) { dst[1] = mix(dst[1], blendPix, b1); dst[2] = mix(dst[2], blendPix, b2); dst[3] = mix(dst[3], blendPix, b3); }
157
+ else if (corner == 1) { dst[0] = mix(dst[0], blendPix, b1); dst[1] = mix(dst[1], blendPix, b2); dst[2] = mix(dst[2], blendPix, b3); }
158
+ else if (corner == 2) { dst[3] = mix(dst[3], blendPix, b1); dst[0] = mix(dst[0], blendPix, b2); dst[1] = mix(dst[1], blendPix, b3); }
159
+ else { dst[2] = mix(dst[2], blendPix, b1); dst[3] = mix(dst[3], blendPix, b2); dst[0] = mix(dst[0], blendPix, b3); }
160
+ }
161
+ }
162
+
163
+ vec2 ff = step(0.5, f);
164
+ vec3 res = mix(mix(dst[0], dst[1], ff.x), mix(dst[3], dst[2], ff.x), ff.y);
165
+ outColor = vec4(res, tex(tc).a);
166
+ }`;
167
+
168
+ // 3x xBRZ fragment shader
169
+ const FRAG_3X = COMMON_HEADER + `
170
+ const float one_third = 1.0 / 3.0;
171
+ const float two_third = 2.0 / 3.0;
172
+
173
+ void main() {
174
+ vec2 texelSize = 1.0 / uInputRes;
175
+ vec2 srcPos = vUv * uInputRes;
176
+ vec2 srcPixel = floor(srcPos);
177
+ vec2 f = fract(srcPos);
178
+ vec2 tc = (srcPixel + 0.5) * texelSize;
179
+
180
+ vec3 src[25]; float v[9];
181
+ sampleNeighborhood(tc, texelSize.x, texelSize.y, src, v);
182
+ ivec4 blendResult = detectCorners(src, v);
183
+
184
+ // Output: 06|07|08 / 05|00|01 / 04|03|02
185
+ vec3 dst[9];
186
+ for (int i = 0; i < 9; i++) dst[i] = src[0];
187
+
188
+ if (IsBlendingNeeded(blendResult)) {
189
+ for (int corner = 0; corner < 4; corner++) {
190
+ vec3 k1, k3, k4, k5, k7, k8;
191
+ int blendIdx;
192
+ if (corner == 0) { k1=src[1]; k3=src[3]; k4=src[4]; k5=src[5]; k7=src[7]; k8=src[8]; blendIdx=2; }
193
+ else if (corner == 1) { k1=src[7]; k3=src[1]; k4=src[2]; k5=src[3]; k7=src[5]; k8=src[6]; blendIdx=1; }
194
+ else if (corner == 2) { k1=src[5]; k3=src[7]; k4=src[8]; k5=src[1]; k7=src[3]; k8=src[4]; blendIdx=0; }
195
+ else { k1=src[3]; k3=src[5]; k4=src[6]; k5=src[7]; k7=src[1]; k8=src[2]; blendIdx=3; }
341
196
 
342
- } else {
343
- // 4x, 5x, 6x - use 4x algorithm with interpolation
344
- vec3 dst[16];
345
- for (int i = 0; i < 16; i++) dst[i] = src[0];
197
+ float vk0 = v[0], vk4 = reduce(k4), vk5 = reduce(k5), vk7 = reduce(k7), vk8 = reduce(k8);
198
+ float dist_01_04 = DistYCbCr(k1, k4);
199
+ float dist_03_08 = DistYCbCr(k3, k8);
200
+ bool haveShallowLine = (uSteepDirectionThreshold * dist_01_04 <= dist_03_08) && (vk0 != vk4) && (vk5 != vk4);
201
+ bool haveSteepLine = (uSteepDirectionThreshold * dist_03_08 <= dist_01_04) && (vk0 != vk8) && (vk7 != vk8);
202
+ bool needBlend = (blendResult[blendIdx] != BLEND_NONE);
203
+ bool doLineBlend = blendResult[blendIdx] >= BLEND_DOMINANT ||
204
+ !((blendResult[(blendIdx+3)%4] != BLEND_NONE && !IsPixEqual(src[0], k4)) ||
205
+ (blendResult[(blendIdx+1)%4] != BLEND_NONE && !IsPixEqual(src[0], k8)) ||
206
+ (IsPixEqual(k4, k3) && IsPixEqual(k3, k8) && !IsPixEqual(src[0], k3)));
346
207
 
347
- // Process all 4 corners
348
- for (int rot = 0; rot < 4; rot++) {
349
- ivec4 blend = (rot == 0) ? blendResult :
350
- (rot == 1) ? blendResult.wxyz :
351
- (rot == 2) ? blendResult.zwxy : blendResult.yzwx;
352
-
353
- vec3 k[9];
354
- k[0] = src[0];
355
- k[1] = (rot == 0) ? src[1] : (rot == 1) ? src[7] : (rot == 2) ? src[5] : src[3];
356
- k[2] = (rot == 0) ? src[2] : (rot == 1) ? src[8] : (rot == 2) ? src[6] : src[4];
357
- k[3] = (rot == 0) ? src[3] : (rot == 1) ? src[1] : (rot == 2) ? src[7] : src[5];
358
- k[4] = (rot == 0) ? src[4] : (rot == 1) ? src[2] : (rot == 2) ? src[8] : src[6];
359
- k[5] = (rot == 0) ? src[5] : (rot == 1) ? src[3] : (rot == 2) ? src[1] : src[7];
360
- k[6] = (rot == 0) ? src[6] : (rot == 1) ? src[4] : (rot == 2) ? src[2] : src[8];
361
- k[7] = (rot == 0) ? src[7] : (rot == 1) ? src[5] : (rot == 2) ? src[3] : src[1];
362
- k[8] = (rot == 0) ? src[8] : (rot == 1) ? src[6] : (rot == 2) ? src[4] : src[2];
363
-
364
- float vk0 = reduce(k[0]);
365
- float vk4 = reduce(k[4]);
366
- float vk5 = reduce(k[5]);
367
- float vk7 = reduce(k[7]);
368
- float vk8 = reduce(k[8]);
369
-
370
- float dist_01_04 = distYCbCr(k[1], k[4]);
371
- float dist_03_08 = distYCbCr(k[3], k[8]);
372
- bool haveShallowLine = (uSteepDirectionThreshold * dist_01_04 <= dist_03_08) && (vk0 != vk4) && (vk5 != vk4);
373
- bool haveSteepLine = (uSteepDirectionThreshold * dist_03_08 <= dist_01_04) && (vk0 != vk8) && (vk7 != vk8);
374
- bool needBlend = (blend[2] != BLEND_NONE);
375
- bool doLineBlend = blend[2] >= BLEND_DOMINANT ||
376
- !((blend[1] != BLEND_NONE && !isPixEqual(k[0], k[4])) ||
377
- (blend[3] != BLEND_NONE && !isPixEqual(k[0], k[8])) ||
378
- (isPixEqual(k[4], k[3]) && isPixEqual(k[3], k[2]) &&
379
- isPixEqual(k[2], k[1]) && isPixEqual(k[1], k[8]) && !isPixEqual(k[0], k[2])));
380
-
381
- vec3 blendPix = (distYCbCr(k[0], k[1]) <= distYCbCr(k[0], k[3])) ? k[1] : k[3];
382
-
383
- // 4x layout with rotation
384
- // Output mapping: 06|07|08|09
385
- // 05|00|01|10
386
- // 04|03|02|11
387
- // 15|14|13|12
388
-
389
- const float NOLINEBLEND = 0.08677704501;
390
- const float LINEBLEND_CENTER = 0.6848532563;
391
-
392
- // Apply blends based on rotation
393
- if (rot == 0) {
394
- dst[2] = mix(dst[2], blendPix, (needBlend && doLineBlend) ?
395
- (haveShallowLine ? (haveSteepLine ? 1.0/3.0 : 0.25) : (haveSteepLine ? 0.25 : 0.0)) : 0.0);
396
- dst[9] = mix(dst[9], blendPix, (needBlend && doLineBlend && haveSteepLine) ? 0.25 : 0.0);
397
- dst[10] = mix(dst[10], blendPix, (needBlend && doLineBlend && haveSteepLine) ? 0.75 : 0.0);
398
- dst[11] = mix(dst[11], blendPix, needBlend ? (doLineBlend ?
399
- (haveSteepLine ? 1.0 : (haveShallowLine ? 0.75 : 0.5)) : NOLINEBLEND) : 0.0);
400
- dst[12] = mix(dst[12], blendPix, needBlend ? (doLineBlend ? 1.0 : LINEBLEND_CENTER) : 0.0);
401
- dst[13] = mix(dst[13], blendPix, needBlend ? (doLineBlend ?
402
- (haveShallowLine ? 1.0 : (haveSteepLine ? 0.75 : 0.5)) : NOLINEBLEND) : 0.0);
403
- dst[14] = mix(dst[14], blendPix, (needBlend && doLineBlend && haveShallowLine) ? 0.75 : 0.0);
404
- dst[15] = mix(dst[15], blendPix, (needBlend && doLineBlend && haveShallowLine) ? 0.25 : 0.0);
405
- } else if (rot == 1) {
406
- dst[1] = mix(dst[1], blendPix, (needBlend && doLineBlend) ?
407
- (haveShallowLine ? (haveSteepLine ? 1.0/3.0 : 0.25) : (haveSteepLine ? 0.25 : 0.0)) : 0.0);
408
- dst[6] = mix(dst[6], blendPix, (needBlend && doLineBlend && haveSteepLine) ? 0.25 : 0.0);
409
- dst[7] = mix(dst[7], blendPix, (needBlend && doLineBlend && haveSteepLine) ? 0.75 : 0.0);
410
- dst[8] = mix(dst[8], blendPix, needBlend ? (doLineBlend ?
411
- (haveSteepLine ? 1.0 : (haveShallowLine ? 0.75 : 0.5)) : NOLINEBLEND) : 0.0);
412
- dst[9] = mix(dst[9], blendPix, needBlend ? (doLineBlend ? 1.0 : LINEBLEND_CENTER) : 0.0);
413
- dst[10] = mix(dst[10], blendPix, needBlend ? (doLineBlend ?
414
- (haveShallowLine ? 1.0 : (haveSteepLine ? 0.75 : 0.5)) : NOLINEBLEND) : 0.0);
415
- dst[11] = mix(dst[11], blendPix, (needBlend && doLineBlend && haveShallowLine) ? 0.75 : 0.0);
416
- dst[12] = mix(dst[12], blendPix, (needBlend && doLineBlend && haveShallowLine) ? 0.25 : 0.0);
417
- } else if (rot == 2) {
418
- dst[0] = mix(dst[0], blendPix, (needBlend && doLineBlend) ?
419
- (haveShallowLine ? (haveSteepLine ? 1.0/3.0 : 0.25) : (haveSteepLine ? 0.25 : 0.0)) : 0.0);
420
- dst[15] = mix(dst[15], blendPix, (needBlend && doLineBlend && haveSteepLine) ? 0.25 : 0.0);
421
- dst[4] = mix(dst[4], blendPix, (needBlend && doLineBlend && haveSteepLine) ? 0.75 : 0.0);
422
- dst[5] = mix(dst[5], blendPix, needBlend ? (doLineBlend ?
423
- (haveSteepLine ? 1.0 : (haveShallowLine ? 0.75 : 0.5)) : NOLINEBLEND) : 0.0);
424
- dst[6] = mix(dst[6], blendPix, needBlend ? (doLineBlend ? 1.0 : LINEBLEND_CENTER) : 0.0);
425
- dst[7] = mix(dst[7], blendPix, needBlend ? (doLineBlend ?
426
- (haveShallowLine ? 1.0 : (haveSteepLine ? 0.75 : 0.5)) : NOLINEBLEND) : 0.0);
427
- dst[8] = mix(dst[8], blendPix, (needBlend && doLineBlend && haveShallowLine) ? 0.75 : 0.0);
428
- dst[9] = mix(dst[9], blendPix, (needBlend && doLineBlend && haveShallowLine) ? 0.25 : 0.0);
429
- } else {
430
- dst[3] = mix(dst[3], blendPix, (needBlend && doLineBlend) ?
431
- (haveShallowLine ? (haveSteepLine ? 1.0/3.0 : 0.25) : (haveSteepLine ? 0.25 : 0.0)) : 0.0);
432
- dst[12] = mix(dst[12], blendPix, (needBlend && doLineBlend && haveSteepLine) ? 0.25 : 0.0);
433
- dst[13] = mix(dst[13], blendPix, (needBlend && doLineBlend && haveSteepLine) ? 0.75 : 0.0);
434
- dst[14] = mix(dst[14], blendPix, needBlend ? (doLineBlend ?
435
- (haveSteepLine ? 1.0 : (haveShallowLine ? 0.75 : 0.5)) : NOLINEBLEND) : 0.0);
436
- dst[15] = mix(dst[15], blendPix, needBlend ? (doLineBlend ? 1.0 : LINEBLEND_CENTER) : 0.0);
437
- dst[4] = mix(dst[4], blendPix, needBlend ? (doLineBlend ?
438
- (haveShallowLine ? 1.0 : (haveSteepLine ? 0.75 : 0.5)) : NOLINEBLEND) : 0.0);
439
- dst[5] = mix(dst[5], blendPix, (needBlend && doLineBlend && haveShallowLine) ? 0.75 : 0.0);
440
- dst[6] = mix(dst[6], blendPix, (needBlend && doLineBlend && haveShallowLine) ? 0.25 : 0.0);
441
- }
442
- }
208
+ vec3 blendPix = (DistYCbCr(src[0], k1) <= DistYCbCr(src[0], k3)) ? k1 : k3;
209
+ float b1 = (needBlend && doLineBlend) ? (haveSteepLine ? 0.75 : (haveShallowLine ? 0.25 : 0.125)) : 0.0;
210
+ float b2 = needBlend ? (doLineBlend ? ((!haveShallowLine && !haveSteepLine) ? 0.875 : 1.0) : 0.4545939598) : 0.0;
211
+ float b3 = (needBlend && doLineBlend) ? (haveShallowLine ? 0.75 : (haveSteepLine ? 0.25 : 0.125)) : 0.0;
212
+ float b4 = (needBlend && doLineBlend && haveShallowLine) ? 0.25 : 0.0;
213
+ float b8 = (needBlend && doLineBlend && haveSteepLine) ? 0.25 : 0.0;
443
214
 
444
- // Map 4x4 to output with bilinear interpolation for 5x, 6x
445
- float scale4 = 4.0;
446
- float fx = f.x * scale4;
447
- float fy = f.y * scale4;
215
+ if (corner == 0) { dst[1]=mix(dst[1],blendPix,b1); dst[2]=mix(dst[2],blendPix,b2); dst[3]=mix(dst[3],blendPix,b3); dst[4]=mix(dst[4],blendPix,b4); dst[8]=mix(dst[8],blendPix,b8); }
216
+ else if (corner == 1) { dst[7]=mix(dst[7],blendPix,b1); dst[8]=mix(dst[8],blendPix,b2); dst[1]=mix(dst[1],blendPix,b3); dst[2]=mix(dst[2],blendPix,b4); dst[6]=mix(dst[6],blendPix,b8); }
217
+ else if (corner == 2) { dst[5]=mix(dst[5],blendPix,b1); dst[6]=mix(dst[6],blendPix,b2); dst[7]=mix(dst[7],blendPix,b3); dst[8]=mix(dst[8],blendPix,b4); dst[4]=mix(dst[4],blendPix,b8); }
218
+ else { dst[3]=mix(dst[3],blendPix,b1); dst[4]=mix(dst[4],blendPix,b2); dst[5]=mix(dst[5],blendPix,b3); dst[6]=mix(dst[6],blendPix,b4); dst[2]=mix(dst[2],blendPix,b8); }
219
+ }
220
+ }
221
+
222
+ vec3 res = mix(mix(dst[6], mix(dst[7], dst[8], step(two_third, f.x)), step(one_third, f.x)),
223
+ mix(mix(dst[5], mix(dst[0], dst[1], step(two_third, f.x)), step(one_third, f.x)),
224
+ mix(dst[4], mix(dst[3], dst[2], step(two_third, f.x)), step(one_third, f.x)), step(two_third, f.y)),
225
+ step(one_third, f.y));
226
+ outColor = vec4(res, tex(tc).a);
227
+ }`;
228
+
229
+ // 4x xBRZ fragment shader
230
+ const FRAG_4X = COMMON_HEADER + `
231
+ void main() {
232
+ vec2 texelSize = 1.0 / uInputRes;
233
+ vec2 srcPos = vUv * uInputRes;
234
+ vec2 srcPixel = floor(srcPos);
235
+ vec2 f = fract(srcPos);
236
+ vec2 tc = (srcPixel + 0.5) * texelSize;
237
+
238
+ vec3 src[25]; float v[9];
239
+ sampleNeighborhood(tc, texelSize.x, texelSize.y, src, v);
240
+ ivec4 blendResult = detectCorners(src, v);
241
+
242
+ // Output: 06|07|08|09 / 05|00|01|10 / 04|03|02|11 / 15|14|13|12
243
+ vec3 dst[16];
244
+ for (int i = 0; i < 16; i++) dst[i] = src[0];
245
+
246
+ if (IsBlendingNeeded(blendResult)) {
247
+ for (int corner = 0; corner < 4; corner++) {
248
+ vec3 k1, k3, k4, k5, k7, k8;
249
+ int blendIdx;
250
+ if (corner == 0) { k1=src[1]; k3=src[3]; k4=src[4]; k5=src[5]; k7=src[7]; k8=src[8]; blendIdx=2; }
251
+ else if (corner == 1) { k1=src[7]; k3=src[1]; k4=src[2]; k5=src[3]; k7=src[5]; k8=src[6]; blendIdx=1; }
252
+ else if (corner == 2) { k1=src[5]; k3=src[7]; k4=src[8]; k5=src[1]; k7=src[3]; k8=src[4]; blendIdx=0; }
253
+ else { k1=src[3]; k3=src[5]; k4=src[6]; k5=src[7]; k7=src[1]; k8=src[2]; blendIdx=3; }
448
254
 
449
- // 4x layout: 06|07|08|09
450
- // 05|00|01|10
451
- // 04|03|02|11
452
- // 15|14|13|12
453
- int mapping[16] = int[16](6, 7, 8, 9, 5, 0, 1, 10, 4, 3, 2, 11, 15, 14, 13, 12);
255
+ float vk0 = v[0], vk4 = reduce(k4), vk5 = reduce(k5), vk7 = reduce(k7), vk8 = reduce(k8);
256
+ float dist_01_04 = DistYCbCr(k1, k4);
257
+ float dist_03_08 = DistYCbCr(k3, k8);
258
+ bool haveShallowLine = (uSteepDirectionThreshold * dist_01_04 <= dist_03_08) && (vk0 != vk4) && (vk5 != vk4);
259
+ bool haveSteepLine = (uSteepDirectionThreshold * dist_03_08 <= dist_01_04) && (vk0 != vk8) && (vk7 != vk8);
260
+ bool needBlend = (blendResult[blendIdx] != BLEND_NONE);
261
+ bool doLineBlend = blendResult[blendIdx] >= BLEND_DOMINANT ||
262
+ !((blendResult[(blendIdx+3)%4] != BLEND_NONE && !IsPixEqual(src[0], k4)) ||
263
+ (blendResult[(blendIdx+1)%4] != BLEND_NONE && !IsPixEqual(src[0], k8)) ||
264
+ (IsPixEqual(k4, k3) && IsPixEqual(k3, k8) && !IsPixEqual(src[0], k3)));
454
265
 
455
- // Bilinear interpolation within 4x grid
456
- int xi = clamp(int(fx), 0, 3);
457
- int yi = clamp(int(fy), 0, 3);
458
- float xf = fract(fx);
459
- float yf = fract(fy);
266
+ vec3 blendPix = (DistYCbCr(src[0], k1) <= DistYCbCr(src[0], k3)) ? k1 : k3;
460
267
 
461
- int i00 = mapping[yi * 4 + xi];
462
- int i10 = mapping[yi * 4 + min(xi + 1, 3)];
463
- int i01 = mapping[min(yi + 1, 3) * 4 + xi];
464
- int i11 = mapping[min(yi + 1, 3) * 4 + min(xi + 1, 3)];
268
+ // 4x specific blend weights
269
+ float bCorner = (needBlend && doLineBlend) ? (haveShallowLine ? (haveSteepLine ? 1.0/3.0 : 0.25) : (haveSteepLine ? 0.25 : 0.0)) : 0.0;
270
+ float bSteep1 = (needBlend && doLineBlend && haveSteepLine) ? 0.25 : 0.0;
271
+ float bSteep2 = (needBlend && doLineBlend && haveSteepLine) ? 0.75 : 0.0;
272
+ float bEdge1 = needBlend ? (doLineBlend ? (haveSteepLine ? 1.0 : (haveShallowLine ? 0.75 : 0.5)) : 0.08677704501) : 0.0;
273
+ float bCenter = needBlend ? (doLineBlend ? 1.0 : 0.6848532563) : 0.0;
274
+ float bEdge2 = needBlend ? (doLineBlend ? (haveShallowLine ? 1.0 : (haveSteepLine ? 0.75 : 0.5)) : 0.08677704501) : 0.0;
275
+ float bShallow1 = (needBlend && doLineBlend && haveShallowLine) ? 0.75 : 0.0;
276
+ float bShallow2 = (needBlend && doLineBlend && haveShallowLine) ? 0.25 : 0.0;
465
277
 
466
- result = mix(mix(dst[i00], dst[i10], xf),
467
- mix(dst[i01], dst[i11], xf), yf);
278
+ if (corner == 0) {
279
+ dst[2]=mix(dst[2],blendPix,bCorner); dst[9]=mix(dst[9],blendPix,bSteep1); dst[10]=mix(dst[10],blendPix,bSteep2);
280
+ dst[11]=mix(dst[11],blendPix,bEdge1); dst[12]=mix(dst[12],blendPix,bCenter); dst[13]=mix(dst[13],blendPix,bEdge2);
281
+ dst[14]=mix(dst[14],blendPix,bShallow1); dst[15]=mix(dst[15],blendPix,bShallow2);
282
+ } else if (corner == 1) {
283
+ dst[1]=mix(dst[1],blendPix,bCorner); dst[6]=mix(dst[6],blendPix,bSteep1); dst[7]=mix(dst[7],blendPix,bSteep2);
284
+ dst[8]=mix(dst[8],blendPix,bEdge1); dst[9]=mix(dst[9],blendPix,bCenter); dst[10]=mix(dst[10],blendPix,bEdge2);
285
+ dst[11]=mix(dst[11],blendPix,bShallow1); dst[12]=mix(dst[12],blendPix,bShallow2);
286
+ } else if (corner == 2) {
287
+ dst[0]=mix(dst[0],blendPix,bCorner); dst[15]=mix(dst[15],blendPix,bSteep1); dst[4]=mix(dst[4],blendPix,bSteep2);
288
+ dst[5]=mix(dst[5],blendPix,bEdge1); dst[6]=mix(dst[6],blendPix,bCenter); dst[7]=mix(dst[7],blendPix,bEdge2);
289
+ dst[8]=mix(dst[8],blendPix,bShallow1); dst[9]=mix(dst[9],blendPix,bShallow2);
290
+ } else {
291
+ dst[3]=mix(dst[3],blendPix,bCorner); dst[12]=mix(dst[12],blendPix,bSteep1); dst[13]=mix(dst[13],blendPix,bSteep2);
292
+ dst[14]=mix(dst[14],blendPix,bEdge1); dst[15]=mix(dst[15],blendPix,bCenter); dst[4]=mix(dst[4],blendPix,bEdge2);
293
+ dst[5]=mix(dst[5],blendPix,bShallow1); dst[6]=mix(dst[6],blendPix,bShallow2);
294
+ }
468
295
  }
469
296
  }
470
297
 
471
- outColor = vec4(result, tex(tc).a);
298
+ vec3 res = mix(mix(mix(mix(dst[6], dst[7], step(0.25, f.x)), mix(dst[8], dst[9], step(0.75, f.x)), step(0.5, f.x)),
299
+ mix(mix(dst[5], dst[0], step(0.25, f.x)), mix(dst[1], dst[10], step(0.75, f.x)), step(0.5, f.x)), step(0.25, f.y)),
300
+ mix(mix(mix(dst[4], dst[3], step(0.25, f.x)), mix(dst[2], dst[11], step(0.75, f.x)), step(0.5, f.x)),
301
+ mix(mix(dst[15], dst[14], step(0.25, f.x)), mix(dst[13], dst[12], step(0.75, f.x)), step(0.5, f.x)), step(0.75, f.y)),
302
+ step(0.5, f.y));
303
+ outColor = vec4(res, tex(tc).a);
472
304
  }`;
473
305
 
306
+ // 5x and 6x use 4x with interpolation
307
+ const FRAG_5X = FRAG_4X;
308
+ const FRAG_6X = FRAG_4X;
309
+
474
310
  /** xBRZ GPU Renderer */
475
311
  export class XbrzGpuRenderer implements Renderer<XbrzOptions> {
476
312
  private gl: WebGL2RenderingContext | null = null;
477
313
  private canvas: OffscreenCanvas | null = null;
478
- private program: WebGLProgram | null = null;
314
+ private programs: Map<number, WebGLProgram> = new Map();
479
315
  private texture: WebGLTexture | null = null;
480
- private uniforms: Record<string, WebGLUniformLocation | null> = {};
316
+ private uniforms: Map<number, Record<string, WebGLUniformLocation | null>> = new Map();
481
317
  private initialized = false;
482
318
  private currentCanvasSize = { width: 0, height: 0 };
483
319
  private currentTexSize = { width: 0, height: 0 };
320
+ private currentScale = 0;
484
321
 
485
- /** Create a new xBRZ GPU renderer */
486
322
  static create(): XbrzGpuRenderer {
487
323
  const renderer = new XbrzGpuRenderer();
488
324
  renderer.init();
@@ -490,72 +326,46 @@ export class XbrzGpuRenderer implements Renderer<XbrzOptions> {
490
326
  }
491
327
 
492
328
  private init(): void {
493
- if (typeof OffscreenCanvas === 'undefined') {
494
- throw new Error('OffscreenCanvas not supported');
495
- }
496
-
329
+ if (typeof OffscreenCanvas === 'undefined') throw new Error('OffscreenCanvas not supported');
497
330
  this.canvas = new OffscreenCanvas(1, 1);
498
- this.gl = this.canvas.getContext('webgl2', {
499
- alpha: true,
500
- premultipliedAlpha: false,
501
- desynchronized: true,
502
- powerPreference: 'high-performance',
503
- antialias: false,
504
- });
505
-
506
- if (!this.gl) {
507
- throw new Error('WebGL2 not supported');
508
- }
331
+ this.gl = this.canvas.getContext('webgl2', { alpha: true, premultipliedAlpha: false, desynchronized: true, powerPreference: 'high-performance', antialias: false });
332
+ if (!this.gl) throw new Error('WebGL2 not supported');
509
333
 
510
334
  const gl = this.gl;
511
-
512
- // Create shaders
513
335
  const vs = this.createShader(gl.VERTEX_SHADER, VERTEX_SHADER);
514
- const fs = this.createShader(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
515
-
516
- // Create program
517
- this.program = gl.createProgram()!;
518
- gl.attachShader(this.program, vs);
519
- gl.attachShader(this.program, fs);
520
- gl.linkProgram(this.program);
521
-
522
- if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
523
- throw new Error('Shader program link failed: ' + gl.getProgramInfoLog(this.program));
336
+ const shaders: Record<number, string> = { 2: FRAG_2X, 3: FRAG_3X, 4: FRAG_4X, 5: FRAG_5X, 6: FRAG_6X };
337
+
338
+ for (const [scale, fragSource] of Object.entries(shaders)) {
339
+ const fs = this.createShader(gl.FRAGMENT_SHADER, fragSource);
340
+ const program = gl.createProgram()!;
341
+ gl.attachShader(program, vs);
342
+ gl.attachShader(program, fs);
343
+ gl.linkProgram(program);
344
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(`Shader link failed for ${scale}x: ` + gl.getProgramInfoLog(program));
345
+ this.programs.set(Number(scale), program);
346
+ this.uniforms.set(Number(scale), {
347
+ uTex: gl.getUniformLocation(program, 'uTex'),
348
+ uInputRes: gl.getUniformLocation(program, 'uInputRes'),
349
+ uLuminanceWeight: gl.getUniformLocation(program, 'uLuminanceWeight'),
350
+ uEqualColorTolerance: gl.getUniformLocation(program, 'uEqualColorTolerance'),
351
+ uSteepDirectionThreshold: gl.getUniformLocation(program, 'uSteepDirectionThreshold'),
352
+ uDominantDirectionThreshold: gl.getUniformLocation(program, 'uDominantDirectionThreshold'),
353
+ });
524
354
  }
525
355
 
526
- gl.useProgram(this.program);
527
-
528
- // Get uniform locations
529
- this.uniforms = {
530
- uTex: gl.getUniformLocation(this.program, 'uTex'),
531
- uInputRes: gl.getUniformLocation(this.program, 'uInputRes'),
532
- uOutputRes: gl.getUniformLocation(this.program, 'uOutputRes'),
533
- uScale: gl.getUniformLocation(this.program, 'uScale'),
534
- uLuminanceWeight: gl.getUniformLocation(this.program, 'uLuminanceWeight'),
535
- uEqualColorTolerance: gl.getUniformLocation(this.program, 'uEqualColorTolerance'),
536
- uSteepDirectionThreshold: gl.getUniformLocation(this.program, 'uSteepDirectionThreshold'),
537
- uDominantDirectionThreshold: gl.getUniformLocation(this.program, 'uDominantDirectionThreshold'),
538
- };
539
-
540
- gl.uniform1i(this.uniforms.uTex, 0);
541
-
542
- // Setup geometry (fullscreen triangle)
543
356
  const buf = gl.createBuffer();
544
357
  gl.bindBuffer(gl.ARRAY_BUFFER, buf);
545
358
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
546
359
  gl.enableVertexAttribArray(0);
547
360
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
548
361
 
549
- // Create texture with NEAREST filtering for pixel art
550
362
  this.texture = gl.createTexture();
551
363
  gl.bindTexture(gl.TEXTURE_2D, this.texture);
552
364
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
553
365
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
554
366
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
555
367
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
556
-
557
368
  gl.clearColor(0.0, 0.0, 0.0, 0.0);
558
-
559
369
  this.initialized = true;
560
370
  }
561
371
 
@@ -564,37 +374,36 @@ export class XbrzGpuRenderer implements Renderer<XbrzOptions> {
564
374
  const shader = gl.createShader(type)!;
565
375
  gl.shaderSource(shader, source);
566
376
  gl.compileShader(shader);
567
-
568
377
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
569
378
  const info = gl.getShaderInfoLog(shader);
570
379
  gl.deleteShader(shader);
571
380
  throw new Error('Shader compile failed: ' + info);
572
381
  }
573
-
574
382
  return shader;
575
383
  }
576
384
 
577
- /** Check if renderer is ready */
578
- isReady(): boolean {
579
- return this.initialized;
580
- }
385
+ isReady(): boolean { return this.initialized; }
581
386
 
582
- /** Render xBRZ effect */
583
387
  render(input: ImageInput | ImageData, options: XbrzOptions = {}): ImageOutput {
584
- if (!this.initialized || !this.gl || !this.canvas) {
585
- throw new Error('Renderer not initialized');
586
- }
587
-
388
+ if (!this.initialized || !this.gl || !this.canvas) throw new Error('Renderer not initialized');
588
389
  const gl = this.gl;
589
390
  const data = input instanceof ImageData ? input.data : input.data;
590
391
  const width = input.width;
591
392
  const height = input.height;
592
-
593
393
  const scale = Math.min(6, Math.max(2, options.scale ?? 2));
594
394
  const outWidth = width * scale;
595
395
  const outHeight = height * scale;
596
396
 
597
- // Resize canvas if needed
397
+ const program = this.programs.get(scale);
398
+ const uniforms = this.uniforms.get(scale);
399
+ if (!program || !uniforms) throw new Error(`No program for scale ${scale}`);
400
+
401
+ if (this.currentScale !== scale) {
402
+ gl.useProgram(program);
403
+ gl.uniform1i(uniforms.uTex, 0);
404
+ this.currentScale = scale;
405
+ }
406
+
598
407
  if (this.currentCanvasSize.width !== outWidth || this.currentCanvasSize.height !== outHeight) {
599
408
  this.canvas.width = outWidth;
600
409
  this.canvas.height = outHeight;
@@ -602,16 +411,12 @@ export class XbrzGpuRenderer implements Renderer<XbrzOptions> {
602
411
  gl.viewport(0, 0, outWidth, outHeight);
603
412
  }
604
413
 
605
- // Update uniforms
606
- gl.uniform2f(this.uniforms.uInputRes, width, height);
607
- gl.uniform2f(this.uniforms.uOutputRes, outWidth, outHeight);
608
- gl.uniform1i(this.uniforms.uScale, scale);
609
- gl.uniform1f(this.uniforms.uLuminanceWeight, options.luminanceWeight ?? 1.0);
610
- gl.uniform1f(this.uniforms.uEqualColorTolerance, (options.equalColorTolerance ?? 30) / 255.0);
611
- gl.uniform1f(this.uniforms.uSteepDirectionThreshold, options.steepDirectionThreshold ?? 2.2);
612
- gl.uniform1f(this.uniforms.uDominantDirectionThreshold, options.dominantDirectionThreshold ?? 3.6);
414
+ gl.uniform2f(uniforms.uInputRes, width, height);
415
+ gl.uniform1f(uniforms.uLuminanceWeight, options.luminanceWeight ?? 1.0);
416
+ gl.uniform1f(uniforms.uEqualColorTolerance, (options.equalColorTolerance ?? 30) / 255.0);
417
+ gl.uniform1f(uniforms.uSteepDirectionThreshold, options.steepDirectionThreshold ?? 2.2);
418
+ gl.uniform1f(uniforms.uDominantDirectionThreshold, options.dominantDirectionThreshold ?? 3.6);
613
419
 
614
- // Update texture
615
420
  gl.bindTexture(gl.TEXTURE_2D, this.texture);
616
421
  if (this.currentTexSize.width !== width || this.currentTexSize.height !== height) {
617
422
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
@@ -620,52 +425,30 @@ export class XbrzGpuRenderer implements Renderer<XbrzOptions> {
620
425
  gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
621
426
  }
622
427
 
623
- // Render
624
428
  gl.clear(gl.COLOR_BUFFER_BIT);
625
429
  gl.drawArrays(gl.TRIANGLES, 0, 3);
626
430
 
627
- // Read pixels
628
431
  const pixels = new Uint8ClampedArray(outWidth * outHeight * 4);
629
432
  gl.readPixels(0, 0, outWidth, outHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
630
-
631
- return {
632
- data: pixels,
633
- width: outWidth,
634
- height: outHeight,
635
- };
433
+ return { data: pixels, width: outWidth, height: outHeight };
636
434
  }
637
435
 
638
- /** Dispose resources */
639
436
  dispose(): void {
640
437
  if (this.gl) {
641
438
  if (this.texture) this.gl.deleteTexture(this.texture);
642
- if (this.program) this.gl.deleteProgram(this.program);
439
+ for (const program of this.programs.values()) this.gl.deleteProgram(program);
643
440
  this.gl = null;
644
441
  }
645
442
  this.canvas = null;
443
+ this.programs.clear();
444
+ this.uniforms.clear();
646
445
  this.initialized = false;
647
446
  }
648
447
  }
649
448
 
650
- /** xBRZ presets */
651
449
  export const XBRZ_PRESETS: Record<string, Partial<XbrzOptions>> = {
652
450
  default: {},
653
- sharp: {
654
- luminanceWeight: 1.0,
655
- equalColorTolerance: 20,
656
- steepDirectionThreshold: 2.0,
657
- dominantDirectionThreshold: 3.2,
658
- },
659
- smooth: {
660
- luminanceWeight: 1.0,
661
- equalColorTolerance: 40,
662
- steepDirectionThreshold: 2.4,
663
- dominantDirectionThreshold: 4.0,
664
- },
665
- colorful: {
666
- luminanceWeight: 0.5,
667
- equalColorTolerance: 30,
668
- steepDirectionThreshold: 2.2,
669
- dominantDirectionThreshold: 3.6,
670
- },
451
+ sharp: { luminanceWeight: 1.0, equalColorTolerance: 20, steepDirectionThreshold: 2.0, dominantDirectionThreshold: 3.2 },
452
+ smooth: { luminanceWeight: 1.0, equalColorTolerance: 40, steepDirectionThreshold: 2.4, dominantDirectionThreshold: 4.0 },
453
+ colorful: { luminanceWeight: 0.5, equalColorTolerance: 30, steepDirectionThreshold: 2.2, dominantDirectionThreshold: 3.6 },
671
454
  };