mmpx.wgsl222 lines · 6.9 KB · raw
1/*
2   Copyright 2020 Morgan McGuire & Mara Gagiu.
3   Provided under the Open Source MIT license https://opensource.org/licenses/MIT
4*/
5
6// WGSL port of MMPX GLSL
7// Copyright (C) 2026 James Groth
8
9// MMPX pixel art magnification algorithm: https://casual-effects.com/research/McGuire2021PixelArt/index.html
10
11@group(0) @binding(0) var input_frame: texture_2d<f32>;
12@group(0) @binding(1) var output_frame: texture_storage_2d<rgba8unorm, write>;
13
14var<private> input_size: vec2i;
15
16fn load_input(position: vec2i) -> vec3f {
17    return textureLoad(
18        input_frame,
19        clamp(position, vec2i(0), input_size - 1),
20        0,
21    ).rgb;
22}
23
24// Coefficients used for Y in RGB-to-YUV conversion
25const RGB_TO_Y: vec3f = vec3f(0.299, 0.587, 0.114);
26
27fn luma(c: vec3f) -> f32 {
28    return dot(c, RGB_TO_Y);
29}
30
31fn eq(a: vec3f, b: vec3f) -> bool {
32    return all(a == b);
33}
34
35fn ne(a: vec3f, b: vec3f) -> bool {
36    return any(a != b);
37}
38
39fn all_eq2(B: vec3f, A0: vec3f, A1: vec3f) -> bool {
40    return all((B == A0) & (B == A1));
41}
42
43fn all_eq3(B: vec3f, A0: vec3f, A1: vec3f, A2: vec3f) -> bool {
44    return all((B == A0) & (B == A1) & (B == A2));
45}
46
47fn all_eq4(B: vec3f, A0: vec3f, A1: vec3f, A2: vec3f, A3: vec3f) -> bool {
48    return all((B == A0) & (B == A1) & (B == A2) & (B == A3));
49}
50
51fn any_eq3(B: vec3f, A0: vec3f, A1: vec3f, A2: vec3f) -> bool {
52    return eq(B, A0) || eq(B, A1) || eq(B, A2);
53}
54
55fn none_eq2(B: vec3f, A0: vec3f, A1: vec3f) -> bool {
56    return ne(B, A0) && ne(B, A1);
57}
58
59fn none_eq4(B: vec3f, A0: vec3f, A1: vec3f, A2: vec3f, A3: vec3f) -> bool {
60    return ne(B, A0) && ne(B, A1) && ne(B, A2) && ne(B, A3);
61}
62
63@compute @workgroup_size(16, 16, 1)
64fn mmpx(@builtin(global_invocation_id) invocation: vec3u) {
65    let in_pos = vec2i(invocation.xy);
66    input_size = vec2i(textureDimensions(input_frame));
67    if any(in_pos >= input_size) {
68        return;
69    }
70
71    // Input pixels (5x5, E is pixel at current position):
72    //   - - P - -
73    //   - A B C -
74    //   Q D E F R
75    //   - G H I -
76    //   - - S - -
77    // Certain branches load additional pixels beyond these
78
79    let A = load_input(in_pos + vec2i(-1, -1));
80    let B = load_input(in_pos + vec2i( 0, -1));
81    let C = load_input(in_pos + vec2i( 1, -1));
82    let D = load_input(in_pos + vec2i(-1,  0));
83    let E = load_input(in_pos + vec2i( 0,  0));
84    let F = load_input(in_pos + vec2i( 1,  0));
85    let G = load_input(in_pos + vec2i(-1,  1));
86    let H = load_input(in_pos + vec2i( 0,  1));
87    let I = load_input(in_pos + vec2i( 1,  1));
88
89    // Output pixels (2x2):
90    //   J K
91    //   L M
92
93    var J = E;
94    var K = E;
95    var L = E;
96    var M = E;
97
98    if any((A != E) | (B != E) | (C != E) | (D != E) | (F != E) | (G != E) | (H != E) | (I != E)) {
99        let P = load_input(in_pos + vec2i( 0, -2));
100        let S = load_input(in_pos + vec2i( 0,  2));
101        let Q = load_input(in_pos + vec2i(-2,  0));
102        let R = load_input(in_pos + vec2i( 2,  0));
103
104        let Bl = luma(B);
105        let Dl = luma(D);
106        let El = luma(E);
107        let Fl = luma(F);
108        let Hl = luma(H);
109
110        // 1:1 slope rules
111        if (eq(D, B) && ne(D, H) && ne(D, F))
112            && (El >= Dl || eq(E, A))
113            && any_eq3(E, A, C, G)
114            && ((El < Dl) || ne(A, D) || ne(E, P) || ne(E, Q))
115        {
116            J = D;
117        }
118        if (eq(B, F) && ne(B, D) && ne(B, H))
119            && (El >= Bl || eq(E, C))
120            && any_eq3(E, A, C, I)
121            && ((El < Bl) || ne(C, B) || ne(E, P) || ne(E, R))
122        {
123            K = B;
124        }
125        if (eq(H, D) && ne(H, F) && ne(H, B))
126            && (El >= Hl || eq(E, G))
127            && any_eq3(E, A, G, I)
128            && ((El < Hl) || ne(G, H) || ne(E, S) || ne(E, Q))
129        {
130            L = H;
131        }
132        if (eq(F, H) && ne(F, B) && ne(F, D))
133            && (El >= Fl || eq(E, I))
134            && any_eq3(E, C, G, I)
135            && ((El < Fl) || ne(I, H) || ne(E, R) || ne(E, S))
136        {
137            M = F;
138        }
139
140        // Intersection rules
141        if ne(E, F) && all_eq4(E, C, I, D, Q) && all_eq2(F, B, H) && ne(F, load_input(in_pos + vec2i(3, 0))) {
142            M = F;
143            K = F;
144        }
145        if ne(E, D) && all_eq4(E, A, G, F, R) && all_eq2(D, B, H) && ne(D, load_input(in_pos + vec2i(-3, 0))) {
146            L = D;
147            J = D;
148        }
149        if ne(E, H) && all_eq4(E, G, I, B, P) && all_eq2(H, D, F) && ne(H, load_input(in_pos + vec2i(0, 3))) {
150            M = H;
151            L = H;
152        }
153        if ne(E, B) && all_eq4(E, A, C, H, S) && all_eq2(B, D, F) && ne(B, load_input(in_pos + vec2i(0, -3))) {
154            K = B;
155            J = B;
156        }
157        if Bl < El && all_eq4(E, G, H, I, S) && none_eq4(E, A, D, C, F) {
158            K = B;
159            J = B;
160        }
161        if Hl < El && all_eq4(E, A, B, C, P) && none_eq4(E, D, G, I, F) {
162            M = H;
163            L = H;
164        }
165        if Fl < El && all_eq4(E, A, D, G, Q) && none_eq4(E, B, C, I, H) {
166            M = F;
167            K = F;
168        }
169        if Dl < El && all_eq4(E, C, F, I, R) && none_eq4(E, B, A, G, H) {
170            L = D;
171            J = D;
172        }
173
174        // 2:1 slope rules
175        if ne(H, B) {
176            if ne(H, A) && ne(H, E) && ne(H, C) {
177                if all_eq3(H, G, F, R) && none_eq2(H, D, load_input(in_pos + vec2i(2, -1))) {
178                    L = M;
179                }
180                if all_eq3(H, I, D, Q) && none_eq2(H, F, load_input(in_pos + vec2i(-2, -1))) {
181                    M = L;
182                }
183            }
184
185            if ne(B, I) && ne(B, G) && ne(B, E) {
186                if all_eq3(B, A, F, R) && none_eq2(B, D, load_input(in_pos + vec2i(2, 1))) {
187                    J = K;
188                }
189                if all_eq3(B, C, D, Q) && none_eq2(B, F, load_input(in_pos + vec2i(-2, 1))) {
190                    K = J;
191                }
192            }
193        } // H !== B
194
195        if ne(F, D) {
196            if ne(D, I) && ne(D, E) && ne(D, C) {
197                if all_eq3(D, A, H, S) && none_eq2(D, B, load_input(in_pos + vec2i(1, 2))) {
198                    J = L;
199                }
200                if all_eq3(D, G, B, P) && none_eq2(D, H, load_input(in_pos + vec2i(1, -2))) {
201                    L = J;
202                }
203            }
204
205            if ne(F, E) && ne(F, A) && ne(F, G) {
206                if all_eq3(F, C, H, S) && none_eq2(F, B, load_input(in_pos + vec2i(-1, 2))) {
207                    K = M;
208                }
209                if all_eq3(F, I, B, P) && none_eq2(F, H, load_input(in_pos + vec2i(-1, -2))) {
210                    M = K;
211                }
212            }
213        } // F !== D
214    } // not constant
215
216    // Write four pixels at once
217    let out_pos_tl = 2 * in_pos;
218    textureStore(output_frame, out_pos_tl + vec2i(0, 0), vec4f(J, 1.0));
219    textureStore(output_frame, out_pos_tl + vec2i(1, 0), vec4f(K, 1.0));
220    textureStore(output_frame, out_pos_tl + vec2i(0, 1), vec4f(L, 1.0));
221    textureStore(output_frame, out_pos_tl + vec2i(1, 1), vec4f(M, 1.0));
222}