1enable dual_source_blending;
2/* Functions useful for debugging:
3
4// A heat map color for debugging (blue -> cyan -> green -> yellow -> red).
5fn heat_map_color(value: f32, minValue: f32, maxValue: f32, position: vec2<f32>) -> vec4<f32> {
6 // Normalize value to 0-1 range
7 let t = clamp((value - minValue) / (maxValue - minValue), 0.0, 1.0);
8
9 // Heat map color calculation
10 let r = t * t;
11 let g = 4.0 * t * (1.0 - t);
12 let b = (1.0 - t) * (1.0 - t);
13 let heat_color = vec3<f32>(r, g, b);
14
15 // Create a checkerboard pattern (black and white)
16 let sum = floor(position.x / 3) + floor(position.y / 3);
17 let is_odd = fract(sum * 0.5); // 0.0 for even, 0.5 for odd
18 let checker_value = is_odd * 2.0; // 0.0 for even, 1.0 for odd
19 let checker_color = vec3<f32>(checker_value);
20
21 // Determine if value is in range (1.0 if in range, 0.0 if out of range)
22 let in_range = step(minValue, value) * step(value, maxValue);
23
24 // Mix checkerboard and heat map based on whether value is in range
25 let final_color = mix(checker_color, heat_color, in_range);
26
27 return vec4<f32>(final_color, 1.0);
28}
29
30*/
31
32// Contrast and gamma correction adapted from https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.hlsl
33// Copyright (c) Microsoft Corporation.
34// Licensed under the MIT license.
35fn color_brightness(color: vec3<f32>) -> f32 {
36 // REC. 601 luminance coefficients for perceived brightness
37 return dot(color, vec3<f32>(0.30, 0.59, 0.11));
38}
39
40fn light_on_dark_contrast(enhancedContrast: f32, color: vec3<f32>) -> f32 {
41 let brightness = color_brightness(color);
42 let multiplier = saturate(4.0 * (0.75 - brightness));
43 return enhancedContrast * multiplier;
44}
45
46fn enhance_contrast(alpha: f32, k: f32) -> f32 {
47 return alpha * (k + 1.0) / (alpha * k + 1.0);
48}
49
50fn enhance_contrast3(alpha: vec3<f32>, k: f32) -> vec3<f32> {
51 return alpha * (k + 1.0) / (alpha * k + 1.0);
52}
53
54fn apply_alpha_correction(a: f32, b: f32, g: vec4<f32>) -> f32 {
55 let brightness_adjustment = g.x * b + g.y;
56 let correction = brightness_adjustment * a + (g.z * b + g.w);
57 return a + a * (1.0 - a) * correction;
58}
59
60fn apply_alpha_correction3(a: vec3<f32>, b: vec3<f32>, g: vec4<f32>) -> vec3<f32> {
61 let brightness_adjustment = g.x * b + g.y;
62 let correction = brightness_adjustment * a + (g.z * b + g.w);
63 return a + a * (1.0 - a) * correction;
64}
65
66fn apply_contrast_and_gamma_correction(sample: f32, color: vec3<f32>, enhanced_contrast_factor: f32, gamma_ratios: vec4<f32>) -> f32 {
67 let enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
68 let brightness = color_brightness(color);
69
70 let contrasted = enhance_contrast(sample, enhanced_contrast);
71 return apply_alpha_correction(contrasted, brightness, gamma_ratios);
72}
73
74fn apply_contrast_and_gamma_correction3(sample: vec3<f32>, color: vec3<f32>, enhanced_contrast_factor: f32, gamma_ratios: vec4<f32>) -> vec3<f32> {
75 let enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
76
77 let contrasted = enhance_contrast3(sample, enhanced_contrast);
78 return apply_alpha_correction3(contrasted, color, gamma_ratios);
79}
80
81struct GlobalParams {
82 viewport_size: vec2<f32>,
83 premultiplied_alpha: u32,
84 pad: u32,
85}
86
87var<uniform> globals: GlobalParams;
88var<uniform> gamma_ratios: vec4<f32>;
89var<uniform> grayscale_enhanced_contrast: f32;
90var<uniform> subpixel_enhanced_contrast: f32;
91var t_sprite: texture_2d<f32>;
92var s_sprite: sampler;
93
94const M_PI_F: f32 = 3.1415926;
95const GRAYSCALE_FACTORS: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);
96
97struct Bounds {
98 origin: vec2<f32>,
99 size: vec2<f32>,
100}
101
102struct Corners {
103 top_left: f32,
104 top_right: f32,
105 bottom_right: f32,
106 bottom_left: f32,
107}
108
109struct Edges {
110 top: f32,
111 right: f32,
112 bottom: f32,
113 left: f32,
114}
115
116struct Hsla {
117 h: f32,
118 s: f32,
119 l: f32,
120 a: f32,
121}
122
123struct LinearColorStop {
124 color: Hsla,
125 percentage: f32,
126}
127
128struct Background {
129 // 0u is Solid
130 // 1u is LinearGradient
131 // 2u is PatternSlash
132 tag: u32,
133 // 0u is sRGB linear color
134 // 1u is Oklab color
135 color_space: u32,
136 solid: Hsla,
137 gradient_angle_or_pattern_height: f32,
138 colors: array<LinearColorStop, 2>,
139 pad: u32,
140}
141
142struct AtlasTextureId {
143 index: u32,
144 kind: u32,
145}
146
147struct AtlasBounds {
148 origin: vec2<i32>,
149 size: vec2<i32>,
150}
151
152struct AtlasTile {
153 texture_id: AtlasTextureId,
154 tile_id: u32,
155 padding: u32,
156 bounds: AtlasBounds,
157}
158
159struct TransformationMatrix {
160 rotation_scale: mat2x2<f32>,
161 translation: vec2<f32>,
162}
163
164fn to_device_position_impl(position: vec2<f32>) -> vec4<f32> {
165 let device_position = position / globals.viewport_size * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0);
166 return vec4<f32>(device_position, 0.0, 1.0);
167}
168
169fn to_device_position(unit_vertex: vec2<f32>, bounds: Bounds) -> vec4<f32> {
170 let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
171 return to_device_position_impl(position);
172}
173
174fn to_device_position_transformed(unit_vertex: vec2<f32>, bounds: Bounds, transform: TransformationMatrix) -> vec4<f32> {
175 let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
176 //Note: Rust side stores it as row-major, so transposing here
177 let transformed = transpose(transform.rotation_scale) * position + transform.translation;
178 return to_device_position_impl(transformed);
179}
180
181fn to_tile_position(unit_vertex: vec2<f32>, tile: AtlasTile) -> vec2<f32> {
182 let atlas_size = vec2<f32>(textureDimensions(t_sprite, 0));
183 return (vec2<f32>(tile.bounds.origin) + unit_vertex * vec2<f32>(tile.bounds.size)) / atlas_size;
184}
185
186fn distance_from_clip_rect_impl(position: vec2<f32>, clip_bounds: Bounds) -> vec4<f32> {
187 let tl = position - clip_bounds.origin;
188 let br = clip_bounds.origin + clip_bounds.size - position;
189 return vec4<f32>(tl.x, br.x, tl.y, br.y);
190}
191
192fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds: Bounds) -> vec4<f32> {
193 let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
194 return distance_from_clip_rect_impl(position, clip_bounds);
195}
196
197fn distance_from_clip_rect_transformed(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds: Bounds, transform: TransformationMatrix) -> vec4<f32> {
198 let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
199 let transformed = transpose(transform.rotation_scale) * position + transform.translation;
200 return distance_from_clip_rect_impl(transformed, clip_bounds);
201}
202
203// https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl
204fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
205 let cutoff = srgb < vec3<f32>(0.04045);
206 let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
207 let lower = srgb / vec3<f32>(12.92);
208 return select(higher, lower, cutoff);
209}
210
211fn srgb_to_linear_component(a: f32) -> f32 {
212 let cutoff = a < 0.04045;
213 let higher = pow((a + 0.055) / 1.055, 2.4);
214 let lower = a / 12.92;
215 return select(higher, lower, cutoff);
216}
217
218fn linear_to_srgb(linear: vec3<f32>) -> vec3<f32> {
219 let cutoff = linear < vec3<f32>(0.0031308);
220 let higher = vec3<f32>(1.055) * pow(linear, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
221 let lower = linear * vec3<f32>(12.92);
222 return select(higher, lower, cutoff);
223}
224
225/// Convert a linear color to sRGBA space.
226fn linear_to_srgba(color: vec4<f32>) -> vec4<f32> {
227 return vec4<f32>(linear_to_srgb(color.rgb), color.a);
228}
229
230/// Convert a sRGBA color to linear space.
231fn srgba_to_linear(color: vec4<f32>) -> vec4<f32> {
232 return vec4<f32>(srgb_to_linear(color.rgb), color.a);
233}
234
235/// Hsla to linear RGBA conversion.
236fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
237 let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
238 let s = hsla.s;
239 let l = hsla.l;
240 let a = hsla.a;
241
242 let c = (1.0 - abs(2.0 * l - 1.0)) * s;
243 let x = c * (1.0 - abs(h % 2.0 - 1.0));
244 let m = l - c / 2.0;
245 var color = vec3<f32>(m);
246
247 if (h >= 0.0 && h < 1.0) {
248 color.r += c;
249 color.g += x;
250 } else if (h >= 1.0 && h < 2.0) {
251 color.r += x;
252 color.g += c;
253 } else if (h >= 2.0 && h < 3.0) {
254 color.g += c;
255 color.b += x;
256 } else if (h >= 3.0 && h < 4.0) {
257 color.g += x;
258 color.b += c;
259 } else if (h >= 4.0 && h < 5.0) {
260 color.r += x;
261 color.b += c;
262 } else {
263 color.r += c;
264 color.b += x;
265 }
266
267 return vec4<f32>(color, a);
268}
269
270/// Convert a linear sRGB to Oklab space.
271/// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
272fn linear_srgb_to_oklab(color: vec4<f32>) -> vec4<f32> {
273 let l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
274 let m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
275 let s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
276
277 let l_ = pow(l, 1.0 / 3.0);
278 let m_ = pow(m, 1.0 / 3.0);
279 let s_ = pow(s, 1.0 / 3.0);
280
281 return vec4<f32>(
282 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
283 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
284 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
285 color.a
286 );
287}
288
289/// Convert an Oklab color to linear sRGB space.
290fn oklab_to_linear_srgb(color: vec4<f32>) -> vec4<f32> {
291 let l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b;
292 let m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b;
293 let s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b;
294
295 let l = l_ * l_ * l_;
296 let m = m_ * m_ * m_;
297 let s = s_ * s_ * s_;
298
299 return vec4<f32>(
300 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
301 -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
302 -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
303 color.a
304 );
305}
306
307fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
308 let alpha = above.a + below.a * (1.0 - above.a);
309 let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
310 return vec4<f32>(color, alpha);
311}
312
313// A standard gaussian function, used for weighting samples
314fn gaussian(x: f32, sigma: f32) -> f32{
315 return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * M_PI_F) * sigma);
316}
317
318// This approximates the error function, needed for the gaussian integral
319fn erf(v: vec2<f32>) -> vec2<f32> {
320 let s = sign(v);
321 let a = abs(v);
322 let r1 = 1.0 + (0.278393 + (0.230389 + (0.000972 + 0.078108 * a) * a) * a) * a;
323 let r2 = r1 * r1;
324 return s - s / (r2 * r2);
325}
326
327fn blur_along_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2<f32>) -> f32 {
328 let delta = min(half_size.y - corner - abs(y), 0.0);
329 let curved = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
330 let integral = 0.5 + 0.5 * erf((x + vec2<f32>(-curved, curved)) * (sqrt(0.5) / sigma));
331 return integral.y - integral.x;
332}
333
334// Selects corner radius based on quadrant.
335fn pick_corner_radius(center_to_point: vec2<f32>, radii: Corners) -> f32 {
336 if (center_to_point.x < 0.0) {
337 if (center_to_point.y < 0.0) {
338 return radii.top_left;
339 } else {
340 return radii.bottom_left;
341 }
342 } else {
343 if (center_to_point.y < 0.0) {
344 return radii.top_right;
345 } else {
346 return radii.bottom_right;
347 }
348 }
349}
350
351// Signed distance of the point to the quad's border - positive outside the
352// border, and negative inside.
353//
354// See comments on similar code using `quad_sdf_impl` in `fs_quad` for
355// explanation.
356fn quad_sdf(point: vec2<f32>, bounds: Bounds, corner_radii: Corners) -> f32 {
357 let half_size = bounds.size / 2.0;
358 let center = bounds.origin + half_size;
359 let center_to_point = point - center;
360 let corner_radius = pick_corner_radius(center_to_point, corner_radii);
361 let corner_to_point = abs(center_to_point) - half_size;
362 let corner_center_to_point = corner_to_point + corner_radius;
363 return quad_sdf_impl(corner_center_to_point, corner_radius);
364}
365
366fn quad_sdf_impl(corner_center_to_point: vec2<f32>, corner_radius: f32) -> f32 {
367 if (corner_radius == 0.0) {
368 // Fast path for unrounded corners.
369 return max(corner_center_to_point.x, corner_center_to_point.y);
370 } else {
371 // Signed distance of the point from a quad that is inset by corner_radius.
372 // It is negative inside this quad, and positive outside.
373 let signed_distance_to_inset_quad =
374 // 0 inside the inset quad, and positive outside.
375 length(max(vec2<f32>(0.0), corner_center_to_point)) +
376 // 0 outside the inset quad, and negative inside.
377 min(0.0, max(corner_center_to_point.x, corner_center_to_point.y));
378
379 return signed_distance_to_inset_quad - corner_radius;
380 }
381}
382
383// Abstract away the final color transformation based on the
384// target alpha compositing mode.
385fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
386 let alpha = color.a * alpha_factor;
387 let multiplier = select(1.0, alpha, globals.premultiplied_alpha != 0u);
388 return vec4<f32>(color.rgb * multiplier, alpha);
389}
390
391
392struct GradientColor {
393 solid: vec4<f32>,
394 color0: vec4<f32>,
395 color1: vec4<f32>,
396}
397
398fn prepare_gradient_color(tag: u32, color_space: u32,
399 solid: Hsla, colors: array<LinearColorStop, 2>) -> GradientColor {
400 var result = GradientColor();
401
402 if (tag == 0u || tag == 2u) {
403 result.solid = hsla_to_rgba(solid);
404 } else if (tag == 1u) {
405 // The hsla_to_rgba is returns a linear sRGB color
406 result.color0 = hsla_to_rgba(colors[0].color);
407 result.color1 = hsla_to_rgba(colors[1].color);
408
409 // Prepare color space in vertex for avoid conversion
410 // in fragment shader for performance reasons
411 if (color_space == 0u) {
412 // sRGB
413 result.color0 = linear_to_srgba(result.color0);
414 result.color1 = linear_to_srgba(result.color1);
415 } else if (color_space == 1u) {
416 // Oklab
417 result.color0 = linear_srgb_to_oklab(result.color0);
418 result.color1 = linear_srgb_to_oklab(result.color1);
419 }
420 }
421
422 return result;
423}
424
425fn gradient_color(background: Background, position: vec2<f32>, bounds: Bounds,
426 solid_color: vec4<f32>, color0: vec4<f32>, color1: vec4<f32>) -> vec4<f32> {
427 var background_color = vec4<f32>(0.0);
428
429 switch (background.tag) {
430 default: {
431 return solid_color;
432 }
433 case 1u: {
434 // Linear gradient background.
435 // -90 degrees to match the CSS gradient angle.
436 let angle = background.gradient_angle_or_pattern_height;
437 let radians = (angle % 360.0 - 90.0) * M_PI_F / 180.0;
438 var direction = vec2<f32>(cos(radians), sin(radians));
439 let stop0_percentage = background.colors[0].percentage;
440 let stop1_percentage = background.colors[1].percentage;
441
442 // Expand the short side to be the same as the long side
443 if (bounds.size.x > bounds.size.y) {
444 direction.y *= bounds.size.y / bounds.size.x;
445 } else {
446 direction.x *= bounds.size.x / bounds.size.y;
447 }
448
449 // Get the t value for the linear gradient with the color stop percentages.
450 let half_size = bounds.size / 2.0;
451 let center = bounds.origin + half_size;
452 let center_to_point = position - center;
453 var t = dot(center_to_point, direction) / length(direction);
454 // Check the direct to determine the use x or y
455 if (abs(direction.x) > abs(direction.y)) {
456 t = (t + half_size.x) / bounds.size.x;
457 } else {
458 t = (t + half_size.y) / bounds.size.y;
459 }
460
461 // Adjust t based on the stop percentages
462 t = (t - stop0_percentage) / (stop1_percentage - stop0_percentage);
463 t = clamp(t, 0.0, 1.0);
464
465 switch (background.color_space) {
466 default: {
467 background_color = srgba_to_linear(mix(color0, color1, t));
468 }
469 case 1u: {
470 let oklab_color = mix(color0, color1, t);
471 background_color = oklab_to_linear_srgb(oklab_color);
472 }
473 }
474 }
475 case 2u: {
476 let gradient_angle_or_pattern_height = background.gradient_angle_or_pattern_height;
477 let pattern_width = (gradient_angle_or_pattern_height / 65535.0f) / 255.0f;
478 let pattern_interval = (gradient_angle_or_pattern_height % 65535.0f) / 255.0f;
479 let pattern_height = pattern_width + pattern_interval;
480 let stripe_angle = M_PI_F / 4.0;
481 let pattern_period = pattern_height * sin(stripe_angle);
482 let rotation = mat2x2<f32>(
483 cos(stripe_angle), -sin(stripe_angle),
484 sin(stripe_angle), cos(stripe_angle)
485 );
486 let relative_position = position - bounds.origin;
487 let rotated_point = rotation * relative_position;
488 let pattern = rotated_point.x % pattern_period;
489 let distance = min(pattern, pattern_period - pattern) - pattern_period * (pattern_width / pattern_height) / 2.0f;
490 background_color = solid_color;
491 background_color.a *= saturate(0.5 - distance);
492 }
493 }
494
495 return background_color;
496}
497
498// --- quads --- //
499
500struct Quad {
501 order: u32,
502 border_style: u32,
503 bounds: Bounds,
504 content_mask: Bounds,
505 background: Background,
506 border_color: Hsla,
507 corner_radii: Corners,
508 border_widths: Edges,
509}
510var<storage, read> b_quads: array<Quad>;
511
512struct QuadVarying {
513 @builtin(position) position: vec4<f32>,
514 @location(0) @interpolate(flat) border_color: vec4<f32>,
515 @location(1) @interpolate(flat) quad_id: u32,
516 // TODO: use `clip_distance` once Naga supports it
517 @location(2) clip_distances: vec4<f32>,
518 @location(3) @interpolate(flat) background_solid: vec4<f32>,
519 @location(4) @interpolate(flat) background_color0: vec4<f32>,
520 @location(5) @interpolate(flat) background_color1: vec4<f32>,
521}
522
523@vertex
524fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying {
525 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
526 let quad = b_quads[instance_id];
527
528 var out = QuadVarying();
529 out.position = to_device_position(unit_vertex, quad.bounds);
530
531 let gradient = prepare_gradient_color(
532 quad.background.tag,
533 quad.background.color_space,
534 quad.background.solid,
535 quad.background.colors
536 );
537 out.background_solid = gradient.solid;
538 out.background_color0 = gradient.color0;
539 out.background_color1 = gradient.color1;
540 out.border_color = hsla_to_rgba(quad.border_color);
541 out.quad_id = instance_id;
542 out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
543 return out;
544}
545
546@fragment
547fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
548 // Alpha clip first, since we don't have `clip_distance`.
549 if (any(input.clip_distances < vec4<f32>(0.0))) {
550 return vec4<f32>(0.0);
551 }
552
553 let quad = b_quads[input.quad_id];
554
555 let background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
556 input.background_solid, input.background_color0, input.background_color1);
557
558 let unrounded = quad.corner_radii.top_left == 0.0 &&
559 quad.corner_radii.bottom_left == 0.0 &&
560 quad.corner_radii.top_right == 0.0 &&
561 quad.corner_radii.bottom_right == 0.0;
562
563 // Fast path when the quad is not rounded and doesn't have any border
564 if (quad.border_widths.top == 0.0 &&
565 quad.border_widths.left == 0.0 &&
566 quad.border_widths.right == 0.0 &&
567 quad.border_widths.bottom == 0.0 &&
568 unrounded) {
569 return blend_color(background_color, 1.0);
570 }
571
572 let size = quad.bounds.size;
573 let half_size = size / 2.0;
574 let point = input.position.xy - quad.bounds.origin;
575 let center_to_point = point - half_size;
576
577 // Signed distance field threshold for inclusion of pixels. 0.5 is the
578 // minimum distance between the center of the pixel and the edge.
579 let antialias_threshold = 0.5;
580
581 // Radius of the nearest corner
582 let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
583
584 // Width of the nearest borders
585 let border = vec2<f32>(
586 select(
587 quad.border_widths.right,
588 quad.border_widths.left,
589 center_to_point.x < 0.0),
590 select(
591 quad.border_widths.bottom,
592 quad.border_widths.top,
593 center_to_point.y < 0.0));
594
595 // 0-width borders are reduced so that `inner_sdf >= antialias_threshold`.
596 // The purpose of this is to not draw antialiasing pixels in this case.
597 let reduced_border =
598 vec2<f32>(select(border.x, -antialias_threshold, border.x == 0.0),
599 select(border.y, -antialias_threshold, border.y == 0.0));
600
601 // Vector from the corner of the quad bounds to the point, after mirroring
602 // the point into the bottom right quadrant. Both components are <= 0.
603 let corner_to_point = abs(center_to_point) - half_size;
604
605 // Vector from the point to the center of the rounded corner's circle, also
606 // mirrored into bottom right quadrant.
607 let corner_center_to_point = corner_to_point + corner_radius;
608
609 // Whether the nearest point on the border is rounded
610 let is_near_rounded_corner =
611 corner_center_to_point.x >= 0 &&
612 corner_center_to_point.y >= 0;
613
614 // Vector from straight border inner corner to point.
615 let straight_border_inner_corner_to_point = corner_to_point + reduced_border;
616
617 // Whether the point is beyond the inner edge of the straight border.
618 let is_beyond_inner_straight_border =
619 straight_border_inner_corner_to_point.x > 0 ||
620 straight_border_inner_corner_to_point.y > 0;
621
622 // Whether the point is far enough inside the quad, such that the pixels are
623 // not affected by the straight border.
624 let is_within_inner_straight_border =
625 straight_border_inner_corner_to_point.x < -antialias_threshold &&
626 straight_border_inner_corner_to_point.y < -antialias_threshold;
627
628 // Fast path for points that must be part of the background.
629 //
630 // This could be optimized further for large rounded corners by including
631 // points in an inscribed rectangle, or some other quick linear check.
632 // However, that might negatively impact performance in the case of
633 // reasonable sizes for rounded corners.
634 if (is_within_inner_straight_border && !is_near_rounded_corner) {
635 return blend_color(background_color, 1.0);
636 }
637
638 // Signed distance of the point to the outside edge of the quad's border. It
639 // is positive outside this edge, and negative inside.
640 let outer_sdf = quad_sdf_impl(corner_center_to_point, corner_radius);
641
642 // Approximate signed distance of the point to the inside edge of the quad's
643 // border. It is negative outside this edge (within the border), and
644 // positive inside.
645 //
646 // This is not always an accurate signed distance:
647 // * The rounded portions with varying border width use an approximation of
648 // nearest-point-on-ellipse.
649 // * When it is quickly known to be outside the edge, -1.0 is used.
650 var inner_sdf = 0.0;
651 if (corner_center_to_point.x <= 0 || corner_center_to_point.y <= 0) {
652 // Fast paths for straight borders.
653 inner_sdf = -max(straight_border_inner_corner_to_point.x,
654 straight_border_inner_corner_to_point.y);
655 } else if (is_beyond_inner_straight_border) {
656 // Fast path for points that must be outside the inner edge.
657 inner_sdf = -1.0;
658 } else if (reduced_border.x == reduced_border.y) {
659 // Fast path for circular inner edge.
660 inner_sdf = -(outer_sdf + reduced_border.x);
661 } else {
662 let ellipse_radii = max(vec2<f32>(0.0), corner_radius - reduced_border);
663 inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii);
664 }
665
666 // Negative when inside the border
667 let border_sdf = max(inner_sdf, outer_sdf);
668
669 var color = background_color;
670 if (border_sdf < antialias_threshold) {
671 var border_color = input.border_color;
672
673 // Dashed border logic when border_style == 1
674 if (quad.border_style == 1) {
675 // Position along the perimeter in "dash space", where each dash
676 // period has length 1
677 var t = 0.0;
678
679 // Total number of dash periods, so that the dash spacing can be
680 // adjusted to evenly divide it
681 var max_t = 0.0;
682
683 // Border width is proportional to dash size. This is the behavior
684 // used by browsers, but also avoids dashes from different segments
685 // overlapping when dash size is smaller than the border width.
686 //
687 // Dash pattern: (2 * border width) dash, (1 * border width) gap
688 let dash_length_per_width = 2.0;
689 let dash_gap_per_width = 1.0;
690 let dash_period_per_width = dash_length_per_width + dash_gap_per_width;
691
692 // Since the dash size is determined by border width, the density of
693 // dashes varies. Multiplying a pixel distance by this returns a
694 // position in dash space - it has units (dash period / pixels). So
695 // a dash velocity of (1 / 10) is 1 dash every 10 pixels.
696 var dash_velocity = 0.0;
697
698 // Dividing this by the border width gives the dash velocity
699 let dv_numerator = 1.0 / dash_period_per_width;
700
701 if (unrounded) {
702 // When corners aren't rounded, the dashes are separately laid
703 // out on each straight line, rather than around the whole
704 // perimeter. This way each line starts and ends with a dash.
705 let is_horizontal =
706 corner_center_to_point.x <
707 corner_center_to_point.y;
708
709 // When applying dashed borders to just some, not all, the sides.
710 // The way we chose border widths above sometimes comes with a 0 width value.
711 // So we choose again to avoid division by zero.
712 // TODO: A better solution exists taking a look at the whole file.
713 // this does not fix single dashed borders at the corners
714 let dashed_border = vec2<f32>(
715 max(
716 quad.border_widths.bottom,
717 quad.border_widths.top,
718 ),
719 max(
720 quad.border_widths.right,
721 quad.border_widths.left,
722 )
723 );
724
725 let border_width = select(dashed_border.y, dashed_border.x, is_horizontal);
726 dash_velocity = dv_numerator / border_width;
727 t = select(point.y, point.x, is_horizontal) * dash_velocity;
728 max_t = select(size.y, size.x, is_horizontal) * dash_velocity;
729 } else {
730 // When corners are rounded, the dashes are laid out clockwise
731 // around the whole perimeter.
732
733 let r_tr = quad.corner_radii.top_right;
734 let r_br = quad.corner_radii.bottom_right;
735 let r_bl = quad.corner_radii.bottom_left;
736 let r_tl = quad.corner_radii.top_left;
737
738 let w_t = quad.border_widths.top;
739 let w_r = quad.border_widths.right;
740 let w_b = quad.border_widths.bottom;
741 let w_l = quad.border_widths.left;
742
743 // Straight side dash velocities
744 let dv_t = select(dv_numerator / w_t, 0.0, w_t <= 0.0);
745 let dv_r = select(dv_numerator / w_r, 0.0, w_r <= 0.0);
746 let dv_b = select(dv_numerator / w_b, 0.0, w_b <= 0.0);
747 let dv_l = select(dv_numerator / w_l, 0.0, w_l <= 0.0);
748
749 // Straight side lengths in dash space
750 let s_t = (size.x - r_tl - r_tr) * dv_t;
751 let s_r = (size.y - r_tr - r_br) * dv_r;
752 let s_b = (size.x - r_br - r_bl) * dv_b;
753 let s_l = (size.y - r_bl - r_tl) * dv_l;
754
755 let corner_dash_velocity_tr = corner_dash_velocity(dv_t, dv_r);
756 let corner_dash_velocity_br = corner_dash_velocity(dv_b, dv_r);
757 let corner_dash_velocity_bl = corner_dash_velocity(dv_b, dv_l);
758 let corner_dash_velocity_tl = corner_dash_velocity(dv_t, dv_l);
759
760 // Corner lengths in dash space
761 let c_tr = r_tr * (M_PI_F / 2.0) * corner_dash_velocity_tr;
762 let c_br = r_br * (M_PI_F / 2.0) * corner_dash_velocity_br;
763 let c_bl = r_bl * (M_PI_F / 2.0) * corner_dash_velocity_bl;
764 let c_tl = r_tl * (M_PI_F / 2.0) * corner_dash_velocity_tl;
765
766 // Cumulative dash space upto each segment
767 let upto_tr = s_t;
768 let upto_r = upto_tr + c_tr;
769 let upto_br = upto_r + s_r;
770 let upto_b = upto_br + c_br;
771 let upto_bl = upto_b + s_b;
772 let upto_l = upto_bl + c_bl;
773 let upto_tl = upto_l + s_l;
774 max_t = upto_tl + c_tl;
775
776 if (is_near_rounded_corner) {
777 let radians = atan2(corner_center_to_point.y,
778 corner_center_to_point.x);
779 let corner_t = radians * corner_radius;
780
781 if (center_to_point.x >= 0.0) {
782 if (center_to_point.y < 0.0) {
783 dash_velocity = corner_dash_velocity_tr;
784 // Subtracted because radians is pi/2 to 0 when
785 // going clockwise around the top right corner,
786 // since the y axis has been flipped
787 t = upto_r - corner_t * dash_velocity;
788 } else {
789 dash_velocity = corner_dash_velocity_br;
790 // Added because radians is 0 to pi/2 when going
791 // clockwise around the bottom-right corner
792 t = upto_br + corner_t * dash_velocity;
793 }
794 } else {
795 if (center_to_point.y >= 0.0) {
796 dash_velocity = corner_dash_velocity_bl;
797 // Subtracted because radians is pi/2 to 0 when
798 // going clockwise around the bottom-left corner,
799 // since the x axis has been flipped
800 t = upto_l - corner_t * dash_velocity;
801 } else {
802 dash_velocity = corner_dash_velocity_tl;
803 // Added because radians is 0 to pi/2 when going
804 // clockwise around the top-left corner, since both
805 // axis were flipped
806 t = upto_tl + corner_t * dash_velocity;
807 }
808 }
809 } else {
810 // Straight borders
811 let is_horizontal =
812 corner_center_to_point.x <
813 corner_center_to_point.y;
814 if (is_horizontal) {
815 if (center_to_point.y < 0.0) {
816 dash_velocity = dv_t;
817 t = (point.x - r_tl) * dash_velocity;
818 } else {
819 dash_velocity = dv_b;
820 t = upto_bl - (point.x - r_bl) * dash_velocity;
821 }
822 } else {
823 if (center_to_point.x < 0.0) {
824 dash_velocity = dv_l;
825 t = upto_tl - (point.y - r_tl) * dash_velocity;
826 } else {
827 dash_velocity = dv_r;
828 t = upto_r + (point.y - r_tr) * dash_velocity;
829 }
830 }
831 }
832 }
833
834 let dash_length = dash_length_per_width / dash_period_per_width;
835 let desired_dash_gap = dash_gap_per_width / dash_period_per_width;
836
837 // Straight borders should start and end with a dash, so max_t is
838 // reduced to cause this.
839 max_t -= select(0.0, dash_length, unrounded);
840 if (max_t >= 1.0) {
841 // Adjust dash gap to evenly divide max_t.
842 let dash_count = floor(max_t);
843 let dash_period = max_t / dash_count;
844 border_color.a *= dash_alpha(
845 t,
846 dash_period,
847 dash_length,
848 dash_velocity,
849 antialias_threshold);
850 } else if (unrounded) {
851 // When there isn't enough space for the full gap between the
852 // two start / end dashes of a straight border, reduce gap to
853 // make them fit.
854 let dash_gap = max_t - dash_length;
855 if (dash_gap > 0.0) {
856 let dash_period = dash_length + dash_gap;
857 border_color.a *= dash_alpha(
858 t,
859 dash_period,
860 dash_length,
861 dash_velocity,
862 antialias_threshold);
863 }
864 }
865 }
866
867 // Blend the border on top of the background and then linearly interpolate
868 // between the two as we slide inside the background.
869 let blended_border = over(background_color, border_color);
870 color = mix(background_color, blended_border,
871 saturate(antialias_threshold - inner_sdf));
872 }
873
874 return blend_color(color, saturate(antialias_threshold - outer_sdf));
875}
876
877// Returns the dash velocity of a corner given the dash velocity of the two
878// sides, by returning the slower velocity (larger dashes).
879//
880// Since 0 is used for dash velocity when the border width is 0 (instead of
881// +inf), this returns the other dash velocity in that case.
882//
883// An alternative to this might be to appropriately interpolate the dash
884// velocity around the corner, but that seems overcomplicated.
885fn corner_dash_velocity(dv1: f32, dv2: f32) -> f32 {
886 if (dv1 == 0.0) {
887 return dv2;
888 } else if (dv2 == 0.0) {
889 return dv1;
890 } else {
891 return min(dv1, dv2);
892 }
893}
894
895// Returns alpha used to render antialiased dashes.
896// `t` is within the dash when `fmod(t, period) < length`.
897fn dash_alpha(t: f32, period: f32, length: f32, dash_velocity: f32, antialias_threshold: f32) -> f32 {
898 let half_period = period / 2;
899 let half_length = length / 2;
900 // Value in [-half_period, half_period].
901 // The dash is in [-half_length, half_length].
902 let centered = fmod(t + half_period - half_length, period) - half_period;
903 // Signed distance for the dash, negative values are inside the dash.
904 let signed_distance = abs(centered) - half_length;
905 // Antialiased alpha based on the signed distance.
906 return saturate(antialias_threshold - signed_distance / dash_velocity);
907}
908
909// This approximates distance to the nearest point to a quarter ellipse in a way
910// that is sufficient for anti-aliasing when the ellipse is not very eccentric.
911// The components of `point` are expected to be positive.
912//
913// Negative on the outside and positive on the inside.
914fn quarter_ellipse_sdf(point: vec2<f32>, radii: vec2<f32>) -> f32 {
915 // Scale the space to treat the ellipse like a unit circle.
916 let circle_vec = point / radii;
917 let unit_circle_sdf = length(circle_vec) - 1.0;
918 // Approximate up-scaling of the length by using the average of the radii.
919 //
920 // TODO: A better solution would be to use the gradient of the implicit
921 // function for an ellipse to approximate a scaling factor.
922 return unit_circle_sdf * (radii.x + radii.y) * -0.5;
923}
924
925// Modulus that has the same sign as `a`.
926fn fmod(a: f32, b: f32) -> f32 {
927 return a - b * trunc(a / b);
928}
929
930// --- shadows --- //
931
932struct Shadow {
933 order: u32,
934 blur_radius: f32,
935 bounds: Bounds,
936 corner_radii: Corners,
937 content_mask: Bounds,
938 color: Hsla,
939}
940var<storage, read> b_shadows: array<Shadow>;
941
942struct ShadowVarying {
943 @builtin(position) position: vec4<f32>,
944 @location(0) @interpolate(flat) color: vec4<f32>,
945 @location(1) @interpolate(flat) shadow_id: u32,
946 //TODO: use `clip_distance` once Naga supports it
947 @location(3) clip_distances: vec4<f32>,
948}
949
950@vertex
951fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> ShadowVarying {
952 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
953 var shadow = b_shadows[instance_id];
954
955 let margin = 3.0 * shadow.blur_radius;
956 // Set the bounds of the shadow and adjust its size based on the shadow's
957 // spread radius to achieve the spreading effect
958 shadow.bounds.origin -= vec2<f32>(margin);
959 shadow.bounds.size += 2.0 * vec2<f32>(margin);
960
961 var out = ShadowVarying();
962 out.position = to_device_position(unit_vertex, shadow.bounds);
963 out.color = hsla_to_rgba(shadow.color);
964 out.shadow_id = instance_id;
965 out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask);
966 return out;
967}
968
969@fragment
970fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
971 // Alpha clip first, since we don't have `clip_distance`.
972 if (any(input.clip_distances < vec4<f32>(0.0))) {
973 return vec4<f32>(0.0);
974 }
975
976 let shadow = b_shadows[input.shadow_id];
977 let half_size = shadow.bounds.size / 2.0;
978 let center = shadow.bounds.origin + half_size;
979 let center_to_point = input.position.xy - center;
980
981 let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii);
982
983 // The signal is only non-zero in a limited range, so don't waste samples
984 let low = center_to_point.y - half_size.y;
985 let high = center_to_point.y + half_size.y;
986 let start = clamp(-3.0 * shadow.blur_radius, low, high);
987 let end = clamp(3.0 * shadow.blur_radius, low, high);
988
989 // Accumulate samples (we can get away with surprisingly few samples)
990 let step = (end - start) / 4.0;
991 var y = start + step * 0.5;
992 var alpha = 0.0;
993 for (var i = 0; i < 4; i += 1) {
994 let blur = blur_along_x(center_to_point.x, center_to_point.y - y,
995 shadow.blur_radius, corner_radius, half_size);
996 alpha += blur * gaussian(y, shadow.blur_radius) * step;
997 y += step;
998 }
999
1000 return blend_color(input.color, alpha);
1001}
1002
1003// --- path rasterization --- //
1004
1005struct PathRasterizationVertex {
1006 xy_position: vec2<f32>,
1007 st_position: vec2<f32>,
1008 color: Background,
1009 bounds: Bounds,
1010}
1011
1012var<storage, read> b_path_vertices: array<PathRasterizationVertex>;
1013
1014struct PathRasterizationVarying {
1015 @builtin(position) position: vec4<f32>,
1016 @location(0) st_position: vec2<f32>,
1017 @location(1) vertex_id: u32,
1018 //TODO: use `clip_distance` once Naga supports it
1019 @location(3) clip_distances: vec4<f32>,
1020}
1021
1022@vertex
1023fn vs_path_rasterization(@builtin(vertex_index) vertex_id: u32) -> PathRasterizationVarying {
1024 let v = b_path_vertices[vertex_id];
1025
1026 var out = PathRasterizationVarying();
1027 out.position = to_device_position_impl(v.xy_position);
1028 out.st_position = v.st_position;
1029 out.vertex_id = vertex_id;
1030 out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.bounds);
1031 return out;
1032}
1033
1034@fragment
1035fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) vec4<f32> {
1036 let dx = dpdx(input.st_position);
1037 let dy = dpdy(input.st_position);
1038 if (any(input.clip_distances < vec4<f32>(0.0))) {
1039 return vec4<f32>(0.0);
1040 }
1041
1042 let v = b_path_vertices[input.vertex_id];
1043 let background = v.color;
1044 let bounds = v.bounds;
1045
1046 var alpha: f32;
1047 if (length(vec2<f32>(dx.x, dy.x)) < 0.001) {
1048 // If the gradient is too small, return a solid color.
1049 alpha = 1.0;
1050 } else {
1051 let gradient = 2.0 * input.st_position.xx * vec2<f32>(dx.x, dy.x) - vec2<f32>(dx.y, dy.y);
1052 let f = input.st_position.x * input.st_position.x - input.st_position.y;
1053 let distance = f / length(gradient);
1054 alpha = saturate(0.5 - distance);
1055 }
1056 let gradient_color = prepare_gradient_color(
1057 background.tag,
1058 background.color_space,
1059 background.solid,
1060 background.colors,
1061 );
1062 let color = gradient_color(background, input.position.xy, bounds,
1063 gradient_color.solid, gradient_color.color0, gradient_color.color1);
1064 return vec4<f32>(color.rgb * color.a * alpha, color.a * alpha);
1065}
1066
1067// --- paths --- //
1068
1069struct PathSprite {
1070 bounds: Bounds,
1071}
1072var<storage, read> b_path_sprites: array<PathSprite>;
1073
1074struct PathVarying {
1075 @builtin(position) position: vec4<f32>,
1076 @location(0) texture_coords: vec2<f32>,
1077}
1078
1079@vertex
1080fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PathVarying {
1081 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
1082 let sprite = b_path_sprites[instance_id];
1083 // Don't apply content mask because it was already accounted for when rasterizing the path.
1084 let device_position = to_device_position(unit_vertex, sprite.bounds);
1085 // For screen-space intermediate texture, convert screen position to texture coordinates
1086 let screen_position = sprite.bounds.origin + unit_vertex * sprite.bounds.size;
1087 let texture_coords = screen_position / globals.viewport_size;
1088
1089 var out = PathVarying();
1090 out.position = device_position;
1091 out.texture_coords = texture_coords;
1092
1093 return out;
1094}
1095
1096@fragment
1097fn fs_path(input: PathVarying) -> @location(0) vec4<f32> {
1098 let sample = textureSample(t_sprite, s_sprite, input.texture_coords);
1099 return sample;
1100}
1101
1102// --- underlines --- //
1103
1104struct Underline {
1105 order: u32,
1106 pad: u32,
1107 bounds: Bounds,
1108 content_mask: Bounds,
1109 color: Hsla,
1110 thickness: f32,
1111 wavy: u32,
1112}
1113var<storage, read> b_underlines: array<Underline>;
1114
1115struct UnderlineVarying {
1116 @builtin(position) position: vec4<f32>,
1117 @location(0) @interpolate(flat) color: vec4<f32>,
1118 @location(1) @interpolate(flat) underline_id: u32,
1119 //TODO: use `clip_distance` once Naga supports it
1120 @location(3) clip_distances: vec4<f32>,
1121}
1122
1123@vertex
1124fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> UnderlineVarying {
1125 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
1126 let underline = b_underlines[instance_id];
1127
1128 var out = UnderlineVarying();
1129 out.position = to_device_position(unit_vertex, underline.bounds);
1130 out.color = hsla_to_rgba(underline.color);
1131 out.underline_id = instance_id;
1132 out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask);
1133 return out;
1134}
1135
1136@fragment
1137fn fs_underline(input: UnderlineVarying) -> @location(0) vec4<f32> {
1138 const WAVE_FREQUENCY: f32 = 2.0;
1139 const WAVE_HEIGHT_RATIO: f32 = 0.8;
1140
1141 // Alpha clip first, since we don't have `clip_distance`.
1142 if (any(input.clip_distances < vec4<f32>(0.0))) {
1143 return vec4<f32>(0.0);
1144 }
1145
1146 let underline = b_underlines[input.underline_id];
1147 if ((underline.wavy & 0xFFu) == 0u)
1148 {
1149 return blend_color(input.color, input.color.a);
1150 }
1151
1152 let half_thickness = underline.thickness * 0.5;
1153
1154 let st = (input.position.xy - underline.bounds.origin) / underline.bounds.size.y - vec2<f32>(0.0, 0.5);
1155 let frequency = M_PI_F * WAVE_FREQUENCY * underline.thickness / underline.bounds.size.y;
1156 let amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.y;
1157
1158 let sine = sin(st.x * frequency) * amplitude;
1159 let dSine = cos(st.x * frequency) * amplitude * frequency;
1160 let distance = (st.y - sine) / sqrt(1.0 + dSine * dSine);
1161 let distance_in_pixels = distance * underline.bounds.size.y;
1162 let distance_from_top_border = distance_in_pixels - half_thickness;
1163 let distance_from_bottom_border = distance_in_pixels + half_thickness;
1164 let alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border));
1165 return blend_color(input.color, alpha * input.color.a);
1166}
1167
1168// --- monochrome sprites --- //
1169
1170struct MonochromeSprite {
1171 order: u32,
1172 pad: u32,
1173 bounds: Bounds,
1174 content_mask: Bounds,
1175 color: Hsla,
1176 tile: AtlasTile,
1177 transformation: TransformationMatrix,
1178}
1179var<storage, read> b_mono_sprites: array<MonochromeSprite>;
1180
1181struct MonoSpriteVarying {
1182 @builtin(position) position: vec4<f32>,
1183 @location(0) tile_position: vec2<f32>,
1184 @location(1) @interpolate(flat) color: vec4<f32>,
1185 @location(3) clip_distances: vec4<f32>,
1186}
1187
1188@vertex
1189fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> MonoSpriteVarying {
1190 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
1191 let sprite = b_mono_sprites[instance_id];
1192
1193 var out = MonoSpriteVarying();
1194 out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
1195
1196 out.tile_position = to_tile_position(unit_vertex, sprite.tile);
1197 out.color = hsla_to_rgba(sprite.color);
1198 out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
1199 return out;
1200}
1201
1202@fragment
1203fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4<f32> {
1204 let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
1205 let alpha_corrected = apply_contrast_and_gamma_correction(sample, input.color.rgb, grayscale_enhanced_contrast, gamma_ratios);
1206
1207 // Alpha clip after using the derivatives.
1208 if (any(input.clip_distances < vec4<f32>(0.0))) {
1209 return vec4<f32>(0.0);
1210 }
1211
1212 return blend_color(input.color, alpha_corrected);
1213}
1214
1215// --- polychrome sprites --- //
1216
1217struct PolychromeSprite {
1218 order: u32,
1219 pad: u32,
1220 grayscale: u32,
1221 opacity: f32,
1222 bounds: Bounds,
1223 content_mask: Bounds,
1224 corner_radii: Corners,
1225 tile: AtlasTile,
1226}
1227var<storage, read> b_poly_sprites: array<PolychromeSprite>;
1228
1229struct PolySpriteVarying {
1230 @builtin(position) position: vec4<f32>,
1231 @location(0) tile_position: vec2<f32>,
1232 @location(1) @interpolate(flat) sprite_id: u32,
1233 @location(3) clip_distances: vec4<f32>,
1234}
1235
1236@vertex
1237fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PolySpriteVarying {
1238 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
1239 let sprite = b_poly_sprites[instance_id];
1240
1241 var out = PolySpriteVarying();
1242 out.position = to_device_position(unit_vertex, sprite.bounds);
1243 out.tile_position = to_tile_position(unit_vertex, sprite.tile);
1244 out.sprite_id = instance_id;
1245 out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
1246 return out;
1247}
1248
1249@fragment
1250fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4<f32> {
1251 let sample = textureSample(t_sprite, s_sprite, input.tile_position);
1252 // Alpha clip after using the derivatives.
1253 if (any(input.clip_distances < vec4<f32>(0.0))) {
1254 return vec4<f32>(0.0);
1255 }
1256
1257 let sprite = b_poly_sprites[input.sprite_id];
1258 let distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii);
1259
1260 var color = sample;
1261 if ((sprite.grayscale & 0xFFu) != 0u) {
1262 let grayscale = dot(color.rgb, GRAYSCALE_FACTORS);
1263 color = vec4<f32>(vec3<f32>(grayscale), sample.a);
1264 }
1265 return blend_color(color, sprite.opacity * saturate(0.5 - distance));
1266}
1267
1268// --- surfaces --- //
1269
1270struct SurfaceParams {
1271 bounds: Bounds,
1272 content_mask: Bounds,
1273}
1274
1275var<uniform> surface_locals: SurfaceParams;
1276var t_y: texture_2d<f32>;
1277var t_cb_cr: texture_2d<f32>;
1278var s_surface: sampler;
1279
1280const ycbcr_to_RGB = mat4x4<f32>(
1281 vec4<f32>( 1.0000f, 1.0000f, 1.0000f, 0.0),
1282 vec4<f32>( 0.0000f, -0.3441f, 1.7720f, 0.0),
1283 vec4<f32>( 1.4020f, -0.7141f, 0.0000f, 0.0),
1284 vec4<f32>(-0.7010f, 0.5291f, -0.8860f, 1.0),
1285);
1286
1287struct SurfaceVarying {
1288 @builtin(position) position: vec4<f32>,
1289 @location(0) texture_position: vec2<f32>,
1290 @location(3) clip_distances: vec4<f32>,
1291}
1292
1293@vertex
1294fn vs_surface(@builtin(vertex_index) vertex_id: u32) -> SurfaceVarying {
1295 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
1296
1297 var out = SurfaceVarying();
1298 out.position = to_device_position(unit_vertex, surface_locals.bounds);
1299 out.texture_position = unit_vertex;
1300 out.clip_distances = distance_from_clip_rect(unit_vertex, surface_locals.bounds, surface_locals.content_mask);
1301 return out;
1302}
1303
1304@fragment
1305fn fs_surface(input: SurfaceVarying) -> @location(0) vec4<f32> {
1306 // Alpha clip after using the derivatives.
1307 if (any(input.clip_distances < vec4<f32>(0.0))) {
1308 return vec4<f32>(0.0);
1309 }
1310
1311 let y_cb_cr = vec4<f32>(
1312 textureSampleLevel(t_y, s_surface, input.texture_position, 0.0).r,
1313 textureSampleLevel(t_cb_cr, s_surface, input.texture_position, 0.0).rg,
1314 1.0);
1315
1316 return ycbcr_to_RGB * y_cb_cr;
1317}
1318
1319// --- subpixel sprites --- //
1320
1321struct SubpixelSprite {
1322 order: u32,
1323 pad: u32,
1324 bounds: Bounds,
1325 content_mask: Bounds,
1326 color: Hsla,
1327 tile: AtlasTile,
1328 transformation: TransformationMatrix,
1329}
1330var<storage, read> b_subpixel_sprites: array<SubpixelSprite>;
1331
1332struct SubpixelSpriteOutput {
1333 @builtin(position) position: vec4<f32>,
1334 @location(0) tile_position: vec2<f32>,
1335 @location(1) @interpolate(flat) color: vec4<f32>,
1336 @location(3) clip_distances: vec4<f32>,
1337}
1338
1339struct SubpixelSpriteFragmentOutput {
1340 @location(0) @blend_src(0) foreground: vec4<f32>,
1341 @location(0) @blend_src(1) alpha: vec4<f32>,
1342}
1343
1344@vertex
1345fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> SubpixelSpriteOutput {
1346 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
1347 let sprite = b_subpixel_sprites[instance_id];
1348
1349 var out = SubpixelSpriteOutput();
1350 out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
1351 out.tile_position = to_tile_position(unit_vertex, sprite.tile);
1352 out.color = hsla_to_rgba(sprite.color);
1353 out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
1354 return out;
1355}
1356
1357@fragment
1358fn fs_subpixel_sprite(input: SubpixelSpriteOutput) -> SubpixelSpriteFragmentOutput {
1359 let sample = textureSample(t_sprite, s_sprite, input.tile_position).rgb;
1360 let alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, subpixel_enhanced_contrast, gamma_ratios);
1361
1362 // Alpha clip after using the derivatives.
1363 if (any(input.clip_distances < vec4<f32>(0.0))) {
1364 return SubpixelSpriteFragmentOutput(vec4<f32>(0.0), vec4<f32>(0.0));
1365 }
1366
1367 var out = SubpixelSpriteFragmentOutput();
1368 out.foreground = vec4<f32>(input.color.rgb, 1.0);
1369 out.alpha = vec4<f32>(input.color.a * alpha_corrected, 1.0);
1370 return out;
1371}