shaders.wgsl

  1struct GlobalParams {
  2    viewport_size: vec2<f32>,
  3    premultiplied_alpha: u32,
  4    pad: u32,
  5}
  6
  7var<uniform> globals: GlobalParams;
  8var t_sprite: texture_2d<f32>;
  9var s_sprite: sampler;
 10
 11const M_PI_F: f32 = 3.1415926;
 12const GRAYSCALE_FACTORS: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);
 13
 14struct Bounds {
 15    origin: vec2<f32>,
 16    size: vec2<f32>,
 17}
 18struct Corners {
 19    top_left: f32,
 20    top_right: f32,
 21    bottom_right: f32,
 22    bottom_left: f32,
 23}
 24struct Edges {
 25    top: f32,
 26    right: f32,
 27    bottom: f32,
 28    left: f32,
 29}
 30struct Hsla {
 31    h: f32,
 32    s: f32,
 33    l: f32,
 34    a: f32,
 35}
 36
 37struct AtlasTextureId {
 38    index: u32,
 39    kind: u32,
 40}
 41
 42struct AtlasBounds {
 43    origin: vec2<i32>,
 44    size: vec2<i32>,
 45}
 46struct AtlasTile {
 47    texture_id: AtlasTextureId,
 48    tile_id: u32,
 49    padding: u32,
 50    bounds: AtlasBounds,
 51}
 52
 53struct TransformationMatrix {
 54    rotation_scale: mat2x2<f32>,
 55    translation: vec2<f32>,
 56}
 57
 58fn to_device_position_impl(position: vec2<f32>) -> vec4<f32> {
 59    let device_position = position / globals.viewport_size * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0);
 60    return vec4<f32>(device_position, 0.0, 1.0);
 61}
 62
 63fn to_device_position(unit_vertex: vec2<f32>, bounds: Bounds) -> vec4<f32> {
 64    let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
 65    return to_device_position_impl(position);
 66}
 67
 68fn to_device_position_transformed(unit_vertex: vec2<f32>, bounds: Bounds, transform: TransformationMatrix) -> vec4<f32> {
 69    let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
 70    //Note: Rust side stores it as row-major, so transposing here
 71    let transformed = transpose(transform.rotation_scale) * position + transform.translation;
 72    return to_device_position_impl(transformed);
 73}
 74
 75fn to_tile_position(unit_vertex: vec2<f32>, tile: AtlasTile) -> vec2<f32> {
 76  let atlas_size = vec2<f32>(textureDimensions(t_sprite, 0));
 77  return (vec2<f32>(tile.bounds.origin) + unit_vertex * vec2<f32>(tile.bounds.size)) / atlas_size;
 78}
 79
 80fn distance_from_clip_rect_impl(position: vec2<f32>, clip_bounds: Bounds) -> vec4<f32> {
 81    let tl = position - clip_bounds.origin;
 82    let br = clip_bounds.origin + clip_bounds.size - position;
 83    return vec4<f32>(tl.x, br.x, tl.y, br.y);
 84}
 85
 86fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds: Bounds) -> vec4<f32> {
 87    let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
 88    return distance_from_clip_rect_impl(position, clip_bounds);
 89}
 90
 91// https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl
 92fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
 93    let cutoff = srgb < vec3<f32>(0.04045);
 94    let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
 95    let lower = srgb / vec3<f32>(12.92);
 96    return select(higher, lower, cutoff);
 97}
 98
 99fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
100    let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
101    let s = hsla.s;
102    let l = hsla.l;
103    let a = hsla.a;
104
105    let c = (1.0 - abs(2.0 * l - 1.0)) * s;
106    let x = c * (1.0 - abs(h % 2.0 - 1.0));
107    let m = l - c / 2.0;
108    var color = vec3<f32>(m);
109
110    if (h >= 0.0 && h < 1.0) {
111        color.r += c;
112        color.g += x;
113    } else if (h >= 1.0 && h < 2.0) {
114        color.r += x;
115        color.g += c;
116    } else if (h >= 2.0 && h < 3.0) {
117        color.g += c;
118        color.b += x;
119    } else if (h >= 3.0 && h < 4.0) {
120        color.g += x;
121        color.b += c;
122    } else if (h >= 4.0 && h < 5.0) {
123        color.r += x;
124        color.b += c;
125    } else {
126        color.r += c;
127        color.b += x;
128    }
129
130    // Input colors are assumed to be in sRGB space,
131    // but blending and rendering needs to happen in linear space.
132    // The output will be converted to sRGB by either the target
133    // texture format or the swapchain color space.
134    let linear = srgb_to_linear(color);
135    return vec4<f32>(linear, a);
136}
137
138fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
139    let alpha = above.a + below.a * (1.0 - above.a);
140    let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
141    return vec4<f32>(color, alpha);
142}
143
144// A standard gaussian function, used for weighting samples
145fn gaussian(x: f32, sigma: f32) -> f32{
146    return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * M_PI_F) * sigma);
147}
148
149// This approximates the error function, needed for the gaussian integral
150fn erf(v: vec2<f32>) -> vec2<f32> {
151    let s = sign(v);
152    let a = abs(v);
153    let r1 = 1.0 + (0.278393 + (0.230389 + (0.000972 + 0.078108 * a) * a) * a) * a;
154    let r2 = r1 * r1;
155    return s - s / (r2 * r2);
156}
157
158fn blur_along_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2<f32>) -> f32 {
159  let delta = min(half_size.y - corner - abs(y), 0.0);
160  let curved = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
161  let integral = 0.5 + 0.5 * erf((x + vec2<f32>(-curved, curved)) * (sqrt(0.5) / sigma));
162  return integral.y - integral.x;
163}
164
165fn pick_corner_radius(point: vec2<f32>, radii: Corners) -> f32 {
166    if (point.x < 0.0) {
167        if (point.y < 0.0) {
168            return radii.top_left;
169        } else {
170            return radii.bottom_left;
171        }
172    } else {
173        if (point.y < 0.0) {
174            return radii.top_right;
175        } else {
176            return radii.bottom_right;
177        }
178    }
179}
180
181fn quad_sdf(point: vec2<f32>, bounds: Bounds, corner_radii: Corners) -> f32 {
182    let half_size = bounds.size / 2.0;
183    let center = bounds.origin + half_size;
184    let center_to_point = point - center;
185    let corner_radius = pick_corner_radius(center_to_point, corner_radii);
186    let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
187    return length(max(vec2<f32>(0.0), rounded_edge_to_point)) +
188        min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
189        corner_radius;
190}
191
192// Abstract away the final color transformation based on the
193// target alpha compositing mode.
194fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
195    let alpha = color.a * alpha_factor;
196    let multiplier = select(1.0, alpha, globals.premultiplied_alpha != 0u);
197    return vec4<f32>(color.rgb * multiplier, alpha);
198}
199
200// --- quads --- //
201
202struct Quad {
203    order: u32,
204    pad: u32,
205    bounds: Bounds,
206    content_mask: Bounds,
207    background: Hsla,
208    border_color: Hsla,
209    corner_radii: Corners,
210    border_widths: Edges,
211}
212var<storage, read> b_quads: array<Quad>;
213
214struct QuadVarying {
215    @builtin(position) position: vec4<f32>,
216    @location(0) @interpolate(flat) background_color: vec4<f32>,
217    @location(1) @interpolate(flat) border_color: vec4<f32>,
218    @location(2) @interpolate(flat) quad_id: u32,
219    //TODO: use `clip_distance` once Naga supports it
220    @location(3) clip_distances: vec4<f32>,
221}
222
223@vertex
224fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying {
225    let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
226    let quad = b_quads[instance_id];
227
228    var out = QuadVarying();
229    out.position = to_device_position(unit_vertex, quad.bounds);
230    out.background_color = hsla_to_rgba(quad.background);
231    out.border_color = hsla_to_rgba(quad.border_color);
232    out.quad_id = instance_id;
233    out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
234    return out;
235}
236
237@fragment
238fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
239    // Alpha clip first, since we don't have `clip_distance`.
240    if (any(input.clip_distances < vec4<f32>(0.0))) {
241        return vec4<f32>(0.0);
242    }
243
244    let quad = b_quads[input.quad_id];
245    // Fast path when the quad is not rounded and doesn't have any border.
246    if (quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 &&
247        quad.corner_radii.top_right == 0.0 &&
248        quad.corner_radii.bottom_right == 0.0 && quad.border_widths.top == 0.0 &&
249        quad.border_widths.left == 0.0 && quad.border_widths.right == 0.0 &&
250        quad.border_widths.bottom == 0.0) {
251        return blend_color(input.background_color, 1.0);
252    }
253
254    let half_size = quad.bounds.size / 2.0;
255    let center = quad.bounds.origin + half_size;
256    let center_to_point = input.position.xy - center;
257
258    let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
259
260    let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
261    let distance =
262      length(max(vec2<f32>(0.0), rounded_edge_to_point)) +
263      min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
264      corner_radius;
265
266    let vertical_border = select(quad.border_widths.left, quad.border_widths.right, center_to_point.x > 0.0);
267    let horizontal_border = select(quad.border_widths.top, quad.border_widths.bottom, center_to_point.y > 0.0);
268    let inset_size = half_size - corner_radius - vec2<f32>(vertical_border, horizontal_border);
269    let point_to_inset_corner = abs(center_to_point) - inset_size;
270
271    var border_width = 0.0;
272    if (point_to_inset_corner.x < 0.0 && point_to_inset_corner.y < 0.0) {
273        border_width = 0.0;
274    } else if (point_to_inset_corner.y > point_to_inset_corner.x) {
275        border_width = horizontal_border;
276    } else {
277        border_width = vertical_border;
278    }
279
280    var color = input.background_color;
281    if (border_width > 0.0) {
282        let inset_distance = distance + border_width;
283        // Blend the border on top of the background and then linearly interpolate
284        // between the two as we slide inside the background.
285        let blended_border = over(input.background_color, input.border_color);
286        color = mix(blended_border, input.background_color,
287                    saturate(0.5 - inset_distance));
288    }
289
290    return blend_color(color, saturate(0.5 - distance));
291}
292
293// --- shadows --- //
294
295struct Shadow {
296    order: u32,
297    blur_radius: f32,
298    bounds: Bounds,
299    corner_radii: Corners,
300    content_mask: Bounds,
301    color: Hsla,
302}
303var<storage, read> b_shadows: array<Shadow>;
304
305struct ShadowVarying {
306    @builtin(position) position: vec4<f32>,
307    @location(0) @interpolate(flat) color: vec4<f32>,
308    @location(1) @interpolate(flat) shadow_id: u32,
309    //TODO: use `clip_distance` once Naga supports it
310    @location(3) clip_distances: vec4<f32>,
311}
312
313@vertex
314fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> ShadowVarying {
315    let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
316    var shadow = b_shadows[instance_id];
317
318    let margin = 3.0 * shadow.blur_radius;
319    // Set the bounds of the shadow and adjust its size based on the shadow's
320    // spread radius to achieve the spreading effect
321    shadow.bounds.origin -= vec2<f32>(margin);
322    shadow.bounds.size += 2.0 * vec2<f32>(margin);
323
324    var out = ShadowVarying();
325    out.position = to_device_position(unit_vertex, shadow.bounds);
326    out.color = hsla_to_rgba(shadow.color);
327    out.shadow_id = instance_id;
328    out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask);
329    return out;
330}
331
332@fragment
333fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
334    // Alpha clip first, since we don't have `clip_distance`.
335    if (any(input.clip_distances < vec4<f32>(0.0))) {
336        return vec4<f32>(0.0);
337    }
338
339    let shadow = b_shadows[input.shadow_id];
340    let half_size = shadow.bounds.size / 2.0;
341    let center = shadow.bounds.origin + half_size;
342    let center_to_point = input.position.xy - center;
343
344    let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii);
345
346    // The signal is only non-zero in a limited range, so don't waste samples
347    let low = center_to_point.y - half_size.y;
348    let high = center_to_point.y + half_size.y;
349    let start = clamp(-3.0 * shadow.blur_radius, low, high);
350    let end = clamp(3.0 * shadow.blur_radius, low, high);
351
352    // Accumulate samples (we can get away with surprisingly few samples)
353    let step = (end - start) / 4.0;
354    var y = start + step * 0.5;
355    var alpha = 0.0;
356    for (var i = 0; i < 4; i += 1) {
357        let blur = blur_along_x(center_to_point.x, center_to_point.y - y,
358            shadow.blur_radius, corner_radius, half_size);
359        alpha +=  blur * gaussian(y, shadow.blur_radius) * step;
360        y += step;
361    }
362
363    return blend_color(input.color, alpha);
364}
365
366// --- path rasterization --- //
367
368struct PathVertex {
369    xy_position: vec2<f32>,
370    st_position: vec2<f32>,
371    content_mask: Bounds,
372}
373var<storage, read> b_path_vertices: array<PathVertex>;
374
375struct PathRasterizationVarying {
376    @builtin(position) position: vec4<f32>,
377    @location(0) st_position: vec2<f32>,
378    //TODO: use `clip_distance` once Naga supports it
379    @location(3) clip_distances: vec4<f32>,
380}
381
382@vertex
383fn vs_path_rasterization(@builtin(vertex_index) vertex_id: u32) -> PathRasterizationVarying {
384    let v = b_path_vertices[vertex_id];
385
386    var out = PathRasterizationVarying();
387    out.position = to_device_position_impl(v.xy_position);
388    out.st_position = v.st_position;
389    out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.content_mask);
390    return out;
391}
392
393@fragment
394fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) f32 {
395    let dx = dpdx(input.st_position);
396    let dy = dpdy(input.st_position);
397    if (any(input.clip_distances < vec4<f32>(0.0))) {
398        return 0.0;
399    }
400
401    let gradient = 2.0 * input.st_position.xx * vec2<f32>(dx.x, dy.x) - vec2<f32>(dx.y, dy.y);
402    let f = input.st_position.x * input.st_position.x - input.st_position.y;
403    let distance = f / length(gradient);
404    return saturate(0.5 - distance);
405}
406
407// --- paths --- //
408
409struct PathSprite {
410    bounds: Bounds,
411    color: Hsla,
412    tile: AtlasTile,
413}
414var<storage, read> b_path_sprites: array<PathSprite>;
415
416struct PathVarying {
417    @builtin(position) position: vec4<f32>,
418    @location(0) tile_position: vec2<f32>,
419    @location(1) color: vec4<f32>,
420}
421
422@vertex
423fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PathVarying {
424    let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
425    let sprite = b_path_sprites[instance_id];
426    // Don't apply content mask because it was already accounted for when rasterizing the path.
427
428    var out = PathVarying();
429    out.position = to_device_position(unit_vertex, sprite.bounds);
430    out.tile_position = to_tile_position(unit_vertex, sprite.tile);
431    out.color = hsla_to_rgba(sprite.color);
432    return out;
433}
434
435@fragment
436fn fs_path(input: PathVarying) -> @location(0) vec4<f32> {
437    let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
438    let mask = 1.0 - abs(1.0 - sample % 2.0);
439    return blend_color(input.color, mask);
440}
441
442// --- underlines --- //
443
444struct Underline {
445    order: u32,
446    pad: u32,
447    bounds: Bounds,
448    content_mask: Bounds,
449    color: Hsla,
450    thickness: f32,
451    wavy: u32,
452}
453var<storage, read> b_underlines: array<Underline>;
454
455struct UnderlineVarying {
456    @builtin(position) position: vec4<f32>,
457    @location(0) @interpolate(flat) color: vec4<f32>,
458    @location(1) @interpolate(flat) underline_id: u32,
459    //TODO: use `clip_distance` once Naga supports it
460    @location(3) clip_distances: vec4<f32>,
461}
462
463@vertex
464fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> UnderlineVarying {
465    let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
466    let underline = b_underlines[instance_id];
467
468    var out = UnderlineVarying();
469    out.position = to_device_position(unit_vertex, underline.bounds);
470    out.color = hsla_to_rgba(underline.color);
471    out.underline_id = instance_id;
472    out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask);
473    return out;
474}
475
476@fragment
477fn fs_underline(input: UnderlineVarying) -> @location(0) vec4<f32> {
478    // Alpha clip first, since we don't have `clip_distance`.
479    if (any(input.clip_distances < vec4<f32>(0.0))) {
480        return vec4<f32>(0.0);
481    }
482
483    let underline = b_underlines[input.underline_id];
484    if ((underline.wavy & 0xFFu) == 0u)
485    {
486        return blend_color(input.color, input.color.a);
487    }
488
489    let half_thickness = underline.thickness * 0.5;
490    let st = (input.position.xy - underline.bounds.origin) / underline.bounds.size.y - vec2<f32>(0.0, 0.5);
491    let frequency = M_PI_F * 3.0 * underline.thickness / 3.0;
492    let amplitude = 1.0 / (4.0 * underline.thickness);
493    let sine = sin(st.x * frequency) * amplitude;
494    let dSine = cos(st.x * frequency) * amplitude * frequency;
495    let distance = (st.y - sine) / sqrt(1.0 + dSine * dSine);
496    let distance_in_pixels = distance * underline.bounds.size.y;
497    let distance_from_top_border = distance_in_pixels - half_thickness;
498    let distance_from_bottom_border = distance_in_pixels + half_thickness;
499    let alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border));
500    return blend_color(input.color, alpha * input.color.a);
501}
502
503// --- monochrome sprites --- //
504
505struct MonochromeSprite {
506    order: u32,
507    pad: u32,
508    bounds: Bounds,
509    content_mask: Bounds,
510    color: Hsla,
511    tile: AtlasTile,
512    transformation: TransformationMatrix,
513}
514var<storage, read> b_mono_sprites: array<MonochromeSprite>;
515
516struct MonoSpriteVarying {
517    @builtin(position) position: vec4<f32>,
518    @location(0) tile_position: vec2<f32>,
519    @location(1) @interpolate(flat) color: vec4<f32>,
520    @location(3) clip_distances: vec4<f32>,
521}
522
523@vertex
524fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> MonoSpriteVarying {
525    let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
526    let sprite = b_mono_sprites[instance_id];
527
528    var out = MonoSpriteVarying();
529    out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
530
531    out.tile_position = to_tile_position(unit_vertex, sprite.tile);
532    out.color = hsla_to_rgba(sprite.color);
533    out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
534    return out;
535}
536
537@fragment
538fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4<f32> {
539    let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
540    // Alpha clip after using the derivatives.
541    if (any(input.clip_distances < vec4<f32>(0.0))) {
542        return vec4<f32>(0.0);
543    }
544    return blend_color(input.color, sample);
545}
546
547// --- polychrome sprites --- //
548
549struct PolychromeSprite {
550    order: u32,
551    pad: u32,
552    grayscale: u32,
553    opacity: f32,
554    bounds: Bounds,
555    content_mask: Bounds,
556    corner_radii: Corners,
557    tile: AtlasTile,
558}
559var<storage, read> b_poly_sprites: array<PolychromeSprite>;
560
561struct PolySpriteVarying {
562    @builtin(position) position: vec4<f32>,
563    @location(0) tile_position: vec2<f32>,
564    @location(1) @interpolate(flat) sprite_id: u32,
565    @location(3) clip_distances: vec4<f32>,
566}
567
568@vertex
569fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PolySpriteVarying {
570    let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
571    let sprite = b_poly_sprites[instance_id];
572
573    var out = PolySpriteVarying();
574    out.position = to_device_position(unit_vertex, sprite.bounds);
575    out.tile_position = to_tile_position(unit_vertex, sprite.tile);
576    out.sprite_id = instance_id;
577    out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
578    return out;
579}
580
581@fragment
582fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4<f32> {
583    let sample = textureSample(t_sprite, s_sprite, input.tile_position);
584    // Alpha clip after using the derivatives.
585    if (any(input.clip_distances < vec4<f32>(0.0))) {
586        return vec4<f32>(0.0);
587    }
588
589    let sprite = b_poly_sprites[input.sprite_id];
590    let distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii);
591
592    var color = sample;
593    if ((sprite.grayscale & 0xFFu) != 0u) {
594        let grayscale = dot(color.rgb, GRAYSCALE_FACTORS);
595        color = vec4<f32>(vec3<f32>(grayscale), sample.a);
596    }
597    return blend_color(color, sprite.opacity * saturate(0.5 - distance));
598}
599
600// --- surfaces --- //
601
602struct SurfaceParams {
603    bounds: Bounds,
604    content_mask: Bounds,
605}
606
607var<uniform> surface_locals: SurfaceParams;
608var t_y: texture_2d<f32>;
609var t_cb_cr: texture_2d<f32>;
610var s_surface: sampler;
611
612const ycbcr_to_RGB = mat4x4<f32>(
613    vec4<f32>( 1.0000f,  1.0000f,  1.0000f, 0.0),
614    vec4<f32>( 0.0000f, -0.3441f,  1.7720f, 0.0),
615    vec4<f32>( 1.4020f, -0.7141f,  0.0000f, 0.0),
616    vec4<f32>(-0.7010f,  0.5291f, -0.8860f, 1.0),
617);
618
619struct SurfaceVarying {
620    @builtin(position) position: vec4<f32>,
621    @location(0) texture_position: vec2<f32>,
622    @location(3) clip_distances: vec4<f32>,
623}
624
625@vertex
626fn vs_surface(@builtin(vertex_index) vertex_id: u32) -> SurfaceVarying {
627    let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
628
629    var out = SurfaceVarying();
630    out.position = to_device_position(unit_vertex, surface_locals.bounds);
631    out.texture_position = unit_vertex;
632    out.clip_distances = distance_from_clip_rect(unit_vertex, surface_locals.bounds, surface_locals.content_mask);
633    return out;
634}
635
636@fragment
637fn fs_surface(input: SurfaceVarying) -> @location(0) vec4<f32> {
638    // Alpha clip after using the derivatives.
639    if (any(input.clip_distances < vec4<f32>(0.0))) {
640        return vec4<f32>(0.0);
641    }
642
643    let y_cb_cr = vec4<f32>(
644        textureSampleLevel(t_y, s_surface, input.texture_position, 0.0).r,
645        textureSampleLevel(t_cb_cr, s_surface, input.texture_position, 0.0).rg,
646        1.0);
647
648    return ycbcr_to_RGB * y_cb_cr;
649}