shaders.hlsl

  1cbuffer GlobalParams: register(b0) {
  2    float2 global_viewport_size;
  3    uint2 _global_pad;
  4};
  5
  6Texture2D<float4> t_sprite: register(t0);
  7SamplerState s_sprite: register(s0);
  8
  9struct Bounds {
 10    float2 origin;
 11    float2 size;
 12};
 13
 14struct Corners {
 15    float top_left;
 16    float top_right;
 17    float bottom_right;
 18    float bottom_left;
 19};
 20
 21struct Edges {
 22    float top;
 23    float right;
 24    float bottom;
 25    float left;
 26};
 27
 28struct Hsla {
 29    float h;
 30    float s;
 31    float l;
 32    float a;
 33};
 34
 35struct LinearColorStop {
 36    Hsla color;
 37    float percentage;
 38};
 39
 40struct Background {
 41    // 0u is Solid
 42    // 1u is LinearGradient
 43    // 2u is PatternSlash
 44    uint tag;
 45    // 0u is sRGB linear color
 46    // 1u is Oklab color
 47    uint color_space;
 48    Hsla solid;
 49    float gradient_angle_or_pattern_height;
 50    LinearColorStop colors[2];
 51    uint pad;
 52};
 53
 54struct GradientColor {
 55  float4 solid;
 56  float4 color0;
 57  float4 color1;
 58};
 59
 60struct AtlasTextureId {
 61    uint index;
 62    uint kind;
 63};
 64
 65struct AtlasBounds {
 66    int2 origin;
 67    int2 size;
 68};
 69
 70struct AtlasTile {
 71    AtlasTextureId texture_id;
 72    uint tile_id;
 73    uint padding;
 74    AtlasBounds bounds;
 75};
 76
 77struct TransformationMatrix {
 78    float2x2 rotation_scale;
 79    float2 translation;
 80};
 81
 82static const float M_PI_F = 3.141592653f;
 83static const float3 GRAYSCALE_FACTORS = float3(0.2126f, 0.7152f, 0.0722f);
 84
 85float4 to_device_position_impl(float2 position) {
 86    float2 device_position = position / global_viewport_size * float2(2.0, -2.0) + float2(-1.0, 1.0);
 87    return float4(device_position, 0., 1.);
 88}
 89
 90float4 to_device_position(float2 unit_vertex, Bounds bounds) {
 91    float2 position = unit_vertex * bounds.size + bounds.origin;
 92    return to_device_position_impl(position);
 93}
 94
 95float4 distance_from_clip_rect_impl(float2 position, Bounds clip_bounds) {
 96    return float4(position.x - clip_bounds.origin.x,
 97                    clip_bounds.origin.x + clip_bounds.size.x - position.x,
 98                    position.y - clip_bounds.origin.y,
 99                    clip_bounds.origin.y + clip_bounds.size.y - position.y);
100}
101
102float4 distance_from_clip_rect(float2 unit_vertex, Bounds bounds, Bounds clip_bounds) {
103    float2 position = unit_vertex * bounds.size + bounds.origin;
104    return distance_from_clip_rect_impl(position, clip_bounds);
105}
106
107// Convert linear RGB to sRGB
108float3 linear_to_srgb(float3 color) {
109    return pow(color, float3(2.2, 2.2, 2.2));
110}
111
112// Convert sRGB to linear RGB
113float3 srgb_to_linear(float3 color) {
114    return pow(color, float3(1.0 / 2.2, 1.0 / 2.2, 1.0 / 2.2));
115}
116
117/// Hsla to linear RGBA conversion.
118float4 hsla_to_rgba(Hsla hsla) {
119    float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
120    float s = hsla.s;
121    float l = hsla.l;
122    float a = hsla.a;
123
124    float c = (1.0 - abs(2.0 * l - 1.0)) * s;
125    float x = c * (1.0 - abs(fmod(h, 2.0) - 1.0));
126    float m = l - c / 2.0;
127
128    float r = 0.0;
129    float g = 0.0;
130    float b = 0.0;
131
132    if (h >= 0.0 && h < 1.0) {
133        r = c;
134        g = x;
135        b = 0.0;
136    } else if (h >= 1.0 && h < 2.0) {
137        r = x;
138        g = c;
139        b = 0.0;
140    } else if (h >= 2.0 && h < 3.0) {
141        r = 0.0;
142        g = c;
143        b = x;
144    } else if (h >= 3.0 && h < 4.0) {
145        r = 0.0;
146        g = x;
147        b = c;
148    } else if (h >= 4.0 && h < 5.0) {
149        r = x;
150        g = 0.0;
151        b = c;
152    } else {
153        r = c;
154        g = 0.0;
155        b = x;
156    }
157
158    float4 rgba;
159    rgba.x = (r + m);
160    rgba.y = (g + m);
161    rgba.z = (b + m);
162    rgba.w = a;
163    return rgba;
164}
165
166// Converts a sRGB color to the Oklab color space.
167// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
168float4 srgb_to_oklab(float4 color) {
169    // Convert non-linear sRGB to linear sRGB
170    color = float4(srgb_to_linear(color.rgb), color.a);
171
172    float l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
173    float m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
174    float s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
175
176    float l_ = pow(l, 1.0/3.0);
177    float m_ = pow(m, 1.0/3.0);
178    float s_ = pow(s, 1.0/3.0);
179
180    return float4(
181        0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
182        1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
183        0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
184        color.a
185    );
186}
187
188// Converts an Oklab color to the sRGB color space.
189float4 oklab_to_srgb(float4 color) {
190    float l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b;
191    float m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b;
192    float s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b;
193
194    float l = l_ * l_ * l_;
195    float m = m_ * m_ * m_;
196    float s = s_ * s_ * s_;
197
198    float3 linear_rgb = float3(
199        4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
200        -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
201        -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
202    );
203
204    // Convert linear sRGB to non-linear sRGB
205    return float4(linear_to_srgb(linear_rgb), color.a);
206}
207
208// This approximates the error function, needed for the gaussian integral
209float2 erf(float2 x) {
210    float2 s = sign(x);
211    float2 a = abs(x);
212    x = 1. + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
213    x *= x;
214    return s - s / (x * x);
215}
216
217float blur_along_x(float x, float y, float sigma, float corner, float2 half_size) {
218    float delta = min(half_size.y - corner - abs(y), 0.);
219    float curved = half_size.x - corner + sqrt(max(0., corner * corner - delta * delta));
220    float2 integral = 0.5 + 0.5 * erf((x + float2(-curved, curved)) * (sqrt(0.5) / sigma));
221    return integral.y - integral.x;
222}
223
224// A standard gaussian function, used for weighting samples
225float gaussian(float x, float sigma) {
226    return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * M_PI_F) * sigma);
227}
228
229float4 over(float4 below, float4 above) {
230    float4 result;
231    float alpha = above.a + below.a * (1.0 - above.a);
232    result.rgb = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
233    result.a = alpha;
234    return result;
235}
236
237float2 to_tile_position(float2 unit_vertex, AtlasTile tile) {
238    float2 atlas_size;
239    t_sprite.GetDimensions(atlas_size.x, atlas_size.y);
240    return (float2(tile.bounds.origin) + unit_vertex * float2(tile.bounds.size)) / atlas_size;
241}
242
243float4 to_device_position_transformed(float2 unit_vertex, Bounds bounds, 
244                                      TransformationMatrix transformation) {
245    float2 position = unit_vertex * bounds.size + bounds.origin;
246    float2 transformed = mul(position, transformation.rotation_scale) + transformation.translation;
247    float2 device_position = transformed / global_viewport_size * float2(2.0, -2.0) + float2(-1.0, 1.0);
248    return float4(device_position, 0.0, 1.0);
249}
250
251float quad_sdf(float2 pt, Bounds bounds, Corners corner_radii) {
252    float2 half_size = bounds.size / 2.;
253    float2 center = bounds.origin + half_size;
254    float2 center_to_point = pt - center;
255    float corner_radius;
256    if (center_to_point.x < 0.) {
257        if (center_to_point.y < 0.) {
258            corner_radius = corner_radii.top_left;
259        } else {
260            corner_radius = corner_radii.bottom_left;
261        }
262    } else {
263        if (center_to_point.y < 0.) {
264            corner_radius = corner_radii.top_right;
265        } else {
266            corner_radius = corner_radii.bottom_right;
267        }
268    }
269
270    float2 rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
271    float distance =
272        length(max(0., rounded_edge_to_point)) +
273        min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
274        corner_radius;
275
276    return distance;
277}
278
279GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid, Hsla color0, Hsla color1) {
280    GradientColor output;
281    if (tag == 0) {
282        output.solid = hsla_to_rgba(solid);
283    } else if (tag == 1) {
284        output.color0 = hsla_to_rgba(color0);
285        output.color1 = hsla_to_rgba(color1);
286
287        // Prepare color space in vertex for avoid conversion
288        // in fragment shader for performance reasons
289        if (color_space == 1) {
290        // Oklab
291        output.color0 = srgb_to_oklab(output.color0);
292        output.color1 = srgb_to_oklab(output.color1);
293        }
294    }
295
296    return output;
297}
298
299float2x2 rotate2d(float angle) {
300    float s = sin(angle);
301    float c = cos(angle);
302    return float2x2(c, -s, s, c);
303}
304
305float4 gradient_color(Background background,
306                      float2 position,
307                      Bounds bounds,
308                      float4 solid_color, float4 color0, float4 color1) {
309    float4 color;
310
311    switch (background.tag) {
312        case 0:
313            color = solid_color;
314            break;
315        case 1: {
316            // -90 degrees to match the CSS gradient angle.
317            float gradient_angle = background.gradient_angle_or_pattern_height;
318            float radians = (fmod(gradient_angle, 360.0) - 90.0) * (M_PI_F / 180.0);
319            float2 direction = float2(cos(radians), sin(radians));
320
321            // Expand the short side to be the same as the long side
322            if (bounds.size.x > bounds.size.y) {
323                direction.y *= bounds.size.y / bounds.size.x;
324            } else {
325                direction.x *=  bounds.size.x / bounds.size.y;
326            }
327
328            // Get the t value for the linear gradient with the color stop percentages.
329            float2 half_size = float2(bounds.size.x, bounds.size.y) / 2.;
330            float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size;
331            float2 center_to_point = position - center;
332            float t = dot(center_to_point, direction) / length(direction);
333            // Check the direct to determine the use x or y
334            if (abs(direction.x) > abs(direction.y)) {
335                t = (t + half_size.x) / bounds.size.x;
336            } else {
337                t = (t + half_size.y) / bounds.size.y;
338            }
339
340            // Adjust t based on the stop percentages
341            t = (t - background.colors[0].percentage)
342                / (background.colors[1].percentage
343                - background.colors[0].percentage);
344            t = clamp(t, 0.0, 1.0);
345
346            switch (background.color_space) {
347                case 0:
348                    color = lerp(color0, color1, t);
349                    break;
350                case 1: {
351                    float4 oklab_color = lerp(color0, color1, t);
352                    color = oklab_to_srgb(oklab_color);
353                    break;
354                }
355            }
356            break;
357        }
358        case 2: {
359            float gradient_angle_or_pattern_height = background.gradient_angle_or_pattern_height;
360            float pattern_width = (gradient_angle_or_pattern_height / 65535.0f) / 255.0f;
361            float pattern_interval = fmod(gradient_angle_or_pattern_height, 65535.0f) / 255.0f;
362            float pattern_height = pattern_width + pattern_interval;
363            float stripe_angle = M_PI_F / 4.0;
364            float pattern_period = pattern_height * sin(stripe_angle);
365            float2x2 rotation = rotate2d(stripe_angle);
366            float2 relative_position = position - bounds.origin;
367            float2 rotated_point = mul(rotation, relative_position);
368            float pattern = fmod(rotated_point.x, pattern_period);
369            float distance = min(pattern, pattern_period - pattern) - pattern_period * (pattern_width / pattern_height) /  2.0f;
370            color = solid_color;
371            color.a *= saturate(0.5 - distance);
372            break;
373        }
374    }
375
376    return color;
377}
378
379/*
380**
381**              Shadows
382**
383*/
384
385struct ShadowVertexOutput {
386    float4 position: SV_Position;
387    float4 color: COLOR;
388    uint shadow_id: FLAT;
389    float4 clip_distance: SV_ClipDistance;
390};
391
392struct ShadowFragmentInput {
393  float4 position: SV_Position;
394  float4 color: COLOR;
395  uint shadow_id: FLAT;
396};
397
398struct Shadow {
399    uint order;
400    float blur_radius;
401    Bounds bounds;
402    Corners corner_radii;
403    Bounds content_mask;
404    Hsla color;
405};
406
407StructuredBuffer<Shadow> shadows: register(t1);
408
409ShadowVertexOutput shadow_vertex(uint vertex_id: SV_VertexID, uint shadow_id: SV_InstanceID) {
410    float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u));
411    Shadow shadow = shadows[shadow_id];
412
413    float margin = 3.0 * shadow.blur_radius;
414    Bounds bounds = shadow.bounds;
415    bounds.origin -= margin;
416    bounds.size += 2.0 * margin;
417
418    float4 device_position = to_device_position(unit_vertex, bounds);
419    float4 clip_distance = distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask);
420    float4 color = hsla_to_rgba(shadow.color);
421
422    ShadowVertexOutput output;
423    output.position = device_position;
424    output.color = color;
425    output.shadow_id = shadow_id;
426    output.clip_distance = clip_distance;
427
428    return output;
429}
430
431float4 shadow_fragment(ShadowFragmentInput input): SV_TARGET {
432    Shadow shadow = shadows[input.shadow_id];
433
434    float2 half_size = shadow.bounds.size / 2.;
435    float2 center = shadow.bounds.origin + half_size;
436    float2 point0 = input.position.xy - center;
437    float corner_radius;
438    if (point0.x < 0.) {
439        if (point0.y < 0.) {
440            corner_radius = shadow.corner_radii.top_left;
441        } else {
442            corner_radius = shadow.corner_radii.bottom_left;
443        }
444    } else {
445        if (point0.y < 0.) {
446            corner_radius = shadow.corner_radii.top_right;
447        } else {
448            corner_radius = shadow.corner_radii.bottom_right;
449        }
450    }
451
452    // The signal is only non-zero in a limited range, so don't waste samples
453    float low = point0.y - half_size.y;
454    float high = point0.y + half_size.y;
455    float start = clamp(-3. * shadow.blur_radius, low, high);
456    float end = clamp(3. * shadow.blur_radius, low, high);
457
458    // Accumulate samples (we can get away with surprisingly few samples)
459    float step = (end - start) / 4.;
460    float y = start + step * 0.5;
461    float alpha = 0.;
462    for (int i = 0; i < 4; i++) {
463        alpha += blur_along_x(point0.x, point0.y - y, shadow.blur_radius,
464                            corner_radius, half_size) *
465                gaussian(y, shadow.blur_radius) * step;
466        y += step;
467    }
468
469    return input.color * float4(1., 1., 1., alpha);
470}
471
472/*
473**
474**              Quads
475**
476*/
477
478struct Quad {
479    uint order;
480    uint pad;
481    Bounds bounds;
482    Bounds content_mask;
483    Background background;
484    Hsla border_color;
485    Corners corner_radii;
486    Edges border_widths;
487};
488
489struct QuadVertexOutput {
490    float4 position: SV_Position;
491    nointerpolation float4 border_color: COLOR0;
492    nointerpolation uint quad_id: TEXCOORD0;
493    nointerpolation float4 background_solid: COLOR1;
494    nointerpolation float4 background_color0: COLOR2;
495    nointerpolation float4 background_color1: COLOR3;
496    float4 clip_distance: SV_ClipDistance;
497};
498
499struct QuadFragmentInput {
500    nointerpolation uint quad_id: TEXCOORD0;
501    float4 position: SV_Position;
502    nointerpolation float4 border_color: COLOR0;
503    nointerpolation float4 background_solid: COLOR1;
504    nointerpolation float4 background_color0: COLOR2;
505    nointerpolation float4 background_color1: COLOR3;
506};
507
508StructuredBuffer<Quad> quads: register(t1);
509
510QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint quad_id: SV_InstanceID) {
511    float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u));
512    Quad quad = quads[quad_id];
513    float4 device_position = to_device_position(unit_vertex, quad.bounds);
514    float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
515    float4 border_color = hsla_to_rgba(quad.border_color);
516
517    GradientColor gradient = prepare_gradient_color(
518        quad.background.tag,
519        quad.background.color_space,
520        quad.background.solid,
521        quad.background.colors[0].color,
522        quad.background.colors[1].color
523    );
524
525    QuadVertexOutput output;
526    output.position = device_position;
527    output.border_color = border_color;
528    output.quad_id = quad_id;
529    output.background_solid = gradient.solid;
530    output.background_color0 = gradient.color0;
531    output.background_color1 = gradient.color1;
532    output.clip_distance = clip_distance;
533    return output;
534}
535
536float4 quad_fragment(QuadFragmentInput input): SV_Target {
537    Quad quad = quads[input.quad_id];
538    float2 half_size = quad.bounds.size / 2.;
539    float2 center = quad.bounds.origin + half_size;
540    float2 center_to_point = input.position.xy - center;
541    float4 color = gradient_color(quad.background, input.position.xy, quad.bounds,
542    input.background_solid, input.background_color0, input.background_color1);
543
544    // Fast path when the quad is not rounded and doesn't have any border.
545    if (quad.corner_radii.top_left == 0. && quad.corner_radii.bottom_left == 0. &&
546        quad.corner_radii.top_right == 0. &&
547        quad.corner_radii.bottom_right == 0. && quad.border_widths.top == 0. &&
548        quad.border_widths.left == 0. && quad.border_widths.right == 0. &&
549        quad.border_widths.bottom == 0.) {
550        return color;
551    }
552
553    float corner_radius;
554    if (center_to_point.x < 0.) {
555        if (center_to_point.y < 0.) {
556            corner_radius = quad.corner_radii.top_left;
557        } else {
558            corner_radius = quad.corner_radii.bottom_left;
559        }
560    } else {
561        if (center_to_point.y < 0.) {
562            corner_radius = quad.corner_radii.top_right;
563        } else {
564            corner_radius = quad.corner_radii.bottom_right;
565        }
566    }
567
568    float2 rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
569    float distance =
570        length(max(0., rounded_edge_to_point)) +
571        min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
572        corner_radius;
573
574    float vertical_border = center_to_point.x <= 0. ? quad.border_widths.left
575                                                    : quad.border_widths.right;
576    float horizontal_border = center_to_point.y <= 0. ? quad.border_widths.top
577                                                        : quad.border_widths.bottom;
578    float2 inset_size = half_size - corner_radius - float2(vertical_border, horizontal_border);
579    float2 point_to_inset_corner = abs(center_to_point) - inset_size;
580    float border_width;
581    if (point_to_inset_corner.x < 0. && point_to_inset_corner.y < 0.) {
582        border_width = 0.;
583    } else if (point_to_inset_corner.y > point_to_inset_corner.x) {
584        border_width = horizontal_border;
585    } else {
586        border_width = vertical_border;
587    }
588
589    if (border_width != 0.) {
590        float inset_distance = distance + border_width;
591        // Blend the border on top of the background and then linearly interpolate
592        // between the two as we slide inside the background.
593        float4 blended_border = over(color, input.border_color);
594        color = lerp(blended_border, color, saturate(0.5 - inset_distance));
595    }
596
597    return color * float4(1., 1., 1., saturate(0.5 - distance));
598}
599
600struct PathVertex {
601    float2 xy_position;
602    Bounds content_mask;
603};
604
605/*
606**
607**              Paths
608**
609*/
610
611struct PathSprite {
612    Bounds bounds;
613    Background color;
614};
615
616struct PathVertexOutput {
617    float4 position: SV_Position;
618    float4 clip_distance: SV_ClipDistance;
619    nointerpolation uint sprite_id: TEXCOORD0;
620    nointerpolation float4 solid_color: COLOR0;
621    nointerpolation float4 color0: COLOR1;
622    nointerpolation float4 color1: COLOR2;
623};
624
625StructuredBuffer<PathVertex> path_vertices: register(t1);
626StructuredBuffer<PathSprite> path_sprites: register(t2);
627
628PathVertexOutput paths_vertex(uint vertex_id: SV_VertexID, uint instance_id: SV_InstanceID) {
629    PathVertex v = path_vertices[vertex_id];
630    PathSprite sprite = path_sprites[instance_id];
631
632    PathVertexOutput output;
633    output.position = to_device_position_impl(v.xy_position);
634    output.clip_distance = distance_from_clip_rect_impl(v.xy_position, v.content_mask);
635    output.sprite_id = instance_id;
636
637    GradientColor gradient = prepare_gradient_color(
638        sprite.color.tag,
639        sprite.color.color_space,
640        sprite.color.solid,
641        sprite.color.colors[0].color,
642        sprite.color.colors[1].color
643    );
644
645    output.solid_color = gradient.solid;
646    output.color0 = gradient.color0;
647    output.color1 = gradient.color1;
648    return output;
649}
650
651float4 paths_fragment(PathVertexOutput input): SV_Target {
652    float4 zero = 0.0;
653    if (any(input.clip_distance < zero)) {
654        return zero;
655    }
656    
657    PathSprite sprite = path_sprites[input.sprite_id];
658    Background background = sprite.color;
659    float4 color = gradient_color(background, input.position.xy, sprite.bounds,
660        input.solid_color, input.color0, input.color1);
661    return color;
662}
663
664/*
665**
666**              Underlines
667**
668*/
669
670struct Underline {
671    uint order;
672    uint pad;
673    Bounds bounds;
674    Bounds content_mask;
675    Hsla color;
676    float thickness;
677    uint wavy;
678};
679
680struct UnderlineVertexOutput {
681  float4 position: SV_Position;
682  float4 color: COLOR;
683  uint underline_id: FLAT;
684  float4 clip_distance: SV_ClipDistance;
685};
686
687struct UnderlineFragmentInput {
688  float4 position: SV_Position;
689  float4 color: COLOR;
690  uint underline_id: FLAT;
691};
692
693StructuredBuffer<Underline> underlines: register(t1);
694
695UnderlineVertexOutput underline_vertex(uint vertex_id: SV_VertexID, uint underline_id: SV_InstanceID) {
696    float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u));
697    Underline underline = underlines[underline_id];
698    float4 device_position = to_device_position(unit_vertex, underline.bounds);
699    float4 clip_distance = distance_from_clip_rect(unit_vertex, underline.bounds, 
700                                                    underline.content_mask);
701    float4 color = hsla_to_rgba(underline.color);
702
703    UnderlineVertexOutput output;
704    output.position = device_position;
705    output.color = color;
706    output.underline_id = underline_id;
707    output.clip_distance = clip_distance;
708    return output;
709}
710
711float4 underline_fragment(UnderlineFragmentInput input): SV_Target {
712    Underline underline = underlines[input.underline_id];
713    if (underline.wavy) {
714        float half_thickness = underline.thickness * 0.5;
715        float2 origin =
716            float2(underline.bounds.origin.x, underline.bounds.origin.y);
717        float2 st = ((input.position.xy - origin) / underline.bounds.size.y) -
718                    float2(0., 0.5);
719        float frequency = (M_PI_F * (3. * underline.thickness)) / 8.;
720        float amplitude = 1. / (2. * underline.thickness);
721        float sine = sin(st.x * frequency) * amplitude;
722        float dSine = cos(st.x * frequency) * amplitude * frequency;
723        float distance = (st.y - sine) / sqrt(1. + dSine * dSine);
724        float distance_in_pixels = distance * underline.bounds.size.y;
725        float distance_from_top_border = distance_in_pixels - half_thickness;
726        float distance_from_bottom_border = distance_in_pixels + half_thickness;
727        float alpha = saturate(
728            0.5 - max(-distance_from_bottom_border, distance_from_top_border));
729        return input.color * float4(1., 1., 1., alpha);
730    } else {
731        return input.color;
732    }
733}
734
735/*
736**
737**              Monochrome sprites
738**
739*/
740
741struct MonochromeSprite {
742    uint order;
743    uint pad;
744    Bounds bounds;
745    Bounds content_mask;
746    Hsla color;
747    AtlasTile tile;
748    TransformationMatrix transformation;
749};
750
751struct MonochromeSpriteVertexOutput {
752    float4 position: SV_Position;
753    float2 tile_position: POSITION;
754    float4 color: COLOR;
755    float4 clip_distance: SV_ClipDistance;
756};
757
758struct MonochromeSpriteFragmentInput {
759    float4 position: SV_Position;
760    float2 tile_position: POSITION;
761    float4 color: COLOR;
762};
763
764StructuredBuffer<MonochromeSprite> mono_sprites: register(t1);
765
766MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexID, uint sprite_id: SV_InstanceID) {
767    float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u));
768    MonochromeSprite sprite = mono_sprites[sprite_id];
769    float4 device_position =
770        to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
771    float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
772    float2 tile_position = to_tile_position(unit_vertex, sprite.tile);
773    float4 color = hsla_to_rgba(sprite.color);
774
775    MonochromeSpriteVertexOutput output;
776    output.position = device_position;
777    output.tile_position = tile_position;
778    output.color = color;
779    output.clip_distance = clip_distance;
780    return output;
781}
782
783float4 monochrome_sprite_fragment(MonochromeSpriteFragmentInput input): SV_Target {
784    float4 sample = t_sprite.Sample(s_sprite, input.tile_position);
785    float4 color = input.color;
786    color.a *= sample.a;
787    return color;
788}
789
790/*
791**
792**              Polychrome sprites
793**
794*/
795
796struct PolychromeSprite {
797    uint order;
798    uint grayscale;
799    Bounds bounds;
800    Bounds content_mask;
801    Corners corner_radii;
802    AtlasTile tile;
803};
804
805struct PolychromeSpriteVertexOutput {
806    float4 position: SV_Position;
807    float2 tile_position: POSITION;
808    uint sprite_id: FLAT;
809    float4 clip_distance: SV_ClipDistance;
810};
811
812struct PolychromeSpriteFragmentInput {
813    float4 position: SV_Position;
814    float2 tile_position: POSITION;
815    uint sprite_id: FLAT;
816};
817
818StructuredBuffer<PolychromeSprite> poly_sprites: register(t1);
819
820PolychromeSpriteVertexOutput polychrome_sprite_vertex(uint vertex_id: SV_VertexID, uint sprite_id: SV_InstanceID) {
821    float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u));
822    PolychromeSprite sprite = poly_sprites[sprite_id];
823    float4 device_position = to_device_position(unit_vertex, sprite.bounds);
824    float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds,
825                                                    sprite.content_mask);
826    float2 tile_position = to_tile_position(unit_vertex, sprite.tile);
827
828    PolychromeSpriteVertexOutput output;
829    output.position = device_position;
830    output.tile_position = tile_position;
831    output.sprite_id = sprite_id;
832    output.clip_distance = clip_distance;
833    return output;
834}
835
836float4 polychrome_sprite_fragment(PolychromeSpriteFragmentInput input): SV_Target {
837    PolychromeSprite sprite = poly_sprites[input.sprite_id];
838    float4 sample = t_sprite.Sample(s_sprite, input.tile_position);
839    float distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii);
840
841    float4 color = sample;
842    if ((sprite.grayscale & 0xFFu) != 0u) {
843        float3 grayscale = dot(color.rgb, GRAYSCALE_FACTORS);
844        color = float4(grayscale, sample.a);
845    }
846    color.a *= saturate(0.5 - distance);
847    return color;
848}