xbrz.wgsl777 lines · 19.4 KB · raw
1// ****************************************************************************
2// * This file is part of the xBRZ project. It is distributed under           *
3// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0         *
4// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved          *
5// *                                                                          *
6// * Additionally and as a special exception, the author gives permission     *
7// * to link the code of this program with the following libraries            *
8// * (or with modified versions that use the same licenses), and distribute   *
9// * linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe *
10// *                                                                          *
11// * You must obey the GNU General Public License in all respects for all of  *
12// * the code used other than MAME, FreeFileSync, Snes9x, ePSXe.              *
13// * If you modify this file, you may extend this exception to your version   *
14// * of the file, but you are not obligated to do so. If you do not wish to   *
15// * do so, delete this exception statement from your version.                *
16// ****************************************************************************
17
18// WGSL shader port of xBRZ v1.9
19// Copyright (C) 2026 James Groth
20
21// Assumed to be between 2 and 6
22override scale_factor: i32;
23
24override equal_color_tolerance: f32 = 30.0 / 255.0;
25override center_direction_bias: f32 = 4.0;
26override dominant_direction_threshold: f32 = 3.6;
27override steep_direction_threshold: f32 = 2.4;
28
29@group(0) @binding(0) var input_frame: texture_2d<f32>;
30@group(0) @binding(1) var output_frame: texture_storage_2d<rgba8unorm, write>;
31
32// 5x5 grid of pixels with the pixel at (X, Y) in the center
33var<private> input_pixels: array<array<vec3f, 5>, 5>;
34
35// NxN grid of pixels to output starting at (N*X, N*Y)
36var<private> output_pixels: array<array<vec3f, 6>, 6>;
37
38const BLEND_NONE: u32 = 0;
39const BLEND_NORMAL: u32 = 1;
40const BLEND_DOMINANT: u32 = 2;
41
42// Computed blend mode in each diagonal from the center pixel
43struct BlendModes {
44    tl: u32,
45    tr: u32,
46    bl: u32,
47    br: u32,
48}
49
50fn any_blend_mode(blend: BlendModes) -> bool {
51    return any(vec4u(blend.tl, blend.tr, blend.bl, blend.br) != vec4u(BLEND_NONE));
52}
53
54@compute @workgroup_size(16, 16, 1)
55fn xbrz(@builtin(global_invocation_id) invocation: vec3u) {
56    let position = vec2i(invocation.xy);
57    let input_size = vec2i(textureDimensions(input_frame));
58    if any(position >= input_size) {
59        return;
60    }
61
62    // Load input pixels surrounding current position
63    load_input(position, input_size);
64
65    // Initialize output full of center input pixel
66    for (var i = 0; i < scale_factor; i++) {
67        for (var j = 0; j < scale_factor; j++) {
68            output_pixels[i][j] = input_pixels[2][2];
69        }
70    }
71
72    // Compute blend mode for each diagonal
73    var blend_modes = compute_blend_modes();
74
75    if any_blend_mode(blend_modes) {
76        // Perform blending in each diagonal direction
77
78        // Bottom-right diagonal
79        if blend_modes.br != BLEND_NONE {
80            // - - - - -
81            // - - B C -
82            // - D E F -
83            // - G H I -
84            // - - - - -
85            blend_pixel(
86                blend_modes,
87                input_pixels[1][2],
88                input_pixels[1][3],
89                input_pixels[2][1],
90                input_pixels[2][2],
91                input_pixels[2][3],
92                input_pixels[3][1],
93                input_pixels[3][2],
94                input_pixels[3][3],
95            );
96        }
97
98        rotate_output();
99        blend_modes = rotate_blend_modes(blend_modes);
100
101        // Top-right diagonal
102        if blend_modes.br != BLEND_NONE {
103            // - - - - -
104            // - C F I -
105            // - B E H -
106            // - - D G -
107            // - - - - -
108            blend_pixel(
109                blend_modes,
110                input_pixels[2][1],
111                input_pixels[1][1],
112                input_pixels[3][2],
113                input_pixels[2][2],
114                input_pixels[1][2],
115                input_pixels[3][3],
116                input_pixels[2][3],
117                input_pixels[1][3],
118            );
119        }
120
121        rotate_output();
122        blend_modes = rotate_blend_modes(blend_modes);
123
124        // Top-left diagonal
125        if blend_modes.br != BLEND_NONE {
126            // - - - - -
127            // - I H G -
128            // - F E D -
129            // - C B - -
130            // - - - - -
131            blend_pixel(
132                blend_modes,
133                input_pixels[3][2],
134                input_pixels[3][1],
135                input_pixels[2][3],
136                input_pixels[2][2],
137                input_pixels[2][1],
138                input_pixels[1][3],
139                input_pixels[1][2],
140                input_pixels[1][1],
141            );
142        }
143
144        rotate_output();
145        blend_modes = rotate_blend_modes(blend_modes);
146
147        // Bottom-left diagonal
148        if blend_modes.br != BLEND_NONE {
149            // - - - - -
150            // - G D - -
151            // - H E B -
152            // - I F C -
153            // - - - - -
154            blend_pixel(
155                blend_modes,
156                input_pixels[2][3],
157                input_pixels[3][3],
158                input_pixels[1][2],
159                input_pixels[2][2],
160                input_pixels[3][2],
161                input_pixels[1][1],
162                input_pixels[2][1],
163                input_pixels[3][1],
164            );
165        }
166
167        rotate_output();
168    }
169
170    // Write output pixels
171    let out_pos_tl = scale_factor * position;
172    for (var y = 0; y < scale_factor; y++) {
173        for (var x = 0; x < scale_factor; x++) {
174            let output_pixel = output_pixels[y][x];
175            textureStore(output_frame, out_pos_tl + vec2i(x, y), vec4f(output_pixel, 1.0));
176        }
177    }
178}
179
180// Load the 5x5 grid of pixels surrounding (X, Y), clamping to edge
181fn load_input(position: vec2i, input_size: vec2i) {
182    let upper_bound = input_size - 1;
183
184    for (var dy = -2; dy <= 2; dy++) {
185        for (var dx = -2; dx <= 2; dx++) {
186            if abs(dx) + abs(dy) == 4 {
187                // The 4 corner pixels are not used; don't bother to load
188                continue;
189            }
190
191            let input_pixel = textureLoad(
192                input_frame,
193                clamp(position + vec2i(dx, dy), vec2i(0), upper_bound),
194                0,
195            ).rgb;
196            input_pixels[dy + 2][dx + 2] = input_pixel;
197        }
198    }
199}
200
201// Rotate output matrix 90 degrees clockwise
202fn rotate_output() {
203    var rotated: array<array<vec3f, 6>, 6>;
204
205    for (var i = 0; i < scale_factor; i++) {
206        for (var j = 0; j < scale_factor; j++) {
207            rotated[i][j] = output_pixels[scale_factor - 1 - j][i];
208        }
209    }
210
211    output_pixels = rotated;
212}
213
214// Rotate blend modes 90 degrees clockwise
215fn rotate_blend_modes(blend: BlendModes) -> BlendModes {
216    var rotated: BlendModes;
217    rotated.tl = blend.bl;
218    rotated.tr = blend.tl;
219    rotated.br = blend.tr;
220    rotated.bl = blend.br;
221    return rotated;
222}
223
224// https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.2020_conversion
225const K_B: f32 = 0.0593;
226const K_R: f32 = 0.2627;
227const K_G: f32 = 1.0 - K_B - K_R;
228const RGB_TO_YCBCR: mat3x3f = mat3x3f(
229    vec3f(K_R, -0.5 * K_R / (1.0 - K_B),           0.5           ),
230    vec3f(K_G, -0.5 * K_G / (1.0 - K_B), -0.5 * K_G / (1.0 - K_R)),
231    vec3f(K_B,           0.5           , -0.5 * K_B / (1.0 - K_R)),
232);
233
234fn color_distance(c0: vec3f, c1: vec3f) -> f32 {
235    let diff_ycbcr = RGB_TO_YCBCR * (c0 - c1);
236    return sqrt(dot(diff_ycbcr, diff_ycbcr));
237}
238
239fn equal_within_tolerance(c0: vec3f, c1: vec3f) -> bool {
240    return color_distance(c0, c1) < equal_color_tolerance;
241}
242
243fn compute_blend_modes() -> BlendModes {
244    var blend: BlendModes;
245    blend.tl = compute_blend_mode(0, 0);
246    blend.tr = compute_blend_mode(0, 1);
247    blend.bl = compute_blend_mode(1, 0);
248    blend.br = compute_blend_mode(1, 1);
249    return blend;
250}
251
252// Compute the blend mode for a single diagonal
253fn compute_blend_mode(row: i32, col: i32) -> u32 {
254    // - B C -
255    // D E F O
256    // G H I N
257    // - K L -
258
259    let e = input_pixels[row + 1][col + 1];
260    let f = input_pixels[row + 1][col + 2];
261    let h = input_pixels[row + 2][col + 1];
262    let i = input_pixels[row + 2][col + 2];
263
264    if all((e == f) & (h == i)) || all((e == h) & (f == i)) {
265        return BLEND_NONE;
266    }
267
268    let b = input_pixels[row + 0][col + 1];
269    let c = input_pixels[row + 0][col + 2];
270    let d = input_pixels[row + 1][col + 0];
271    let g = input_pixels[row + 2][col + 0];
272    let k = input_pixels[row + 3][col + 1];
273    let l = input_pixels[row + 3][col + 2];
274    let n = input_pixels[row + 2][col + 3];
275    let o = input_pixels[row + 1][col + 3];
276
277    let hf =
278          color_distance(g, e)
279        + color_distance(e, c)
280        + color_distance(k, i)
281        + color_distance(i, o)
282        + center_direction_bias * color_distance(h, f);
283    let ei =
284          color_distance(d, h)
285        + color_distance(h, l)
286        + color_distance(b, f)
287        + color_distance(f, n)
288        + center_direction_bias * color_distance(e, i);
289
290    switch (2 * row + col) {
291        case 0: {
292            // (0, 0): Top-left diagonal from I
293            if hf < ei && any(i != h) && any(i != f) {
294                let dominant = dominant_direction_threshold * hf < ei;
295                return select(BLEND_NORMAL, BLEND_DOMINANT, dominant);
296            }
297        }
298        case 1: {
299            // (0, 1): Top-right diagonal from H
300            if ei < hf && any(h != e) && any(h != i) {
301                let dominant = dominant_direction_threshold * ei < hf;
302                return select(BLEND_NORMAL, BLEND_DOMINANT, dominant);
303            }
304        }
305        case 2: {
306            // (1, 0): Bottom-left diagonal from F
307            if ei < hf && any(f != e) && any(f != i) {
308                let dominant = dominant_direction_threshold * ei < hf;
309                return select(BLEND_NORMAL, BLEND_DOMINANT, dominant);
310            }
311        }
312        case 3: {
313            // (1, 1): Bottom-right diagonal from E
314            if hf < ei && any(e != f) && any(e != h) {
315                let dominant = dominant_direction_threshold * hf < ei;
316                return select(BLEND_NORMAL, BLEND_DOMINANT, dominant);
317            }
318        }
319        default: {}
320    }
321
322    return BLEND_NONE;
323}
324
325// Perform blending in the bottom-right diagonal from the center pixel
326fn blend_pixel(
327    blend: BlendModes,
328    b: vec3f,
329    c: vec3f,
330    d: vec3f,
331    e: vec3f,
332    f: vec3f,
333    g: vec3f,
334    h: vec3f,
335    i: vec3f,
336) {
337    // - - - - -
338    // - - B C -
339    // - D E F -
340    // - G H I -
341    // - - - - -
342
343    let do_line_blend = (blend.br == BLEND_DOMINANT) || !(
344        (blend.tr != BLEND_NONE && !equal_within_tolerance(e, g))
345            || (blend.bl != BLEND_NONE && !equal_within_tolerance(e, c))
346            || (!equal_within_tolerance(e, i)
347                && equal_within_tolerance(g, h)
348                && equal_within_tolerance(h, i)
349                && equal_within_tolerance(i, f)
350                && equal_within_tolerance(f, c))
351    );
352
353    let fg = color_distance(f, g);
354    let hc = color_distance(h, c);
355
356    let shallow_line = do_line_blend && steep_direction_threshold * fg <= hc && any(e != g) && any(d != g);
357    let steep_line   = do_line_blend && steep_direction_threshold * hc <= fg && any(e != c) && any(b != c);
358
359    let blend_color = select(h, f, color_distance(e, f) <= color_distance(e, h)); //choose most similar color
360
361    switch (scale_factor) {
362        case 2: {
363            scale_pixel_2x(blend_color, do_line_blend, shallow_line, steep_line);
364        }
365        case 3: {
366            scale_pixel_3x(blend_color, do_line_blend, shallow_line, steep_line);
367        }
368        case 4: {
369            scale_pixel_4x(blend_color, do_line_blend, shallow_line, steep_line);
370        }
371        case 5: {
372            scale_pixel_5x(blend_color, do_line_blend, shallow_line, steep_line);
373        }
374        case 6: {
375            scale_pixel_6x(blend_color, do_line_blend, shallow_line, steep_line);
376        }
377        default: {}
378    }
379}
380
381// Alpha blend into output pixel at [i][j], in linear color space
382fn alpha_blend(i: u32, j: u32, color: vec3f, alpha: f32) {
383    let back = srgb_to_linear(output_pixels[i][j]);
384    let front = srgb_to_linear(color);
385    let blended = (1.0 - alpha) * back + alpha * front;
386    output_pixels[i][j] = linear_to_srgb(blended);
387}
388
389fn srgb_to_linear(c: vec3f) -> vec3f {
390    return pow(c, vec3f(2.2));
391}
392
393fn linear_to_srgb(c: vec3f) -> vec3f {
394    return pow(c, vec3f(1.0 / 2.2));
395}
396
397fn scale_pixel_2x(c: vec3f, do_line_blend: bool, shallow_line: bool, steep_line: bool) {
398    alpha_blend(0, 1, c, select(
399        0.0,
400        1.0 / 4.0,
401        steep_line,
402    ));
403
404    alpha_blend(1, 0, c, select(
405        0.0,
406        1.0 / 4.0,
407        shallow_line,
408    ));
409
410    alpha_blend(1, 1, c, select(
411        select(
412            select(
413                0.2146018366,
414                1.0 / 2.0,
415                do_line_blend,
416            ),
417            3.0 / 4.0,
418            steep_line || shallow_line,
419        ),
420        5.0 / 6.0,
421        steep_line && shallow_line,
422    ));
423}
424
425fn scale_pixel_3x(c: vec3f, do_line_blend: bool, shallow_line: bool, steep_line: bool) {
426    alpha_blend(0, 2, c, select(
427        0.0,
428        1.0 / 4.0,
429        steep_line,
430    ));
431
432    alpha_blend(2, 0, c, select(
433        0.0,
434        1.0 / 4.0,
435        shallow_line,
436    ));
437
438    alpha_blend(1, 2, c, select(
439        select(
440            select(
441                0.0,
442                1.0 / 8.0,
443                do_line_blend,
444            ),
445            1.0 / 4.0,
446            shallow_line,
447        ),
448        3.0 / 4.0,
449        steep_line,
450    ));
451
452    alpha_blend(2, 1, c, select(
453        select(
454            select(
455                0.0,
456                1.0 / 8.0,
457                do_line_blend,
458            ),
459            1.0 / 4.0,
460            steep_line,
461        ),
462        3.0 / 4.0,
463        shallow_line,
464    ));
465
466    alpha_blend(2, 2, c, select(
467        select(
468            0.4545939598,
469            7.0 / 8.0,
470            do_line_blend,
471        ),
472        1.0,
473        shallow_line || steep_line,
474    ));
475}
476
477fn scale_pixel_4x(c: vec3f, do_line_blend: bool, shallow_line: bool, steep_line: bool) {
478    alpha_blend(3, 0, c, select(
479        0.0,
480        1.0 / 4.0,
481        shallow_line,
482    ));
483
484    alpha_blend(0, 3, c, select(
485        0.0,
486        1.0 / 4.0,
487        steep_line,
488    ));
489
490    alpha_blend(3, 1, c, select(
491        0.0,
492        3.0 / 4.0,
493        shallow_line,
494    ));
495
496    alpha_blend(1, 3, c, select(
497        0.0,
498        3.0 / 4.0,
499        steep_line,
500    ));
501
502    alpha_blend(3, 2, c, select(
503        select(
504            select(
505                0.08677704501,
506                1.0 / 2.0,
507                do_line_blend,
508            ),
509            3.0 / 4.0,
510            steep_line,
511        ),
512        1.0,
513        shallow_line,
514    ));
515
516    alpha_blend(2, 3, c, select(
517        select(
518            select(
519                0.08677704501,
520                1.0 / 2.0,
521                do_line_blend,
522            ),
523            3.0 / 4.0,
524            shallow_line,
525        ),
526        1.0,
527        steep_line,
528    ));
529
530    alpha_blend(2, 2, c, select(
531        select(
532            0.0,
533            1.0 / 4.0,
534            steep_line || shallow_line,
535        ),
536        1.0 / 3.0,
537        steep_line && shallow_line,
538    ));
539
540    alpha_blend(3, 3, c, select(
541        0.6848532563,
542        1.0,
543        do_line_blend,
544    ));
545}
546
547fn scale_pixel_5x(c: vec3f, do_line_blend: bool, shallow_line: bool, steep_line: bool) {
548    alpha_blend(4, 0, c, select(
549        0.0,
550        1.0 / 4.0,
551        shallow_line,
552    ));
553
554    alpha_blend(0, 4, c, select(
555        0.0,
556        1.0 / 4.0,
557        steep_line,
558    ));
559
560    alpha_blend(3, 2, c, select(
561        0.0,
562        1.0 / 4.0,
563        shallow_line,
564    ));
565
566    alpha_blend(2, 3, c, select(
567        0.0,
568        1.0 / 4.0,
569        steep_line,
570    ));
571
572    alpha_blend(4, 1, c, select(
573        0.0,
574        3.0 / 4.0,
575        shallow_line,
576    ));
577
578    alpha_blend(1, 4, c, select(
579        0.0,
580        3.0 / 4.0,
581        steep_line,
582    ));
583
584    alpha_blend(3, 3, c, select(
585        select(
586            select(
587                0.0,
588                1.0 / 8.0,
589                do_line_blend,
590            ),
591            3.0 / 4.0,
592            steep_line || shallow_line,
593        ),
594        2.0 / 3.0,
595        steep_line && shallow_line,
596    ));
597
598    alpha_blend(2, 4, c, select(
599        select(
600            select(
601                0.0,
602                1.0 / 8.0,
603                do_line_blend,
604            ),
605            1.0 / 4.0,
606            shallow_line,
607        ),
608        1.0,
609        steep_line,
610    ));
611
612    alpha_blend(4, 2, c, select(
613        select(
614            select(
615                0.0,
616                1.0 / 8.0,
617                do_line_blend,
618            ),
619            1.0 / 4.0,
620            steep_line,
621        ),
622        1.0,
623        shallow_line,
624    ));
625
626    alpha_blend(4, 3, c, select(
627        select(
628            0.2306749731,
629            7.0 / 8.0,
630            do_line_blend,
631        ),
632        1.0,
633        steep_line || shallow_line,
634    ));
635
636    alpha_blend(3, 4, c, select(
637        select(
638            0.2306749731,
639            7.0 / 8.0,
640            do_line_blend,
641        ),
642        1.0,
643        steep_line || shallow_line,
644    ));
645
646    alpha_blend(4, 4, c, select(
647        0.8631434088,
648        1.0,
649        do_line_blend,
650    ));
651}
652
653fn scale_pixel_6x(c: vec3f, do_line_blend: bool, shallow_line: bool, steep_line: bool) {
654    alpha_blend(0, 5, c, select(
655        0.0,
656        1.0 / 4.0,
657        steep_line,
658    ));
659
660    alpha_blend(5, 0, c, select(
661        0.0,
662        1.0 / 4.0,
663        shallow_line,
664    ));
665
666    alpha_blend(2, 4, c, select(
667        0.0,
668        1.0 / 4.0,
669        steep_line,
670    ));
671
672    alpha_blend(4, 2, c, select(
673        0.0,
674        1.0 / 4.0,
675        shallow_line,
676    ));
677
678    alpha_blend(1, 5, c, select(
679        0.0,
680        3.0 / 4.0,
681        steep_line,
682    ));
683
684    alpha_blend(5, 1, c, select(
685        0.0,
686        3.0 / 4.0,
687        shallow_line,
688    ));
689
690    alpha_blend(3, 4, c, select(
691        select(
692            0.0,
693            1.0 / 4.0,
694            shallow_line,
695        ),
696        3.0 / 4.0,
697        steep_line,
698    ));
699
700    alpha_blend(4, 3, c, select(
701        select(
702            0.0,
703            1.0 / 4.0,
704            steep_line,
705        ),
706        3.0 / 4.0,
707        shallow_line,
708    ));
709
710    alpha_blend(2, 5, c, select(
711        0.0,
712        1.0,
713        steep_line,
714    ));
715
716    alpha_blend(5, 2, c, select(
717        0.0,
718        1.0,
719        shallow_line,
720    ));
721
722    alpha_blend(3, 5, c, select(
723        select(
724            select(
725                0.05652034508,
726                1.0 / 2.0,
727                do_line_blend,
728            ),
729            3.0 / 4.0,
730            shallow_line,
731        ),
732        1.0,
733        steep_line,
734    ));
735
736    alpha_blend(5, 3, c, select(
737        select(
738            select(
739                0.05652034508,
740                1.0 / 2.0,
741                do_line_blend,
742            ),
743            3.0 / 4.0,
744            steep_line,
745        ),
746        1.0,
747        shallow_line,
748    ));
749
750    alpha_blend(4, 5, c, select(
751        0.4236372243,
752        1.0,
753        do_line_blend,
754    ));
755
756    alpha_blend(5, 4, c, select(
757        0.4236372243,
758        1.0,
759        do_line_blend,
760    ));
761
762    alpha_blend(4, 4, c, select(
763        select(
764            0.0,
765            1.0 / 2.0,
766            do_line_blend,
767        ),
768        1.0,
769        steep_line || shallow_line,
770    ));
771
772    alpha_blend(5, 5, c, select(
773        0.9711013910,
774        1.0,
775        do_line_blend,
776    ));
777}