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}