valo-renderer 0.7.1

The wgpu core of valo: frame planner, encoder, pipelines, glyph atlases, caches
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
//! What a draw's fragment reads, made together: its [`Shading`] — the
//! fragment, its uniform record's tint and payload, and its bindings — so a
//! fragment and what it reads cannot disagree. There is one encoder per
//! fragment family; the payload slots and ids are `shader_abi`'s, and the
//! record's transform is written when the draw is placed (`emit`).

use valo_dl::{
    ColorFilter, FocalCircle, GradientStop, Image, MaskBlur, MaskKind, Paint, Sampling, Shader,
    SpreadMode, TileMode, MAX_GRADIENT_STOPS,
};
use valo_geometry::{Color, Matrix, Point, Rect};

use crate::frame::Bindings;
use crate::pipelines::{AdvancedBlend, Blend, Frag};
use crate::shader_abi::{
    blur_style_id, conical, gamma, mask_kind_id, slot, spread_id, UniformRecord,
};

use super::emit::StepEmitter;
use super::filter_output::Merge;
use super::gaussian::{BlurPass, DownsampleTaps};
use super::snapshot::Snapshot;

/// `Shading` is how a draw's pixels are coloured: its fragment, the
/// record it reads, and what it reads besides.
pub(super) struct Shading {
    pub frag: Frag,
    pub record: UniformRecord,
    pub bindings: Bindings,
    /// The colour covers every pixel it touches with full alpha: an opaque
    /// fill may write its depth and be drawn early.
    pub opaque: bool,
}

impl Shading {
    /// `none` is the shading of a draw that writes no colour: a clip.
    pub fn none() -> Self {
        Self::of(
            Frag::Solid,
            UniformRecord::tinted([0.0; 4]),
            Bindings::Plain,
        )
    }

    /// `solid` is one premultiplied colour.
    pub fn solid(tint: [f32; 4]) -> Self {
        Self::of(Frag::Solid, UniformRecord::tinted(tint), Bindings::Plain)
    }

    /// `rrect_blur` is the closed-form blurred (r)rect: coverage evaluated
    /// analytically over `rect` with per-corner `radii`, `blur`'s σ and
    /// style, in `color` at `alpha`.
    pub fn rrect_blur(
        rect: &Rect,
        radii: [f32; 4],
        color: Color,
        blur: MaskBlur,
        alpha: f32,
    ) -> Self {
        let mut record = UniformRecord::tinted(scaled_premul(color, alpha));
        record.set(slot::GEOM, [rect.x, rect.y, rect.right(), rect.bottom()]);
        record.set(
            slot::MISC,
            [blur.sigma.max(0.05), blur_style_id(blur.style), 0.0, 0.0],
        );
        record.set(slot::RADII, radii);
        Self::of(Frag::RRectBlur, record, Bindings::Plain)
    }

    /// `glyphs` is one atlas page's glyphs, through `frag`, tinted `tint`.
    pub fn glyphs(frag: Frag, tint: [f32; 4], page: wgpu::BindGroup) -> Self {
        Self::of(frag, UniformRecord::tinted(tint), Bindings::Textured(page))
    }

    /// `colour_glyphs` is colour glyphs (emoji) from `page`: their own
    /// pixels at `alpha`, through `filter` when there is one (Skia filters
    /// a colour glyph's pixel at the paint's alpha), then at `group_alpha`.
    pub fn colour_glyphs(
        page: wgpu::BindGroup,
        filter: Option<ColorFilter>,
        alpha: f32,
        group_alpha: f32,
    ) -> Self {
        let Some(filter) = filter else {
            return Self::glyphs(Frag::GlyphColor, alpha_tint(alpha * group_alpha), page);
        };
        let mut record = UniformRecord::tinted(alpha_tint(group_alpha));
        let frag = encode_color_filter(&mut record, filter, alpha).glyph_fragment();
        Self::of(frag, record, Bindings::Textured(page))
    }

    fn of(frag: Frag, record: UniformRecord, bindings: Bindings) -> Self {
        Self {
            frag,
            record,
            bindings,
            opaque: false,
        }
    }
}

impl StepEmitter<'_> {
    /// `paint_shading` is a paint's colour or shader at `alpha` times its
    /// own: a solid, a gradient (its stops in the record, or baked into a
    /// ramp past the record's budget), or a pattern. It is opaque when the
    /// paint covers every pixel it touches.
    pub fn paint_shading(&mut self, paint: &Paint, alpha: f32) -> Shading {
        let mut record = UniformRecord::tinted(tinted(paint, alpha));
        let (frag, bindings) = match PaintSource::of(paint) {
            PaintSource::Solid => (Frag::Solid, Bindings::Plain),
            PaintSource::Pattern {
                image,
                sampling,
                local,
            } => {
                fill_pattern_payload(&mut record, image, sampling, local);
                (
                    Frag::Pattern,
                    Bindings::Textured(self.image_bind(image, sampling)),
                )
            }
            PaintSource::Gradient(gradient) if gradient.stops.len() > MAX_GRADIENT_STOPS => {
                let (ramp, texels) = self.ramp(gradient.stops);
                fill_gradient_payload(&mut record, &gradient, Some(texels));
                (gradient.ramp_frag(), Bindings::Textured(ramp))
            }
            PaintSource::Gradient(gradient) => {
                fill_gradient_payload(&mut record, &gradient, None);
                (gradient.frag(), Bindings::Plain)
            }
        };
        Shading {
            opaque: alpha >= 1.0 && is_opaque_paint(paint),
            ..Shading::of(frag, record, bindings)
        }
    }

    /// `image_shading` is `src` of `image` sampled over `dst` at `alpha`,
    /// through `color_filter` on each sample (Impeller's atlas path). Tints
    /// with ALPHA only — the image shader multiplies samples by its tint,
    /// and the default paint colour is black.
    pub fn image_shading(
        &mut self,
        image: &Image,
        src: &Rect,
        dst: &Rect,
        sampling: Sampling,
        color_filter: Option<ColorFilter>,
        alpha: f32,
    ) -> Shading {
        let mut record = UniformRecord::tinted(alpha_tint(alpha));
        record.set(slot::GEOM, image_uv(image, src, dst));
        record.set(slot::DECAL, decal_flags(sampling));
        let frag = match color_filter {
            None => Frag::Image,
            Some(filter) => encode_color_filter(&mut record, filter, 1.0).image_fragment(),
        };
        Shading::of(
            frag,
            record,
            Bindings::Textured(self.image_bind(image, sampling)),
        )
    }

    /// `texture_shading` is `snapshot` drawn as it is over what it covers,
    /// at `alpha`.
    pub fn texture_shading(&self, snapshot: &Snapshot, alpha: f32) -> Shading {
        let mut record = UniformRecord::tinted(alpha_tint(alpha));
        record.set(slot::GEOM, snapshot.placement.uv_mapping());
        Shading::of(Frag::Image, record, self.textured(&snapshot.view))
    }

    /// `raster_shading` is a cached list's whole texture stretched over
    /// `sampled`, a rect the texture's size in destination units, at
    /// `alpha`: one premultiplied composite, exactly like a layer's.
    pub fn raster_shading(&self, view: &wgpu::TextureView, sampled: &Rect, alpha: f32) -> Shading {
        let mut record = UniformRecord::tinted(alpha_tint(alpha));
        record.set(slot::GEOM, full_rect_uv(sampled));
        Shading::of(Frag::Image, record, self.textured(view))
    }

    /// `recoloured_shading` is `input` drawn through `filter`, `alpha`
    /// taken in before the filter as Impeller's colour filters absorb a
    /// layer's opacity.
    pub fn recoloured_shading(&self, input: &Snapshot, filter: ColorFilter, alpha: f32) -> Shading {
        let mut record = UniformRecord::tinted([1.0; 4]);
        record.set(slot::GEOM, input.placement.uv_mapping());
        let frag = encode_color_filter(&mut record, filter, alpha).image_fragment();
        Shading::of(frag, record, self.textured(&input.view))
    }

    /// `merged_shading` is two snapshots merged in one fragment, at
    /// `alpha`.
    pub fn merged_shading(
        &self,
        first: &Snapshot,
        second: &Snapshot,
        merge: Merge,
        alpha: f32,
    ) -> Shading {
        let mut record = UniformRecord::tinted(alpha_tint(alpha));
        let frag = encode_merge(
            &mut record,
            first.placement.uv_mapping(),
            second.placement.uv_mapping(),
            merge,
        );
        let bindings = Bindings::Textured(self.blend_bind(&first.view, &second.view));
        Shading::of(frag, record, bindings)
    }

    /// `mask_composite_shading` samples a `mask` layer as coverage, its
    /// luminance or alpha as `kind` says, at `alpha`.
    pub fn mask_composite_shading(&self, mask: &Snapshot, kind: MaskKind, alpha: f32) -> Shading {
        let mut record = UniformRecord::tinted(alpha_tint(alpha));
        record.set(slot::GEOM, mask.placement.uv_mapping());
        record.set(slot::MISC, [mask_kind_id(kind), 0.0, 0.0, 0.0]);
        Shading::of(Frag::MaskComposite, record, self.textured(&mask.view))
    }

    /// `blended_solid_shading` is a solid `color` at `alpha` blended by
    /// `mode` in the fragment against `destination`, a copy of a target
    /// `size` texels big: the result replaces what the copy captured.
    pub fn blended_solid_shading(
        &self,
        color: Color,
        alpha: f32,
        mode: AdvancedBlend,
        destination: &wgpu::TextureView,
        size: [u32; 2],
    ) -> Shading {
        let mut record = UniformRecord::tinted(scaled_premul(color, alpha));
        set_destination_read(&mut record, mode, size);
        Shading::of(Frag::BlendSolid, record, self.textured(destination))
    }

    /// `blended_texture_shading` is `source` at `alpha` blended by `mode`
    /// in the fragment against `destination`: an advanced blend's
    /// composite.
    pub fn blended_texture_shading(
        &self,
        source: &Snapshot,
        alpha: f32,
        mode: AdvancedBlend,
        destination: &wgpu::TextureView,
        size: [u32; 2],
    ) -> Shading {
        let mut record = UniformRecord::tinted(alpha_tint(alpha));
        record.set(slot::GEOM, source.placement.uv_mapping());
        set_destination_read(&mut record, mode, size);
        let bindings = Bindings::Textured(self.blend_bind(destination, &source.view));
        Shading::of(Frag::BlendTexture, record, bindings)
    }

    /// `resample_shading` is a filter pass copying what `source` holds of
    /// `region`, one bilinear tap per pixel.
    pub fn resample_shading(&self, source: &Snapshot, region: &Rect) -> Shading {
        let mut record = UniformRecord::tinted([1.0; 4]);
        let texel = source.placement.size.map(|size| 1.0 / size as f32);
        let uv = source.placement.region_uv_mapping(region);
        encode_downsample(
            &mut record,
            uv,
            DownsampleKernel::ONE_TAP,
            texel,
            None,
            None,
        );
        Shading::of(Frag::Downsample, record, self.textured(&source.view))
    }

    /// `recolour_pass_shading` is a filter pass recolouring what `source`
    /// holds of `region`, `input_alpha` taken in before the filter.
    pub fn recolour_pass_shading(
        &self,
        source: &Snapshot,
        region: &Rect,
        filter: ColorFilter,
        input_alpha: f32,
    ) -> Shading {
        let mut record = UniformRecord::tinted([1.0; 4]);
        record.set(slot::GEOM, source.placement.region_uv_mapping(region));
        let frag = encode_color_filter(&mut record, filter, input_alpha).pass_fragment();
        Shading::of(frag, record, self.textured(&source.view))
    }

    /// `merge_pass_shading` is a filter pass merging what `first` and
    /// `second` hold of `region`.
    pub fn merge_pass_shading(
        &self,
        first: &Snapshot,
        second: &Snapshot,
        region: &Rect,
        merge: Merge,
    ) -> Shading {
        let mut record = UniformRecord::tinted([1.0; 4]);
        let frag = encode_merge(
            &mut record,
            first.placement.region_uv_mapping(region),
            second.placement.region_uv_mapping(region),
            merge,
        );
        let bindings = Bindings::Textured(self.blend_bind(&first.view, &second.view));
        Shading::of(frag, record, bindings)
    }

    /// `bake_shading` is a filter pass recolouring the whole of `source`
    /// through `filter`, into a copy of its size.
    pub fn bake_shading(&self, source: &Image, filter: ColorFilter) -> Shading {
        let size = source.size();
        let mut record = UniformRecord::tinted([1.0; 4]);
        record.set(
            slot::GEOM,
            [1.0 / size[0] as f32, 1.0 / size[1] as f32, 0.0, 0.0],
        );
        let frag = encode_color_filter(&mut record, filter, 1.0).pass_fragment();
        Shading::of(frag, record, self.textured(source.view()))
    }

    /// `downsample_shading` is a blur's downsample reading `input` as
    /// `taps` say.
    pub fn downsample_shading(&self, taps: &DownsampleTaps, input: &wgpu::TextureView) -> Shading {
        let mut record = UniformRecord::tinted([1.0; 4]);
        let edges = taps.edges.map(|edges| edges.0);
        let decal_border =
            (taps.tile_mode == TileMode::Decal).then(|| read_level_size(input.texture(), taps));
        encode_downsample(
            &mut record,
            taps.uv,
            taps.kernel,
            taps.texel,
            edges.as_ref(),
            decal_border,
        );
        let bindings = Bindings::Textured(self.tiled_texture_bind(input, taps.tile_mode));
        Shading::of(Frag::Downsample, record, bindings)
    }

    /// `blur_pass_shading` is one direction of the Gaussian over `input`,
    /// a texture `size` texels big, its kernel beside the record.
    pub fn blur_pass_shading(
        &mut self,
        pass: &BlurPass,
        input: &wgpu::TextureView,
        size: [u32; 2],
    ) -> Shading {
        let mut record = UniformRecord::tinted([1.0; 4]);
        let texel = size.map(|size| 1.0 / size as f32);
        record.set(slot::GEOM, [texel[0], texel[1], 0.0, 0.0]);
        record.set(
            slot::MISC,
            [
                pass.sample_count() as f32,
                f32::from(pass.divide_by_alpha),
                0.0,
                0.0,
            ],
        );
        let bindings = Bindings::Blurred {
            texture: self.texture_bind(input),
            kernel: self.alloc_kernel(&pass.kernel_uniform()),
        };
        Shading::of(Frag::Blur, record, bindings)
    }

    /// `textured` binds one texture with the sampler that clamps at its
    /// edge.
    fn textured(&self, view: &wgpu::TextureView) -> Bindings {
        Bindings::Textured(self.texture_bind(view))
    }
}

/// `set_destination_read` writes an advanced blend's mode and the size of
/// the target whose copy the fragment reads at its framebuffer position.
fn set_destination_read(record: &mut UniformRecord, mode: AdvancedBlend, size: [u32; 2]) {
    record.set(
        slot::MISC,
        [mode.id() as f32, 0.0, size[0] as f32, size[1] as f32],
    );
}

/// `tinted` is what multiplies the fragment family's output. Solid = the
/// color itself; image/gradient sources use paint ALPHA only (Skia's
/// drawImage semantics). `extra` folds in an elided group's alpha.
pub(super) fn tinted(paint: &Paint, extra: f32) -> [f32; 4] {
    if paint.shader.is_none() {
        scaled_premul(paint.color, extra)
    } else {
        alpha_tint(paint.color.a * extra)
    }
}

pub(super) fn scaled_premul(color: Color, alpha: f32) -> [f32; 4] {
    let [r, g, b, a] = color.premultiplied();
    [r * alpha, g * alpha, b * alpha, a * alpha]
}

pub(super) fn alpha_tint(a: f32) -> [f32; 4] {
    [a, a, a, a]
}

/// `full_rect_uv` maps a full texture stretched across `rect` into uv.
fn full_rect_uv(rect: &Rect) -> [f32; 4] {
    let sx = 1.0 / rect.width;
    let sy = 1.0 / rect.height;
    [sx, sy, -rect.x * sx, -rect.y * sy]
}

/// `PaintSource` is where a paint's colour comes from: the one place a
/// shader is told apart, so what follows handles each kind alone.
enum PaintSource<'a> {
    Solid,
    Pattern {
        image: &'a Image,
        sampling: Sampling,
        local: &'a Matrix,
    },
    Gradient(Gradient<'a>),
}

impl<'a> PaintSource<'a> {
    fn of(paint: &'a Paint) -> Self {
        let Some(shader) = &paint.shader else {
            return PaintSource::Solid;
        };
        let (kind, stops, local) = match shader {
            Shader::Image {
                image,
                sampling,
                local,
            } => {
                return PaintSource::Pattern {
                    image,
                    sampling: *sampling,
                    local,
                }
            }
            Shader::Linear {
                start,
                end,
                stops,
                spread,
                local,
            } => (
                GradientKind::Linear {
                    start: *start,
                    end: *end,
                    spread: *spread,
                },
                stops,
                local,
            ),
            Shader::Radial {
                center,
                radius,
                focus,
                stops,
                spread,
                local,
            } => (
                GradientKind::Radial {
                    center: *center,
                    radius: *radius,
                    focus: *focus,
                    spread: *spread,
                },
                stops,
                local,
            ),
            Shader::Sweep {
                center,
                start_angle,
                stops,
                local,
            } => (
                GradientKind::Sweep {
                    center: *center,
                    start_angle: *start_angle,
                },
                stops,
                local,
            ),
        };
        PaintSource::Gradient(Gradient { kind, stops, local })
    }
}

/// `Gradient` is a gradient shader: its geometry, its stops and its local
/// matrix.
struct Gradient<'a> {
    kind: GradientKind,
    stops: &'a [GradientStop],
    local: &'a Matrix,
}

/// `GradientKind` is a gradient's geometry, and how it spreads past 0..1.
enum GradientKind {
    Linear {
        start: Point,
        end: Point,
        spread: SpreadMode,
    },
    Radial {
        center: Point,
        radius: f32,
        focus: Option<FocalCircle>,
        spread: SpreadMode,
    },
    /// A sweep is periodic by nature.
    Sweep { center: Point, start_angle: f32 },
}

impl Gradient<'_> {
    /// `frag` is the fragment of a gradient whose stops fit the record.
    fn frag(&self) -> Frag {
        match self.kind {
            GradientKind::Linear { .. } => Frag::Linear,
            GradientKind::Radial { .. } => Frag::Radial,
            GradientKind::Sweep { .. } => Frag::Sweep,
        }
    }

    /// `ramp_frag` is the fragment of a gradient baked into a ramp.
    fn ramp_frag(&self) -> Frag {
        match self.kind {
            GradientKind::Linear { .. } => Frag::LinearRamp,
            GradientKind::Radial { .. } => Frag::RadialRamp,
            GradientKind::Sweep { .. } => Frag::SweepRamp,
        }
    }
}

/// `is_opaque_paint` reports whether `paint`'s colour covers every pixel it
/// touches with full alpha.
fn is_opaque_paint(paint: &Paint) -> bool {
    paint.mask_blur.is_none()
        && paint.effective_image_filter().is_none()
        && paint.color.a >= 1.0
        && paint.shader.as_ref().is_none_or(shader_opaque)
}

fn shader_opaque(shader: &Shader) -> bool {
    // A two-point conical gradient with a real start circle does not cover
    // the plane: outside its cone nothing is painted at all, so opaque
    // promotion would turn those pixels into replaced black. A gradient that
    // can leave gaps never qualifies, however opaque its stops are.
    if let Shader::Radial {
        center,
        focus: Some(circle),
        ..
    } = shader
    {
        if circle.radius > 0.0 || circle.center != *center {
            return false;
        }
    }
    let stops = match shader {
        Shader::Linear { stops, .. }
        | Shader::Radial { stops, .. }
        | Shader::Sweep { stops, .. } => stops,
        // A pattern's alpha lives in texels nobody has read at plan time.
        Shader::Image { .. } => return false,
    };
    stops.iter().all(|stop| stop.color.a >= 1.0)
}

/// `EncodedColorFilter` names which fragment variant consumes the payload
/// [`encode_color_filter`] wrote.
#[derive(Clone, Copy)]
enum EncodedColorFilter {
    Matrix,
    Blend,
    Gamma,
}

impl EncodedColorFilter {
    /// `pass_fragment` runs the filter over a whole texture in a filter pass.
    fn pass_fragment(self) -> Frag {
        match self {
            EncodedColorFilter::Matrix => Frag::ColorMatrix,
            EncodedColorFilter::Blend => Frag::ColorBlend,
            EncodedColorFilter::Gamma => Frag::ColorGamma,
        }
    }

    /// `image_fragment` runs the filter on each sample of an image draw.
    fn image_fragment(self) -> Frag {
        match self {
            EncodedColorFilter::Matrix => Frag::ImageMatrix,
            EncodedColorFilter::Blend => Frag::ImageBlend,
            EncodedColorFilter::Gamma => Frag::ImageGamma,
        }
    }

    /// `glyph_fragment` runs the filter on each pixel of a colour glyph.
    fn glyph_fragment(self) -> Frag {
        match self {
            EncodedColorFilter::Matrix => Frag::GlyphColorMatrix,
            EncodedColorFilter::Blend => Frag::GlyphColorBlend,
            EncodedColorFilter::Gamma => Frag::GlyphColorGamma,
        }
    }
}

/// `encode_color_filter` writes Impeller's mat4-plus-translation-vector
/// layout, its constant premultiplied blend source, or the direction of the
/// sRGB gamma curve, and `input_alpha`: what the texel is multiplied by
/// before the filter (Impeller's `input_alpha`, which absorbs a layer's
/// opacity). Draw and filter-pass shaders share this ABI.
fn encode_color_filter(
    record: &mut UniformRecord,
    filter: ColorFilter,
    input_alpha: f32,
) -> EncodedColorFilter {
    match filter {
        ColorFilter::Matrix(matrix) => {
            for row in 0..4 {
                let start = row * 5;
                record.set(
                    slot::COLOR_MATRIX + row,
                    [
                        matrix[start],
                        matrix[start + 1],
                        matrix[start + 2],
                        matrix[start + 3],
                    ],
                );
            }
            record.set(
                slot::COLOR_MATRIX + 4,
                [matrix[4], matrix[9], matrix[14], matrix[19]],
            );
            record.set(slot::MISC, [0.0, 0.0, 0.0, input_alpha]);
            EncodedColorFilter::Matrix
        }
        ColorFilter::Blend(color, mode) => {
            record.set(slot::COLOR_MATRIX, color.premultiplied());
            let id = Blend::of(mode).filter_id() as f32;
            record.set(slot::MISC, [id, 0.0, 0.0, input_alpha]);
            EncodedColorFilter::Blend
        }
        ColorFilter::LinearToSrgbGamma => encode_gamma(record, gamma::LINEAR_TO_SRGB, input_alpha),
        ColorFilter::SrgbToLinearGamma => encode_gamma(record, gamma::SRGB_TO_LINEAR, input_alpha),
    }
}

fn encode_gamma(
    record: &mut UniformRecord,
    direction: f32,
    input_alpha: f32,
) -> EncodedColorFilter {
    record.set(slot::MISC, [direction, 0.0, 0.0, input_alpha]);
    EncodedColorFilter::Gamma
}

/// `encode_merge` writes the two-input fragments' payload (`fs_drop_shadow`,
/// `fs_mask_combine`): each input's uv mapping and the merge's switch, and
/// returns the fragment.
fn encode_merge(
    record: &mut UniformRecord,
    first_uv: [f32; 4],
    second_uv: [f32; 4],
    merge: Merge,
) -> Frag {
    let (frag, switch) = match merge {
        Merge::DropShadow => (Frag::DropShadow, 0.0),
        Merge::BlurStyle(style) => (Frag::MaskCombine, blur_style_id(style)),
    };
    record.set(slot::GEOM, first_uv);
    record.set(slot::SECOND_UV, second_uv);
    record.set(slot::MISC, [switch, 0.0, 0.0, 0.0]);
    frag
}

/// `DownsampleKernel` is the downsample fragment's taps: at odd texel
/// offsets out to `edge`, each weighted `ratio`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct DownsampleKernel {
    pub edge: f32,
    pub ratio: f32,
}

impl DownsampleKernel {
    /// `ONE_TAP` is one bilinear tap per pixel: a plain resample.
    pub const ONE_TAP: Self = Self {
        edge: 0.0,
        ratio: 1.0,
    };
}

/// `read_level_size` is the size in texels of the mip level of `texture`
/// that `taps` read: the grid a decal's transparent border lies along. A
/// blur's input is a view of its whole texture, every level included.
fn read_level_size(texture: &wgpu::Texture, taps: &DownsampleTaps) -> [f32; 2] {
    let level = taps.read_level(texture.mip_level_count());
    let size = texture.size().mip_level_size(level, texture.dimension());
    [size.width as f32, size.height as f32]
}

/// `encode_downsample` writes the downsample fragment's payload: `uv` maps
/// the pass's own pixels into the input's uv as `uv = p · [x, y] + [z, w]`,
/// `kernel` is its taps, one input texel (`texel`, in uv) apart, `bounds`
/// the edge lines a bounded blur tests its taps against, and
/// `decal_border`, for a decal blur, the size in texels of the level its
/// taps read, past whose edge they find a transparent border.
fn encode_downsample(
    record: &mut UniformRecord,
    uv: [f32; 4],
    kernel: DownsampleKernel,
    texel: [f32; 2],
    bounds: Option<&[[f32; 4]; 4]>,
    decal_border: Option<[f32; 2]>,
) {
    record.set(slot::GEOM, uv);
    record.set(slot::MISC, [kernel.edge, kernel.ratio, texel[0], texel[1]]);
    if let Some(lines) = bounds {
        for (index, line) in lines.iter().enumerate() {
            record.set(slot::DOWNSAMPLE_EDGES + index, *line);
        }
    }
    let [width, height] = decal_border.unwrap_or([0.0; 2]);
    record.set(
        slot::DOWNSAMPLE_MODES,
        [
            f32::from(bounds.is_some()),
            f32::from(decal_border.is_some()),
            width,
            height,
        ],
    );
}

/// `fill_gradient_payload` writes gradient geometry + stops into the
/// payload. A focal radial's fx/fy ride the two spare floats (geom.w /
/// misc.w); focus == center encodes "classic". The INVERSE of the shader's
/// local matrix lands in the local slots — fragments map draw space into
/// gradient space with it (identity for plain gradients; a non-invertible
/// matrix degenerates to a constant ramp sample, never UB). `ramp_texels` =
/// Some(N) when the stops ride a baked texture: the count lane carries N
/// for the fragment's half-texel mapping, and the uniform stop arrays stay
/// untouched (the texture IS the ramp).
fn fill_gradient_payload(
    record: &mut UniformRecord,
    gradient: &Gradient,
    ramp_texels: Option<u32>,
) {
    let (geom, angle, misc_w, spread) = match gradient.kind {
        GradientKind::Linear { start, end, spread } => {
            ([start.x, start.y, end.x, end.y], 0.0, 0.0, spread)
        }
        GradientKind::Radial {
            center,
            radius,
            focus,
            spread,
        } => {
            let f = focus.map_or(center, |circle| circle.center);
            ([center.x, center.y, radius, f.x], 0.0, f.y, spread)
        }
        GradientKind::Sweep {
            center,
            start_angle,
        } => (
            [center.x, center.y, 0.0, 0.0],
            start_angle,
            0.0,
            SpreadMode::Pad,
        ),
    };
    record.set(slot::GEOM, geom);
    let mut inverse = gradient
        .local
        .invert()
        .unwrap_or(Matrix::from_affine(0.0, 0.0, 0.0, 0.0, 0.0, 0.0));

    // A two-point conical gradient is solved in a space where the focal
    // point sits at the origin and the end circle is the unit circle. That
    // mapping is constant per draw, so it folds into the inverse local
    // matrix here and the fragment only runs the per-pixel half.
    let setup = match gradient.kind {
        GradientKind::Radial {
            center,
            radius,
            focus,
            ..
        } => ConicalSetup::solve(center, radius, focus),
        _ => ConicalSetup::UNUSED,
    };
    if let Some(focal_map) = setup.focal_map {
        inverse = focal_map.then(&inverse);
    }
    record.set(slot::CONICAL, setup.constants);
    record.set(slot::CONICAL_FLAGS, setup.flags);

    // Gradient locals are affine by construction — the 2D block is exact.
    let [a, b, c, d, tx, ty] = inverse.to_affine();
    record.set(slot::LOCAL, [a, b, c, d]);
    record.set(slot::LOCAL + 1, [tx, ty, 0.0, 0.0]);

    let count = gradient.stops.len().min(MAX_GRADIENT_STOPS);
    let count_lane = match ramp_texels {
        Some(texels) => texels as f32,
        None => count as f32,
    };
    record.set(slot::MISC, [count_lane, angle, spread_id(spread), misc_w]);
    if ramp_texels.is_some() {
        return;
    }
    let mut offsets = [0.0f32; MAX_GRADIENT_STOPS];
    for (i, stop) in gradient.stops.iter().take(count).enumerate() {
        offsets[i] = stop.offset;
        record.set(slot::COLORS + i, stop.color.components());
    }
    record.set(
        slot::OFFSETS,
        [offsets[0], offsets[1], offsets[2], offsets[3]],
    );
    record.set(
        slot::OFFSETS + 1,
        [offsets[4], offsets[5], offsets[6], offsets[7]],
    );
}

/// `fill_pattern_payload` writes a pattern's mapping: `local⁻¹` into
/// pattern pixels, then the reciprocal image size to reach uv. Tiling and
/// filtering ride the sampler.
fn fill_pattern_payload(
    record: &mut UniformRecord,
    image: &Image,
    sampling: Sampling,
    local: &Matrix,
) {
    let size = image.size();
    record.set(
        slot::GEOM,
        [1.0 / size[0] as f32, 1.0 / size[1] as f32, 0.0, 0.0],
    );
    record.set(slot::DECAL, decal_flags(sampling));
    let inverse = local
        .invert()
        .unwrap_or(Matrix::from_affine(0.0, 0.0, 0.0, 0.0, 0.0, 0.0));
    let [a, b, c, d, tx, ty] = inverse.to_affine();
    record.set(slot::LOCAL, [a, b, c, d]);
    record.set(slot::LOCAL + 1, [tx, ty, 0.0, 0.0]);
}

/// `image_uv` is uv = local × scale + offset, mapping `dst` (local px) onto
/// `src` (texture px, normalized) — out-of-range uv is the sampler's
/// business.
fn image_uv(image: &Image, src: &Rect, dst: &Rect) -> [f32; 4] {
    let (tw, th) = (image.width(), image.height());
    let sx = src.width / (dst.width * tw);
    let sy = src.height / (dst.height * th);
    [sx, sy, src.x / tw - dst.x * sx, src.y / th - dst.y * sy]
}

/// `decal_flags` is the per-axis decal switch: 1 where the fragment must
/// cut off outside the image, 0 where the sampler's own address mode
/// already produces the right pixels. Every image-sampling fragment reads
/// these from the decal slot, so a pattern and a direct `drawImage` honour
/// `TileMode::Decal` identically.
fn decal_flags(sampling: Sampling) -> [f32; 4] {
    [
        f32::from(sampling.tile_x == TileMode::Decal),
        f32::from(sampling.tile_y == TileMode::Decal),
        0.0,
        0.0,
    ]
}

/// `ConicalSetup` is which formula the fragment runs for a radial gradient,
/// plus the constants it needs. Skia's two-point conical algorithm
/// (skia.org/docs/dev/design/conical) splits into cases by where the focal
/// point lands; the choice and all of its precomputation are per-draw, so
/// they happen here rather than per fragment the way Impeller does it.
struct ConicalSetup {
    /// `(kind, local_r1, f, d_radius_sign)` — see `radial_t` in the shader.
    constants: [f32; 4],
    /// `(is_swapped, is_focal_on_circle, is_well_behaved, unused)`.
    flags: [f32; 4],
    /// Gradient space → focal space, when the general case needs it.
    focal_map: Option<Matrix>,
}

impl ConicalSetup {
    const UNUSED: Self = Self {
        constants: [conical::CONCENTRIC, 0.0, 0.0, 0.0],
        flags: [0.0; 4],
        focal_map: None,
    };

    /// `solve` runs Skia's `SkConicalGradient` decomposition, once per draw.
    fn solve(center: Point, radius: f32, focus: Option<FocalCircle>) -> Self {
        // Two different epsilons on purpose, following Impeller: case
        // SELECTION uses the looser `kEhCloseEnough` (conical_gradient_contents
        // .cc), because a separation just above the tight one produces a
        // near-singular 1/length map and fp32 noise with it. The tight
        // 1/4096 stays for the in-shader constants (gradient.glsl).
        const CASE_EPSILON: f32 = 1.0e-3;
        const NEARLY_ZERO: f32 = 1.0 / (1 << 12) as f32;
        let start = focus.unwrap_or(FocalCircle::point(center));
        let separation = (center.x - start.center.x).hypot(center.y - start.center.y);

        // Concentric circles need no focal machinery: t is just how far the
        // point sits between the two radii.
        if separation < CASE_EPSILON {
            if (radius - start.radius).abs() < CASE_EPSILON {
                return Self {
                    constants: [conical::EMPTY, 0.0, 0.0, 0.0],
                    ..Self::UNUSED
                };
            }
            return Self {
                constants: [conical::CONCENTRIC, start.radius, radius, 0.0],
                flags: [0.0; 4],
                focal_map: None,
            };
        }

        // Equal radii have no focal point at all — the circles sweep a strip
        // between their common tangents, and `focal` below would divide by
        // zero. Skia and Impeller both carve this out as its own case.
        if (radius - start.radius).abs() < CASE_EPSILON {
            let radius_in_unit_space = start.radius / separation;
            return Self {
                constants: [
                    conical::STRIP,
                    radius_in_unit_space * radius_in_unit_space,
                    0.0,
                    0.0,
                ],
                flags: [0.0; 4],
                focal_map: Some(map_to_unit_x(start.center, center)),
            };
        }

        // Steps 1-2: the focal parameter, and the swap that keeps it finite
        // when the two radii are equal.
        let (mut first, mut second) = (start.center, center);
        let mut focal = start.radius / (start.radius - radius);
        let is_swapped = (focal - 1.0).abs() < NEARLY_ZERO;
        if is_swapped {
            std::mem::swap(&mut first, &mut second);
            focal = 0.0f32;
        }

        // Steps 3-4: map [focal centre, end centre] onto [(0,0), (1,0)], then
        // scale so the end circle becomes the unit circle.
        let focal_center = Point::new(
            first.x * (1.0 - focal) + second.x * focal,
            first.y * (1.0 - focal) + second.y * focal,
        );
        let radius_in_unit_space = (radius - start.radius).abs() / separation;
        let is_focal_on_circle = (radius_in_unit_space - 1.0).abs() < NEARLY_ZERO;
        let span = (1.0 - focal).abs();
        let (scale_x, scale_y) = if is_focal_on_circle {
            (span * 0.5, span * 0.5)
        } else {
            let squared = radius_in_unit_space * radius_in_unit_space;
            (
                span * radius_in_unit_space / (squared - 1.0),
                span / (squared - 1.0).abs().sqrt(),
            )
        };

        let is_well_behaved = !is_focal_on_circle && radius_in_unit_space > 1.0;
        Self {
            constants: [
                conical::GENERAL,
                radius_in_unit_space,
                focal,
                (1.0 - focal).signum(),
            ],
            flags: [
                is_swapped as u32 as f32,
                is_focal_on_circle as u32 as f32,
                is_well_behaved as u32 as f32,
                0.0,
            ],
            focal_map: Some(scale_after(
                map_to_unit_x(focal_center, second),
                scale_x,
                scale_y,
            )),
        }
    }
}

/// `map_to_unit_x` maps `[from, to]` onto `[(0, 0), (1, 0)]`.
fn map_to_unit_x(from: Point, to: Point) -> Matrix {
    let (dx, dy) = (to.x - from.x, to.y - from.y);
    let length = dx.hypot(dy);
    let (ux, uy) = (dx / length, dy / length);
    Matrix::from_affine(
        ux / length,
        -uy / length,
        uy / length,
        ux / length,
        -(ux * from.x + uy * from.y) / length,
        (uy * from.x - ux * from.y) / length,
    )
}

/// `scale_after` is `scale(x, y) ∘ matrix` for an affine 2D matrix.
fn scale_after(matrix: Matrix, x: f32, y: f32) -> Matrix {
    let [a, b, c, d, tx, ty] = matrix.to_affine();
    Matrix::from_affine(a * x, b * y, c * x, d * y, tx * x, ty * y)
}