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