Skip to main content

impeller_core/
canvas.rs

1//! The recording surface.
2//!
3//! A canvas is where drawing is expressed. It holds a transform stack and
4//! accumulates shapes into a batch, which is what a backend is eventually
5//! handed. Recording is separate from submitting so that a whole frame can be
6//! described before any of it reaches the GPU — that separation is what lets
7//! draws be batched into one pass rather than submitted one at a time.
8
9use crate::paint::{GradientStop, ImageFilter, MaskBlurStyle, Paint, Shader, Style};
10use crate::ramp::Ramp;
11use crate::vertices::{Sprite, VertexMode, Vertices};
12use crate::Color;
13use glam::{Affine2, Mat2, Vec2};
14use impeller_geometry::stroke::{LineCap, LineJoin, StrokeStyle};
15use impeller_geometry::transform::{
16    invert_to_local, max_scale, preserves_axis_alignment, to_local_columns, transformed_bounds,
17    unbounded, viewport_projection, Transform2D,
18};
19use impeller_geometry::{FillRule, Path, PathBuilder, Rect as GeometryRect};
20use impeller_hal::{
21    Batch, BlendMode, ClipState, ColorFilter, Error, Extent2D, Material, PassDescriptor,
22    PassViewport, Result, Sampling, Scissor, Stop, TileMode, Vertex, MAX_STOPS, MORPHOLOGY_TAPS,
23};
24use impeller_renderer::{Paint as RenderPaint, Renderer, TOLERANCE};
25use impeller_text::{Atlas, PositionedGlyph};
26
27/// A rectangle in user coordinates.
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct Rect {
30    pub left: f32,
31    pub top: f32,
32    pub right: f32,
33    pub bottom: f32,
34}
35
36impl Rect {
37    pub fn new(left: f32, top: f32, right: f32, bottom: f32) -> Self {
38        Self {
39            left,
40            top,
41            right,
42            bottom,
43        }
44    }
45
46    pub fn from_size(width: f32, height: f32) -> Self {
47        Self::new(0.0, 0.0, width, height)
48    }
49
50    pub fn width(&self) -> f32 {
51        self.right - self.left
52    }
53
54    pub fn height(&self) -> f32 {
55        self.bottom - self.top
56    }
57
58    pub fn is_empty(&self) -> bool {
59        self.width() <= 0.0 || self.height() <= 0.0
60    }
61
62    /// The same rectangle, grown by `reach` on every side.
63    ///
64    /// What a blur needs from the bounds of what it blurs: the result spreads
65    /// outward, so a target sized to the content alone cuts the halo off square
66    /// at the edge -- the failure looking exactly like a shadow with a straight
67    /// side. Written once because it was written out twice, four lines at a
68    /// time, and a third place needs it with one side extended further.
69    pub fn outset(self, reach: f32) -> Self {
70        Self::new(
71            self.left - reach,
72            self.top - reach,
73            self.right + reach,
74            self.bottom + reach,
75        )
76    }
77
78    fn to_path(self) -> Path {
79        let mut b = PathBuilder::new();
80        b.move_to(Vec2::new(self.left, self.top))
81            .line_to(Vec2::new(self.right, self.top))
82            .line_to(Vec2::new(self.right, self.bottom))
83            .line_to(Vec2::new(self.left, self.bottom))
84            .close();
85        // A rectangle is a rounded one whose corners are not rounded, and
86        // recording it as such is what lets a blurred rectangle be evaluated
87        // rather than blurred. Not a special case in the expression that draws
88        // it: a corner radius of zero is where that approximation is at its
89        // most accurate, since there is no corner to approximate.
90        b.as_rounded_rect(
91            GeometryRect::new(
92                Vec2::new(self.left, self.top),
93                Vec2::new(self.right, self.bottom),
94            ),
95            0.0,
96        );
97        b.build()
98    }
99
100    /// This rectangle with its corners rounded to `radius`.
101    ///
102    /// The radius is clamped to half the shorter side. Larger is not an error
103    /// worth refusing -- a caller asking for a fully rounded end usually says
104    /// so by passing something enormous -- and left unclamped the corner arcs
105    /// would overlap and the outline would cross itself, which fills as
106    /// something nobody asked for. Clamped, the shape degenerates to a stadium
107    /// and then a circle, which is what asking for a huge radius means.
108    ///
109    /// A radius at or below zero gives the plain rectangle, so a caller can
110    /// pass a radius that happens to be zero without special-casing it.
111    pub fn to_rounded_path(self, radius: f32) -> Path {
112        if self.is_empty() {
113            return Path::default();
114        }
115        // NaN is checked before clamping rather than after, because `f32::min`
116        // ignores a NaN operand: clamping first turns it into half the shorter
117        // side, so arithmetic that went wrong would come back as a large
118        // rounded rectangle instead of as the plain one asked for.
119        //
120        // An infinite radius is not rejected with it. It clamps like any other
121        // large number, because a caller who writes one means the roundest
122        // shape available rather than none at all, and answering with a square
123        // would be the opposite of what was asked.
124        if radius.is_nan() || radius <= 0.0 {
125            return self.to_path();
126        }
127        let mut path = PathBuilder::new();
128        self.add_rounded_contour(&mut path, radius);
129        // Recorded beside the drawing rather than inferred later, so a route
130        // that wants the shape rather than the outline can have it -- a mask
131        // blur on this is evaluated in the fragment stage and needs a rect and
132        // a radius, which eight cubics cannot give back.
133        path.as_rounded_rect(
134            GeometryRect::new(
135                Vec2::new(self.left, self.top),
136                Vec2::new(self.right, self.bottom),
137            ),
138            radius.min(self.width() / 2.0).min(self.height() / 2.0),
139        );
140        path.build()
141    }
142
143    /// This rectangle with each corner rounded to its own pair of radii.
144    ///
145    /// The eight numbers `dart:ui`'s `RRect` carries, in upstream's order:
146    /// top-left, top-right, bottom-left, bottom-right, each an x radius and a
147    /// y radius. [`Self::to_rounded_path`] is this with all four the same and
148    /// circular, and goes through the same code.
149    pub fn to_rounded_path_with_radii(self, radii: RoundingRadii) -> Path {
150        if self.is_empty() {
151            return Path::default();
152        }
153        let radii = self.fitted_radii(radii);
154        if radii.iter().flatten().all(|v| *v <= 0.0) {
155            return self.to_path();
156        }
157        let mut path = PathBuilder::new();
158        self.add_rounded_contour_with_radii(&mut path, radii);
159        path.build()
160    }
161
162    /// Add this rectangle's rounded outline to a path already being built.
163    ///
164    /// [`Self::to_rounded_path_with_radii`] is this and nothing else when the
165    /// path holds one contour. It is public for the case that one cannot
166    /// serve: a shape made of two of these -- the ring `drawDRRect` fills, or a
167    /// caller's own compound outline -- needs both contours in one path, and a
168    /// path built from two paths is not something this crate offers.
169    ///
170    /// The radii are fitted on the way in, so a
171    /// caller passing radii that overrun a side gets the same scaling
172    /// `drawRRect` would apply rather than an outline that crosses itself.
173    pub fn add_rounded_outline(self, path: &mut PathBuilder, radii: RoundingRadii) {
174        if self.is_empty() {
175            return;
176        }
177        let radii = self.fitted_radii(radii);
178        self.add_rounded_contour_with_radii(path, radii);
179    }
180
181    /// The radii this rectangle can actually carry, made finite and made to fit.
182    ///
183    /// The fitting is `dart:ui`'s rule: find the edge whose two radii overrun
184    /// it worst, and scale *every* radius by that one ratio. Scaling only the
185    /// offending pair would also keep the outline from crossing itself, and
186    /// would change the shape's proportions -- the difference between a rounded
187    /// rectangle whose corners all got smaller and one that came back with a
188    /// lopsided pair nobody asked for.
189    ///
190    /// It is applied to the radii as given, which is the part that has to be
191    /// resisted tidying. Holding each radius to the side it runs along first
192    /// looks like a harmless guard and is not: it changes the ratios the rule
193    /// then works from, so a circular radius larger than the rectangle comes
194    /// back elliptical -- half the width across and half the height down,
195    /// instead of the round end the caller asked for.
196    ///
197    /// Which leaves the two values the rule cannot divide by. NaN and anything
198    /// at or below zero become zero, so a corner whose arithmetic went wrong is
199    /// square rather than enormous. An infinity becomes the longer side, which
200    /// is not arbitrary: once a uniform radius is large enough to bind, the
201    /// result stops depending on how large it was -- the ratio shrinks exactly
202    /// as fast as the radius grows -- so any sufficiently large stand-in gives
203    /// the same shape a huge finite radius gives, which is what an infinite one
204    /// has always meant here.
205    fn fitted_radii(self, radii: RoundingRadii) -> RoundingRadii {
206        let (w, h) = (self.width(), self.height());
207        let mut r = radii;
208        for corner in r.iter_mut() {
209            for value in corner.iter_mut() {
210                *value = if value.is_nan() || *value <= 0.0 {
211                    0.0
212                } else if value.is_infinite() {
213                    w.max(h)
214                } else {
215                    *value
216                };
217            }
218        }
219        let [tl, tr, bl, br] = r;
220        // Each edge against the two radii that meet along it. A sum of zero
221        // cannot bind, and is skipped rather than divided by.
222        let mut scale = 1.0f32;
223        for (side, sum) in [
224            (w, tl[0] + tr[0]),
225            (h, tr[1] + br[1]),
226            (w, bl[0] + br[0]),
227            (h, tl[1] + bl[1]),
228        ] {
229            if sum > side {
230                scale = scale.min(side / sum);
231            }
232        }
233        if scale < 1.0 {
234            for corner in r.iter_mut() {
235                for value in corner.iter_mut() {
236                    *value *= scale;
237                }
238            }
239        }
240        r
241    }
242
243    /// Append this rectangle's rounded outline to a builder, as one contour.
244    ///
245    /// Separate from [`Self::to_rounded_path`] because a shape made of two of
246    /// these -- a ring between an outer rectangle and an inner one -- needs
247    /// both in one path, and a path built from two paths is not something this
248    /// crate offers.
249    fn add_rounded_contour(self, path: &mut PathBuilder, radius: f32) {
250        let radii = self.fitted_radii([[radius; 2]; 4]);
251        self.add_rounded_contour_with_radii(path, radii);
252    }
253
254    /// The same, for radii that have already been fitted by `fitted_radii`.
255    fn add_rounded_contour_with_radii(self, path: &mut PathBuilder, radii: RoundingRadii) {
256        let [tl, tr, bl, br] = radii;
257        // The same constant that makes four cubics a circle. A corner here is a
258        // quarter *ellipse* rather than a quarter circle, and the constant
259        // carries over unchanged: an ellipse is a circle scaled along each axis
260        // independently, and scaling a cubic's control points by the same
261        // factors scales the curve they describe.
262        let k = |v: f32| KAPPA * v;
263        let (l, t, r, b) = (self.left, self.top, self.right, self.bottom);
264        path.move_to(Vec2::new(l + tl[0], t))
265            .line_to(Vec2::new(r - tr[0], t))
266            .cubic_to(
267                Vec2::new(r - tr[0] + k(tr[0]), t),
268                Vec2::new(r, t + tr[1] - k(tr[1])),
269                Vec2::new(r, t + tr[1]),
270            )
271            .line_to(Vec2::new(r, b - br[1]))
272            .cubic_to(
273                Vec2::new(r, b - br[1] + k(br[1])),
274                Vec2::new(r - br[0] + k(br[0]), b),
275                Vec2::new(r - br[0], b),
276            )
277            .line_to(Vec2::new(l + bl[0], b))
278            .cubic_to(
279                Vec2::new(l + bl[0] - k(bl[0]), b),
280                Vec2::new(l, b - bl[1] + k(bl[1])),
281                Vec2::new(l, b - bl[1]),
282            )
283            .line_to(Vec2::new(l, t + tl[1]))
284            .cubic_to(
285                Vec2::new(l, t + tl[1] - k(tl[1])),
286                Vec2::new(l + tl[0] - k(tl[0]), t),
287                Vec2::new(l + tl[0], t),
288            )
289            .close();
290    }
291}
292
293/// The single radius these corners all are, if they are all the same one.
294///
295/// `None` for anything the fragment-evaluated route cannot describe: a corner
296/// that differs from another, or one whose two radii differ from each other.
297fn uniform_circular_radius(radii: RoundingRadii) -> Option<f32> {
298    let radius = radii[0][0];
299    radii
300        .iter()
301        .flatten()
302        .all(|v| *v == radius)
303        .then_some(radius)
304}
305
306/// A rounded rectangle's four corners, each with an x and a y radius.
307///
308/// In upstream's order -- top-left, top-right, bottom-left, bottom-right --
309/// rather than in the order the outline is traced, so that a caller porting
310/// from `RoundingRadii` or from `dart:ui`'s `RRect` can copy the four across
311/// without reordering them. The tracing order is the contour's business.
312pub type RoundingRadii = [[f32; 2]; 4];
313
314/// The outline of a rounded superellipse in `rect` with the given radii.
315///
316/// Here rather than on `Rect` beside `to_rounded_path_with_radii`, because a
317/// superellipse is not a rounded rectangle with a different corner and putting
318/// it there would suggest the two are interchangeable. They take the same four
319/// numbers and draw measurably different shapes.
320fn rsuperellipse_path(rect: Rect, radii: RoundingRadii) -> Path {
321    use impeller_geometry::superellipse::{CornerRadii, RoundSuperellipse};
322    let bounds = GeometryRect::new(
323        Vec2::new(rect.left, rect.top),
324        Vec2::new(rect.right, rect.bottom),
325    );
326    RoundSuperellipse::new(
327        bounds,
328        CornerRadii {
329            top_left: Vec2::new(radii[0][0], radii[0][1]),
330            top_right: Vec2::new(radii[1][0], radii[1][1]),
331            bottom_left: Vec2::new(radii[2][0], radii[2][1]),
332            bottom_right: Vec2::new(radii[3][0], radii[3][1]),
333        },
334    )
335    .to_path()
336}
337
338/// Where a pass's texture slot gets its content.
339///
340/// A recording is produced without touching a device, so it can name neither a
341/// texture nor an allocation. It names either an image the caller will supply
342/// or an earlier pass in the same recording, and whoever executes it resolves
343/// both.
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum TextureSource {
346    /// The caller's image at this index in the table given at draw time.
347    Image(u32),
348    /// The output of the pass at this index in [`Recording::passes`].
349    Layer(usize),
350    /// The baked gradient at this index in [`Recording::ramps`].
351    ///
352    /// Unlike the other two this names something the recorder made rather than
353    /// something it was given or rendered, so it is uploaded at submission and
354    /// discarded with the frame.
355    Ramp(usize),
356}
357
358/// One target's worth of drawing.
359///
360/// Cloneable so that a finished recording can be drawn into another one: the
361/// passes are taken as they are, and the recording being drawn keeps its own
362/// copy for a caller who draws it more than once.
363#[derive(Clone)]
364pub struct Pass {
365    pub batch: Batch,
366    pub descriptor: PassDescriptor,
367    /// What each texture slot this pass samples refers to, in slot order.
368    pub sources: Vec<TextureSource>,
369    /// The size of the target this pass renders into.
370    ///
371    /// Per pass rather than per recording, because a layer given bounds
372    /// renders into a target the size of those bounds. The root pass always
373    /// carries the recording's extent, since that is the caller's surface.
374    pub extent: Extent2D,
375}
376
377/// How a run of points is joined up.
378#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
379pub enum PointMode {
380    /// Each point on its own, drawn as the stroke's cap.
381    #[default]
382    Points,
383    /// Each pair a segment, and an odd point at the end drawn as nothing.
384    Lines,
385    /// One open run through all of them.
386    Polygon,
387}
388
389/// A finished recording, ready to submit.
390///
391/// More than one pass where the recording used layers. A layer is drawn into a
392/// target of its own and then composited back, which cannot happen in the same
393/// pass that samples it: reading an attachment being written needs machinery
394/// this does not have, and the separation is what makes group opacity mean
395/// "make this subtree, then fade it" rather than "fade each shape in it".
396pub struct Recording {
397    /// Layers first, each before whatever samples it; the root target last.
398    ///
399    /// The order falls out of how they are recorded rather than being sorted:
400    /// a layer is finished when it is restored, which is necessarily before the
401    /// draw that composites it.
402    pub passes: Vec<Pass>,
403    /// The size of the caller's surface, which is what the root pass renders
404    /// at. A layer pass may be smaller; see [`Pass::extent`].
405    pub extent: Extent2D,
406    /// Gradients whose colors were too many to carry in a material, tabulated.
407    ///
408    /// Recorded here rather than per pass because a slot table is per pass and
409    /// this is not: the same gradient drawn into a layer and again over it is
410    /// one ramp named twice.
411    pub ramps: Vec<Ramp>,
412}
413
414impl Recording {
415    pub fn draw_count(&self) -> usize {
416        self.passes.iter().map(|p| p.batch.draw_count()).sum()
417    }
418
419    pub fn is_empty(&self) -> bool {
420        self.passes.iter().all(|p| p.batch.is_empty())
421    }
422
423    /// The pass drawn into the caller's surface.
424    pub fn root(&self) -> &Pass {
425        self.passes.last().expect("a recording always has a root")
426    }
427
428    /// How many offscreen targets executing this needs.
429    pub fn layer_count(&self) -> usize {
430        self.passes.len() - 1
431    }
432}
433
434/// The target a pass renders into, placed within the frame.
435///
436/// A layer with bounds renders into a target the size of those bounds rather
437/// than the size of the frame, which is most of the point of giving bounds: a
438/// layer covering a tenth of the frame costs a tenth of the memory and a tenth
439/// of the fill. Everything that turns a device position into a clip position
440/// has to know where that target sits, which is what `origin` carries.
441#[derive(Debug, Clone, Copy)]
442struct Target {
443    /// Where this target's top-left corner sits in the frame, in device pixels.
444    /// Whole pixels, so that the composite samples texel centers exactly.
445    origin: Vec2,
446    extent: Extent2D,
447}
448
449impl Target {
450    fn frame(extent: Extent2D) -> Self {
451        Self {
452            origin: Vec2::ZERO,
453            extent,
454        }
455    }
456
457    /// Device position to clip position for this target.
458    ///
459    /// Affine, and stays so: a viewport is a scale, a flip and an offset. It is
460    /// lifted here because everything it composes with may not be.
461    fn projection(&self) -> Transform2D {
462        (viewport_projection(self.extent.width, self.extent.height)
463            * Affine2::from_translation(-self.origin))
464        .into()
465    }
466
467    /// The whole target, in the device space the projection expects.
468    fn path(&self) -> Path {
469        Rect::new(
470            self.origin.x,
471            self.origin.y,
472            self.origin.x + self.extent.width as f32,
473            self.origin.y + self.extent.height as f32,
474        )
475        .to_path()
476    }
477}
478
479/// One stencil clip in force, kept so it can be made again.
480///
481/// A stencil clip is a *draw*: it steps the stencil forward where the shape
482/// covers, and every content draw after it tests against what that draw left.
483/// A pass cut starts a new pass, whose stencil attachment begins cleared -- so
484/// unless the narrowing draws are made again, every draw after the cut tests a
485/// buffer nothing wrote and lands nowhere.
486///
487/// Kept as the shape rather than as the draw, because the shape is what
488/// `clip_path` needs to make one and the draw lives in a batch that the cut has
489/// already taken away.
490#[derive(Debug, Clone)]
491struct StencilClip {
492    path: Path,
493    /// The transform the shape was stated in. `clip_out_rect` builds its
494    /// contours in device pixels and passes the identity; `clip_path` passes
495    /// whatever was in force.
496    transform: Transform2D,
497    /// The scissor in force when it was made, which narrows the narrowing.
498    scissor: Option<Scissor>,
499}
500
501/// Transform and clip, saved together.
502///
503/// One stack rather than two: a `save` and its `restore` bracket a subtree, and
504/// letting the two pieces of state unwind independently would mean a caller
505/// could balance one while leaving the other adrift.
506#[derive(Debug)]
507struct SavedState {
508    transform: Transform2D,
509    clip: Option<Scissor>,
510    clip_bounds: Rect,
511    depth: u32,
512    /// How many stencil clips were in force, so `restore` can drop the ones
513    /// this save added. Redundant with `depth` and kept beside it for the same
514    /// reason the bounds are kept beside the scissor: one of them is the truth
515    /// the device holds and the other is what would be needed to rebuild it.
516    clips: usize,
517    /// Set where this save opened a layer, holding what the layer displaced.
518    layer: Option<LayerFrame>,
519}
520
521/// A layer in progress: the parent's recording, set aside until it returns.
522#[derive(Debug)]
523struct LayerFrame {
524    batch: Batch,
525    sources: Vec<TextureSource>,
526    /// The stencil clips in force on the parent, set aside with its batch.
527    ///
528    /// A layer starts unclipped -- the composite that returns it is subject to
529    /// the parent's clip instead -- so it needs a list of its own, and the
530    /// parent's has to come back when it does. Held here rather than as a
531    /// length in `SavedState`, because unlike every other save this one does
532    /// not *extend* the list it found.
533    clips: Vec<StencilClip>,
534    paint: Layer,
535    /// An image filter to run over the finished layer, if one was asked for.
536    ///
537    /// Beside the layer rather than inside it, because [`Layer`] is `Copy` and
538    /// small and is copied at every `save_layer`, while a filter can hold a
539    /// composition or a program's sixty-four uniform floats. The fields on the
540    /// layer cover the four kinds that fit in one; this covers the rest, and
541    /// the two orders a composition can take.
542    filter: Option<ImageFilter>,
543    /// The target the parent was drawing into, restored when the layer closes.
544    parent: Target,
545    /// Whether the parent's own batch had asked for antialiasing yet.
546    ///
547    /// Displaced with the batch and for the same reason: the sample count
548    /// describes a pass, and a layer's draws belong to the layer's pass. A
549    /// parent that has drawn nothing needing multisampling does not start
550    /// needing it because something inside a layer did.
551    anti_alias: bool,
552    /// The directions this layer's blur runs along.
553    ///
554    /// Taken from the transform in force when the layer opened, for the same
555    /// reason its sigma is: the transform that decides them may be gone by the
556    /// time the layer is composited, and both halves of a blur -- how far it
557    /// reaches and which way -- have to be settled together or a target is
558    /// sized for one blur and filled by another.
559    blur_basis: BlurBasis,
560}
561
562/// The two directions a blur runs along, in device pixels.
563///
564/// A blur is two separable passes, and which directions they take is the whole
565/// of the difference between a blur that turns with the caller's transform and
566/// one that does not. `dart:ui` states a deviation per axis in the space the
567/// caller was drawing in; if that space is turned relative to the target, the
568/// passes have to turn with it or a horizontal smear stays horizontal on
569/// screen while the thing it is smearing rotates underneath.
570///
571/// **This is not how upstream does it, and the difference is worth stating.**
572/// `GaussianBlurFilterContents` removes the rotation instead: it re-renders its
573/// input into what its own comment calls "un-rotated local space", scaled by
574/// the transform but not turned by it, blurs along that space's own axes, and
575/// applies the rotation to the *result*. Its reason is quality rather than
576/// correctness -- the comment says the un-rotated space "is a requirement for
577/// text to be rendered correctly", because taps landing on texel centers is
578/// what keeps a glyph sharp.
579///
580/// That arrangement is not available here. A layer is a recorded pass with a
581/// device-space target, a device-space scissor and a stencil buffer to match,
582/// so its content cannot be drawn into a space of its own choosing after the
583/// fact. What *is* available is the pass's `step`, which was already a free
584/// two-vector rather than an axis flag -- the shader walks its taps along
585/// whatever direction it is given. So the passes turn instead of the space.
586///
587/// The two are the same Gaussian. A blur with deviations along orthogonal
588/// directions is separable along exactly those directions, so the picture is
589/// upstream's; what differs is that a tap here lands between texels and is
590/// resolved by the sampler, which costs a little sharpness that upstream's
591/// arrangement does not.
592#[derive(Debug, Clone, Copy, PartialEq)]
593struct BlurBasis {
594    /// Unit vectors in device space, in the order the deviations are stated.
595    directions: [Vec2; 2],
596}
597
598impl BlurBasis {
599    /// The target's own axes, which is what every blur used before this and
600    /// what a transform this cannot decompose still gets.
601    const AXES: Self = Self {
602        directions: [Vec2::X, Vec2::Y],
603    };
604
605    /// Where the caller's axes point once the transform has been applied.
606    ///
607    /// Refused rather than approximated in two cases. A transform with
608    /// perspective has no single basis -- the directions would differ per
609    /// fragment, which a pass walking a constant step cannot express. And a
610    /// transform whose image axes are not perpendicular, which is a shear,
611    /// leaves a Gaussian that two separable passes cannot state at all:
612    /// separability is a property of orthogonal directions, and running the
613    /// passes along oblique ones would not be a wrong blur so much as not a
614    /// blur. Upstream is no better off here -- its `ExtractScale` takes the
615    /// lengths of the image axes and drops the shear entirely.
616    fn of(transform: Transform2D) -> Self {
617        let Some(affine) = transform.to_affine() else {
618            return Self::AXES;
619        };
620        let (x, y) = (affine.matrix2.x_axis, affine.matrix2.y_axis);
621        let (lx, ly) = (x.length(), y.length());
622        if !lx.is_finite() || !ly.is_finite() || lx <= 0.0 || ly <= 0.0 {
623            return Self::AXES;
624        }
625        let directions = [x / lx, y / ly];
626        // The cosine of the angle between them, both being unit vectors. A
627        // thousandth is well inside what a rotation composed with a scale
628        // produces and well outside anything a shear does.
629        if directions[0].dot(directions[1]).abs() > 1e-3 {
630            return Self::AXES;
631        }
632        Self { directions }
633    }
634
635    /// Whether these are the target's own axes, which several things below can
636    /// take shortcuts for.
637    fn is_axis_aligned(&self) -> bool {
638        *self == Self::AXES
639    }
640
641    /// How far past its content a blur along these directions carries, as an
642    /// outset on each of the target's axes.
643    ///
644    /// The bounding box of the two reaches laid along the two directions,
645    /// which for the target's own axes is the pair of reaches unchanged.
646    fn reach(&self, sigma: Vec2) -> Vec2 {
647        let [u, v] = self.directions;
648        let (ru, rv) = (blur_reach(sigma.x), blur_reach(sigma.y));
649        Vec2::new(
650            u.x.abs() * ru + v.x.abs() * rv,
651            u.y.abs() * ru + v.y.abs() * rv,
652        )
653    }
654}
655
656/// Spreading or shrinking a finished layer, one axis at a time.
657///
658/// The pair `dart:ui` calls `ImageFilter.dilate` and `ImageFilter.erode`: each
659/// output pixel takes the largest, or the smallest, of the input within a
660/// rectangle around it. The rectangle is why the radii are per axis and why the
661/// filter is separable at all.
662#[derive(Debug, Clone, Copy, PartialEq)]
663pub struct Morphology {
664    /// How far the structuring element reaches along each axis, in device
665    /// pixels.
666    pub radius: [f32; 2],
667    /// Take the largest sample in reach rather than the smallest.
668    pub dilate: bool,
669}
670
671impl Morphology {
672    /// Spread the layer: each pixel becomes the largest within the radii.
673    pub fn dilate(x: f32, y: f32) -> Self {
674        Self {
675            radius: Self::sane(x, y),
676            dilate: true,
677        }
678    }
679
680    /// Shrink the layer: each pixel becomes the smallest within the radii.
681    pub fn erode(x: f32, y: f32) -> Self {
682        Self {
683            radius: Self::sane(x, y),
684            dilate: false,
685        }
686    }
687
688    /// Whole texels, not negative, and finite.
689    ///
690    /// Rounded here rather than in the shader so that the radius a caller can
691    /// observe -- through the bounds a dilated layer takes, which grow by it --
692    /// is the radius that actually gets applied. A structuring element is a set
693    /// of sample positions; there is no half of one to keep.
694    fn sane(x: f32, y: f32) -> [f32; 2] {
695        let one = |v: f32| {
696            if v.is_finite() {
697                v.max(0.0).round()
698            } else {
699                0.0
700            }
701        };
702        [one(x), one(y)]
703    }
704
705    /// Whether this would leave every pixel where it was.
706    pub fn is_identity(&self) -> bool {
707        self.radius == [0.0, 0.0]
708    }
709}
710
711/// What a mask blur is blurring: a shape, or a run of glyphs.
712///
713/// An enum rather than a closure because every style draws its content two or
714/// three times and a closure taking `&mut Canvas` cannot be called twice while
715/// the canvas is borrowed. Two variants is also the whole set: a mask blur
716/// needs the content's coverage times one solid color, which a mesh's per-vertex
717/// colors rule out and an image's texels rule out.
718#[derive(Clone, Copy)]
719enum Masked<'a> {
720    Path(&'a Path),
721    /// A mesh carrying no per-vertex colors, so the paint supplies the color
722    /// the way it does for a path. One that carries them is refused before it
723    /// reaches here: see `draw_vertices`.
724    Mesh(&'a Vertices),
725    Glyphs {
726        glyphs: &'a [PositionedGlyph],
727        atlas: &'a Atlas,
728        slot: u32,
729    },
730}
731
732/// How a layer is composited back onto what was underneath it.
733///
734/// A separate type from [`Paint`] rather than a reuse of it, because only two
735/// of a paint's parts mean anything here — there is no shape to fill and no
736/// geometry to stroke — and a caller handed a `Paint` would reasonably expect
737/// its shader to matter.
738#[derive(Debug, Clone, Copy, PartialEq)]
739pub struct Layer {
740    /// Blur the group before compositing it, with this standard deviation per
741    /// axis in device pixels.
742    ///
743    /// Zero on both for no blur, which is the default and costs nothing: the
744    /// extra passes are only recorded where a caller asked for one, and an
745    /// axis whose deviation is zero costs no pass of its own.
746    ///
747    /// Two deviations rather than one because `dart:ui`'s `ImageFilter.blur`
748    /// states `sigmaX` and `sigmaY` separately, and because the blur is two
749    /// separable passes here regardless -- so the second is a field rather
750    /// than a mechanism. [`Self::with_blur`] sets both.
751    ///
752    /// On the group rather than on a paint, because a blur is a function of a
753    /// finished image and a paint describes one shape. Blurring each shape and
754    /// compositing the results is a different picture from blurring the
755    /// composite, and the second is what a shadow or a frosted panel means.
756    pub blur: Vec2,
757    /// Scales the whole layer on the way back. This is what makes group opacity
758    /// differ from per-shape opacity: two overlapping half-transparent shapes
759    /// in a layer show one blended edge, where the same shapes drawn directly
760    /// show where they cross.
761    pub alpha: f32,
762    /// How the finished layer meets what was underneath it.
763    pub blend: BlendMode,
764    /// Transform the finished layer on the way back, in device pixels.
765    ///
766    /// Distinct from transforming what goes into it, which the canvas's own
767    /// transform already does. This resamples the finished image: magnifying
768    /// through it gives the layer's own pixels enlarged, where drawing the
769    /// same shapes under a larger transform would give them redrawn at the
770    /// larger size. That difference is the whole of what a matrix image filter
771    /// means -- a caller wanting the sharp one already has the transform
772    /// stack.
773    ///
774    /// It resamples what the layer *captured*, and a layer captures no more
775    /// than its target holds. So a matrix moves an image and cannot recover
776    /// what fell outside the layer to begin with: a shape drawn past the edge
777    /// of the frame is gone before this runs, where upstream sizes the layer
778    /// through the filter and brings it back. `docs/non-parity.md` §12 has the
779    /// case and what fixing it would cost.
780    ///
781    /// `None` composites the layer where it was drawn, which is what every
782    /// layer did before this existed.
783    pub matrix: Option<Transform2D>,
784    /// Blur what is already on the target before the layer draws over it.
785    ///
786    /// This is the other blur, and the difference is which image is filtered.
787    /// [`Self::blur`] softens the layer's own content, which is what a shadow
788    /// is. This softens what is *behind* it and hands the result to the layer
789    /// as its starting content, which is what frosted glass is: a panel that
790    /// obscures rather than one that is itself indistinct.
791    ///
792    /// Zero for none, and it costs nothing when zero. When it is not zero it
793    /// costs a copy of the target and two blur passes over it, because reading
794    /// what a pass is writing is not something this renderer can do -- see the
795    /// architecture note on cutting a pass.
796    ///
797    /// # Bounds stop being an optimization
798    ///
799    /// For every other layer, [`Canvas::save_layer_bounds`] only says where the
800    /// content is, and the picture is the same either way. For this one the
801    /// bounds are the region that gets filtered: a frosted panel is a bounded
802    /// layer, and the same layer without bounds blurs the whole frame and
803    /// composites the whole frame back. Both are meaningful and they are
804    /// different pictures, so state the bounds when you mean a panel.
805    pub backdrop_blur: f32,
806    /// Which captured backdrop this layer filters, if it shares one.
807    ///
808    /// `dart:ui`'s `pushBackdropFilter` takes a `backdropId` and says what it
809    /// means: "when the first backdrop filter with a given id is processed
810    /// during rasterization, the state of the backdrop is recorded and cached.
811    /// All subsequent backdrop filters with the same identifier will apply
812    /// their filter to the cached backdrop." So it is a different picture, not
813    /// only a faster one -- overlapping panels sharing an id each filter what
814    /// was behind all of them, rather than each filtering the one before it.
815    ///
816    /// That is the case it exists for. A list of frosted rows over one
817    /// background wants each row to show the background, and without the id
818    /// the second row shows the first row's blur blurred again. With it, the
819    /// capture happens once and each row filters it: the picture the caller
820    /// meant, and one pass cut instead of one per row.
821    ///
822    /// An id keys a captured image and nothing more. A layer naming one from
823    /// inside another layer still filters what the id captured rather than its
824    /// own target's content, which is upstream's reading and is the point of
825    /// the case it tests with each panel wrapped in a save layer of its own.
826    ///
827    /// `None` captures afresh, which is what every layer did before this
828    /// existed and is right whenever the backdrops genuinely differ.
829    pub backdrop_id: Option<i64>,
830    /// Spread or shrink the group before compositing it. See [`Morphology`].
831    ///
832    /// On the group for the same reason [`Self::blur`] is: a maximum over a
833    /// window is a function of a finished image. Dilating each shape and then
834    /// compositing is a different picture from dilating the composite wherever
835    /// two shapes overlap.
836    ///
837    /// `None` for no morphology, which is the default and costs nothing.
838    pub morphology: Option<Morphology>,
839    /// Recolor the finished group on its way back.
840    ///
841    /// What `dart:ui` means by the `colorFilter` on the paint `saveLayer`
842    /// takes, and the same distinction group opacity has: filtering each shape
843    /// and compositing the results differs from filtering the composite
844    /// wherever two of them overlap, because a filter is not linear in general.
845    ///
846    /// It is also the only way to filter what a caller's own fragment program
847    /// drew. A runtime effect is a whole pipeline rather than a material this
848    /// renderer's shader evaluates, so there is nowhere in it to apply a
849    /// matrix -- the filter has to act on the image the program produced.
850    pub color_filter: ColorFilter,
851}
852
853impl Default for Layer {
854    fn default() -> Self {
855        Self {
856            blur: Vec2::ZERO,
857            alpha: 1.0,
858            blend: BlendMode::SrcOver,
859            matrix: None,
860            backdrop_blur: 0.0,
861            backdrop_id: None,
862            morphology: None,
863            color_filter: ColorFilter::None,
864        }
865    }
866}
867
868impl Layer {
869    /// Transform the finished layer on the way back. See [`Self::matrix`].
870    ///
871    /// Takes an affine or a projective transform, the same as `concat`. A layer
872    /// moved by one of these is a finished image being placed, so perspective
873    /// here gives the image seen at an angle rather than the content redrawn at
874    /// one -- which is the whole distinction this filter exists to make, and it
875    /// survives the widening unchanged.
876    pub fn with_matrix(mut self, matrix: impl Into<Transform2D>) -> Self {
877        self.matrix = Some(matrix.into());
878        self
879    }
880
881    pub fn opacity(alpha: f32) -> Self {
882        Self {
883            alpha,
884            ..Self::default()
885        }
886    }
887
888    pub fn with_blend(mut self, blend: BlendMode) -> Self {
889        self.blend = blend;
890        self
891    }
892
893    /// Recolor the finished group on its way back. See [`Self::color_filter`].
894    pub fn with_color_filter(mut self, filter: ColorFilter) -> Self {
895        self.color_filter = filter;
896        self
897    }
898
899    /// Spread or shrink the finished group. See [`Morphology`].
900    ///
901    /// A morphology that would move nothing becomes `None`, on the same
902    /// reasoning as a blur of zero: a caller animating a radius down to nothing
903    /// should get the unfiltered thing at the end, not two passes that copy the
904    /// image twice to say so.
905    pub fn with_morphology(mut self, morphology: Morphology) -> Self {
906        self.morphology = if morphology.is_identity() {
907            None
908        } else {
909            Some(morphology)
910        };
911        self
912    }
913
914    /// How far past the content this layer's filters reach, per axis, in device
915    /// pixels.
916    ///
917    /// A blur carries color outward and so does a dilation; an erosion only
918    /// eats inward and needs no room. The two can be asked for together, so
919    /// this is their sum rather than whichever is larger -- the passes run one
920    /// after the other, and the second reaches out from where the first put
921    /// things.
922    /// The same layer with its lengths converted into device pixels.
923    ///
924    /// A sigma reaches the caller in the space they were drawing in and every
925    /// pass that acts on it works on a target, so one of the two has to move.
926    /// This is that move, and it happens once at `save_layer` rather than at
927    /// each use -- the transform that decides it is the one in force when the
928    /// layer is opened, and it may be gone by the time the layer is composited.
929    ///
930    /// The morphology is left alone: it is stated in device pixels and says so,
931    /// and upstream has no morphology to be in parity with.
932    fn scaled_by(self, scale: f32) -> Self {
933        if !scale.is_finite() || scale <= 0.0 {
934            return self;
935        }
936        Self {
937            blur: self.blur * scale,
938            backdrop_blur: self.backdrop_blur * scale,
939            ..self
940        }
941    }
942
943    fn reach(&self, basis: BlurBasis) -> Vec2 {
944        let blur = basis.reach(self.blur);
945        match self.morphology {
946            Some(m) if m.dilate => blur + Vec2::new(m.radius[0], m.radius[1]),
947            _ => blur,
948        }
949    }
950
951    /// Blur the finished group, with `sigma` in device pixels.
952    ///
953    /// Anything at or below zero, or not a number, means no blur rather than an
954    /// error: a caller animating a shadow's softness to nothing should get the
955    /// sharp thing, not a refusal at the end of the animation.
956    /// Blur the backdrop this layer is drawn over.
957    ///
958    /// Ignored unless finite and positive, on the same terms as
959    /// [`Self::with_blur`]: a sigma that is not a length describes no blur.
960    pub fn with_backdrop_blur(mut self, sigma: f32) -> Self {
961        self.backdrop_blur = if sigma.is_finite() && sigma > 0.0 {
962            sigma
963        } else {
964            0.0
965        };
966        self
967    }
968
969    /// Share one captured backdrop with every layer naming the same `id`.
970    ///
971    /// See [`Self::backdrop_id`] for what is shared and why it changes the
972    /// picture rather than only the cost. The id is the caller's, and means
973    /// nothing beyond identity: any two values that differ keep their
974    /// backdrops apart.
975    ///
976    /// The id outlives the target it captured. A layer naming one from inside
977    /// another layer is seeded with the captured image placed where that
978    /// layer's target sits within it, which is the same arithmetic a bounded
979    /// layer is already seeded by.
980    pub fn with_backdrop_id(mut self, id: i64) -> Self {
981        self.backdrop_id = Some(id);
982        self
983    }
984
985    pub fn with_blur(self, sigma: f32) -> Self {
986        self.with_blur_xy(sigma, sigma)
987    }
988
989    /// Blur the finished group by a deviation per axis, in device pixels.
990    ///
991    /// Two, because `dart:ui`'s `ImageFilter.blur` states `sigmaX` and
992    /// `sigmaY` separately. A blur here is two separable passes already, one
993    /// along each axis, so each simply uses its own -- and a pass whose
994    /// deviation is zero is skipped rather than run, which is what keeps a
995    /// blur along one axis from resampling the other for nothing.
996    pub fn with_blur_xy(mut self, sigma_x: f32, sigma_y: f32) -> Self {
997        // Clamped to upstream's `kMaxSigma`, and clamped *after* the check
998        // rather than before it: `f32::min` returns the other operand when one
999        // is NaN, so clamping first turns a NaN into five hundred and asks for
1000        // the widest blur there is. The same trap is named at `Rect::outset`,
1001        // which is where this was learned the first time.
1002        let one = |sigma: f32| {
1003            if sigma.is_finite() && sigma > 0.0 {
1004                sigma.min(MAX_SIGMA)
1005            } else {
1006                0.0
1007            }
1008        };
1009        self.blur = Vec2::new(one(sigma_x), one(sigma_y));
1010        self
1011    }
1012}
1013
1014/// Records drawing commands for one frame.
1015pub struct Canvas {
1016    renderer: Renderer,
1017    batch: Batch,
1018    transform: Transform2D,
1019    /// The region drawing is confined to, or `None` for the whole target.
1020    ///
1021    /// Kept in device pixels rather than user space because that is what it
1022    /// means: a clip is fixed at the moment it is applied, and a later
1023    /// transform moves the shapes drawn inside it without moving the clip.
1024    clip: Option<Scissor>,
1025    /// A rectangle around everything the clip still admits, in device pixels.
1026    ///
1027    /// Kept beside the scissor rather than derived from it, because a path
1028    /// clip narrows what may be drawn without narrowing the scissor at all --
1029    /// it goes to the stencil instead. A caller asking what is still reachable
1030    /// wants both accounted for, and only this has seen both.
1031    clip_bounds: Rect,
1032    /// How many clips of a shape a scissor cannot express are in force.
1033    ///
1034    /// Content draws where the stencil holds this, which is true only where
1035    /// every one of those clips admitted the pixel.
1036    depth: u32,
1037    /// Those clips, in the order they were built. See [`StencilClip`].
1038    clips: Vec<StencilClip>,
1039    stack: Vec<SavedState>,
1040    /// What the batch being recorded samples, in slot order.
1041    sources: Vec<TextureSource>,
1042    /// Layers already finished, in the order they were restored.
1043    ///
1044    /// A layer is restored before anything can composite it, so this order is
1045    /// already an order the passes can be executed in.
1046    finished: Vec<Pass>,
1047    extent: Extent2D,
1048    /// The target being recorded into: the frame, or a bounded layer's.
1049    target: Target,
1050    background: Option<Color>,
1051    /// Set once anything asks for antialiasing.
1052    ///
1053    /// Multisampling is a property of the pass, so it cannot vary per shape.
1054    /// Turning it on for the whole frame when any shape wants it is the
1055    /// behavior that surprises least; the alternative silently ignores the
1056    /// request on some shapes.
1057    anti_alias: bool,
1058    samples: u32,
1059    /// Gradients tabulated because their stops did not fit in a material.
1060    ///
1061    /// Across the whole recording rather than per pass, so the same gradient
1062    /// drawn into a layer and again over it is baked once.
1063    ramps: Vec<Ramp>,
1064    /// Backdrops captured once and filtered by every layer naming the same id.
1065    ///
1066    /// A `Canvas` records one frame and `finish` consumes it, so this lives
1067    /// exactly as long as the ids are meaningful and there is nothing to
1068    /// clear. Upstream's `Canvas` is reused across frames and empties its
1069    /// `backdrop_data_` at the end of each one.
1070    backdrops: std::collections::HashMap<i64, BackdropShare>,
1071}
1072
1073/// A backdrop ready to be laid under a layer's own content.
1074///
1075/// Three things rather than a pass index, because a seed is a draw and a draw
1076/// needs a mapping: which pass holds the filtered image, which target that pass
1077/// is an image *of* -- they differ when a bounded layer seeds from its parent
1078/// -- and the matrix the filter asked for, which is the one filter that moves
1079/// the image rather than recomputing it where it stands.
1080#[derive(Clone, Copy)]
1081struct Backdrop {
1082    pass: usize,
1083    of: Target,
1084    /// `None` for every filter but a matrix, which is what it means for the
1085    /// image to be read where it was written.
1086    moved_by: Option<Affine2>,
1087}
1088
1089/// One captured backdrop, and what has already been made of it.
1090///
1091/// The capture is a pass index, which is all a backdrop needs to be here: a
1092/// pass is the unit a later pass samples, so sharing one is sharing the index
1093/// rather than managing a texture. Upstream carries a `std::shared_ptr<Texture>`
1094/// in the same slot, and a second one for the filtered result, because its
1095/// filters produce snapshots rather than passes.
1096struct BackdropShare {
1097    /// The target the capture is an image of, which need not be the one a
1098    /// later layer naming the id is drawn into.
1099    ///
1100    /// An id keys a captured image and nothing else, so a layer opened inside
1101    /// some other layer still filters what the id captured. That is upstream's
1102    /// reading and it is exercised there:
1103    /// `CanRenderMultipleBackdropBlurWithSingleBackdropIdDifferentLayers` wraps
1104    /// every panel but the first in a save layer of its own and expects all six
1105    /// to show the same ground. Seeding needs no case for it -- a filtered pass
1106    /// is drawn into a rectangle *of* its source, which is already how a
1107    /// bounded layer is seeded from a full-target capture.
1108    captured_from: Target,
1109    /// The pass holding the backdrop as it was when the id was first used.
1110    captured: usize,
1111    /// Filters already run over `captured`, and where each landed.
1112    ///
1113    /// A filter is a function of its input, so the same filter over the same
1114    /// capture is the same image and there is no reason to compute it twice.
1115    /// This is upstream's `all_filters_equal` and `shared_filter_snapshot`,
1116    /// with the restriction lifted: upstream shares one snapshot only when
1117    /// *every* filter on the id is equal, which a list of rows that blur by
1118    /// different amounts fails outright. Keyed per filter, two amounts share
1119    /// what they have in common -- the capture -- and each still gets its own.
1120    ///
1121    /// A list rather than a map because a shared backdrop has a handful of
1122    /// distinct filters at most, and `ImageFilter` holds a program's uniforms.
1123    filtered: Vec<(ImageFilter, usize)>,
1124}
1125
1126impl Canvas {
1127    /// Start recording for a target of the given size.
1128    pub fn new(extent: Extent2D) -> Self {
1129        let mut renderer = Renderer::new();
1130        renderer.begin_frame(extent, TOLERANCE);
1131        Self {
1132            renderer,
1133            batch: Batch::new(),
1134            transform: Transform2D::IDENTITY,
1135            clip: None,
1136            // The whole target to start with, which is what an unclipped
1137            // canvas may reach.
1138            clip_bounds: Rect::new(0.0, 0.0, extent.width as f32, extent.height as f32),
1139            depth: 0,
1140            clips: Vec::new(),
1141            stack: Vec::new(),
1142            sources: Vec::new(),
1143            finished: Vec::new(),
1144            extent,
1145            target: Target::frame(extent),
1146            background: None,
1147            anti_alias: false,
1148            samples: 4,
1149            ramps: Vec::new(),
1150            backdrops: std::collections::HashMap::new(),
1151        }
1152    }
1153
1154    /// Sample count to use when anything is antialiased.
1155    pub fn with_samples(mut self, samples: u32) -> Self {
1156        self.samples = samples.max(1);
1157        self
1158    }
1159
1160    pub fn extent(&self) -> Extent2D {
1161        self.extent
1162    }
1163
1164    /// Fill the whole target before drawing anything else.
1165    ///
1166    /// Required, not merely usual, if the frame itself draws an antialiased
1167    /// shape that is not analytic. Such a shape renders through a multisample
1168    /// buffer, and there is no way to seed that buffer with what the target
1169    /// already held -- so a multisampled pass has to clear, and a canvas with
1170    /// no background gives its root pass nothing to clear to. Drawing one is
1171    /// refused rather than quietly discarding whatever was on the surface.
1172    ///
1173    /// The frame itself, and not the picture: a sample count describes one
1174    /// pass. The same shape drawn inside a layer needs the layer's pass
1175    /// multisampled and leaves the frame alone, since what the frame does with
1176    /// a finished layer is draw one image quad. So a shadow, which is a layer
1177    /// underneath, needs no background at all.
1178    ///
1179    /// [`Paint::with_anti_alias`] is the other end of the choice.
1180    pub fn clear(&mut self, color: Color) -> &mut Self {
1181        self.background = Some(color);
1182        self
1183    }
1184
1185    /// Current transform, mapping user coordinates to device pixels.
1186    pub fn transform(&self) -> Transform2D {
1187        self.transform
1188    }
1189
1190    /// The region drawing is currently confined to, in device pixels.
1191    ///
1192    /// `None` is the whole target.
1193    pub fn clip(&self) -> Option<Scissor> {
1194        self.clip
1195    }
1196
1197    /// The region drawing may still reach, in device pixels.
1198    ///
1199    /// Conservative: it is a rectangle around the clip rather than the clip
1200    /// itself, so a path clip reports its bounding box and a rotated
1201    /// rectangle reports the axis-aligned box around it. That is what makes it
1202    /// useful for the thing it is for -- deciding not to draw something --
1203    /// since a caller may skip anything outside it and must not assume
1204    /// everything inside it is visible.
1205    pub fn destination_clip_bounds(&self) -> Rect {
1206        self.clip_bounds
1207    }
1208
1209    /// The same region in the coordinates the caller is currently drawing in.
1210    ///
1211    /// The device bounds carried back through the transform, which under a
1212    /// rotation gives the box around the rotated box and so grows a little
1213    /// each time. Conservative in the same direction as everything else here:
1214    /// too large is a missed optimization, too small is a missing shape.
1215    ///
1216    /// A transform that cannot be inverted has collapsed the plane, and
1217    /// nothing drawn through it reaches anything; the empty rectangle says so.
1218    pub fn local_clip_bounds(&self) -> Rect {
1219        let Some(inverse) = self.transform.inverse() else {
1220            return Rect::new(0.0, 0.0, 0.0, 0.0);
1221        };
1222        // Two different ways this can have no answer, and they take opposite
1223        // ones. A transform that has collapsed leaves nothing reachable, which
1224        // the check above answers with an empty rectangle. A transform whose
1225        // inverse carries the clip across the vanishing line leaves *more*
1226        // reachable than a box can say -- and since this rectangle is a promise
1227        // about what lies outside it, the safe failure there is everything.
1228        let (min, max) = transformed_bounds(
1229            inverse,
1230            Vec2::new(self.clip_bounds.left, self.clip_bounds.top),
1231            Vec2::new(self.clip_bounds.right, self.clip_bounds.bottom),
1232        )
1233        .unwrap_or_else(unbounded);
1234        Rect::new(min.x, min.y, max.x, max.y)
1235    }
1236
1237    /// Narrow the tracked bounds to a rectangle already in device space.
1238    fn narrow_bounds(&mut self, min: Vec2, max: Vec2) {
1239        let current = self.clip_bounds;
1240        self.clip_bounds = Rect::new(
1241            current.left.max(min.x),
1242            current.top.max(min.y),
1243            current.right.min(max.x),
1244            current.bottom.min(max.y),
1245        );
1246        // An intersection that crossed over is empty rather than inside out,
1247        // which every consumer of a rectangle here would otherwise have to
1248        // check for itself.
1249        if self.clip_bounds.right < self.clip_bounds.left
1250            || self.clip_bounds.bottom < self.clip_bounds.top
1251        {
1252            self.clip_bounds = Rect::new(
1253                self.clip_bounds.left,
1254                self.clip_bounds.top,
1255                self.clip_bounds.left,
1256                self.clip_bounds.top,
1257            );
1258        }
1259    }
1260
1261    /// Narrow the clip to a rectangle in user space.
1262    ///
1263    /// Intersects with the clip already in force rather than replacing it,
1264    /// which is what makes the clip stack compose: a subtree can only ever
1265    /// narrow what its parent allowed. The rectangle goes through the current
1266    /// transform, so it moves with the coordinate system the caller set up.
1267    ///
1268    /// # Rotation
1269    ///
1270    /// Returns [`Error::Unsupported`] when the current transform rotates or
1271    /// skews by anything other than a quarter turn, because the result is then
1272    /// a rotated quadrilateral and this clip is a rectangle. Refusing is the
1273    /// point: the tempting alternative is to use the rotated shape's bounding
1274    /// box, which admits pixels the caller asked to remove, and produces a
1275    /// picture that is wrong in a way that looks like a rendering bug rather
1276    /// than like a clip that was never applied. Arbitrary clip shapes need a
1277    /// stencil pass, which is a separate piece of machinery.
1278    pub fn clip_rect(&mut self, rect: Rect) -> Result<&mut Self> {
1279        // Under a transform that keeps rectangles rectangular, this is exactly
1280        // a scissor, which costs nothing and is exact. Under anything else the
1281        // result is a rotated quadrilateral, and taking its bounding box would
1282        // admit pixels the caller asked to remove -- so it goes through the
1283        // stencil like any other shape. That the fast path survives is the
1284        // point: a scissor stays the right answer for an axis-aligned clip even
1285        // now that the general one exists.
1286        if !preserves_axis_alignment(self.transform) {
1287            return self.clip_path(&rect.to_path());
1288        }
1289        let Some((min, max)) = transformed_bounds(
1290            self.transform,
1291            Vec2::new(rect.left, rect.top),
1292            Vec2::new(rect.right, rect.bottom),
1293        ) else {
1294            // Unreachable through the check above, which has already refused
1295            // anything but an affine -- and written as a fallback rather than
1296            // an assertion because the two are coupled only by argument, and
1297            // the stencil path is the right answer either way.
1298            return self.clip_path(&rect.to_path());
1299        };
1300        let narrowed = Scissor::from_device_bounds(
1301            (min - self.target.origin).into(),
1302            (max - self.target.origin).into(),
1303            self.target.extent,
1304        );
1305        self.clip = Some(match self.clip {
1306            Some(existing) => existing.intersect(narrowed),
1307            None => narrowed,
1308        });
1309        self.narrow_bounds(min, max);
1310        Ok(self)
1311    }
1312
1313    /// Narrow the clip to everything *outside* a rectangle in user space.
1314    ///
1315    /// `dart:ui` spells this `clipRect` with `ClipOp.difference`, and it is the
1316    /// only clip that operation applies to there -- `clipPath` and `clipRRect`
1317    /// intersect and take no operation.
1318    ///
1319    /// Never a scissor, whatever the transform: the complement of a rectangle
1320    /// is not one, and a scissor is. So this goes through the stencil like an
1321    /// arbitrary shape, and the shape it narrows by is the target with the
1322    /// rectangle taken out of it -- two contours filled by the even-odd rule,
1323    /// which is what makes the second a hole in the first rather than a second
1324    /// region drawn over it. `draw_drrect` builds a ring the same way.
1325    ///
1326    /// Both contours are built in device coordinates and filled through the
1327    /// identity, rather than in user space through the transform. The reason is
1328    /// the target: it is a device rectangle, and expressing it in user space
1329    /// would mean inverting a transform that may not be invertible. The
1330    /// rectangle's own corners go through the transform here instead, so a
1331    /// rotation gives the quadrilateral it should rather than a box around one.
1332    pub fn clip_out_rect(&mut self, rect: Rect) -> Result<&mut Self> {
1333        if self.clip.is_some_and(Scissor::is_empty) {
1334            return Ok(self);
1335        }
1336        let mut builder = PathBuilder::new().with_fill_rule(FillRule::EvenOdd);
1337        // The whole target, which is what "outside the rectangle" is measured
1338        // against.
1339        let target = Rect::new(
1340            self.target.origin.x,
1341            self.target.origin.y,
1342            self.target.origin.x + self.target.extent.width as f32,
1343            self.target.origin.y + self.target.extent.height as f32,
1344        );
1345        builder
1346            .move_to(Vec2::new(target.left, target.top))
1347            .line_to(Vec2::new(target.right, target.top))
1348            .line_to(Vec2::new(target.right, target.bottom))
1349            .line_to(Vec2::new(target.left, target.bottom))
1350            .close();
1351        // The rectangle's corners under the transform, in the order that keeps
1352        // the contour simple. A degenerate one encloses no area and takes
1353        // nothing out, which is the right answer for a clip that removes an
1354        // empty rectangle.
1355        let corners = [
1356            Vec2::new(rect.left, rect.top),
1357            Vec2::new(rect.right, rect.top),
1358            Vec2::new(rect.right, rect.bottom),
1359            Vec2::new(rect.left, rect.bottom),
1360        ]
1361        .map(|corner| self.transform.project_point2(corner));
1362        // A corner that is not a number describes no rectangle, so there is
1363        // nothing to take out and the clip is left as it was. Under a transform
1364        // carrying perspective this is also what catches a corner past the
1365        // vanishing line, where the divide reports an infinity rather than a
1366        // wrong finite answer -- and where the four corners would no longer
1367        // describe the region even if they were finite.
1368        if !corners.iter().all(|corner| corner.is_finite()) {
1369            return Ok(self);
1370        }
1371        builder.move_to(corners[0]);
1372        for corner in &corners[1..] {
1373            builder.line_to(*corner);
1374        }
1375        builder.close();
1376
1377        let paint = RenderPaint {
1378            material: Material::solid([1.0, 1.0, 1.0, 1.0]),
1379            filter: ColorFilter::None,
1380            blend: BlendMode::Src,
1381            clip: self.clip,
1382            stencil: ClipState::narrow(self.depth),
1383        };
1384        let path = builder.build();
1385        self.renderer
1386            .fill_into(&mut self.batch, &path, Affine2::IDENTITY, &paint)?;
1387        self.depth += 1;
1388        self.clips.push(StencilClip {
1389            path,
1390            transform: Transform2D::IDENTITY,
1391            scissor: paint.clip,
1392        });
1393        // The tracked bounds are left alone, and that is not an oversight.
1394        // Removing a rectangle from the middle of a region leaves its bounding
1395        // box exactly where it was, and removing one from the edge leaves a box
1396        // that is too large -- which is the safe direction, since these bounds
1397        // decide how much a caller draws and how large a layer is allocated.
1398        Ok(self)
1399    }
1400
1401    /// Narrow the clip to an arbitrary path in user space.
1402    ///
1403    /// Intersects with the clip already in force, like [`Self::clip_rect`], and
1404    /// like it the path travels through the current transform. The shape is
1405    /// recorded into the batch as a draw that writes only the stencil, so it
1406    /// costs one draw where a rectangular clip costs none.
1407    ///
1408    /// The path is filled by the same tessellator that fills a drawn shape, so
1409    /// it obeys the same fill rule and produces the same edges. That matters
1410    /// for more than consistency: the tessellation is a set of non-overlapping
1411    /// triangles, which is what lets the stencil step forward by one rather
1412    /// than needing the parity trick an overlapping fan would.
1413    pub fn clip_path(&mut self, path: &Path) -> Result<&mut Self> {
1414        if self.clip.is_some_and(Scissor::is_empty) {
1415            // Already clipped to nothing; narrowing further changes nothing and
1416            // the draw would be dropped anyway.
1417            return Ok(self);
1418        }
1419        let paint = RenderPaint {
1420            // Masked off entirely by a clip draw, but a material is still
1421            // needed to build one. White says plainly that nothing here is a
1422            // color decision.
1423            material: Material::solid([1.0, 1.0, 1.0, 1.0]),
1424            filter: ColorFilter::None,
1425            blend: BlendMode::Src,
1426            clip: self.clip,
1427            stencil: ClipState::narrow(self.depth),
1428        };
1429        self.renderer
1430            .fill_into(&mut self.batch, path, self.transform, &paint)?;
1431        self.depth += 1;
1432        self.clips.push(StencilClip {
1433            path: path.clone(),
1434            transform: self.transform,
1435            scissor: paint.clip,
1436        });
1437        // A path clip narrows what may be drawn without touching the scissor,
1438        // so the tracked rectangle is the only place it is accounted for. Its
1439        // bounding box rather than the path: this rectangle is a promise about
1440        // what is *outside* it, and a box around a shape keeps that promise.
1441        let bounds = path.bounds();
1442        // A clip whose bounds cannot be computed narrows nothing here. The
1443        // stencil still confines it exactly; what is lost is only the tracked
1444        // rectangle used to skip work, and leaving it wide is the direction
1445        // that costs time rather than pixels.
1446        if let Some((min, max)) = transformed_bounds(self.transform, bounds.min, bounds.max) {
1447            self.narrow_bounds(min, max);
1448        }
1449        Ok(self)
1450    }
1451
1452    /// Save the transform and clip so a later `restore` can return to them.
1453    pub fn save(&mut self) -> &mut Self {
1454        self.stack.push(SavedState {
1455            transform: self.transform,
1456            clip: self.clip,
1457            clip_bounds: self.clip_bounds,
1458            depth: self.depth,
1459            clips: self.clips.len(),
1460            layer: None,
1461        });
1462        self
1463    }
1464
1465    /// Begin drawing into a layer of its own, composited back on `restore`.
1466    ///
1467    /// Everything recorded until the matching restore goes into a separate
1468    /// target and arrives as one image, which is what makes an alpha here apply
1469    /// to the group rather than to each shape in it.
1470    ///
1471    /// The layer starts unclipped and at the identity for clipping purposes,
1472    /// while keeping the current transform. It does not need to inherit the
1473    /// clip because the draw that composites it is subject to it: content the
1474    /// clip excludes is discarded once rather than prevented from being drawn,
1475    /// which costs some work in the layer and keeps a stencil clip from having
1476    /// to be rebuilt in a second target.
1477    pub fn save_layer(&mut self, layer: Layer) -> &mut Self {
1478        // A layer that captures a backdrop is handed to the bounded path with
1479        // no bounds, which narrows it to the clip in force. That path scales
1480        // the layer itself, so this one hands it over unscaled.
1481        //
1482        // A shared key goes that way too. What the key shares is the capture
1483        // and the filtered image over it, both of which stay the parent's size
1484        // because they have to serve every group naming it; the layer's own
1485        // target is bound by the clip like any other.
1486        if layer.backdrop_blur > 0.0 && layer.matrix.is_none() {
1487            let (min, max) = unbounded();
1488            // Cannot fail: only a backdrop *filter* can refuse, and this
1489            // passes none -- the blur rides on the layer.
1490            let _ = self.save_layer_device_bounds_running(layer, min, max, None, None);
1491            return self;
1492        }
1493        // The other entry to `open_layer` converts the sigmas where it computes
1494        // its reach; this one has no bounds to reach past and so converts them
1495        // here. Both are the same conversion at the same moment -- the layer is
1496        // opened under the transform the caller drew in, which is the one its
1497        // sigma is stated against.
1498        let layer = layer.scaled_by(max_scale_of(self.transform));
1499        let widened = self.matrix_preimage(&layer);
1500        let pending = self.open_layer(layer, None, None, None).unwrap_or(None);
1501        if let Some(target) = widened {
1502            self.aim_at(target);
1503        }
1504        self.seed_backdrop(pending);
1505        self
1506    }
1507
1508    /// The region a layer must record over when a matrix will move its result.
1509    ///
1510    /// A layer's target is the parent's, and a draw outside it is scissored
1511    /// away -- which is right for every layer except one whose matrix brings
1512    /// content in from outside. `dart:ui`'s matrix image filter is exactly
1513    /// that, and upstream's `MatrixImageFilterDoesntCullWhenTranslatedFromOffscreen`
1514    /// draws a circle three hundred points to the left of the frame and
1515    /// translates it back.
1516    ///
1517    /// What has to be recorded over is the *pre-image*: the region that, once
1518    /// the matrix has moved it, lands where the parent can see it. Union with
1519    /// the parent, because a matrix that moves some content in leaves the rest
1520    /// where it was.
1521    ///
1522    /// **Opening wide costs no memory, and that is what makes this safe.** The
1523    /// pass's extent comes from the narrowed target in `finish_layer`, which is
1524    /// the content's own bounds -- so a minifying matrix, whose inverse is a
1525    /// magnifying one, widens the region a draw may land in without allocating
1526    /// for a region no draw reached. What is allocated stays bounded by what
1527    /// the caller drew.
1528    fn matrix_preimage(&self, layer: &Layer) -> Option<Target> {
1529        let matrix = layer.matrix?;
1530        let parent = self.target;
1531        let (min, max) = (
1532            parent.origin,
1533            parent.origin + Vec2::new(parent.extent.width as f32, parent.extent.height as f32),
1534        );
1535        let (back_min, back_max) = transformed_bounds(matrix.inverse()?, min, max)?;
1536        let left = min.x.min(back_min.x).floor();
1537        let top = min.y.min(back_min.y).floor();
1538        let right = max.x.max(back_max.x).ceil();
1539        let bottom = max.y.max(back_max.y).ceil();
1540        // A matrix that folds the plane or carries the region across the
1541        // vanishing line has no region to report, and one that widens past what
1542        // an extent can hold is not a picture anybody asked for.
1543        let (width, height) = (right - left, bottom - top);
1544        if !(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0) {
1545            return None;
1546        }
1547        Some(Target {
1548            origin: Vec2::new(left, top),
1549            extent: Extent2D::new(width as u32, height as u32),
1550        })
1551    }
1552
1553    /// Push the layer frame, and cut the backdrop out if one was asked for.
1554    ///
1555    /// Returns what the layer's target must be seeded with once its size is
1556    /// settled, which is why this is separate from the seeding: a bounded layer
1557    /// does not know its own target until after the frame exists, and the seed
1558    /// has to land in the target the content will draw into.
1559    fn open_layer(
1560        &mut self,
1561        layer: Layer,
1562        filter: Option<ImageFilter>,
1563        backdrop: Option<&ImageFilter>,
1564        region: Option<Target>,
1565    ) -> Result<Option<Backdrop>> {
1566        let parent = self.target;
1567        // Where the backdrop's filter writes, which is the target the layer is
1568        // about to take rather than the parent it is captured from. `None` is
1569        // the caller that has not worked one out yet, and gets the parent.
1570        let region = region.unwrap_or(parent);
1571        // The layer's own sigma is the `Copy`-friendly spelling of the same
1572        // thing, so it becomes a filter here and there is one path below.
1573        let asked = match backdrop {
1574            Some(filter) => filter.clone(),
1575            None if layer.backdrop_blur > 0.0 => ImageFilter::Blur {
1576                sigma_x: layer.backdrop_blur,
1577                sigma_y: layer.backdrop_blur,
1578            },
1579            None => ImageFilter::None,
1580        };
1581        let filtered = if asked.is_identity() {
1582            None
1583        } else {
1584            // The frame is pushed whether or not this succeeds: a layer that
1585            // fails to filter its backdrop still has to be a layer, or the
1586            // `restore` the caller has already written closes something else.
1587            match self.backdrop_pass(layer.backdrop_id, parent, region, &asked) {
1588                Ok(pending) => Some(pending),
1589                Err(e) => {
1590                    self.push_layer_frame(layer, filter);
1591                    return Err(e);
1592                }
1593            }
1594        };
1595        self.push_layer_frame(layer, filter);
1596        Ok(filtered)
1597    }
1598
1599    /// The pass a backdrop-filtered layer starts from, and what it is an image
1600    /// of.
1601    ///
1602    /// Without an id this is the whole of what a backdrop filter was: cut the
1603    /// target's content out into a pass, and filter that. With one, both halves
1604    /// are shared with whatever named the id before -- the capture always, so
1605    /// that the second layer filters what was behind the first rather than the
1606    /// first's own result, and the filtered pass as well when the filter
1607    /// matches one already run over that capture.
1608    ///
1609    /// The second element is the target the pass covers, which is `parent` for
1610    /// a fresh capture and whatever the id captured for a shared one. Seeding
1611    /// needs it to place the image, and the two can differ; see
1612    /// [`BackdropShare::captured_from`].
1613    ///
1614    /// The cut happens before the filter can refuse, which is the order the
1615    /// no-id path always had. A refusal therefore leaves the capture recorded,
1616    /// which is right: the backdrop was captured, and the next layer naming the
1617    /// id should filter that same image whether or not this one could.
1618    fn backdrop_pass(
1619        &mut self,
1620        id: Option<i64>,
1621        parent: Target,
1622        region: Target,
1623        filter: &ImageFilter,
1624    ) -> Result<Backdrop> {
1625        // A matrix is peeled off here rather than run as a pass. Every other
1626        // filter reads its input where the fragment is and writes there, so it
1627        // is a pass over a target the size of what it was handed; a matrix
1628        // moves the image instead, and a pass for it would need a target sized
1629        // for where the content went. It does not need one: what a backdrop is
1630        // seeded into is the layer's own target, already allocated and the same
1631        // size whatever the matrix says, and the seed is a draw with a mapping
1632        // -- so the matrix goes into that mapping and no pass is spent.
1633        //
1634        // Only at the top. Inside a composition the matrix is between two
1635        // filters that do run as passes, so its result is what the outer one
1636        // reads and there is no seed to fold it into; that stays refused, in
1637        // `filter_passes`.
1638        let (filter, moved_by) = match filter {
1639            ImageFilter::Matrix { transform } => {
1640                let Some(affine) = transform.to_affine() else {
1641                    return Err(Error::Unsupported(
1642                        "a projective matrix is not available as a backdrop filter; \
1643                         it maps the image through a division this seed does not do",
1644                    ));
1645                };
1646                if affine.matrix2.determinant() == 0.0 {
1647                    return Err(Error::Unsupported(
1648                        "a singular matrix is not available as a backdrop filter; \
1649                         it collapses the image and nothing says what it read",
1650                    ));
1651                }
1652                (&ImageFilter::None, Some(affine))
1653            }
1654            other => (other, None),
1655        };
1656        let Some(id) = id else {
1657            // The capture is the pass being cut, so it is the parent's size
1658            // whatever happens next. What the filter *writes* need not be: its
1659            // passes read the whole capture and only have to cover the region
1660            // the layer will hold, which is where the seed reads from.
1661            let cut = self.cut_pass();
1662            let pass = self.filter_passes(cut, region, filter, BlurBasis::of(self.transform))?;
1663            return Ok(Backdrop {
1664                pass,
1665                // Unless the filter had nothing to do -- a matrix backdrop
1666                // moves the capture rather than recomputing it, and hands the
1667                // cut straight back, which is the parent's region and not this
1668                // one.
1669                of: if pass == cut { parent } else { region },
1670                moved_by,
1671            });
1672        };
1673
1674        // Read out by value first: what follows needs `&mut self`, and the
1675        // three things wanted from the entry are all `Copy`.
1676        let existing = self.backdrops.get(&id).map(|share| {
1677            let already = share
1678                .filtered
1679                .iter()
1680                .find(|(known, _)| known == filter)
1681                .map(|(_, pass)| *pass);
1682            (share.captured_from, share.captured, already)
1683        });
1684
1685        let (over, captured) = match existing {
1686            // A filter is a function of its input, so the same filter over the
1687            // same capture is the same image and there is nothing to compute.
1688            Some((from, _, Some(pass))) => {
1689                return Ok(Backdrop {
1690                    pass,
1691                    of: from,
1692                    moved_by,
1693                })
1694            }
1695            Some((from, captured, None)) => (from, captured),
1696            None => {
1697                let cut = self.cut_pass();
1698                self.backdrops.insert(
1699                    id,
1700                    BackdropShare {
1701                        captured_from: parent,
1702                        captured: cut,
1703                        filtered: Vec::new(),
1704                    },
1705                );
1706                (parent, cut)
1707            }
1708        };
1709
1710        let pass = self.filter_passes(captured, over, filter, BlurBasis::of(self.transform))?;
1711        if let Some(share) = self.backdrops.get_mut(&id) {
1712            share.filtered.push((filter.clone(), pass));
1713        }
1714        Ok(Backdrop {
1715            pass,
1716            of: over,
1717            moved_by,
1718        })
1719    }
1720
1721    /// End the current target's pass here, and answer which pass now holds it.
1722    ///
1723    /// The architecture's rule is that a pass cannot sample the attachment it
1724    /// is writing, which is exactly what a backdrop filter asks for. So the
1725    /// pass stops: everything drawn into this target so far becomes a pass of
1726    /// its own, and what follows begins by drawing that pass back in.
1727    ///
1728    /// The redraw is the cost, one full-target copy per backdrop filter, and it
1729    /// is not avoidable without the machinery the rule exists for the absence
1730    /// of. It is a `Src` blit covering the whole target, so it reproduces what
1731    /// was cut exactly rather than compositing with it.
1732    fn cut_pass(&mut self) -> usize {
1733        let target = self.target;
1734        let batch = std::mem::take(&mut self.batch);
1735        let sources = std::mem::take(&mut self.sources);
1736        // The clear belongs to the half that starts from nothing. A layer's
1737        // target clears to transparent and the frame's to its background; after
1738        // the cut neither clears again, since the redraw below covers every
1739        // pixel with `Src`.
1740        let clear = if self.in_layer() {
1741            Some([0.0; 4])
1742        } else {
1743            self.background.take().map(|c| c.to_array())
1744        };
1745        self.finished.push(Pass {
1746            batch,
1747            descriptor: PassDescriptor {
1748                clear,
1749                samples: self.pass_samples(),
1750                viewport: None,
1751            },
1752            sources,
1753            extent: target.extent,
1754        });
1755        // The batch that continues is empty, so it inherits nothing: what
1756        // follows the cut is a full-target `Src` blit, which no sample count
1757        // changes, plus whatever is drawn next. A draw that wants
1758        // multisampling will say so again.
1759        self.anti_alias = false;
1760        let index = self.finished.len() - 1;
1761        self.draw_whole_pass(index, target, target, BlendMode::Src, None);
1762        self.rebuild_stencil_clips();
1763        index
1764    }
1765
1766    /// Make the stencil clips again, in the batch that follows a cut.
1767    ///
1768    /// A stencil clip is a draw and a pass owns its stencil attachment, so
1769    /// cutting the pass throws the clip state away: the redraw restores the
1770    /// color and nothing restores the stencil, leaving every content draw after
1771    /// the cut testing for a depth no pixel holds. Which is to say it draws
1772    /// nothing, silently and everywhere.
1773    ///
1774    /// The narrowings are made again in the order they were made the first
1775    /// time, at the depths they were made at -- which they still are, since
1776    /// `clips` holds exactly those in force and each was made at its own index.
1777    /// A failure to build one is dropped rather than propagated: the shape
1778    /// tessellated once already to get here, and there is no result to fail
1779    /// into halfway through a cut.
1780    fn rebuild_stencil_clips(&mut self) {
1781        for depth in 0..self.clips.len() {
1782            let clip = self.clips[depth].clone();
1783            let paint = RenderPaint {
1784                material: Material::solid([1.0, 1.0, 1.0, 1.0]),
1785                filter: ColorFilter::None,
1786                blend: BlendMode::Src,
1787                clip: clip.scissor,
1788                stencil: ClipState::narrow(depth as u32),
1789            };
1790            let _ = self
1791                .renderer
1792                .fill_into(&mut self.batch, &clip.path, clip.transform, &paint);
1793        }
1794    }
1795
1796    /// Whether a layer is open, which decides what a cut pass clears to.
1797    fn in_layer(&self) -> bool {
1798        self.stack.iter().any(|state| state.layer.is_some())
1799    }
1800
1801    /// Draw a finished pass, sized for `source`, across the whole of `into`.
1802    ///
1803    /// `into` is a rectangle of `source`: the same target when a pass is being
1804    /// redrawn after a cut, and a sub-rectangle of it when a bounded layer is
1805    /// seeded with a backdrop. The mapping is the inverse of the one a layer
1806    /// composite uses, and reduces to the full-texture mapping when the two are
1807    /// the same size, which is what makes the unbounded case share this code.
1808    fn draw_whole_pass(
1809        &mut self,
1810        pass: usize,
1811        source: Target,
1812        into: Target,
1813        blend: BlendMode,
1814        moved_by: Option<Affine2>,
1815    ) {
1816        let slot = self.slot_for(TextureSource::Layer(pass));
1817        let offset = into.origin - source.origin;
1818        let (iw, ih) = (into.extent.width as f32, into.extent.height as f32);
1819        let (sw, sh) = (source.extent.width as f32, source.extent.height as f32);
1820        // Stated in the direction the shader reads it -- clip space to the
1821        // texture -- because the sizes are known and there is no placement to
1822        // invert. The anchor rides inside it like every other paint's.
1823        let anchor = Vec2::new(-1.0 - 2.0 * offset.x / iw, 1.0 + 2.0 * offset.y / ih);
1824        let scale = Mat2::from_diagonal(Vec2::new(0.5 * iw / sw, -0.5 * ih / sh));
1825        let clip_to_texture = Affine2::from_mat2(scale) * Affine2::from_translation(-anchor);
1826        // A matrix moves the image, so the fragment at a device point reads
1827        // where that point came *from*: the mapping gains the inverse, stated
1828        // in device pixels, between the texture coordinates and the device
1829        // position they name. `texels` is that position from a normalized
1830        // coordinate and `normalized` is its inverse, so with no matrix the
1831        // three cancel and this is the mapping above unchanged.
1832        let clip_to_texture = match moved_by {
1833            None => clip_to_texture,
1834            Some(matrix) => {
1835                let texels = Affine2::from_translation(source.origin)
1836                    * Affine2::from_scale(Vec2::new(sw, sh));
1837                let normalized = texels.inverse();
1838                normalized * matrix.inverse() * texels * clip_to_texture
1839            }
1840        };
1841        let material = Material::Image {
1842            to_local: to_local_columns(Transform2D::from(clip_to_texture)),
1843            slot,
1844            alpha: 1.0,
1845            // Clamped when the image stays where it was, which is every case
1846            // but one: the mapping then covers exactly the texture and there is
1847            // nothing outside it to decide about. A matrix leaves parts of the
1848            // target naming no texel, and those have to come out transparent so
1849            // what is underneath shows -- smearing the edge across them is what
1850            // a clamp would do and is not what a filter that moved an image
1851            // says about where the image is not.
1852            tile: if moved_by.is_some() {
1853                TileMode::Decal
1854            } else {
1855                TileMode::Clamp
1856            },
1857            // A layer is composited at its own size, so a texel lands on a
1858            // pixel and the filter has nothing to blend. Linear anyway, which
1859            // is what a target scaled by a resize would want.
1860            sampling: Sampling::Linear,
1861            source: [0.0, 0.0, 1.0, 1.0],
1862            tint: [1.0, 1.0, 1.0, 1.0],
1863        };
1864        let paint = RenderPaint {
1865            material,
1866            filter: ColorFilter::None,
1867            blend,
1868            // Neither clipped nor stencilled: this is the target's own content
1869            // being restored or seeded, not something the caller drew, and a
1870            // clip in force belongs to what comes after it.
1871            clip: None,
1872            stencil: ClipState::UNCLIPPED,
1873        };
1874        let whole = into.path();
1875        let _ = self
1876            .renderer
1877            .fill_into(&mut self.batch, &whole, Affine2::IDENTITY, &paint);
1878    }
1879
1880    /// Put the filtered backdrop under the layer's own content.
1881    ///
1882    /// `Src`, because it is the layer's starting image rather than something
1883    /// composited onto it, and the layer's target was cleared to transparent.
1884    fn seed_backdrop(&mut self, pending: Option<Backdrop>) {
1885        let Some(Backdrop {
1886            pass,
1887            of: parent,
1888            moved_by,
1889        }) = pending
1890        else {
1891            return;
1892        };
1893        let into = self.target;
1894        self.draw_whole_pass(pass, parent, into, BlendMode::Src, moved_by);
1895    }
1896
1897    fn push_layer_frame(&mut self, layer: Layer, filter: Option<ImageFilter>) {
1898        self.stack.push(SavedState {
1899            transform: self.transform,
1900            clip: self.clip,
1901            clip_bounds: self.clip_bounds,
1902            depth: self.depth,
1903            clips: self.clips.len(),
1904            layer: Some(LayerFrame {
1905                batch: std::mem::take(&mut self.batch),
1906                sources: std::mem::take(&mut self.sources),
1907                clips: std::mem::take(&mut self.clips),
1908                paint: layer,
1909                filter,
1910                parent: self.target,
1911                anti_alias: std::mem::take(&mut self.anti_alias),
1912                blur_basis: BlurBasis::of(self.transform),
1913            }),
1914        });
1915        self.clip = None;
1916        self.depth = 0;
1917    }
1918
1919    /// Open a layer that only needs to cover `bounds`.
1920    ///
1921    /// `bounds` is in the current user space, and is the caller's promise that
1922    /// nothing drawn in the layer matters outside it. The layer gets a target
1923    /// the size of that region rather than the size of the frame, which is the
1924    /// whole reason to state it: a layer over a tenth of the frame then costs a
1925    /// tenth of the memory and a tenth of the fill, and a frame that opens
1926    /// several stops being dominated by full-frame allocations it does not use.
1927    ///
1928    /// The promise is enforced by construction rather than trusted. Content
1929    /// outside the region is clipped away by the target's own edges, so a
1930    /// caller who understates the bounds sees the drawing cut off — visible and
1931    /// attributable, rather than reading stale memory.
1932    ///
1933    /// The region is taken to device pixels and rounded outward to whole ones,
1934    /// so a fractional bound never costs coverage at the edge, and is narrowed
1935    /// to what the enclosing target can show, since a layer larger than that
1936    /// renders pixels nothing can sample. A region that comes out empty falls
1937    /// back to a full-size layer, that being a case where a smaller target
1938    /// would be a guess and guessing wrong loses drawing.
1939    ///
1940    /// A layer that filters its backdrop is the exception to all of this: for
1941    /// it the bounds are not an optimization but the region filtered, so
1942    /// omitting them filters the whole target rather than merely allocating
1943    /// more of one. See [`Layer::backdrop_blur`].
1944    ///
1945    /// Under a rotation the region becomes a quadrilateral, and the target is
1946    /// the box around it. That covers more than the caller promised, which is
1947    /// the safe direction: a target is an allocation rather than a clip the
1948    /// caller can observe, so over-covering costs a little memory where
1949    /// under-covering would lose drawing. This is the opposite of what
1950    /// [`Self::clip_rect`] does with the same box, and for the same reason —
1951    /// there the box would admit pixels the caller asked to remove.
1952    pub fn save_layer_bounds(&mut self, layer: Layer, bounds: Rect) -> &mut Self {
1953        let (min, max) = transformed_bounds(
1954            self.transform,
1955            Vec2::new(bounds.left, bounds.top),
1956            Vec2::new(bounds.right, bounds.bottom),
1957        )
1958        .unwrap_or_else(unbounded);
1959        self.save_layer_device_bounds(layer, min, max)
1960    }
1961
1962    /// A layer filtered as a whole by any image filter.
1963    ///
1964    /// [`Layer`]'s own fields carry the four kinds that fit in a `Copy` struct
1965    /// -- a blur, a morphology, a matrix and a color filter -- and apply them
1966    /// in one fixed order. This takes an [`ImageFilter`], which adds the two
1967    /// they cannot say: a caller's fragment program, and a composition in
1968    /// whichever order the caller wrote it.
1969    ///
1970    /// The same operation `dart:ui` spells as an `imageFilter` on the paint
1971    /// handed to `saveLayer`, and the group counterpart of what
1972    /// `Paint::with_image_filter` already does for one draw.
1973    ///
1974    /// Refuses a matrix, which is a redirection rather than an absence: a
1975    /// group's finished image is moved by `Layer::with_matrix`, which states
1976    /// the same thing and is what `peel` turns this filter into everywhere
1977    /// else. Refused here rather than at `restore`, because a `restore` has no
1978    /// result to fail into and by then the caller has drawn into the layer.
1979    ///
1980    /// A *backdrop's* matrix is not refused; see `backdrop_pass`. The two are
1981    /// different questions -- a group's own image is composited by machinery
1982    /// that already takes a matrix, and a backdrop's is seeded by a draw that
1983    /// now does.
1984    pub fn save_layer_filtered(
1985        &mut self,
1986        layer: Layer,
1987        bounds: Option<Rect>,
1988        filter: &ImageFilter,
1989    ) -> Result<&mut Self> {
1990        if matches!(filter, ImageFilter::Matrix { .. }) {
1991            return Err(Error::Unsupported(
1992                "a matrix filters a group through `Layer::with_matrix`, which \
1993                 states where the finished image goes",
1994            ));
1995        }
1996        let (min, max) = match bounds {
1997            Some(bounds) => transformed_bounds(
1998                self.transform,
1999                Vec2::new(bounds.left, bounds.top),
2000                Vec2::new(bounds.right, bounds.bottom),
2001            )
2002            .unwrap_or_else(unbounded),
2003            None => unbounded(),
2004        };
2005        self.save_layer_device_bounds_running(layer, min, max, Some(filter.clone()), None)
2006    }
2007
2008    /// A layer over a backdrop filtered by any image filter.
2009    ///
2010    /// [`Layer::backdrop_blur`] is the same operation with the one filter a
2011    /// `Copy` layer can hold, and is the spelling to reach for when a blur is
2012    /// what is wanted. This takes the rest: a color filter, a morphology, a
2013    /// caller's fragment program, or a composition of them -- which is what
2014    /// `SceneBuilder.pushBackdropFilter` accepts and what upstream's
2015    /// `SaveLayer` takes a `DlImageFilter` for.
2016    ///
2017    /// The bounds are the filtered region rather than an optimization, for the
2018    /// reason [`Layer::backdrop_blur`] gives: a frosted panel is a bounded
2019    /// layer, and the same layer unbounded filters the whole frame. Both are
2020    /// meaningful and they are different pictures.
2021    ///
2022    /// Refuses a matrix, which is the one kind that moves the image rather than
2023    /// recomputing it in place. See `filter_passes`.
2024    pub fn save_layer_backdrop(
2025        &mut self,
2026        layer: Layer,
2027        bounds: Option<Rect>,
2028        backdrop: &ImageFilter,
2029    ) -> Result<&mut Self> {
2030        let (min, max) = match bounds {
2031            Some(bounds) => transformed_bounds(
2032                self.transform,
2033                Vec2::new(bounds.left, bounds.top),
2034                Vec2::new(bounds.right, bounds.bottom),
2035            )
2036            .unwrap_or_else(unbounded),
2037            None => unbounded(),
2038        };
2039        self.save_layer_device_bounds_running(layer, min, max, None, Some(backdrop))
2040    }
2041
2042    /// A bounded layer whose region is already in device pixels.
2043    ///
2044    /// The same thing [`Self::save_layer_bounds`] does, minus the transform.
2045    /// Every filter's reach is a length in device pixels, so a caller that has
2046    /// already worked out where a filtered result will land -- which composing
2047    /// two filters requires, since the inner one's growth has to fit inside the
2048    /// outer one's target -- has device coordinates in hand and would only be
2049    /// mapping them backward to have them mapped forward again. Under a scale
2050    /// the round trip is not the identity: a reach of eight device pixels
2051    /// divided by the scale and multiplied by it again is eight only if nothing
2052    /// rounds, and the bounds are floored and ceiled at the end.
2053    fn save_layer_device_bounds(&mut self, layer: Layer, min: Vec2, max: Vec2) -> &mut Self {
2054        // Cannot fail: only a backdrop filter can refuse, and this passes none.
2055        let _ = self.save_layer_device_bounds_running(layer, min, max, None, None);
2056        self
2057    }
2058
2059    /// The same, with a caller's program to run over the finished layer.
2060    ///
2061    /// Separate rather than a defaulted argument because every caller but the
2062    /// three filtered draws passes `None`, and a parameter that is almost
2063    /// always one value reads as though it were sometimes the other.
2064    fn save_layer_device_bounds_running(
2065        &mut self,
2066        layer: Layer,
2067        min: Vec2,
2068        max: Vec2,
2069        filter: Option<ImageFilter>,
2070        backdrop: Option<&ImageFilter>,
2071    ) -> Result<&mut Self> {
2072        // A caller's sigma is in the space they were drawing in, which is what
2073        // `dart:ui` means by it and what upstream honors -- its
2074        // `GaussianBlurFilterContents` scales by `effect_transform.Basis()`,
2075        // and the backdrop path hands it `transform.Basis()`. Everything below
2076        // this line works in device pixels: the reach is applied to bounds
2077        // already transformed, and the passes that do the blurring run on a
2078        // target. So the conversion happens once, here, where the transform
2079        // that decides it is still the one the caller drew under.
2080        let basis = BlurBasis::of(self.transform);
2081        let layer = layer.scaled_by(max_scale_of(self.transform));
2082        let reach = layer.reach(basis);
2083        // A layer opened under a clip cannot draw outside it, and neither can
2084        // the draw that composites it, so an unbounded one does not need a
2085        // target the size of its parent. A frosted panel is the case that
2086        // pays: the clip is the panel and the parent is the frame, and the
2087        // capture, the blur's passes and the intermediate were all frame-sized
2088        // for a panel a fortieth of the area.
2089        //
2090        // Only where a backdrop is being captured, and only for a layer that
2091        // shares no key and has no matrix. Each of those wants a region this
2092        // does not know how to compute: a shared capture has to cover every
2093        // group naming the id -- which is upstream's coverage union and is not
2094        // this -- a matrix records its pre-image, which is where the content
2095        // will come *from* rather than where the clip is, and a destructive
2096        // group naming the id -- which is upstream's coverage union and is not
2097        // this -- and a matrix records its pre-image, which is where the
2098        // content will come *from* rather than where the clip is.
2099        //
2100        // A destructive mode is not excluded, and the difference from
2101        // `narrowed` is the whole of why. That one shrinks to the layer's
2102        // *content*, which such a mode reaches past -- it writes where the
2103        // source drew nothing. This shrinks to the clip, which is the region
2104        // the composite can touch at all, so there is nothing outside it left
2105        // to affect.
2106        let captures_backdrop = backdrop.is_some() || layer.backdrop_blur > 0.0;
2107        let (min, max) = if captures_backdrop && layer.matrix.is_none() {
2108            let clip = self.clip_bounds;
2109            (
2110                Vec2::new(min.x.max(clip.left), min.y.max(clip.top)),
2111                Vec2::new(max.x.min(clip.right), max.y.min(clip.bottom)),
2112            )
2113        } else {
2114            (min, max)
2115        };
2116        // A filter given to the layer as a whole reaches past what it is handed
2117        // too, and by how much only the filter knows. Asked here rather than
2118        // left to the narrowing below, because the narrowing only runs for a
2119        // layer that was *not* given bounds: a caller who states bounds gets
2120        // the target those bounds describe, and a filter's spread has to be
2121        // added to it or the spread is cut off at the bound.
2122        //
2123        // Missing this made the two spellings of a layer blur disagree. A sigma
2124        // on the layer went through `reach` above and spread ten pixels past a
2125        // stated bound; the same blur handed over as the layer's filter stopped
2126        // dead at it. They are the same picture and the same comment two
2127        // functions up says so.
2128        let filter_reach = filter.as_ref().map(|filter| {
2129            let unit = Vec2::ZERO;
2130            let (min, max) = filter.covering(unit, unit, &|sigma| basis.reach(sigma));
2131            (-min).max(max)
2132        });
2133        // A blur reaches past what it was given. The caller states where the
2134        // content is, which is the question they can answer; how far a blur
2135        // carries it is this renderer's arithmetic, and a target sized to the
2136        // content alone would cut the halo off square at the bound -- the
2137        // failure looking exactly like a shadow with a straight edge.
2138        //
2139        // The kernel's own radius, `ceil((sigma - 0.5) * sqrt(3))`, which is
2140        // where the shader stops taking taps -- so the target covers every
2141        // texel the blur will actually read and no more. Nearer one and three
2142        // quarter deviations than three: the truncation is upstream's
2143        // `kKernelRadiusPerSigma`, and `blur_reach` is the one place it is
2144        // stated.
2145        let (min, max) = (min - reach, max + reach);
2146        // After the layer's own blur and morphology, because that is the order
2147        // `finish_layer` applies them in: the filter sees what those produced.
2148        let (min, max) = match filter_reach {
2149            Some(spread) => (min - spread, max + spread),
2150            None => (min, max),
2151        };
2152        let parent = self.target;
2153        let left = min.x.floor().max(parent.origin.x);
2154        let top = min.y.floor().max(parent.origin.y);
2155        let right = max
2156            .x
2157            .ceil()
2158            .min(parent.origin.x + parent.extent.width as f32);
2159        let bottom = max
2160            .y
2161            .ceil()
2162            .min(parent.origin.y + parent.extent.height as f32);
2163        // Worked out before the layer is opened, because opening it is what
2164        // builds the backdrop's passes and those want to know where they are
2165        // writing. The layer is still opened without *seeding*: the seed has to
2166        // land in the target the content will draw into, so it waits until that
2167        // target is the one aimed at.
2168        let narrowed = (right > left && bottom > top).then(|| Target {
2169            origin: Vec2::new(left, top),
2170            extent: Extent2D::new((right - left) as u32, (bottom - top) as u32),
2171        });
2172        let pending = self.open_layer(layer, filter, backdrop, narrowed)?;
2173        let Some(target) = narrowed else {
2174            // Nothing survived the clamp, so the layer keeps the full-size
2175            // target it was opened with and the backdrop is seeded across that.
2176            self.seed_backdrop(pending);
2177            return Ok(self);
2178        };
2179        self.aim_at(target);
2180        self.seed_backdrop(pending);
2181        Ok(self)
2182    }
2183
2184    /// How many layers are open, for a caller checking its own balance.
2185    pub fn layer_depth(&self) -> usize {
2186        self.stack.iter().filter(|s| s.layer.is_some()).count()
2187    }
2188
2189    /// The slot a source occupies in the pass being recorded, adding it if new.
2190    ///
2191    /// Slots are per pass rather than global, because each pass carries its own
2192    /// table. Deduplicating means a recording that draws the same image twenty
2193    /// times binds one texture rather than twenty.
2194    /// The slot holding this gradient's colors, when they cannot be carried.
2195    ///
2196    /// `None` for the ordinary case, which keeps the stops in push constants
2197    /// and costs no texture, no upload and no binding. Only a gradient with
2198    /// more stops than a material can hold pays for a ramp, and it pays once
2199    /// per distinct gradient rather than once per draw.
2200    fn ramp_for(&mut self, stops: &[GradientStop]) -> Option<u32> {
2201        (stops.len() > MAX_STOPS).then(|| self.ramp_slot(stops))
2202    }
2203
2204    /// Tabulate a gradient's colors and return the slot holding them.
2205    ///
2206    /// Called only where the stops outnumber what a material can carry. The
2207    /// ramp is deduplicated across the recording by its contents, because the
2208    /// same gradient drawn twice is one texture, and a frame that paints a
2209    /// hundred list rows with one theme gradient should upload it once.
2210    fn ramp_slot(&mut self, stops: &[GradientStop]) -> u32 {
2211        let ramp = Ramp::bake(stops);
2212        let index = match self.ramps.iter().position(|r| *r == ramp) {
2213            Some(index) => index,
2214            None => {
2215                self.ramps.push(ramp);
2216                self.ramps.len() - 1
2217            }
2218        };
2219        self.slot_for(TextureSource::Ramp(index))
2220    }
2221
2222    fn slot_for(&mut self, source: TextureSource) -> u32 {
2223        if let Some(index) = self.sources.iter().position(|s| *s == source) {
2224            return index as u32;
2225        }
2226        self.sources.push(source);
2227        (self.sources.len() - 1) as u32
2228    }
2229
2230    /// Return to the most recently saved transform and clip.
2231    ///
2232    /// Restoring without a matching save leaves both alone rather than
2233    /// panicking: an unbalanced pair is a caller bug, but taking down a frame
2234    /// loop for it is worse than continuing with what is already correct.
2235    pub fn restore(&mut self) -> &mut Self {
2236        let Some(previous) = self.stack.pop() else {
2237            return self;
2238        };
2239        self.transform = previous.transform;
2240        self.clip = previous.clip;
2241        self.clip_bounds = previous.clip_bounds;
2242
2243        if let Some(frame) = previous.layer {
2244            // The clip and stencil are restored before the composite is
2245            // recorded, so the layer arrives subject to what was in force when
2246            // it was opened rather than to whatever it did inside itself.
2247            self.depth = previous.depth;
2248            self.clips = frame.clips.clone();
2249            self.finish_layer(frame);
2250            return self;
2251        }
2252
2253        // Everything this save added to the list goes with it. The widening
2254        // draws below undo the same clips on the device; this is the record of
2255        // them a later pass cut would rebuild from.
2256        self.clips.truncate(previous.clips);
2257
2258        // A scissor is state the recorder holds, so restoring it is an
2259        // assignment. A stencil clip lives in a buffer on the device, so
2260        // restoring it is a draw: one covering the whole target per level being
2261        // left, each stepping back the pixels that reached that level.
2262        //
2263        // Deliberately unscissored. The step back lands exactly where the
2264        // matching narrowing landed, because that is where the stencil holds
2265        // the value being tested for -- so it needs no help from a scissor, and
2266        // asking it to agree with one that has already been restored to
2267        // something else would be a way to get it wrong.
2268        if self.depth > previous.depth {
2269            // Stated in device pixels, so it goes through the identity rather
2270            // than whatever transform happens to be in force.
2271            let whole = self.target.path();
2272            while self.depth > previous.depth {
2273                let paint = RenderPaint {
2274                    material: Material::solid([1.0, 1.0, 1.0, 1.0]),
2275                    filter: ColorFilter::None,
2276                    blend: BlendMode::Src,
2277                    clip: None,
2278                    stencil: ClipState::widen(self.depth),
2279                };
2280                let _ = self
2281                    .renderer
2282                    .fill_into(&mut self.batch, &whole, Affine2::IDENTITY, &paint);
2283                self.depth -= 1;
2284            }
2285        }
2286        self
2287    }
2288
2289    /// How many saves are outstanding, for a caller checking its own balance.
2290    pub fn save_depth(&self) -> usize {
2291        self.stack.len()
2292    }
2293
2294    pub fn translate(&mut self, x: f32, y: f32) -> &mut Self {
2295        self.concat(Affine2::from_translation(Vec2::new(x, y)))
2296    }
2297
2298    pub fn scale(&mut self, x: f32, y: f32) -> &mut Self {
2299        self.concat(Affine2::from_scale(Vec2::new(x, y)))
2300    }
2301
2302    /// Rotate by an angle in radians.
2303    pub fn rotate(&mut self, radians: f32) -> &mut Self {
2304        self.concat(Affine2::from_angle(radians))
2305    }
2306
2307    /// Apply an arbitrary transform on top of the current one.
2308    ///
2309    /// Takes anything that is a transform of the plane, which is an `Affine2`
2310    /// for almost every caller and a [`Transform2D`] for one that wants
2311    /// perspective. Widened rather than replaced: nobody should have to build a
2312    /// three-by-three to move something ten pixels to the right.
2313    pub fn concat(&mut self, transform: impl Into<Transform2D>) -> &mut Self {
2314        self.transform = self.transform * transform.into();
2315        self
2316    }
2317
2318    /// Apply a `dart:ui` four-by-four, which is column-major and sixteen long.
2319    ///
2320    /// The entry point for perspective, and the one `dart:ui` states: its
2321    /// `Canvas.transform` takes a matrix of this shape. Named apart from
2322    /// [`Self::transform`], which reports the current one and cannot share a
2323    /// name with a method that changes it.
2324    ///
2325    /// The reduction to a three-by-three is exact rather than a narrowing.
2326    /// Everything drawn here lies on the plane where `z` is zero, and a
2327    /// four-by-four applied to such a point never reads its `z` column; the `z`
2328    /// row yields a depth with nothing to do here. What is left -- rows and
2329    /// columns zero, one and three -- is the whole of what the matrix means on
2330    /// the plane. A caller whose matrix depends on `z` is getting the part of
2331    /// it that acts on what they are drawing, which is all of it.
2332    pub fn concat_4x4(&mut self, matrix: &[f32; 16]) -> &mut Self {
2333        self.concat(Transform2D::from_column_major_4x4(matrix))
2334    }
2335
2336    pub fn draw_path(&mut self, path: &Path, paint: &Paint) -> Result<&mut Self> {
2337        if !paint.is_visible() || path.is_empty() {
2338            return Ok(self);
2339        }
2340        // Before the mask blur, so the two compose in the order they are
2341        // defined in: an image filter acts on what was drawn, and what was
2342        // drawn is whatever the mask blur produced.
2343        if !paint.image_filter.is_identity() {
2344            return self.draw_filtered(path, paint);
2345        }
2346        // A rounded rectangle that still knows it is one can have its mask blur
2347        // evaluated rather than run as passes. This is where a shadow arrives:
2348        // `draw_shadow` takes a path, as `dart:ui`'s `drawShadow` does, so
2349        // without the shape travelling with the outline every shadow would take
2350        // the general route however round it is.
2351        if let Some((bounds, radius)) = path.as_rounded_rect() {
2352            let rect = Rect::new(bounds.min.x, bounds.min.y, bounds.max.x, bounds.max.y);
2353            if let Some((material, pad, drawn)) = self.analytic_rrect_blur_draw(rect, radius, paint)
2354            {
2355                return self.draw_analytic(rect.outset(pad), material, &drawn);
2356            }
2357        }
2358        // A color filter is normally arithmetic in this renderer's own fragment
2359        // shader, which a runtime effect replaces outright -- there is nowhere
2360        // in a caller's program to put the matrix, and the program is what runs.
2361        // Left alone, the filter was accepted and silently did nothing, which
2362        // is the shape of failure this codebase least wants: no error, no
2363        // effect, and a caller with no way to tell.
2364        //
2365        // So the filter is applied to the image the program drew instead, which
2366        // is what it means anyway. It costs a layer, which is what every other
2367        // filter that acts on a finished image already costs.
2368        if matches!(paint.shader, Shader::RuntimeEffect { .. }) && !paint.color_filter.is_identity()
2369        {
2370            return self.draw_effect_filtered(path, paint);
2371        }
2372        if paint.mask_blur > 0.0 {
2373            return self.draw_masked(Masked::Path(path), paint);
2374        }
2375        if paint.anti_alias {
2376            self.anti_alias = true;
2377        }
2378
2379        // A clip narrowed to nothing means this shape cannot reach any pixel.
2380        // Tessellating it to find that out is wasted work, and it is a normal
2381        // state rather than an error: a subtree scrolled out of view is clipped
2382        // away every frame it stays there.
2383        if self.clip.is_some_and(Scissor::is_empty) {
2384            return Ok(self);
2385        }
2386
2387        // Resolved before the clip is read, since it may add a slot to this
2388        // pass's table.
2389        let material = self.material_for(&paint.shader);
2390        // A stroke too thin to cover a pixel is widened to one and dimmed to
2391        // pay for it. See `thin_stroke`; this is the tessellated route, where
2392        // the quantization it exists for is at its worst -- coverage comes
2393        // from a multisample buffer, so a sub-pixel line has four coverages
2394        // available to it and one of them is none.
2395        let (style, coverage) = match &paint.style {
2396            Style::Stroke(stroke) => {
2397                let (width, coverage) =
2398                    thin_stroke(self.transform, stroke.width, Zero::IsAHairline);
2399                (Style::Stroke(stroke.with_width(width)), coverage)
2400            }
2401            other => (*other, 1.0),
2402        };
2403        let render_paint = RenderPaint {
2404            material: material.with_opacity(coverage),
2405            filter: paint.color_filter,
2406            blend: paint.blend,
2407            clip: self.clip,
2408            stencil: ClipState::content(self.depth),
2409        };
2410        match &style {
2411            Style::Fill => {
2412                self.renderer
2413                    .fill_into(&mut self.batch, path, self.transform, &render_paint)?
2414            }
2415            Style::Stroke(stroke) => self.renderer.stroke_into(
2416                &mut self.batch,
2417                path,
2418                stroke,
2419                paint.dash.as_ref(),
2420                self.transform,
2421                &render_paint,
2422            )?,
2423        }
2424        Ok(self)
2425    }
2426
2427    /// Resolve a shader against the current transform.
2428    ///
2429    /// Gradient endpoints go through the same mapping the geometry does, so a
2430    /// gradient rotates and scales with its shape rather than staying fixed to
2431    /// the screen. Doing it here rather than in the fragment stage means the
2432    /// shader receives clip-space endpoints and needs no transform of its own.
2433    fn material_for(&mut self, shader: &Shader) -> Material {
2434        let to_clip = self.target.projection() * self.transform;
2435        self.material_from(shader, to_clip)
2436    }
2437
2438    /// The same, for a draw whose fragments locate themselves some other way.
2439    ///
2440    /// A mesh that states a coordinate per vertex has already put each one in
2441    /// the caller's own space, so the mapping the fragment stage applies must
2442    /// not carry the geometry's transform as well -- it would be applied twice,
2443    /// once by the vertices and once by the mapping. Passing the identity here
2444    /// leaves exactly the paint's own part: the translation that measures from
2445    /// a gradient's start, the rotation that orients a sweep, and nothing else.
2446    ///
2447    /// The shader then runs the same arithmetic on `(uv, 1)` that it runs on a
2448    /// clip position, which is why there is one path in it rather than two.
2449    fn material_at_texture_coords(&mut self, shader: &Shader) -> Material {
2450        self.material_from(shader, Transform2D::IDENTITY)
2451    }
2452
2453    fn material_from(&mut self, shader: &Shader, to_clip: Transform2D) -> Material {
2454        let stops_of = |stops: &[crate::paint::GradientStop]| -> Vec<Stop> {
2455            stops
2456                .iter()
2457                .map(|s| Stop::new(s.color.to_array(), s.offset))
2458                .collect()
2459        };
2460
2461        match shader {
2462            Shader::Solid(color) => Material::solid(color.to_array()),
2463            // Passed through untouched. Every other material here is resolved
2464            // -- geometry carried into clip space, a radius folded into a
2465            // matrix -- because this renderer's own shader expects it that
2466            // way. A caller's program expects whatever the caller wrote, and
2467            // there is nothing here that could resolve it correctly.
2468            Shader::RuntimeEffect {
2469                program,
2470                uniforms,
2471                images,
2472            } => Material::Runtime {
2473                program: *program,
2474                uniforms: uniforms.clone(),
2475                // Through this pass's own table, like every other texture: a
2476                // layer occupies a slot too, so a caller's index and the
2477                // pass's are not the same number once one is opened.
2478                textures: {
2479                    let mut bound = [None; impeller_hal::MAX_EFFECT_TEXTURES];
2480                    // Past the ceiling the extra names are dropped rather than
2481                    // refused: a program cannot declare a binding the layout
2482                    // does not have, so a caller naming more textures than that
2483                    // has named some the program could not read either.
2484                    for (slot, into) in images.iter().zip(bound.iter_mut()) {
2485                        *into = Some(self.slot_for(TextureSource::Image(*slot)));
2486                    }
2487                    bound
2488                },
2489            },
2490            Shader::Image {
2491                slot,
2492                rect,
2493                alpha,
2494                tile,
2495                source,
2496                tint,
2497                sampling,
2498            } => {
2499                // Texture coordinates run from zero to one across the
2500                // destination rectangle, so the mapping is: undo the transform
2501                // that took user space to clip space, then scale by the
2502                // rectangle's own size. Composing the two here means the shader
2503                // receives one matrix and does no inversion of its own.
2504                //
2505                // Built forwards and inverted once: the unit square of texture
2506                // coordinates is scaled by the rectangle's size, placed at its
2507                // corner, and carried to clip space by the transform in force.
2508                // A degenerate transform has no inverse to take, and the
2509                // identity stands in for it -- unobservable, because the
2510                // geometry went through the same matrix and has no area.
2511                let placement = to_clip
2512                    * Affine2::from_translation(Vec2::new(rect.left, rect.top))
2513                    * Affine2::from_scale(Vec2::new(
2514                        rect.right - rect.left,
2515                        rect.bottom - rect.top,
2516                    ));
2517                Material::Image {
2518                    to_local: invert_to_local(placement),
2519                    sampling: *sampling,
2520                    // The caller's index goes through this pass's own table,
2521                    // because a layer occupies a slot too and the two number
2522                    // independently. A recording that never uses a layer maps
2523                    // them one to one, which is why this is invisible until it
2524                    // is not.
2525                    slot: self.slot_for(TextureSource::Image(*slot)),
2526                    alpha: *alpha,
2527                    source: [source.left, source.top, source.right, source.bottom],
2528                    tint: tint.to_array(),
2529                    tile: *tile,
2530                }
2531            }
2532            Shader::LinearGradient {
2533                start,
2534                end,
2535                stops,
2536                tile,
2537            } => {
2538                let axis = *end - *start;
2539                if !axis.is_finite() || !start.is_finite() {
2540                    return Material::Solid([0.0; 4]);
2541                }
2542                let ramp_slot = self.ramp_for(stops);
2543                Material::LinearGradient {
2544                    axis: [axis.x, axis.y],
2545                    // Maps a clip-space position back into the space the axis is
2546                    // stated in, which is the caller's, measured from the
2547                    // gradient's start. Without it the target's aspect ratio
2548                    // leaks into the gradient's direction.
2549                    to_local: invert_to_local(to_clip * Affine2::from_translation(*start)),
2550                    stops: stops_of(stops),
2551                    tile: *tile,
2552                    ramp: ramp_slot,
2553                }
2554            }
2555            Shader::RadialGradient {
2556                center,
2557                radius,
2558                stops,
2559                tile,
2560            } => {
2561                // Folding the center and the radius into the mapping means the
2562                // shader measures against unit distance from the origin and
2563                // never sees either.
2564                let placement = to_clip
2565                    * Affine2::from_translation(*center)
2566                    * Affine2::from_scale(Vec2::splat(*radius));
2567                if !placement.is_finite() {
2568                    return Material::Solid([0.0; 4]);
2569                }
2570                // A radius of nothing folds to a singular mapping, and the
2571                // inversion below answers a singular matrix with the identity.
2572                // That is the right answer for an inversion and the wrong one
2573                // here: it invents a radius of one clip unit, so a gradient the
2574                // caller asked to have no extent comes out spanning half the
2575                // target and changing with the target's size.
2576                //
2577                // What it should be is the limit of the real thing. As the
2578                // radius shrinks, every point but the center runs off the end
2579                // of the ramp -- so under clamp it settles on the last stop,
2580                // and under decal it leaves, because past the end is where
2581                // decal draws nothing. Repeat and mirror have no limit at all,
2582                // the parameter oscillating faster and faster, and they take
2583                // the same answer as clamp because a stable color is worth
2584                // more than an arbitrary one that shimmers.
2585                //
2586                // The limit rather than a refusal, on the same reasoning that
2587                // makes a mask blur of zero the sharp shape: a caller animating
2588                // a radius down to nothing should arrive somewhere, not have
2589                // the gradient disappear at the last frame.
2590                if !radius.is_finite() || *radius <= 0.0 {
2591                    if matches!(tile, TileMode::Decal) {
2592                        return Material::Solid([0.0; 4]);
2593                    }
2594                    let last = stops
2595                        .iter()
2596                        .max_by(|a, b| a.offset.total_cmp(&b.offset))
2597                        .map(|s| s.color.to_array())
2598                        .unwrap_or([0.0; 4]);
2599                    return Material::solid(last);
2600                }
2601                let ramp_slot = self.ramp_for(stops);
2602                Material::RadialGradient {
2603                    to_local: invert_to_local(placement),
2604                    stops: stops_of(stops),
2605                    tile: *tile,
2606                    ramp: ramp_slot,
2607                }
2608            }
2609            Shader::ConicalGradient {
2610                start_center,
2611                start_radius,
2612                end_center,
2613                end_radius,
2614                stops,
2615                tile,
2616            } => {
2617                let axis = *end_center - *start_center;
2618                // The shader is told where the second center is with one float
2619                // instead of two, which is only possible if it already knows
2620                // the direction. Rotating the gradient's space so the axis
2621                // runs along +X is how it comes to know it, and the rotation
2622                // costs nothing: it folds into a matrix that has to be built
2623                // and inverted regardless.
2624                //
2625                // A zero axis leaves the angle undefined and the rotation
2626                // arbitrary, which is correct rather than merely harmless --
2627                // two concentric circles have no direction to preserve, and
2628                // every rotation is the right one.
2629                let angle = if axis == Vec2::ZERO {
2630                    0.0
2631                } else {
2632                    axis.y.atan2(axis.x)
2633                };
2634                let oriented =
2635                    to_clip * Affine2::from_translation(*start_center) * Affine2::from_angle(angle);
2636                if !oriented.is_finite() || !start_radius.is_finite() || !end_radius.is_finite() {
2637                    return Material::Solid([0.0; 4]);
2638                }
2639                let ramp_slot = self.ramp_for(stops);
2640                Material::ConicalGradient {
2641                    to_local: invert_to_local(oriented),
2642                    // In the gradient's own space, which this rotation and the
2643                    // canvas transform's inverse together make into user space
2644                    // -- so both radii are the ones the caller stated, and
2645                    // neither is folded into the matrix the way a radial
2646                    // gradient's single radius is. A scale can normalize one
2647                    // radius; it cannot normalize two.
2648                    start_radius: *start_radius,
2649                    radius_delta: *end_radius - *start_radius,
2650                    separation: axis.length(),
2651                    stops: stops_of(stops),
2652                    tile: *tile,
2653                    ramp: ramp_slot,
2654                }
2655            }
2656            Shader::SweepGradient {
2657                center,
2658                start_angle,
2659                end_angle,
2660                stops,
2661                tile,
2662            } => {
2663                let placement = to_clip * Affine2::from_translation(*center);
2664                if !placement.is_finite() || !start_angle.is_finite() || !end_angle.is_finite() {
2665                    return Material::Solid([0.0; 4]);
2666                }
2667                let ramp_slot = self.ramp_for(stops);
2668                Material::SweepGradient {
2669                    to_local: invert_to_local(placement),
2670                    start_angle: *start_angle,
2671                    end_angle: *end_angle,
2672                    stops: stops_of(stops),
2673                    tile: *tile,
2674                    ramp: ramp_slot,
2675                }
2676            }
2677        }
2678    }
2679
2680    /// Draw a shape through a blurred layer, which is what a mask blur is.
2681    ///
2682    /// Blurring coverage and then filling equals filling and then blurring
2683    /// exactly when the fill is constant, since a blur is linear. That identity
2684    /// is the whole implementation: a bounded layer, blurred, with the shape
2685    /// drawn into it. It also means the machinery is one already tested rather
2686    /// than a second blur written beside the first.
2687    ///
2688    /// The bounds are the shape's, widened by how far the blur reaches. A
2689    /// caller doing this by hand has to know that reach, which is exactly the
2690    /// part that is easy to get wrong and shows as a shadow with a straight
2691    /// edge where it was cut off.
2692    /// Draw into a layer, filter it, and composite it back.
2693    ///
2694    /// The same machinery a mask blur uses, without the restriction: a mask
2695    /// blur is only the same picture as this when the fill does not vary, and
2696    /// so takes a solid color, while filtering a result is defined whatever
2697    /// produced it. What is paid for that is a target of its own.
2698    /// The layer that applies one filter, where that filter is a leaf.
2699    ///
2700    /// `None` for the two that are not: `is_identity` keeps `None` out, and
2701    /// `peel` recurses through a composition's outer half until it reaches
2702    /// something that is not one -- so neither can arrive here. They were
2703    /// reachable when peeling took that half to be a leaf, and composing two
2704    /// compositions was then refused as unimplemented, having been assembled
2705    /// out of nothing but implemented filters.
2706    fn filter_layer(&self, filter: &ImageFilter) -> Option<(Layer, Option<ImageFilter>)> {
2707        if let ImageFilter::Runtime { program, uniforms } = filter {
2708            // The layer itself is plain: what makes it a filter is the pass run
2709            // over it at restore, which needs the program and cannot get it
2710            // from a `Copy` layer.
2711            return Some((
2712                Layer::opacity(1.0),
2713                Some(ImageFilter::Runtime {
2714                    program: *program,
2715                    uniforms: uniforms.clone(),
2716                }),
2717            ));
2718        }
2719        Some((
2720            match *filter {
2721                ImageFilter::Blur { sigma_x, sigma_y } => {
2722                    Layer::opacity(1.0).with_blur_xy(sigma_x, sigma_y)
2723                }
2724                ImageFilter::Matrix { transform } => Layer::opacity(1.0).with_matrix(transform),
2725                ImageFilter::Dilate { radius_x, radius_y } => {
2726                    Layer::opacity(1.0).with_morphology(Morphology::dilate(radius_x, radius_y))
2727                }
2728                ImageFilter::Erode { radius_x, radius_y } => {
2729                    Layer::opacity(1.0).with_morphology(Morphology::erode(radius_x, radius_y))
2730                }
2731                ImageFilter::Color(filter) => Layer::opacity(1.0).with_color_filter(filter),
2732                ImageFilter::Runtime { .. } => unreachable!("handled above"),
2733                ImageFilter::None | ImageFilter::Compose { .. } => return None,
2734            },
2735            None,
2736        ))
2737    }
2738
2739    /// A glyph run drawn into a layer, and the layer filtered.
2740    ///
2741    /// The third of these, after the shape and the mesh, and the same shape as
2742    /// both. The bounds come from the run's own boxes, since a run has neither
2743    /// a path nor vertices to take them from.
2744    fn draw_glyphs_filtered(
2745        &mut self,
2746        glyphs: &[PositionedGlyph],
2747        atlas: &Atlas,
2748        atlas_slot: u32,
2749        paint: &Paint,
2750    ) -> Result<&mut Self> {
2751        let (outermost, rest) = paint.image_filter.peel();
2752        let Some((layer, runtime)) = self.filter_layer(&outermost) else {
2753            return Err(Error::Unsupported("this image filter is not implemented"));
2754        };
2755        let (mut min, mut max) = (Vec2::splat(f32::INFINITY), Vec2::splat(f32::NEG_INFINITY));
2756        for glyph in glyphs {
2757            let corner = Vec2::from(glyph.position);
2758            min = min.min(corner);
2759            max = max.max(corner + Vec2::from(glyph.size));
2760        }
2761        if !min.is_finite() || !max.is_finite() {
2762            return Err(Error::Unsupported(
2763                "a glyph in this run is not at a finite position",
2764            ));
2765        }
2766        let (min, max) = transformed_bounds(self.transform, min, max).unwrap_or_else(unbounded);
2767        let basis = BlurBasis::of(self.transform);
2768        let (min, max) = rest.covering(min, max, &|sigma| basis.reach(sigma));
2769        let _ = self.save_layer_device_bounds_running(
2770            layer.with_blend(paint.blend),
2771            min,
2772            max,
2773            runtime,
2774            None,
2775        );
2776        let inner = paint
2777            .clone()
2778            .with_image_filter(rest)
2779            .with_blend(BlendMode::SrcOver);
2780        let failure = self.draw_glyphs(glyphs, atlas, atlas_slot, &inner).err();
2781        self.restore();
2782        match failure {
2783            Some(e) => Err(e),
2784            None => Ok(self),
2785        }
2786    }
2787
2788    /// A mesh drawn into a layer, and the layer filtered.
2789    ///
2790    /// The path version's twin, and separate rather than shared because the two
2791    /// differ in the only two places that matter -- where the bounds come from,
2792    /// and which draw call the remainder of the chain is handed back to. The
2793    /// peeling, the region arithmetic and the blend placement are the same and
2794    /// are described there.
2795    fn draw_vertices_filtered(&mut self, mesh: &Vertices, paint: &Paint) -> Result<&mut Self> {
2796        let (outermost, rest) = paint.image_filter.peel();
2797        let Some((layer, runtime)) = self.filter_layer(&outermost) else {
2798            return Err(Error::Unsupported("this image filter is not implemented"));
2799        };
2800        // From the vertices themselves. A mesh has no path to take bounds from,
2801        // and the positions are in the same coordinates a path's points would
2802        // be, so the transform below is the one that applies to either.
2803        let positions = mesh.positions();
2804        let (mut min, mut max) = (Vec2::splat(f32::INFINITY), Vec2::splat(f32::NEG_INFINITY));
2805        for point in positions {
2806            min = min.min(*point);
2807            max = max.max(*point);
2808        }
2809        if !min.is_finite() || !max.is_finite() {
2810            return Err(Error::Unsupported("a mesh position is not a finite number"));
2811        }
2812        let (min, max) = transformed_bounds(self.transform, min, max).unwrap_or_else(unbounded);
2813        let basis = BlurBasis::of(self.transform);
2814        let (min, max) = rest.covering(min, max, &|sigma| basis.reach(sigma));
2815        let _ = self.save_layer_device_bounds_running(
2816            layer.with_blend(paint.blend),
2817            min,
2818            max,
2819            runtime,
2820            None,
2821        );
2822        let inner = paint
2823            .clone()
2824            .with_image_filter(rest)
2825            .with_blend(BlendMode::SrcOver);
2826        let failure = self.draw_vertices(mesh, &inner).err();
2827        self.restore();
2828        match failure {
2829            Some(e) => Err(e),
2830            None => Ok(self),
2831        }
2832    }
2833
2834    /// A caller's program drawn into a layer, and the layer recolored.
2835    ///
2836    /// The bounds are the path's own, widened by a stroke where there is one.
2837    /// Nothing here reaches past that: a color filter moves colors rather than
2838    /// pixels, so the region it can affect is the region that was drawn.
2839    fn draw_effect_filtered(&mut self, path: &Path, paint: &Paint) -> Result<&mut Self> {
2840        let bounds = self.filter_bounds(path, paint);
2841        let (min, max) = transformed_bounds(
2842            self.transform,
2843            Vec2::new(bounds.left, bounds.top),
2844            Vec2::new(bounds.right, bounds.bottom),
2845        )
2846        .unwrap_or_else(unbounded);
2847        self.save_layer_device_bounds(
2848            Layer::opacity(1.0)
2849                .with_color_filter(paint.color_filter)
2850                // The blend belongs to the composite, not to the draw inside.
2851                // That is what `saveLayer` means by taking a paint, and it is
2852                // the only placement that gives the right answer for a blend
2853                // reading its destination: inside the layer the destination is
2854                // transparent black, so a mode like `Multiply` applied there
2855                // would come out empty and then be composited over the frame it
2856                // was supposed to darken.
2857                .with_blend(paint.blend),
2858            min,
2859            max,
2860        );
2861        // Without the filter, or this would open a layer inside itself forever,
2862        // and over an empty layer rather than through the caller's blend, which
2863        // the composite above now carries.
2864        let inner = paint
2865            .clone()
2866            .with_color_filter(ColorFilter::None)
2867            .with_blend(BlendMode::SrcOver);
2868        let failure = self.draw_path(path, &inner).err();
2869        self.restore();
2870        match failure {
2871            Some(e) => Err(e),
2872            None => Ok(self),
2873        }
2874    }
2875
2876    fn draw_filtered(&mut self, path: &Path, paint: &Paint) -> Result<&mut Self> {
2877        // One filter is peeled off here and the rest is handed back to
2878        // `draw_path`, which lands in this method again if anything is left.
2879        // A chain of filters is a stack of layers, and this builds the stack
2880        // one frame at a time rather than all at once -- which keeps the
2881        // single-filter case exactly what it was, with the remainder `None`.
2882        let (outermost, rest) = paint.image_filter.peel();
2883        let Some((layer, runtime)) = self.filter_layer(&outermost) else {
2884            return Err(Error::Unsupported("this image filter is not implemented"));
2885        };
2886        let bounds = self.filter_bounds(path, paint);
2887        // In device pixels, because that is where a filter's reach is measured.
2888        // The outer layer has to cover everything the rest of the chain needs,
2889        // both what goes into it and what comes out -- a layer's target is
2890        // clipped to its parent's, so a target sized for the result alone would
2891        // crop the content before the inner filter ever ran.
2892        let (min, max) = transformed_bounds(
2893            self.transform,
2894            Vec2::new(bounds.left, bounds.top),
2895            Vec2::new(bounds.right, bounds.bottom),
2896        )
2897        .unwrap_or_else(unbounded);
2898        let basis = BlurBasis::of(self.transform);
2899        let (min, max) = rest.covering(min, max, &|sigma| basis.reach(sigma));
2900        // The caller's blend rides the outermost composite. Left on the draw
2901        // inside, it was applied against the layer's own transparent black --
2902        // so a mode that reads its destination found nothing there, and the
2903        // result was composited over the frame it was meant to combine with.
2904        // `Plus` over a cyan ground gave the source unchanged instead of the
2905        // sum, for every image filter, since filters were added.
2906        //
2907        // Only the outermost, which is why the inner paint is neutral: peeling
2908        // a chain opens a layer per link, and a blend carried down would be
2909        // applied once per link rather than once.
2910        let _ = self.save_layer_device_bounds_running(
2911            layer.with_blend(paint.blend),
2912            min,
2913            max,
2914            runtime,
2915            None,
2916        );
2917        // The mask blur, if there is one, is left on: it applies to the drawing
2918        // this filter is filtering.
2919        let inner = paint
2920            .clone()
2921            .with_image_filter(rest)
2922            .with_blend(BlendMode::SrcOver);
2923        let failure = self.draw_path(path, &inner).err();
2924        self.restore();
2925        match failure {
2926            Some(e) => Err(e),
2927            None => Ok(self),
2928        }
2929    }
2930
2931    /// A path's bounds, widened by however far a stroke reaches past it.
2932    ///
2933    /// How far a blur reaches is deliberately not added: opening a bounded
2934    /// layer with a blur already widens the region by that, and the layer is
2935    /// the right place for it, since it knows its own sigma.
2936    ///
2937    /// The stroke's own reach was untested for a while, because that blur
2938    /// widening covers any stroke whose half-width is under the blur's own
2939    /// reach, and every test had one. It takes a wide stroke and a small blur to tell the two
2940    /// apart, and there is one now.
2941    fn filter_bounds(&self, path: &Path, paint: &Paint) -> Rect {
2942        let bounds = path.bounds();
2943        let reach = match &paint.style {
2944            // The width the stroke is drawn at, which for a sub-pixel one is
2945            // wider than the width it asked for. A bound taken from the asked
2946            // width would cut the widening off at the edge of the layer.
2947            Style::Stroke(stroke) if stroke.width.is_finite() => {
2948                thin_stroke(self.transform, stroke.width, Zero::IsAHairline).0 / 2.0
2949            }
2950            _ => 0.0,
2951        };
2952        Rect::new(
2953            bounds.min.x - reach,
2954            bounds.min.y - reach,
2955            bounds.max.x + reach,
2956            bounds.max.y + reach,
2957        )
2958    }
2959
2960    /// What a mask blur is blurring.
2961    ///
2962    /// A mask blur draws its content two or three times -- once into a blurred
2963    /// layer, sometimes again at full sharpness to combine with it -- and every
2964    /// style needs the same content each time. Naming the content rather than
2965    /// passing a path is what lets a glyph run take the same four styles a
2966    /// shape does, which matters because a text shadow is a mask blur over a
2967    /// run and is how the common case of shadowed text is drawn.
2968    fn draw_mask_content(&mut self, content: Masked<'_>, paint: &Paint) -> Result<&mut Self> {
2969        match content {
2970            Masked::Path(path) => self.draw_path(path, paint),
2971            Masked::Mesh(mesh) => self.draw_vertices(mesh, paint),
2972            Masked::Glyphs {
2973                glyphs,
2974                atlas,
2975                slot,
2976            } => self.draw_glyphs(glyphs, atlas, slot, paint),
2977        }
2978    }
2979
2980    /// The bounds of what a mask blur is blurring, in the canvas's own units.
2981    fn mask_bounds(&self, content: Masked<'_>, paint: &Paint) -> Rect {
2982        match content {
2983            Masked::Path(path) => self.filter_bounds(path, paint),
2984            Masked::Mesh(mesh) => {
2985                let (mut min, mut max) =
2986                    (Vec2::splat(f32::INFINITY), Vec2::splat(f32::NEG_INFINITY));
2987                for position in mesh.positions() {
2988                    min = min.min(*position);
2989                    max = max.max(*position);
2990                }
2991                Rect::new(min.x, min.y, max.x, max.y)
2992            }
2993            Masked::Glyphs { glyphs, .. } => {
2994                let (mut min, mut max) =
2995                    (Vec2::splat(f32::INFINITY), Vec2::splat(f32::NEG_INFINITY));
2996                for glyph in glyphs {
2997                    let corner = Vec2::from(glyph.position);
2998                    min = min.min(corner);
2999                    max = max.max(corner + Vec2::from(glyph.size));
3000                }
3001                Rect::new(min.x, min.y, max.x, max.y)
3002            }
3003        }
3004    }
3005
3006    /// A mask blur over a fill that varies, built the way `dart:ui` states it.
3007    ///
3008    /// The path below draws the paint through a blurred layer, which is the
3009    /// same picture only where the fill does not vary. This is the other order
3010    /// and the one a mask filter actually means: blur the shape's coverage,
3011    /// then fill through it. It needs no machinery that was not already here --
3012    /// the fill goes down across everything the blur reaches, the coverage is
3013    /// blurred in a layer of its own, and `DstIn` composites the second onto
3014    /// the first as a mask.
3015    ///
3016    /// Only a path. A glyph run reaches this call too, and a run tints one
3017    /// color by its own nature rather than by anything to do with blurring, so
3018    /// a gradient over one is refused here exactly as it is refused when
3019    /// nothing is blurred at all.
3020    fn draw_masked_through_coverage(
3021        &mut self,
3022        content: Masked<'_>,
3023        paint: &Paint,
3024    ) -> Result<&mut Self> {
3025        let bounds = self.mask_bounds(content, paint);
3026        // Wide enough for whichever blur this content's route will draw.
3027        let held = bounds.outset(self.mask_blur_reach(content, paint));
3028        // White, because what is wanted from the shape here is its coverage
3029        // rather than its color: the fill supplies the color and this supplies
3030        // where it lands.
3031        let coverage = Paint::fill(Color::WHITE).with_anti_alias(paint.anti_alias);
3032        // This one floods the held region with the caller's paint, so it has to
3033        // be a *fill* whatever the caller asked for. Left as the caller's own
3034        // style it drew the outline of the held rectangle when the caller was
3035        // stroking, and the mask below then cut that away to nothing -- the
3036        // rectangle's border is nowhere near the shape. A stroked shape with a
3037        // fill that varies came out empty, on both backends.
3038        //
3039        // Only the flood needs it. Handing the coverage the caller's style as
3040        // well changes no pixel of any plate here, so something other than this
3041        // line is already giving the mask the band rather than the region, and
3042        // a line that changes nothing does not go in with a comment saying it
3043        // does.
3044        let fill = paint
3045            .clone()
3046            .with_mask_blur(0.0)
3047            .with_blend(BlendMode::SrcOver)
3048            .with_style(Style::Fill);
3049
3050        self.save_layer_bounds(Layer::opacity(1.0).with_blend(paint.blend), held);
3051        // Across everything the blur reaches, not across the shape: a blurred
3052        // mask is wider than what it was made from, and a fill stopping at the
3053        // shape's own edge would cut the halo off square.
3054        let failure = self.draw_rect(held, &fill).err().or_else(|| {
3055            if paint.mask_blur_style == MaskBlurStyle::Normal {
3056                // The blurred coverage is the whole mask, so the layer that
3057                // carries it can be the layer that blurs it.
3058                let mask = Layer::opacity(1.0)
3059                    .with_blur(paint.mask_blur)
3060                    .with_blend(BlendMode::DstIn);
3061                self.save_layer_bounds(mask, held);
3062                let inner = self.draw_mask_content(content, &coverage).err();
3063                self.restore();
3064                return inner;
3065            }
3066            // The other three combine the blurred coverage with the sharp one,
3067            // and the combination is not itself blurred -- so this layer
3068            // carries no blur, and the blur happens inside it on the operand
3069            // that wants it.
3070            self.save_layer_bounds(Layer::opacity(1.0).with_blend(BlendMode::DstIn), held);
3071            let inner = self.draw_mask_styles(
3072                content,
3073                &coverage,
3074                paint.mask_blur,
3075                paint.mask_blur_style,
3076                bounds,
3077            );
3078            self.restore();
3079            inner
3080        });
3081        self.restore();
3082        match failure {
3083            Some(e) => Err(e),
3084            None => Ok(self),
3085        }
3086    }
3087
3088    fn draw_masked(&mut self, content: Masked<'_>, paint: &Paint) -> Result<&mut Self> {
3089        if !matches!(paint.shader, Shader::Solid(_)) {
3090            if !matches!(content, Masked::Glyphs { .. }) {
3091                return self.draw_masked_through_coverage(content, paint);
3092            }
3093            // A glyph run, and it is the only thing left here. A run tints one
3094            // color by its own nature rather than by anything to do with
3095            // blurring, so a varying fill over one is refused exactly as it is
3096            // refused when nothing is blurred at all.
3097            //
3098            // See `Paint::mask_blur`: drawing the paint through a blurred layer
3099            // is the other order, and for anything that varies the two are
3100            // different pictures. Drawing one while the caller asked for the
3101            // other is the substitution this renderer refuses
3102            // elsewhere.
3103            return Err(Error::Unsupported(
3104                "a mask blur over a glyph run takes a solid color; \
3105                 draw into a blurred layer for anything else",
3106            ));
3107        }
3108        let bounds = self.mask_bounds(content, paint);
3109        // Without the mask, or this would open a layer inside itself forever --
3110        // and with the caller's blend taken off, because it belongs to the
3111        // composite that puts the finished mask on the frame rather than to a
3112        // draw inside a layer that starts empty. Left on, a mode reading its
3113        // destination found transparent black there, and `Plus` over a cyan
3114        // ground gave the source unchanged instead of the sum.
3115        //
3116        // The style's own blends below are a different thing and stay: they
3117        // combine the shape with its blur *within* the layer, which is exactly
3118        // where a destination-reading mode is supposed to look.
3119        let inner = paint
3120            .clone()
3121            .with_mask_blur(0.0)
3122            .with_blend(BlendMode::SrcOver);
3123
3124        // The blurred coverage alone is the whole picture for the default
3125        // style, so it needs one layer and no second draw.
3126        if paint.mask_blur_style == MaskBlurStyle::Normal {
3127            self.save_layer_bounds(
3128                Layer::opacity(1.0)
3129                    .with_blur(paint.mask_blur)
3130                    .with_blend(paint.blend),
3131                bounds,
3132            );
3133            // The result is discarded to end the borrow before restoring, and
3134            // taken up again after: the layer has to be closed whether the
3135            // draw inside it succeeded or not, or every later draw lands in a
3136            // layer nobody composites.
3137            let failure = self.draw_mask_content(content, &inner).err();
3138            self.restore();
3139            return match failure {
3140                Some(e) => Err(e),
3141                None => Ok(self),
3142            };
3143        }
3144
3145        // Every other style combines the blurred coverage with the shape's
3146        // own. That combination is the same whether what is being combined is
3147        // color or coverage, so it lives in `draw_mask_styles` and both routes
3148        // through here call it.
3149        // Wide enough for whichever blur this content's route will draw.
3150        let held = bounds.outset(self.mask_blur_reach(content, paint));
3151        self.save_layer_bounds(Layer::opacity(1.0).with_blend(paint.blend), held);
3152        let failure = self.draw_mask_styles(
3153            content,
3154            &inner,
3155            paint.mask_blur,
3156            paint.mask_blur_style,
3157            bounds,
3158        );
3159        self.restore();
3160        match failure {
3161            Some(e) => Err(e),
3162            None => Ok(self),
3163        }
3164    }
3165
3166    /// How far past its bounds a mask blur on this content will draw.
3167    ///
3168    /// The two routes reach different distances and the layer holding them has
3169    /// to fit whichever is used: a sampled blur stops at `blur_reach`, about
3170    /// 1.73 deviations, and an evaluated one carries to `pad_for_sigma`, about
3171    /// 2.5. Sized for the first, an evaluated blur is cut off square; sized for
3172    /// the second regardless, every *sampled* blur pays for a transient about
3173    /// twice the area to hold a halo it will never draw.
3174    ///
3175    /// So it asks which route the content will take, rather than taking the
3176    /// larger of the two and calling it safe.
3177    fn mask_blur_reach(&mut self, content: Masked<'_>, paint: &Paint) -> f32 {
3178        let sampled = blur_reach(paint.mask_blur);
3179        let probe = paint.clone().with_mask_blur_style(MaskBlurStyle::Normal);
3180        match self.analytic_style_blur(content, &probe, paint.mask_blur) {
3181            Some((_, _, pad)) => sampled.max(pad),
3182            None => sampled,
3183        }
3184    }
3185
3186    /// The evaluated blur for a style's blurred ingredient, where one applies.
3187    ///
3188    /// The styles other than `Normal` combine a blurred shape with its sharp
3189    /// self, and the blurred half is the same thing `Normal` draws — so where
3190    /// `Normal` would be evaluated rather than sampled, this half can be too.
3191    /// `inner` is the paint with the blur already taken off, which is what the
3192    /// styles draw with, so the deviation is handed back in.
3193    ///
3194    /// Returns the rectangle the draw covers as well as the material, because
3195    /// the caller has to widen its layer to hold it: an evaluated blur reaches
3196    /// `pad_for_sigma`, which is further than the `blur_reach` a sampled one is
3197    /// truncated at, and a layer sized for the sampled reach cuts it.
3198    fn analytic_style_blur(
3199        &mut self,
3200        content: Masked<'_>,
3201        inner: &Paint,
3202        sigma: f32,
3203    ) -> Option<(Rect, Material, f32)> {
3204        let Masked::Path(path) = content else {
3205            return None;
3206        };
3207        let (bounds, radius) = path.as_rounded_rect()?;
3208        let rect = Rect::new(bounds.min.x, bounds.min.y, bounds.max.x, bounds.max.y);
3209        // Asked for as `Normal`, whatever style the caller wants: the blurred
3210        // half *is* the normal blur, and the style is the rule for combining it
3211        // with the sharp shape rather than a property of the blur. Left as the
3212        // caller's, the eligibility check refuses it -- which is right when the
3213        // question is "may this whole draw be one expression" and wrong here.
3214        let asked = inner
3215            .clone()
3216            .with_mask_blur(sigma)
3217            .with_mask_blur_style(MaskBlurStyle::Normal);
3218        let (material, pad) = self.analytic_rrect_blur(rect, radius, &asked)?;
3219        Some((rect, material, pad))
3220    }
3221
3222    /// Combine a shape's blurred coverage with its sharp own, per style.
3223    ///
3224    /// Draws into whatever layer the caller has already opened, and that layer
3225    /// is not optional: drawn straight onto the target, a blend that reads the
3226    /// destination would reach what was already there rather than only what
3227    /// this is building.
3228    ///
3229    /// What is being combined is the caller's business. Given the paint the
3230    /// shape is filled with, this composes colors and is the whole picture;
3231    /// given white, it composes coverage, and the caller masks a varying fill
3232    /// through the result. The rules are identical either way, which is why
3233    /// there is one of these rather than two.
3234    ///
3235    /// Which of the two operands is drawn first is not a matter of taste. A
3236    /// blend only runs where its source produces a fragment, and the shape
3237    /// produces none outside itself -- so a rule that has to *remove*
3238    /// something outside the shape cannot be written with the shape as the
3239    /// source. The blurred layer composites as a quad over the whole region, so
3240    /// it is the operand that can act everywhere, and the two rules needing
3241    /// that are the two where the shape goes down first.
3242    fn draw_mask_styles(
3243        &mut self,
3244        content: Masked<'_>,
3245        inner: &Paint,
3246        sigma: f32,
3247        style: MaskBlurStyle,
3248        bounds: Rect,
3249    ) -> Option<Error> {
3250        // Not itself blurred, so nothing widens it on its behalf.
3251        let blurred = Layer::opacity(1.0).with_blur(sigma);
3252        match style {
3253            // Blur first, then the shape over it. `SrcOver` leaves the blur
3254            // where the shape is not, and `DstOut` takes the shape out of it
3255            // -- both of which want the destination untouched outside the
3256            // shape, which is what a source that draws nothing there gives.
3257            MaskBlurStyle::Solid | MaskBlurStyle::Outer => {
3258                let blend = match style {
3259                    MaskBlurStyle::Solid => BlendMode::SrcOver,
3260                    _ => BlendMode::DstOut,
3261                };
3262                // The blurred half, evaluated where it can be. These two
3263                // styles draw it first with the layer's own blend, so nothing
3264                // has to be relaxed to allow it -- unlike `Inner`, whose blur
3265                // is composited with `DstIn` and needs coverage this cannot
3266                // give from a quad.
3267                let first = match self.analytic_style_blur(content, inner, sigma) {
3268                    Some((rect, material, pad)) => {
3269                        self.draw_analytic(rect.outset(pad), material, inner).err()
3270                    }
3271                    None => {
3272                        self.save_layer_bounds(blurred, bounds);
3273                        let failed = self.draw_mask_content(content, inner).err();
3274                        self.restore();
3275                        failed
3276                    }
3277                };
3278                first.or_else(|| {
3279                    self.draw_mask_content(content, &inner.clone().with_blend(blend))
3280                        .err()
3281                })
3282            }
3283            // The shape first, and the blur composited onto it with `DstIn`.
3284            // The other order is the obvious one and is wrong: outside the
3285            // shape there is no fragment for `DstIn` to run on, so the blur
3286            // survives exactly where this style is supposed to discard it.
3287            // Compositing a layer covers the whole region, so putting the
3288            // blur on that side is what makes the rule act everywhere.
3289            MaskBlurStyle::Inner => {
3290                let first = self.draw_mask_content(content, inner).err();
3291                if first.is_some() {
3292                    return first;
3293                }
3294                // `DstIn` is the one blend the analytic route refuses
3295                // everywhere else, because it changes the destination where the
3296                // source drew nothing and a quad is smaller than the region a
3297                // layer composite covers. That argument does not reach here,
3298                // and the reason is specific rather than a relaxation: the only
3299                // thing in this layer is the sharp shape drawn a line above,
3300                // the shape is a fill and so lies within `rect`, and the quad
3301                // is `rect` grown by the blur's reach. Everything the blend
3302                // could have to zero is under the quad; outside it the layer is
3303                // still transparent, where zeroing and leaving alone are the
3304                // same. So the eligibility is asked about the blur -- with
3305                // `SrcOver`, since that is what the question means -- and the
3306                // blend is put on at the draw.
3307                match self.analytic_style_blur(content, inner, sigma) {
3308                    Some((rect, material, pad)) => {
3309                        let composite = inner.clone().with_blend(BlendMode::DstIn);
3310                        self.draw_analytic(rect.outset(pad), material, &composite)
3311                            .err()
3312                    }
3313                    None => {
3314                        self.save_layer_bounds(blurred.with_blend(BlendMode::DstIn), bounds);
3315                        let second = self.draw_mask_content(content, inner).err();
3316                        self.restore();
3317                        second
3318                    }
3319                }
3320            }
3321            MaskBlurStyle::Normal => unreachable!("handled by the caller"),
3322        }
3323    }
3324
3325    /// Fill or stroke a rectangle.
3326    ///
3327    /// An antialiased solid fill takes the same distance field a rounded
3328    /// rectangle does, with no corner to round. The vertex count is the same
3329    /// either way -- a rectangle is four vertices whichever route it takes --
3330    /// so what this buys is the edge: the pass does not have to multisample for
3331    /// a shape that computes its own coverage, which is four times the fill and
3332    /// four times the bandwidth saved on a frame made mostly of rectangles.
3333    pub fn draw_rect(&mut self, rect: Rect, paint: &Paint) -> Result<&mut Self> {
3334        if rect.is_empty() {
3335            return Ok(self);
3336        }
3337        if let Some(material) = self.analytic_rrect(rect, 0.0, paint) {
3338            return self.draw_analytic(rect, material, paint);
3339        }
3340        let path = rect.to_path();
3341        self.draw_path(&path, paint)
3342    }
3343
3344    /// Fill or stroke a rectangle with rounded corners.
3345    ///
3346    /// The shape an interface is mostly made of, and the reason it is here
3347    /// rather than left to the caller: building it from arcs by hand is a dozen
3348    /// lines that have to get the corner tangents right, and getting them
3349    /// slightly wrong shows as a corner that is subtly not round.
3350    ///
3351    /// Tessellated like any other path. The analytic coverage that would let a
3352    /// rounded rectangle skip tessellation entirely is not implemented, and is
3353    /// where the interesting speed is for a real interface -- this is the shape
3354    /// that would benefit most from it.
3355    pub fn draw_rrect(&mut self, rect: Rect, radius: f32, paint: &Paint) -> Result<&mut Self> {
3356        if rect.is_empty() {
3357            return Ok(self);
3358        }
3359        // Before the sharp field, because that one refuses a mask blur and this
3360        // is what answers it: the blur is folded into the expression rather
3361        // than run as passes around the draw.
3362        if let Some((material, pad, drawn)) = self.analytic_rrect_blur_draw(rect, radius, paint) {
3363            return self.draw_analytic(rect.outset(pad), material, &drawn);
3364        }
3365        if let Some(material) = self.analytic_rrect(rect, radius, paint) {
3366            return self.draw_analytic(rect, material, paint);
3367        }
3368        let path = rect.to_rounded_path(radius);
3369        self.draw_path(&path, paint)
3370    }
3371
3372    /// A rounded rectangle whose corners need not match.
3373    ///
3374    /// The eight numbers `dart:ui`'s `RRect` carries, in the order upstream's
3375    /// `RoundingRadii` carries them: top-left, top-right, bottom-left,
3376    /// bottom-right, each an x radius and a y radius. Radii that overrun the
3377    /// side they share are fitted by `dart:ui`'s rule -- see
3378    /// [`Rect::to_rounded_path_with_radii`].
3379    ///
3380    /// Tessellated, always. The fragment-evaluated route [`Self::draw_rrect`]
3381    /// can take is a signed distance to a shape with one circular radius, and
3382    /// eight numbers is a different function rather than that one with more
3383    /// arguments -- so a corner that differs from its neighbors costs the
3384    /// tessellation a uniform one avoids. That is the whole of what this costs,
3385    /// and it is why the uniform call is still the one to reach for: it is not
3386    /// a worse spelling of this, it is the case that has a shader.
3387    pub fn draw_rrect_with_radii(
3388        &mut self,
3389        rect: Rect,
3390        radii: RoundingRadii,
3391        paint: &Paint,
3392    ) -> Result<&mut Self> {
3393        if rect.is_empty() {
3394            return Ok(self);
3395        }
3396        // A caller who spells a uniform shape this way still gets the shader,
3397        // which matters because the general call is the one a `dart:ui` port
3398        // reaches for and most of what it is handed is uniform.
3399        if let Some(radius) = uniform_circular_radius(radii) {
3400            return self.draw_rrect(rect, radius, paint);
3401        }
3402        let path = rect.to_rounded_path_with_radii(radii);
3403        self.draw_path(&path, paint)
3404    }
3405
3406    /// Draw Flutter's rounded superellipse -- the iOS-style squircle.
3407    ///
3408    /// `dart:ui`'s `drawRSuperellipse`. The corners are not arcs of one curve:
3409    /// each is a superellipse arc, a circular arc across the diagonal, and a
3410    /// second superellipse arc, with the superellipse's degree read from a
3411    /// fitted table on the ratio of side to radius. See
3412    /// [`impeller_geometry::superellipse`] for what that means and for what
3413    /// checks the transcription of the table.
3414    ///
3415    /// Tessellated rather than evaluated in the fragment stage. A rounded
3416    /// rectangle of a single radius takes the analytic route because there is
3417    /// a closed form to evaluate; this shape has none, which is the same
3418    /// reason it took so long to arrive.
3419    pub fn draw_rsuperellipse(
3420        &mut self,
3421        rect: Rect,
3422        radii: RoundingRadii,
3423        paint: &Paint,
3424    ) -> Result<&mut Self> {
3425        if rect.is_empty() {
3426            return Ok(self);
3427        }
3428        self.draw_path(&rsuperellipse_path(rect, radii), paint)
3429    }
3430
3431    /// Clip to Flutter's rounded superellipse.
3432    ///
3433    /// `dart:ui`'s `clipRSuperellipse`, and the same shape
3434    /// [`Canvas::draw_rsuperellipse`] draws.
3435    pub fn clip_rsuperellipse(&mut self, rect: Rect, radii: RoundingRadii) -> Result<&mut Self> {
3436        self.clip_path(&rsuperellipse_path(rect, radii))
3437    }
3438
3439    /// How far past the shape a blurred rounded rectangle has to draw.
3440    ///
3441    /// Wider than `blur_reach`, and deliberately: that is where a *sampled*
3442    /// Gaussian's kernel is truncated, and this expression has no kernel to
3443    /// truncate. It is evaluated everywhere and falls off smoothly, so what
3444    /// bounds it is where the falloff stops being visible rather than where the
3445    /// taps stop. Upstream's `PadForSigma`, whose comment records that three
3446    /// deviations was tried and left a cutoff on large blurs.
3447    fn pad_for_sigma(sigma: f32) -> f32 {
3448        sigma * (sigma / 47.6 + 2.5).min(3.5)
3449    }
3450
3451    /// The gate above, with the one substitution upstream makes, and the paint
3452    /// to draw the result with.
3453    ///
3454    /// Three shapes reach the evaluated blur -- a rounded-rectangle path,
3455    /// `draw_rrect` and `draw_circle` -- and all three share this. The fourth
3456    /// caller of [`Self::analytic_rrect_blur`] deliberately does not: it is
3457    /// building the blurred *ingredient* of a style that combines it with the
3458    /// sharp shape inside a layer, and a substitution good for a whole draw is
3459    /// not good for one operand of a combination.
3460    fn analytic_rrect_blur_draw(
3461        &mut self,
3462        rect: Rect,
3463        radius: f32,
3464        paint: &Paint,
3465    ) -> Option<(Material, f32, Paint)> {
3466        let drawn = Self::erasing_blur_paint(paint);
3467        let (material, pad) = self.analytic_rrect_blur(rect, radius, &drawn)?;
3468        Some((material, pad, drawn))
3469    }
3470
3471    /// `Clear` on a blurred shape, expressed as a mode that respects coverage.
3472    ///
3473    /// The gate below refuses a mode that writes where its source is
3474    /// transparent, and it is right to: the field is evaluated across a quad
3475    /// larger than the shape, so such a mode erases what is behind it in the
3476    /// gap. `Clear` is the one mode that can be admitted anyway, because on a
3477    /// coverage it is not what its factors say. Clearing by an amount `c` is
3478    /// `dst * (1 - c)`, which is `DstOut` against a white source -- and white
3479    /// is exact rather than approximate, since `Clear` discards the source
3480    /// color by definition and cannot care which one it had.
3481    ///
3482    /// This is upstream's own special case, in upstream's own one place.
3483    /// `SolidRRectLikeBlurContents::Render` checks for `BlendMode::kClear`,
3484    /// forces the color to white, and sets a flag that turns the pipeline's
3485    /// blend into a reverse subtraction -- destination factor `One`, source
3486    /// factor `DestinationColor`, so `dst - src * dst`. The arithmetic is the
3487    /// same; upstream reaches it by subtraction because its fragment writes
3488    /// coverage directly, and this reaches it by naming the mode that already
3489    /// means it.
3490    ///
3491    /// Only `Clear`. Upstream does not generalize this and neither does this:
3492    /// the other six modes that ignore coverage have no such reading and stay
3493    /// on the route that gets them wrong, which is `docs/non-parity.md`
3494    /// section 15.
3495    fn erasing_blur_paint(paint: &Paint) -> Paint {
3496        if paint.blend != BlendMode::Clear {
3497            return paint.clone();
3498        }
3499        paint
3500            .clone()
3501            .with_shader(Shader::Solid(Color::WHITE))
3502            .with_blend(BlendMode::DstOut)
3503            // Dropped rather than carried, because it provably cannot matter:
3504            // a color filter transforms the source color and `Clear` discards
3505            // it. Carried into the substitution it would stop being a no-op
3506            // and start changing how much is erased.
3507            .with_color_filter(ColorFilter::None)
3508    }
3509
3510    /// Draw a blurred rounded rectangle without a blur pass, if this one can be.
3511    ///
3512    /// The route upstream takes for the same shape, and the reason it is worth
3513    /// taking is that the general one costs three passes per shape where this
3514    /// costs one draw in the pass already open.
3515    ///
3516    /// `analytic_stroke` cannot answer this one, and the difference is the
3517    /// point: it refuses a mask blur outright, because a sharp distance field
3518    /// on an oversized quad cannot be put inside a layer and blurred. That is
3519    /// exactly the refusal this route lifts, by folding the blur into the field
3520    /// rather than wrapping the draw in one. Its other refusals do carry over
3521    /// and are restated here.
3522    /// Returns the material and how far past the rectangle it must be drawn,
3523    /// both in the shape's own space.
3524    fn analytic_rrect_blur(
3525        &mut self,
3526        rect: Rect,
3527        radius: f32,
3528        paint: &Paint,
3529    ) -> Option<(Material, f32)> {
3530        if !paint.is_visible() || self.clip.is_some_and(Scissor::is_empty) {
3531            return None;
3532        }
3533        // This emits a fragment across the whole padded quad, which is much
3534        // larger than the shape, so a mode that touches the destination where
3535        // the source is transparent would erase what is behind it in the gap.
3536        if !paint.blend.respects_coverage() {
3537            return None;
3538        }
3539        // An image filter wants a layer around the draw, and a quad larger than
3540        // its shape is the wrong thing to put in one.
3541        if !paint.image_filter.is_identity() {
3542            return None;
3543        }
3544        // A fill only. A blurred *stroke* is the blur of a band rather than of
3545        // the shape: a different picture, and not one this expression states.
3546        // A dash is refused with it, since there is no stroke to dash.
3547        if !matches!(paint.style, Style::Fill) {
3548            return None;
3549        }
3550        // The blurred coverage itself. The other three styles are that
3551        // combined with the sharp shape, which the general route assembles out
3552        // of two draws and this cannot say in one.
3553        if paint.mask_blur_style != MaskBlurStyle::Normal {
3554            return None;
3555        }
3556        let Shader::Solid(color) = &paint.shader else {
3557            return None;
3558        };
3559        if radius.is_nan() || radius < 0.0 {
3560            return None;
3561        }
3562        // `is_finite` first, so a sigma that is not a number is refused rather
3563        // than compared: every comparison against NaN is false, so a bare
3564        // `<= 0.0` would let it through.
3565        if !paint.mask_blur.is_finite() || paint.mask_blur <= 0.0 {
3566            return None;
3567        }
3568        // The deviation is in device pixels -- see `non-parity.md` on a shadow's
3569        // elevation -- and everything below is in the shape's own space, so it
3570        // has to be carried across. Getting this wrong is invisible at the
3571        // identity and doubles the blur's tail on a canvas scaled by two.
3572        let affine = self.transform.to_affine()?;
3573        let sx = affine.matrix2.x_axis.length();
3574        let sy = affine.matrix2.y_axis.length();
3575        if !sx.is_finite() || !sy.is_finite() || sx <= 0.0 || sy <= 0.0 {
3576            return None;
3577        }
3578        // One deviation cannot describe a blur that is wider along one axis
3579        // than the other, and this expression carries exactly one. A transform
3580        // that scales the axes differently goes to the general route, which
3581        // blurs in device space and does not care.
3582        if (sx - sy).abs() > 1e-3 * sx.max(sy) {
3583            return None;
3584        }
3585        // Stated in the shape's own space already, so there is nothing to
3586        // convert. It used to be `paint.mask_blur / sx`, which is what a
3587        // device-space sigma needed and what made a blur refuse to grow with
3588        // the transform.
3589        let sigma = paint.mask_blur;
3590
3591        let radius = radius.min(rect.width() / 2.0).min(rect.height() / 2.0);
3592        let material = self.rrect_blur_material(rect, radius, sigma, *color)?;
3593        Some((material, Self::pad_for_sigma(sigma)))
3594    }
3595
3596    /// The material for a blurred rounded rectangle drawn without a blur pass.
3597    ///
3598    /// Raph Levien's approximation, transcribed from upstream's
3599    /// `SolidRRectLikeBlurContents::PopulateFragContext`, which evaluates the
3600    /// same method. Every line is arithmetic the shader would otherwise repeat
3601    /// per fragment, and none of it is this renderer's invention -- the
3602    /// constants are upstream's, and where one looks arbitrary it is because
3603    /// it was fitted rather than derived.
3604    ///
3605    /// `rect` and `radius` are in the shape's own space, `sigma` in device
3606    /// pixels, as everywhere else here.
3607    fn rrect_blur_material(
3608        &self,
3609        rect: Rect,
3610        radius: f32,
3611        sigma: f32,
3612        color: Color,
3613    ) -> Option<Material> {
3614        // Below one, the approximation is sharper than the pixel grid can show
3615        // and the error function saturates; upstream floors it in the same
3616        // place. The root of two is the conversion between the deviation this
3617        // renderer states and the one the expression below is written in.
3618        let sigma = (sigma * std::f32::consts::SQRT_2).max(1.0);
3619        let mut size = Vec2::new(rect.width(), rect.height());
3620        if !size.x.is_finite() || !size.y.is_finite() || !sigma.is_finite() {
3621            return None;
3622        }
3623        if size.x <= 0.0 || size.y <= 0.0 {
3624            return None;
3625        }
3626        let min_edge = size.x.min(size.y);
3627        let r_max = 0.5 * min_edge;
3628
3629        // Two corner radii rather than the caller's one: a blurred corner is
3630        // rounder than the shape's, and how much rounder depends on the
3631        // deviation. Their ratio becomes the exponent, so a sharp corner under
3632        // a wide blur is measured with a larger exponent than a round one.
3633        let r0 = radius.hypot(sigma * 1.15).min(r_max);
3634        let r1 = radius.hypot(sigma * 2.0).min(r_max);
3635        let exponent = 2.0 * r1 / r0;
3636        let s_inv = 1.0 / sigma;
3637
3638        // Pull the long end in. A rectangle much longer than it is wide blurs
3639        // to something the axis-wise expression makes too eccentric, and this
3640        // shortens the long axis by an amount that vanishes as either side
3641        // grows past the deviation -- which is why it is a pair of Gaussians
3642        // of the sides rather than a ratio.
3643        let falloff = |v: f32| (-(v * s_inv * 0.5).powi(2)).exp();
3644        let delta = 1.25 * sigma * (falloff(size.x) - falloff(size.y));
3645        // Whichever axis is longer is the one shortened, which is a deliberate
3646        // departure from upstream and the only one in this expression.
3647        // Upstream writes `rSize += NegPos(delta)`, which is
3648        // `{min(delta, 0), max(delta, 0)}` -- so it shortens x when x is the
3649        // long axis and *lengthens* y when y is, leaving the same rectangle
3650        // blurred to two different widths depending on which way it is turned.
3651        // Its own comment says "pull in long end", which is what this does in
3652        // both orientations. See `non-parity.md`.
3653        size.x += delta.min(0.0);
3654        size.y += (-delta).min(0.0);
3655
3656        let adjust = size * 0.5 - Vec2::splat(r1);
3657        // Normalizes the fade, so the middle of a shape large against its blur
3658        // reaches full coverage rather than something near it.
3659        let scale = 0.5 * erf7(s_inv * 0.5 * (size.x.max(size.y) - 0.5 * radius));
3660
3661        let center = Vec2::new(
3662            (rect.left + rect.right) / 2.0,
3663            (rect.top + rect.bottom) / 2.0,
3664        );
3665        let to_clip = self.target.projection() * self.transform;
3666        let material = Material::RoundedRectBlur {
3667            color: color.to_array(),
3668            to_local: invert_to_local(to_clip * Affine2::from_translation(center)),
3669            adjust: [adjust.x, adjust.y],
3670            r1,
3671            exponent,
3672            s_inv,
3673            min_edge,
3674            scale,
3675        };
3676        // A field that is not a number puts NaN through every fragment, and a
3677        // NaN coverage is a shape that vanishes or a frame that does. Upstream
3678        // checks the same eight and declines the same way.
3679        [adjust.x, adjust.y, r1, exponent, s_inv, min_edge, scale]
3680            .iter()
3681            .all(|v| v.is_finite())
3682            .then_some(material)
3683    }
3684
3685    /// A paint for the fragment-evaluated rounded rectangle, where one applies.
3686    ///
3687    /// Only a solid fill that asked for antialiasing. A stroke is a different
3688    /// shape, a gradient or an image would need both its own mapping and this
3689    /// one at once — which is the case the push-constant budget was sized
3690    /// against — and an aliased fill is asking for hard edges, which the
3691    /// tessellated path gives and this one deliberately does not.
3692    fn analytic_rrect(&self, rect: Rect, radius: f32, paint: &Paint) -> Option<Material> {
3693        // Invisible and fully-clipped shapes are rejected before the path is
3694        // built, and this route has to reject them too. It bypasses
3695        // `draw_path`, which is where those checks live -- so an alpha of zero
3696        // went from recording nothing to recording a quad the fragment stage
3697        // discards a pixel at a time.
3698        if !paint.is_visible() || self.clip.is_some_and(Scissor::is_empty) {
3699            return None;
3700        }
3701        // A stroked rectangle with square corners is the one shape this route
3702        // draws differently, and it took a plate sending a rectangle through
3703        // `draw_rect` to notice. A distance field's stroke is the band a fixed
3704        // distance either side of the outline, and at a vertex that band's
3705        // outer edge is an arc -- so the corners come out *rounded*, where
3706        // `dart:ui`'s default join is a miter and the tessellated route gives
3707        // the sharp corner it asks for. Measured on a nine-wide stroke, one
3708        // route draws the corner pixel and the other does not: a difference of
3709        // two hundred and fifty-three out of two hundred and fifty-five.
3710        //
3711        // With a radius the outline has no vertex, so there is no join to get
3712        // wrong and the arc is what the shape is. Only the square-cornered case
3713        // is refused, and only where the caller asked for something other than
3714        // the round join the field would give them anyway.
3715        // A bevel cuts the corner off, which is neither the arc a round join
3716        // gives nor the point a miter does, and no offset of this shape is that
3717        // -- so it is the one join still sent to the tessellator. The other two
3718        // are the outer offset with and without a radius, which is what
3719        // `outer_radius` carries below.
3720        let mitered = match &paint.style {
3721            Style::Stroke(style) if style.width > 0.0 => {
3722                if style.join == LineJoin::Bevel {
3723                    return None;
3724                }
3725                style.join == LineJoin::Miter
3726            }
3727            _ => false,
3728        };
3729        // Widened and dimmed on the same terms the tessellated route is, so
3730        // that which route a shape takes stays invisible. See `thin_stroke`.
3731        let (stroke, coverage) =
3732            thin_stroke(self.transform, analytic_stroke(paint)?, Zero::IsNotAStroke);
3733        let Shader::Solid(color) = &paint.shader else {
3734            return None;
3735        };
3736        // A radius of zero is a plain rectangle, which this field describes as
3737        // well as any other: with no corner to round, the expression is the
3738        // distance to the nearer edge. NaN is not a radius and falls back.
3739        if radius.is_nan() || radius < 0.0 {
3740            return None;
3741        }
3742        let to_clip = self.target.projection() * self.transform;
3743        let center = Vec2::new(
3744            (rect.left + rect.right) / 2.0,
3745            (rect.top + rect.bottom) / 2.0,
3746        );
3747        Some(Material::RoundedRect {
3748            color: dimmed(*color, coverage),
3749            half_size: [rect.width() / 2.0, rect.height() / 2.0],
3750            // Maps a clip-space position back into the shape's own space,
3751            // measured from its center, so the distance is measured where the
3752            // radius means what the caller said. Measuring in clip space would
3753            // round the corners by different amounts on each axis of a target
3754            // that is not square.
3755            to_local: invert_to_local(to_clip * Affine2::from_translation(center)),
3756            radius: radius.min(rect.width() / 2.0).min(rect.height() / 2.0),
3757            // A mitered square corner offsets to a square corner, which is a
3758            // radius of nothing. Everything else grows by half the stroke:
3759            // a rounded corner because that is what offsetting an arc does, and
3760            // a round join because the arc is what it asks for.
3761            outer_radius: if mitered && radius <= 0.0 {
3762                0.0
3763            } else {
3764                radius.min(rect.width() / 2.0).min(rect.height() / 2.0) + stroke / 2.0
3765            },
3766            stroke,
3767        })
3768    }
3769
3770    /// Draw the quad a fragment-evaluated shape is painted onto.
3771    ///
3772    /// Outset by a pixel, because the coverage ramp runs half a pixel either
3773    /// side of the edge: a quad ending exactly at the shape would clip the
3774    /// outer half of its own antialiasing and leave a hard edge on a shape
3775    /// drawn to be soft. Outsetting costs a ring of fragments that compute a
3776    /// coverage of zero.
3777    fn draw_analytic(
3778        &mut self,
3779        rect: Rect,
3780        material: Material,
3781        paint: &Paint,
3782    ) -> Result<&mut Self> {
3783        // A pixel for the coverage ramp, plus half the stroke where one is
3784        // traced: an outline straddles the edge, so it reaches outward by half
3785        // its width beyond the shape it belongs to.
3786        let reach = 1.0
3787            + thin_stroke(
3788                self.transform,
3789                analytic_stroke(paint).unwrap_or(0.0),
3790                Zero::IsNotAStroke,
3791            )
3792            .0 / 2.0;
3793        let outset = Rect::new(
3794            rect.left - reach,
3795            rect.top - reach,
3796            rect.right + reach,
3797            rect.bottom + reach,
3798        );
3799        let render_paint = RenderPaint {
3800            material,
3801            filter: paint.color_filter,
3802            blend: paint.blend,
3803            clip: self.clip,
3804            stencil: ClipState::content(self.depth),
3805        };
3806        self.renderer.fill_into(
3807            &mut self.batch,
3808            &outset.to_path(),
3809            self.transform,
3810            &render_paint,
3811        )?;
3812        Ok(self)
3813    }
3814
3815    /// Fill or stroke a circle.
3816    ///
3817    /// An antialiased solid fill goes through the same distance field a rounded
3818    /// rectangle does, because it is one: a square whose corner radius is half
3819    /// its side has no straight edge left, and the field reduces exactly to the
3820    /// distance from the center less the radius. So a circle costs two
3821    /// triangles and needs no shader of its own, where four cubics flattened to
3822    /// The shadow an object at `elevation` casts, under one light.
3823    ///
3824    /// The rule rather than the picture, which is why this is a call and not a
3825    /// blurred draw a caller assembles: the whole point of an elevation is
3826    /// that everything at the same height casts a consistent shadow, and
3827    /// consistency is what a rule spread across call sites loses first.
3828    ///
3829    /// The model is a single light above and behind the viewer, which is what
3830    /// Impeller uses and what makes a raised object's shadow fall downward on
3831    /// screen. Three things follow from the elevation and nothing else: the
3832    /// shadow is offset downward by it, blurred in proportion to it, and drawn
3833    /// at a quarter of the stated color's alpha before that alpha is adjusted
3834    /// for the color's own luminance.
3835    ///
3836    /// `elevation` is in the same units the canvas draws in. Impeller scales
3837    /// it by a device pixel ratio first, which is a framework concept rather
3838    /// than a rendering one; a caller who has one should apply it here.
3839    ///
3840    /// `transparent_occluder` is accepted and unused, which is what upstream
3841    /// does with it -- `DlDispatcherBase::drawShadow` takes the flag and its
3842    /// drawing code never reads it. It stays on the signature because
3843    /// `dart:ui` has it, and a caller porting a call should not have to find
3844    /// out that one argument went missing.
3845    pub fn draw_shadow(
3846        &mut self,
3847        path: &Path,
3848        color: Color,
3849        elevation: f32,
3850        _transparent_occluder: bool,
3851    ) -> Result<&mut Self> {
3852        if !elevation.is_finite() || elevation <= 0.0 || color.is_invisible() {
3853            // Nothing at ground level: an object resting on the surface casts
3854            // no shadow, which is the same answer as an invisible one.
3855            return Ok(self);
3856        }
3857
3858        // Elevation gives a kernel radius, which the blur takes as a
3859        // deviation only after converting. Skipping the conversion is the
3860        // larger half of what made a shadow here twice as soft as upstream's.
3861        let sigma = sigma_for_radius(LIGHT_RADIUS * elevation);
3862        let shade = tonal_shadow_color(color);
3863        // Divided by the transform's scale, which is upstream's division and
3864        // is here for upstream's reason. A mask blur's sigma is stated in the
3865        // space the drawing is in and grows with the transform, so a shadow
3866        // whose softness comes from an *elevation* rather than from a length
3867        // would grow with it too -- and upstream's does not. Its
3868        // `drawShadow` divides its radius by the canvas scale precisely to
3869        // cancel the multiplication its filter applies, leaving a shadow's
3870        // softness fixed in device pixels however large the caster is drawn.
3871        //
3872        // This division did not exist while the sigma was device-space,
3873        // because there was no multiplication to cancel. Removing it was the
3874        // half of the blur change that a test caught rather than the half that
3875        // was designed: `a_shadows_softness_is_fixed_in_device_pixels` is that
3876        // test, and it was written when the opposite convention made the same
3877        // shadow come out right without any division at all.
3878        let scale = max_scale_of(self.transform);
3879        let sigma = if scale.is_finite() && scale > 0.0 {
3880            sigma / scale
3881        } else {
3882            sigma
3883        };
3884        let paint = Paint::fill(shade).with_mask_blur(sigma);
3885
3886        // The whole shadow, whatever the occluder is, which is what upstream
3887        // draws: `DlDispatcherBase::drawShadow` takes `transparent_occluder`
3888        // and its drawing code never reads it.
3889        //
3890        // This used to punch the caster's outline out of the shadow for an
3891        // opaque one, on the grounds that the part a caster covers is spent.
3892        // The grounds were sound and the result was not worth them. An opaque
3893        // caster drawn over its own shadow is the arrangement the flag
3894        // describes, and there the two are the same picture -- measured at
3895        // zero of sixty-five thousand bytes differing -- while the punch cost a
3896        // layer, so every shadow was five passes where four will do. A
3897        // deviation that produces identical pixels more slowly has nothing to
3898        // recommend it.
3899        self.save();
3900        self.translate(0.0, elevation);
3901        let failure = self.draw_path(path, &paint).err();
3902        self.restore();
3903        match failure {
3904            Some(e) => Err(e),
3905            None => Ok(self),
3906        }
3907    }
3908
3909    /// a tolerance cost vertices in proportion to how large it is drawn.
3910    pub fn draw_circle(&mut self, center: Vec2, radius: f32, paint: &Paint) -> Result<&mut Self> {
3911        // NaN named rather than caught by a negated comparison, which reads as
3912        // a typo and which clippy objects to on exactly those grounds.
3913        if radius.is_nan() || radius <= 0.0 {
3914            return Ok(self);
3915        }
3916        let bounds = Rect::new(
3917            center.x - radius,
3918            center.y - radius,
3919            center.x + radius,
3920            center.y + radius,
3921        );
3922        // A circle is a rounded rectangle whose corners are half its side, which
3923        // is how the line below already treats it and how upstream states it:
3924        // `DrawOval` on a square sends `RRectBlurShape(rect, width * 0.5)`.
3925        if let Some((material, pad, drawn)) = self.analytic_rrect_blur_draw(bounds, radius, paint) {
3926            return self.draw_analytic(bounds.outset(pad), material, &drawn);
3927        }
3928        if let Some(material) = self.analytic_rrect(bounds, radius, paint) {
3929            return self.draw_analytic(bounds, material, paint);
3930        }
3931        let path = circle_path(center, radius);
3932        self.draw_path(&path, paint)
3933    }
3934
3935    /// Fill or stroke an ellipse inscribed in `bounds`.
3936    ///
3937    /// A circle where the bounds are square, and the shape a rounded rectangle
3938    /// cannot become: past half its shorter side a rounded rectangle stops
3939    /// changing, giving a stadium, where an ellipse keeps curving on both axes.
3940    ///
3941    /// An antialiased solid fill is evaluated per fragment like the other
3942    /// shapes here. Everything else is tessellated from four cubics, which
3943    /// approximate an ellipse exactly as well as they approximate a circle --
3944    /// which is to say closely, and not exactly.
3945    pub fn draw_oval(&mut self, bounds: Rect, paint: &Paint) -> Result<&mut Self> {
3946        if bounds.is_empty() {
3947            return Ok(self);
3948        }
3949        if let Some(material) = self.analytic_ellipse(bounds, paint) {
3950            return self.draw_analytic(bounds, material, paint);
3951        }
3952        let path = oval_path(bounds);
3953        self.draw_path(&path, paint)
3954    }
3955
3956    /// A paint for the fragment-evaluated ellipse, where one applies.
3957    ///
3958    /// The same conditions the rounded rectangle answers to, and for the same
3959    /// reasons.
3960    fn analytic_ellipse(&self, bounds: Rect, paint: &Paint) -> Option<Material> {
3961        if !paint.is_visible() || self.clip.is_some_and(Scissor::is_empty) {
3962            return None;
3963        }
3964        let (stroke, coverage) =
3965            thin_stroke(self.transform, analytic_stroke(paint)?, Zero::IsNotAStroke);
3966        let Shader::Solid(color) = &paint.shader else {
3967            return None;
3968        };
3969        let to_clip = self.target.projection() * self.transform;
3970        let center = Vec2::new(
3971            (bounds.left + bounds.right) / 2.0,
3972            (bounds.top + bounds.bottom) / 2.0,
3973        );
3974        Some(Material::Ellipse {
3975            color: dimmed(*color, coverage),
3976            half_size: [bounds.width() / 2.0, bounds.height() / 2.0],
3977            to_local: invert_to_local(to_clip * Affine2::from_translation(center)),
3978            stroke,
3979        })
3980    }
3981
3982    /// Draw a run of positioned glyphs from one atlas, as a single draw.
3983    ///
3984    /// `atlas_slot` indexes the image table supplied at draw time, and each
3985    /// glyph names where in that atlas its coverage sits. A run is one draw
3986    /// however many glyphs it holds, because the coordinates travel on the
3987    /// vertices rather than in the paint — which is the reason vertices carry
3988    /// them at all, text being the highest draw-count content there is.
3989    ///
3990    /// The paint's color is the text color; its shader is otherwise ignored,
3991    /// since the atlas supplies coverage rather than color. Gradient-filled
3992    /// text needs the run drawn into a layer and the gradient drawn through it,
3993    /// which is a thing a caller can already build out of what is here.
3994    pub fn draw_glyphs(
3995        &mut self,
3996        glyphs: &[PositionedGlyph],
3997        atlas: &Atlas,
3998        atlas_slot: u32,
3999        paint: &Paint,
4000    ) -> Result<&mut Self> {
4001        if glyphs.is_empty() || !paint.is_visible() {
4002            return Ok(self);
4003        }
4004        if self.clip.is_some_and(Scissor::is_empty) {
4005            return Ok(self);
4006        }
4007        // A run does not go through `draw_path` either, so it has to notice
4008        // these itself -- and did not, which made it the third call to accept a
4009        // filter and quietly draw without one.
4010        if !paint.image_filter.is_identity() {
4011            return self.draw_glyphs_filtered(glyphs, atlas, atlas_slot, paint);
4012        }
4013        if paint.mask_blur > 0.0 {
4014            // The same four styles a shape gets, over the run instead. A run is
4015            // coverage times one solid color, which is exactly the case where
4016            // blurring the coverage and blurring the result agree -- so unlike
4017            // a mesh, there is nothing here to refuse. This is how a text
4018            // shadow is drawn.
4019            return self.draw_masked(
4020                Masked::Glyphs {
4021                    glyphs,
4022                    atlas,
4023                    slot: atlas_slot,
4024                },
4025                paint,
4026            );
4027        }
4028
4029        let color = match &paint.shader {
4030            Shader::Solid(color) => color.to_array(),
4031            // A run tints one color, so anything else has no meaning here. The
4032            // first stop of a gradient is a guess at what was meant, and a
4033            // guess drawn is worse than a refusal read.
4034            _ => {
4035                return Err(Error::Unsupported(
4036                    "glyphs take a solid color; fill a layer through a gradient instead",
4037                ))
4038            }
4039        };
4040
4041        let to_clip = self.target.projection() * self.transform;
4042        let mut vertices = Vec::with_capacity(glyphs.len() * 4);
4043        let mut indices = Vec::with_capacity(glyphs.len() * 6);
4044        for glyph in glyphs {
4045            let Some(rect) = atlas.get(glyph.key) else {
4046                // A glyph nobody added would sample whatever texel happens to
4047                // sit at the origin, which draws a plausible smudge. Refusing
4048                // names the glyph instead.
4049                return Err(Error::Unsupported(
4050                    "a glyph in this run is not in the atlas",
4051                ));
4052            };
4053            if rect.width == 0 || rect.height == 0 {
4054                // A space occupies no texels and needs no quad, but it still
4055                // travelled through the run rather than being the caller's to
4056                // filter out.
4057                continue;
4058            }
4059
4060            let [u0, v0, u1, v1] = rect.uv(atlas.size());
4061            let [x, y] = glyph.position;
4062            let [w, h] = glyph.size;
4063            let corners = [
4064                ([x, y], [u0, v0]),
4065                ([x + w, y], [u1, v0]),
4066                ([x + w, y + h], [u1, v1]),
4067                ([x, y + h], [u0, v1]),
4068            ];
4069            let base = vertices.len() as u32;
4070            for ([px, py], uv) in corners {
4071                // Homogeneous, so the atlas coordinate beside it is
4072                // interpolated perspective-correctly rather than swimming
4073                // across the quad.
4074                let clip = to_clip.project_homogeneous(Vec2::new(px, py));
4075                vertices.push(Vertex::projected([clip.x, clip.y, clip.z], uv));
4076            }
4077            indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
4078        }
4079        if indices.is_empty() {
4080            return Ok(self);
4081        }
4082
4083        let slot = self.slot_for(TextureSource::Image(atlas_slot));
4084        self.batch.push_mesh(
4085            &vertices,
4086            &indices,
4087            Material::Glyph { color, slot },
4088            paint.color_filter,
4089            paint.blend,
4090            self.clip,
4091            ClipState::content(self.depth),
4092        )?;
4093        Ok(self)
4094    }
4095
4096    /// Draw a mesh of triangles the caller supplied.
4097    ///
4098    /// The one drawing call whose geometry does not come from tessellating a
4099    /// shape, which is why it is also the one that can produce triangles this
4100    /// renderer would never have made: overlapping, degenerate, wound either
4101    /// way. None of that is checked beyond what [`Vertices`] checks at
4102    /// construction, because a mesh is a caller saying what to draw rather
4103    /// than what to draw *around*, and second-guessing it would defeat the
4104    /// point.
4105    ///
4106    /// Not antialiased. Coverage here comes from the rasterizer's own rule
4107    /// rather than from a distance field or a tessellated feather, and the
4108    /// interior edges of a mesh are seams between triangles that must not be
4109    /// feathered at all -- a mesh whose triangles each faded at their borders
4110    /// would show a lattice of its own construction.
4111    ///
4112    /// Texture coordinates, where the mesh carries them, require an image
4113    /// paint: they say where in a texture each vertex reads, and a paint with
4114    /// no texture leaves them meaning nothing. The reverse is allowed -- an
4115    /// image paint with no coordinates maps by position, the same as on any
4116    /// other shape.
4117    pub fn draw_vertices(&mut self, mesh: &Vertices, paint: &Paint) -> Result<&mut Self> {
4118        if mesh.is_empty() || !paint.is_visible() {
4119            return Ok(self);
4120        }
4121        if self.clip.is_some_and(Scissor::is_empty) {
4122            return Ok(self);
4123        }
4124        // The same routing `draw_path` does, and it has to be repeated here
4125        // because a mesh does not go through `draw_path` at all. Left out, an
4126        // image filter on a mesh -- or on an atlas, which is a mesh by the time
4127        // it arrives -- was accepted and silently dropped.
4128        if !paint.image_filter.is_identity() {
4129            return self.draw_vertices_filtered(mesh, paint);
4130        }
4131        if paint.mask_blur > 0.0 {
4132            // A mask blur blurs coverage and then fills through it, so the fill
4133            // has to have a value everywhere the blurred coverage reaches --
4134            // including outside the shape, where the halo is. A paint has one
4135            // there, whether it is a color or a gradient, because a paint is a
4136            // function of position. Per-vertex colors do not: they are defined
4137            // on the mesh's own triangles and nowhere else, and the halo has
4138            // nothing to take its color from.
4139            //
4140            // So the refusal is about where the color comes from rather than
4141            // about meshes. A mesh built from positions alone is filled by the
4142            // paint exactly as a path is, and takes the same route.
4143            if mesh.colors().is_empty() {
4144                return self.draw_masked(Masked::Mesh(mesh), paint);
4145            }
4146            // Refused rather than dropped, and rather than approximated:
4147            // extrapolating vertex colors into the halo would be inventing a
4148            // picture, which is the substitution this renderer refuses.
4149            return Err(Error::Unsupported(
4150                "a mask blur over a mesh with per-vertex colors has no color for \
4151                 its halo; draw the mesh into a blurred layer instead",
4152            ));
4153        }
4154
4155        let textured = !mesh.texture_coords().is_empty();
4156        // An image mesh has a material of its own, which samples the sheet at
4157        // the interpolated coordinate. Every other shader reads its coordinate
4158        // from the fragment's position, and the flag passed to the batch below
4159        // is what redirects it to the vertices instead -- so the material is
4160        // the ordinary one and nothing here has to know which kind it is.
4161        //
4162        // This used to refuse anything but an image, and the refusal said the
4163        // coordinates needed an image paint to read. `dart:ui` reads whatever
4164        // color source the paint carries at a mesh's coordinates, image or not.
4165        //
4166        // It then refused a caller's program alone, on the reasoning that a
4167        // program replaces this renderer's fragment shader outright and so has
4168        // nowhere for a per-vertex coordinate to arrive. That was wrong, and
4169        // wrong about this renderer rather than about upstream: a program
4170        // replaces the *fragment* stage, and the vertex stage it runs behind is
4171        // this renderer's own whatever the fragment does. That stage hands on
4172        // `uv` beside `clip`, and a program declaring the same `VertexOutput`
4173        // -- which one has to, to be a pipeline here at all -- receives both.
4174        //
4175        // So a program reads the mesh's coordinate by reading `in.uv`, and
4176        // chooses, where every built-in shader is told which to use by the flag
4177        // below. `effect_mesh_uv.wgsl` is the fixture that proves it: the
4178        // sibling of `effect.wgsl` differing in one line.
4179        let at_coords = textured && !matches!(paint.shader, Shader::Image { .. });
4180        let material = match (&paint.shader, textured) {
4181            (Shader::Image { .. }, true) => self.mesh_material(&paint.shader)?,
4182            _ if at_coords => self.material_at_texture_coords(&paint.shader),
4183            _ => self.material_for(&paint.shader),
4184        };
4185
4186        let to_clip = self.target.projection() * self.transform;
4187        let coords = mesh.texture_coords();
4188        let colors = mesh.colors();
4189        let vertices: Vec<Vertex> = mesh
4190            .positions()
4191            .iter()
4192            .enumerate()
4193            .map(|(i, position)| {
4194                let clip = to_clip.project_homogeneous(*position);
4195                let uv = coords.get(i).copied().unwrap_or(Vec2::ZERO);
4196                let vertex = Vertex::projected([clip.x, clip.y, clip.z], [uv.x, uv.y]);
4197                match colors.get(i) {
4198                    // Premultiplied here rather than in the shader, because
4199                    // what the rasterizer interpolates between two vertices is
4200                    // what gets multiplied in, and interpolating straight color
4201                    // across an edge whose alpha varies gives a color neither
4202                    // end asked for.
4203                    Some(color) => vertex.with_color(premultiplied(*color)),
4204                    None => vertex,
4205                }
4206            })
4207            .collect();
4208
4209        self.batch.push_mesh_tinted(
4210            &vertices,
4211            mesh.indices(),
4212            material,
4213            paint.color_filter,
4214            paint.blend,
4215            self.clip,
4216            ClipState::content(self.depth),
4217            paint.tint_blend,
4218            // Where the coordinates say something the material cannot work out
4219            // for itself. An image mesh is excluded because it already reads
4220            // them, through a material of its own; everything else -- a
4221            // gradient, a caller's program -- derives its coordinate from the
4222            // fragment's position unless told otherwise, and this is the
4223            // telling. The material above was built to match.
4224            at_coords,
4225        )?;
4226        Ok(self)
4227    }
4228
4229    /// Draw many pieces of one sheet, each with its own transform, in one draw.
4230    ///
4231    /// This is `draw_vertices` with the mesh built for you, and building it is
4232    /// the part worth not getting wrong: each sprite is a quad running from
4233    /// the origin to its source's size, placed by its transform, with the
4234    /// source rectangle divided by the sheet's size to become the
4235    /// coordinates its corners read. A caller assembling that themselves has
4236    /// four chances per sprite to transpose an axis.
4237    ///
4238    /// One draw for the whole batch: a hundred sprites from one sheet differ
4239    /// only in their vertices, so they have no reason to be a hundred draws.
4240    /// Anything that would split them -- a different sheet, a different paint
4241    /// -- is a second call.
4242    ///
4243    /// A loop over [`Self::draw_vertices`] would end up as one draw too, since
4244    /// a batch merges adjacent draws that differ in nothing. What this saves
4245    /// is the per-sprite arithmetic rather than the draw calls.
4246    ///
4247    /// `sheet` is the size of the uploaded texture in texels, which the paint
4248    /// does not carry: a recording is built without touching a device and has
4249    /// never seen how large the texture is.
4250    ///
4251    /// Per-sprite colors, which `dart:ui` offers here, need a color per vertex
4252    /// and this vertex format has none. A tint on the paint applies to the
4253    /// whole batch.
4254    pub fn draw_atlas(
4255        &mut self,
4256        sprites: &[Sprite],
4257        sheet: Extent2D,
4258        paint: &Paint,
4259    ) -> Result<&mut Self> {
4260        if sprites.is_empty() || !paint.is_visible() {
4261            return Ok(self);
4262        }
4263        if sheet.width == 0 || sheet.height == 0 {
4264            return Err(Error::Unsupported(
4265                "a sprite sheet with no texels has nothing to read",
4266            ));
4267        }
4268        if !sprites.iter().all(|s| s.source.is_finite()) {
4269            return Err(Error::Unsupported(
4270                "a sprite's source rectangle is not a finite number",
4271            ));
4272        }
4273
4274        let texels = Vec2::new(sheet.width as f32, sheet.height as f32);
4275        let mut positions = Vec::with_capacity(sprites.len() * 4);
4276        let mut coords = Vec::with_capacity(sprites.len() * 4);
4277        let mut colors = Vec::with_capacity(sprites.len() * 4);
4278        let mut indices = Vec::with_capacity(sprites.len() * 6);
4279        for sprite in sprites {
4280            let source = &sprite.source;
4281            let size = Vec2::new(source.width, source.height);
4282            let origin = Vec2::new(source.x, source.y);
4283            // Corners in the same order for both, so a coordinate and the
4284            // position it belongs to are written by one step of the loop and
4285            // cannot come apart.
4286            let corners = [
4287                Vec2::ZERO,
4288                Vec2::new(size.x, 0.0),
4289                size,
4290                Vec2::new(0.0, size.y),
4291            ];
4292            let base = positions.len() as u32;
4293            for corner in corners {
4294                positions.push(sprite.transform.transform_point2(corner));
4295                coords.push((origin + corner) / texels);
4296                // The same color at all four corners: a sprite is tinted as a
4297                // whole, and a gradient across one is a mesh rather than a
4298                // sprite.
4299                colors.push(sprite.color);
4300            }
4301            indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
4302        }
4303
4304        let mesh = Vertices::full(VertexMode::Triangles, positions, coords, colors, indices)?;
4305        self.draw_vertices(&mesh, paint)
4306    }
4307
4308    /// The material for a mesh reading a texture at its own coordinates.
4309    ///
4310    /// Separate from [`Self::material_for`] because an image drawn on a shape
4311    /// and an image drawn on a mesh are different materials rather than one
4312    /// material in two modes -- the mapping, and the source rectangle that
4313    /// selects part of a sheet, are both things the vertices have already
4314    /// said. A caller wanting one sprite from a sheet states its texels in the
4315    /// coordinates.
4316    fn mesh_material(&mut self, shader: &Shader) -> Result<Material> {
4317        let Shader::Image {
4318            slot,
4319            alpha,
4320            tile,
4321            tint,
4322            source,
4323            sampling,
4324            ..
4325        } = shader
4326        else {
4327            return Err(Error::Unsupported("a mesh material needs an image paint"));
4328        };
4329        // A source rectangle and a per-vertex coordinate are two answers to
4330        // the same question, and applying one on top of the other would mean a
4331        // coordinate of one landed at the rectangle's far edge rather than at
4332        // the texture's. Refused rather than ignored: a caller who set it
4333        // meant something by it, and the mesh is where they say it instead.
4334        if *source != Rect::new(0.0, 0.0, 1.0, 1.0) {
4335            return Err(Error::Unsupported(
4336                "a textured mesh states its own coordinates; put the source rectangle in them",
4337            ));
4338        }
4339        let table_slot = self.slot_for(TextureSource::Image(*slot));
4340        Ok(Material::Mesh {
4341            slot: table_slot,
4342            alpha: *alpha,
4343            tint: tint.to_array(),
4344            tile: *tile,
4345            sampling: *sampling,
4346        })
4347    }
4348
4349    /// Draw a run of points, joined as the mode says.
4350    ///
4351    /// A point is a segment of no length, which is what decides how it looks:
4352    /// the stroke's cap is the whole of the shape. A round cap gives a dot the
4353    /// width across, a square cap a square of that side, and a butt cap --
4354    /// which adds nothing to either end of a segment -- gives nothing at all.
4355    /// That is not a special case being handled; it is the only reading of a
4356    /// zero-length segment that stays consistent with how every other stroke
4357    /// is drawn.
4358    ///
4359    /// Takes the stroke's width even from a paint asking to fill, since there
4360    /// is no interior to fill and a point with no width is not a shape.
4361    pub fn draw_points(
4362        &mut self,
4363        mode: PointMode,
4364        points: &[Vec2],
4365        paint: &Paint,
4366    ) -> Result<&mut Self> {
4367        let stroke = match &paint.style {
4368            Style::Stroke(stroke) => *stroke,
4369            Style::Fill => StrokeStyle::default(),
4370        };
4371        // Negative only, which is upstream's guard -- `PointFieldGeometry`
4372        // refuses `radius_ < 0.0` and widens everything else. A width of zero is
4373        // a point one pixel across, the same reading a zero-width stroke gets.
4374        if !stroke.width.is_finite() || stroke.width < 0.0 {
4375            return Ok(self);
4376        }
4377
4378        match mode {
4379            // One draw for the whole field, which is what upstream's
4380            // `PointFieldGeometry` produces and what this used to fail to.
4381            //
4382            // It used to be a draw per point, and the reason is worth keeping
4383            // because it is not the obvious one. The other two modes below
4384            // issue a call per segment and still come out as one draw: a batch
4385            // merges draws that share a material, and consecutive segments do.
4386            // A point did not, because the analytic circle it went through
4387            // carries its center in `to_local` -- so two points at two places
4388            // were two materials, and no number of them could merge.
4389            //
4390            // `Material::PointField` is the same field with the center taken
4391            // off the paint and put on the vertices, as the unit circle's
4392            // corners in the texture coordinate. Every point is then the same
4393            // material and the field is one draw, with the edge each of them
4394            // had: the shader differentiates the implicit function across the
4395            // pixel rather than forming a distance, and a derivative of an
4396            // interpolated value is as good as one of a computed value.
4397            PointMode::Points => {
4398                let radius = point_radius(self.transform, stroke.width);
4399                if !matches!(stroke.cap, LineCap::Round | LineCap::Square) {
4400                    // A butt cap extends a segment by nothing, and nothing is
4401                    // what a segment of no length becomes.
4402                    return Ok(self);
4403                }
4404                let round = matches!(stroke.cap, LineCap::Round);
4405                // A mask blur or an image filter wants a layer around the
4406                // draw, which is machinery the per-point route already reaches
4407                // through `draw_circle` and this one skips entirely. Two tests
4408                // that walk every call taking a paint caught exactly that: the
4409                // field accepted a blur and drew what it draws without one.
4410                //
4411                // The condition is the same one `analytic_stroke` states for
4412                // the same reason, and it is worth keeping the shape of that
4413                // rule rather than a list: every route that needs a layer has
4414                // to decline here.
4415                if paint.mask_blur > 0.0 || !paint.image_filter.is_identity() {
4416                    return self.draw_points_individually(points, &stroke, paint);
4417                }
4418                let material = self.material_for(&paint.shader);
4419                let Material::Solid(color) = material else {
4420                    // A field takes one color, which is what a point field is.
4421                    // Anything else keeps the old route, where each point is
4422                    // its own shape and can carry its own mapping.
4423                    return self.draw_points_individually(points, &stroke, paint);
4424                };
4425
4426                // A round cap's quad is a pixel wider than the disc, for the
4427                // reason `draw_analytic` outsets its own: the coverage ramp
4428                // runs half a pixel either side of the edge, and a quad ending
4429                // exactly on the circle clips the outer half of its own
4430                // antialiasing. Measured without it, ninety-two pixels of the
4431                // frame differed from the same dots drawn one at a time, at
4432                // the four places the circle meets its quad.
4433                //
4434                // The texture coordinate is scaled to match, so that the unit
4435                // circle still falls where the disc's edge is rather than at
4436                // the corner of the widened quad. A square cap is the quad
4437                // itself and takes neither: its coordinates are zero, where
4438                // the field is saturated, so it covers what it is given.
4439                let (extent, coord) = if round {
4440                    (radius + 1.0, (radius + 1.0) / radius.max(1e-6))
4441                } else {
4442                    (radius, 0.0)
4443                };
4444                let to_clip = self.target.projection() * self.transform;
4445                let mut vertices: Vec<Vertex> = Vec::with_capacity(points.len() * 4);
4446                let mut indices: Vec<u32> = Vec::with_capacity(points.len() * 6);
4447                for point in points {
4448                    if !point.is_finite() {
4449                        continue;
4450                    }
4451                    let base = vertices.len() as u32;
4452                    for (dx, dy) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
4453                        let at = Vec2::new(point.x + dx * extent, point.y + dy * extent);
4454                        let clip = to_clip.project_homogeneous(at);
4455                        vertices.push(Vertex::projected(
4456                            [clip.x, clip.y, clip.z],
4457                            [dx * coord, dy * coord],
4458                        ));
4459                    }
4460                    indices.extend([base, base + 1, base + 2, base, base + 2, base + 3]);
4461                }
4462                if indices.is_empty() {
4463                    return Ok(self);
4464                }
4465                self.batch.push_mesh_tinted(
4466                    &vertices,
4467                    &indices,
4468                    Material::PointField { color },
4469                    paint.color_filter,
4470                    paint.blend,
4471                    self.clip,
4472                    ClipState::content(self.depth),
4473                    paint.tint_blend,
4474                    false,
4475                )?;
4476            }
4477            PointMode::Lines => {
4478                for pair in points.chunks_exact(2) {
4479                    self.draw_line(pair[0], pair[1], paint)?;
4480                }
4481            }
4482            PointMode::Polygon => {
4483                if points.len() >= 2 {
4484                    let mut b = PathBuilder::new();
4485                    b.move_to(points[0]);
4486                    for point in &points[1..] {
4487                        b.line_to(*point);
4488                    }
4489                    self.draw_path(&b.build(), paint)?;
4490                }
4491            }
4492        }
4493        Ok(self)
4494    }
4495
4496    /// The field drawn a point at a time, for a paint a field cannot carry.
4497    ///
4498    /// A point field is one color by construction -- the material holds one --
4499    /// so a gradient or an image paint still goes through the shape each point
4500    /// has, and costs a draw each. That is the old behavior kept for the case
4501    /// it was right for rather than a fallback nobody exercises: `dart:ui`
4502    /// allows any paint on `drawPoints`, and a caller who asks for a gradient
4503    /// across a field should get one.
4504    fn draw_points_individually(
4505        &mut self,
4506        points: &[Vec2],
4507        stroke: &StrokeStyle,
4508        paint: &Paint,
4509    ) -> Result<&mut Self> {
4510        // Widened on the same terms the field route is, so which route a point
4511        // takes stays invisible -- this one is reached for a mask blur, an image
4512        // filter or a shader, none of which changes how small a point may be.
4513        let radius = point_radius(self.transform, stroke.width);
4514        // Filled rather than stroked: what is being drawn is the cap itself,
4515        // and asking the stroker for a segment of no length is asking it for a
4516        // direction that does not exist.
4517        let dot = paint.clone().with_style(Style::Fill);
4518        for point in points {
4519            if !point.is_finite() {
4520                continue;
4521            }
4522            match stroke.cap {
4523                LineCap::Round => {
4524                    self.draw_circle(*point, radius, &dot)?;
4525                }
4526                LineCap::Square => {
4527                    self.draw_rect(
4528                        Rect::new(
4529                            point.x - radius,
4530                            point.y - radius,
4531                            point.x + radius,
4532                            point.y + radius,
4533                        ),
4534                        &dot,
4535                    )?;
4536                }
4537                LineCap::Butt => {}
4538            }
4539        }
4540        Ok(self)
4541    }
4542
4543    /// Draw a finished recording into this one.
4544    ///
4545    /// `dart:ui` calls this `drawPicture`, and it is a smaller thing here than
4546    /// there, which is worth saying plainly. An `SkPicture` is a command list,
4547    /// so replaying one under a new transform re-runs the commands and
4548    /// re-tessellates at the new scale. A [`Recording`] is already tessellated:
4549    /// its paths were flattened at a tolerance taken from the transform in force
4550    /// when they were recorded, and its vertices are in clip space. Magnified,
4551    /// it shows the polygon it was flattened to.
4552    ///
4553    /// So this composes scenes at about the scale they were recorded at. It is
4554    /// not the reuse optimization the same call is elsewhere, and a caller who
4555    /// wants that should re-record.
4556    ///
4557    /// What it costs is a layer, because that is what it is: the recording's
4558    /// passes are taken as they are and its root becomes an image this canvas
4559    /// samples. Nothing is re-recorded and no geometry is touched, which is
4560    /// what makes it cheap and also what makes it unable to re-tessellate.
4561    ///
4562    /// # Slots
4563    ///
4564    /// A recording naming a caller's image by index keeps that index. The two
4565    /// recordings are submitted with one image table, so an index means the
4566    /// same thing in both -- a caller composing recordings that disagree about
4567    /// what image three is has to renumber before recording, which is a thing
4568    /// they can see and this call cannot.
4569    ///
4570    /// Its layers and its baked gradients are renumbered, since those name
4571    /// positions in lists this recording is appending to.
4572    pub fn draw_recording(&mut self, recording: &Recording, paint: &Paint) -> Result<&mut Self> {
4573        if recording.is_empty() || !paint.is_visible() {
4574            return Ok(self);
4575        }
4576        if self.clip.is_some_and(Scissor::is_empty) {
4577            return Ok(self);
4578        }
4579
4580        // Where the passes and ramps being appended will land. Taken before
4581        // anything is pushed, because every index inside the recording is
4582        // relative to lists that start here.
4583        let pass_offset = self.finished.len();
4584        let ramp_offset = self.ramps.len();
4585        self.ramps.extend(recording.ramps.iter().cloned());
4586
4587        for pass in &recording.passes {
4588            let mut pass = pass.clone();
4589            for source in &mut pass.sources {
4590                *source = match *source {
4591                    // A layer names a pass by position in the recording it came
4592                    // from, and that recording's passes are now further along.
4593                    TextureSource::Layer(index) => TextureSource::Layer(index + pass_offset),
4594                    TextureSource::Ramp(index) => TextureSource::Ramp(index + ramp_offset),
4595                    // The caller's own table, which both recordings share.
4596                    TextureSource::Image(index) => TextureSource::Image(index),
4597                };
4598            }
4599            self.finished.push(pass);
4600        }
4601
4602        // The recording's root is the image this canvas now samples, and every
4603        // other pass it brought is a layer feeding that one.
4604        let root = self.finished.len() - 1;
4605        let slot = self.slot_for(TextureSource::Layer(root));
4606
4607        // The recording rendered at its own extent, and it lands here at that
4608        // size with its top-left at the origin of the current transform. A
4609        // recording is a picture rather than a shape, so it has no bounds of
4610        // its own to place -- where it goes is what the transform says.
4611        let extent = recording.extent;
4612        let placement = Rect::new(0.0, 0.0, extent.width as f32, extent.height as f32);
4613        // The mapping from a fragment's clip position back to a texel of the
4614        // recording, which is the same pair a layer composite builds -- except
4615        // that a layer sits at a known place in its parent and a recording is
4616        // placed by the transform, so the mapping is built from that instead.
4617        //
4618        // Clip space runs from -1 to 1 and upward, so the axes are halved and Y
4619        // is negated. Inverting the transform is what carries a fragment back
4620        // into the recording's own coordinates; a transform that folds the
4621        // plane has no inverse, and the geometry collapses with it, so both
4622        // halves go together and nothing is drawn.
4623        let to_clip = self.target.projection() * self.transform;
4624        if to_clip.determinant().abs() <= f32::EPSILON {
4625            return Ok(self);
4626        }
4627        // The recording's texture spans its own extent from the origin, so
4628        // that rectangle carried through the transform is the placement, and
4629        // the mapping the shader wants is its inverse.
4630        let picture_to_clip =
4631            to_clip * Affine2::from_scale(Vec2::new(extent.width as f32, extent.height as f32));
4632        if !picture_to_clip.is_finite() {
4633            return Ok(self);
4634        }
4635        let material = Material::Image {
4636            to_local: invert_to_local(picture_to_clip),
4637            slot,
4638            alpha: 1.0,
4639            tile: TileMode::Clamp,
4640            // At its own size a texel lands on a pixel, and under a transform
4641            // the recording is resampled -- which is what a picture drawn
4642            // scaled means, and the same answer a matrix image filter gives.
4643            sampling: Sampling::Linear,
4644            source: [0.0, 0.0, 1.0, 1.0],
4645            tint: [1.0, 1.0, 1.0, 1.0],
4646        };
4647        let render_paint = RenderPaint {
4648            material,
4649            filter: paint.color_filter,
4650            blend: paint.blend,
4651            clip: self.clip,
4652            stencil: ClipState::content(self.depth),
4653        };
4654        self.renderer.fill_into(
4655            &mut self.batch,
4656            &placement.to_path(),
4657            self.transform,
4658            &render_paint,
4659        )?;
4660        Ok(self)
4661    }
4662
4663    /// Fill the ring between two rounded rectangles.
4664    ///
4665    /// One path of two contours filled by the even-odd rule, which is what
4666    /// makes the inner one a hole rather than a second ring drawn on top. A
4667    /// caller could assemble this, and the reason not to leave them to it is
4668    /// the rule: two contours wound the same way fill solid under the nonzero
4669    /// rule and hollow under even-odd, and which one a border needs is not
4670    /// something to rediscover per call site.
4671    pub fn draw_drrect(
4672        &mut self,
4673        outer: Rect,
4674        outer_radius: f32,
4675        inner: Rect,
4676        inner_radius: f32,
4677        paint: &Paint,
4678    ) -> Result<&mut Self> {
4679        if outer.is_empty() {
4680            return Ok(self);
4681        }
4682        self.draw_drrect_with_radii(
4683            outer,
4684            [[outer_radius; 2]; 4],
4685            inner,
4686            [[inner_radius; 2]; 4],
4687            paint,
4688        )
4689    }
4690
4691    /// A ring between two rounded rectangles whose corners need not match.
4692    ///
4693    /// [`Self::draw_drrect`] with the eight numbers each, and the same shape:
4694    /// two contours filled even-odd, which is what makes the inner one a hole.
4695    /// Each rectangle's radii are fitted to it on its own, since a ring is two
4696    /// rounded rectangles and not one shape with a thickness.
4697    pub fn draw_drrect_with_radii(
4698        &mut self,
4699        outer: Rect,
4700        outer_radii: RoundingRadii,
4701        inner: Rect,
4702        inner_radii: RoundingRadii,
4703        paint: &Paint,
4704    ) -> Result<&mut Self> {
4705        if outer.is_empty() {
4706            return Ok(self);
4707        }
4708        let mut b = PathBuilder::new().with_fill_rule(FillRule::EvenOdd);
4709        outer.add_rounded_contour_with_radii(&mut b, outer.fitted_radii(outer_radii));
4710        if !inner.is_empty() {
4711            inner.add_rounded_contour_with_radii(&mut b, inner.fitted_radii(inner_radii));
4712        }
4713        self.draw_path(&b.build(), paint)
4714    }
4715
4716    /// Fill everything the clip still admits.
4717    ///
4718    /// `dart:ui` calls this `drawPaint`. What it fills is the clip rather than
4719    /// the target, which is why it is a call and not a rectangle a caller
4720    /// writes: the rectangle they would need is the clip's bounds carried back
4721    /// through the current transform, and getting that wrong is invisible
4722    /// until a transform is in force.
4723    ///
4724    /// The bounds are conservative, so this may cover more than the clip
4725    /// admits -- and the clip then removes the excess, which is what makes a
4726    /// superset the safe direction to be wrong in.
4727    pub fn draw_paint(&mut self, paint: &Paint) -> Result<&mut Self> {
4728        let bounds = self.local_clip_bounds();
4729        if bounds.is_empty() {
4730            return Ok(self);
4731        }
4732        self.draw_rect(bounds, paint)
4733    }
4734
4735    /// Fill everything the clip admits with one color.
4736    ///
4737    /// `dart:ui` calls this `drawColor`. Distinct from [`Self::clear`], which
4738    /// replaces the whole target and ignores the clip: this is a draw, so it
4739    /// blends and it obeys what is in force.
4740    pub fn draw_color(&mut self, color: Color, blend: BlendMode) -> Result<&mut Self> {
4741        self.draw_paint(&Paint::fill(color).with_blend(blend))
4742    }
4743
4744    /// Draw an image stretched by its middle, keeping its corners.
4745    ///
4746    /// `dart:ui` calls this `drawImageNine`, and it is the shape every
4747    /// resizable panel with a border is made of. `center` is the part of the
4748    /// image, in texels, that may stretch; what surrounds it is divided into
4749    /// eight pieces that stretch along one axis or neither.
4750    ///
4751    /// A caller could write the nine draws. The reason not to leave them to it
4752    /// is the rule rather than the arithmetic: which pieces stretch in which
4753    /// direction is the whole of what a nine-patch means, and nine call sites
4754    /// are nine chances to stretch a corner.
4755    ///
4756    /// The paint supplies the blend, the filters and whether the edges are
4757    /// antialiased; its shader is replaced, since each of the nine pieces
4758    /// needs its own. A caller wanting the whole thing faded should draw it
4759    /// into a layer, which is what fading a group means everywhere else here.
4760    ///
4761    /// A center reaching an edge leaves pieces of no width or height, and
4762    /// those are skipped rather than drawn empty. A center outside the image
4763    /// entirely is refused, since there is no reading of it that leaves nine
4764    /// pieces.
4765    pub fn draw_image_nine(
4766        &mut self,
4767        slot: u32,
4768        size: Extent2D,
4769        center: Rect,
4770        into: Rect,
4771        paint: &Paint,
4772    ) -> Result<&mut Self> {
4773        if size.width == 0 || size.height == 0 || into.is_empty() {
4774            return Ok(self);
4775        }
4776        let (w, h) = (size.width as f32, size.height as f32);
4777        if center.left < 0.0
4778            || center.top < 0.0
4779            || center.right > w
4780            || center.bottom > h
4781            || center.right < center.left
4782            || center.bottom < center.top
4783        {
4784            return Err(Error::Unsupported(
4785                "a nine-patch center must lie within the image it divides",
4786            ));
4787        }
4788        if paint.mask_blur > 0.0 {
4789            // A nine-patch is nine quads, and each is drawn on its own. A mask
4790            // blur asked for here would soften each of them separately -- nine
4791            // haloes with seams between them rather than one softened patch --
4792            // which is not what was asked for and is not worth guessing at.
4793            //
4794            // Refused deliberately, because it used to be refused by accident:
4795            // the pieces are drawn with an image paint, and a mask blur over
4796            // anything that varied was refused wholesale further down. That is
4797            // no longer true, so the reason has to live where it applies.
4798            return Err(Error::Unsupported(
4799                "a mask blur over a nine-patch; its pieces are drawn separately \
4800                 and would each be softened on their own",
4801            ));
4802        }
4803
4804        // The three spans along each axis, in texels and then in the
4805        // destination. The outer two keep their size and the middle takes
4806        // whatever is left, which is the whole of the rule.
4807        let source_x = [0.0, center.left, center.right, w];
4808        let source_y = [0.0, center.top, center.bottom, h];
4809        let left_keep = center.left;
4810        let right_keep = w - center.right;
4811        let top_keep = center.top;
4812        let bottom_keep = h - center.bottom;
4813        // A destination too small to hold both fixed edges would give the
4814        // middle a negative size, so the edges are scaled down together rather
4815        // than one of them overrunning the other.
4816        let squeeze_x = ((into.right - into.left) / (left_keep + right_keep)).min(1.0);
4817        let squeeze_y = ((into.bottom - into.top) / (top_keep + bottom_keep)).min(1.0);
4818        let dest_x = [
4819            into.left,
4820            into.left + left_keep * squeeze_x,
4821            into.right - right_keep * squeeze_x,
4822            into.right,
4823        ];
4824        let dest_y = [
4825            into.top,
4826            into.top + top_keep * squeeze_y,
4827            into.bottom - bottom_keep * squeeze_y,
4828            into.bottom,
4829        ];
4830
4831        // One mesh rather than nine draws, which is what this used to be.
4832        //
4833        // Each patch reads a different part of the image into a different
4834        // place, and the obvious way to say that is a shader per patch with
4835        // its own source rectangle -- nine paints, nine draws. Carrying the
4836        // coordinates on the vertices instead says the same thing in one
4837        // draw, which is how `draw_atlas` has always done it, and a nine-patch
4838        // is the same shape of work: several quads out of one sheet.
4839        //
4840        // What made the nine separate is that a per-draw source rectangle also
4841        // *clamps* to itself, so a patch could not sample its neighbor. Merged
4842        // naively that clamping goes and the seams bleed -- measured, twelve
4843        // hundred pixels of a hundred-and-twenty-eight-square frame, by as
4844        // much as a hundred and seventy levels, which on the one primitive
4845        // whose whole purpose is stretching without artifacts is not a
4846        // trade worth making.
4847        //
4848        // The half-texel inset below is what buys it back. A linear sample
4849        // reaches exactly half a texel past its coordinate, so pulling each
4850        // patch's edge in by that much removes the reach across the boundary
4851        // rather than compensating for it. With it the merged draw is
4852        // byte-identical to the nine: zero pixels differ, at the same worst
4853        // case of zero. The sampling is this call's own rather than the
4854        // caller's, and is linear, which is what makes half a texel the right
4855        // number rather than a guess.
4856        let (half_u, half_v) = (0.5 / w, 0.5 / h);
4857        let mut positions = Vec::with_capacity(36);
4858        let mut coords = Vec::with_capacity(36);
4859        let mut indices = Vec::with_capacity(54);
4860        for row in 0..3 {
4861            for column in 0..3 {
4862                let source = Rect::new(
4863                    source_x[column] / w,
4864                    source_y[row] / h,
4865                    source_x[column + 1] / w,
4866                    source_y[row + 1] / h,
4867                );
4868                let destination = Rect::new(
4869                    dest_x[column],
4870                    dest_y[row],
4871                    dest_x[column + 1],
4872                    dest_y[row + 1],
4873                );
4874                if source.is_empty() || destination.is_empty() {
4875                    continue;
4876                }
4877                // Inset toward the middle, and never past it: a patch one texel
4878                // across has nothing to give up, and an inset that crossed
4879                // would turn the quad inside out.
4880                let mid_u = (source.left + source.right) * 0.5;
4881                let mid_v = (source.top + source.bottom) * 0.5;
4882                let u0 = (source.left + half_u).min(mid_u);
4883                let u1 = (source.right - half_u).max(mid_u);
4884                let v0 = (source.top + half_v).min(mid_v);
4885                let v1 = (source.bottom - half_v).max(mid_v);
4886
4887                let base = positions.len() as u32;
4888                positions.extend([
4889                    Vec2::new(destination.left, destination.top),
4890                    Vec2::new(destination.right, destination.top),
4891                    Vec2::new(destination.right, destination.bottom),
4892                    Vec2::new(destination.left, destination.bottom),
4893                ]);
4894                coords.extend([
4895                    Vec2::new(u0, v0),
4896                    Vec2::new(u1, v0),
4897                    Vec2::new(u1, v1),
4898                    Vec2::new(u0, v1),
4899                ]);
4900                indices.extend([base, base + 1, base + 2, base, base + 2, base + 3]);
4901            }
4902        }
4903        if positions.is_empty() {
4904            return Ok(self);
4905        }
4906
4907        let mesh = crate::vertices::Vertices::indexed(
4908            crate::vertices::VertexMode::Triangles,
4909            positions,
4910            coords,
4911            indices,
4912        )?;
4913        // The paint supplies everything but the shader -- its blend, its
4914        // filters, whether it antialiases -- and the shader is the sheet, read
4915        // by the coordinates the vertices carry.
4916        let painted = paint.clone().with_shader(Shader::Image {
4917            slot,
4918            rect: into,
4919            alpha: 1.0,
4920            tile: TileMode::Clamp,
4921            source: Rect::new(0.0, 0.0, 1.0, 1.0),
4922            tint: Color::WHITE,
4923            sampling: Sampling::Linear,
4924        });
4925        self.draw_vertices(&mesh, &painted)
4926    }
4927
4928    /// A line, and for a hairline one snapped to a pixel.
4929    ///
4930    /// The snap is the third part of upstream's minimum-size rule and the one
4931    /// that decides whether a hairline *looks* like one. A line one pixel wide
4932    /// whose center sits on a pixel boundary covers half of each of the two rows
4933    /// it straddles, so it draws gray and two pixels soft; the same line centered
4934    /// in a row covers it and draws black and one pixel crisp. Upstream moves it:
4935    /// `LineGeometry::GetPositionBuffer` carries the endpoints into device space,
4936    /// drops the transform, and rounds the constant coordinate to a half with
4937    /// `RoundToHalf` -- which is a pixel's middle.
4938    ///
4939    /// Gated exactly as upstream gates it, on `width_ == 0.f` and
4940    /// `IsTranslationScaleOnly`. A width the caller stated is a width to respect,
4941    /// and under a rotation or a shear no axis-aligned line stays one.
4942    ///
4943    /// Narrower than upstream in one way, which is deliberate: a paint wanting a
4944    /// layer keeps the ordinary path. A mask blur's sigma is stated in user space
4945    /// and this draws in device space, so the two would disagree under a scale --
4946    /// and a snap is worth half a pixel, not a wrong blur.
4947    pub fn draw_line(&mut self, from: Vec2, to: Vec2, paint: &Paint) -> Result<&mut Self> {
4948        if let Some((p0, p1)) = self.hairline_snap(from, to, paint) {
4949            let mut b = PathBuilder::new();
4950            b.move_to(p0).line_to(p1);
4951            let path = b.build();
4952            // Drawn in device space, so the widening reads a basis of one and
4953            // gives exactly the pixel the snap aligned it to.
4954            let held = self.transform;
4955            self.transform = Transform2D::IDENTITY;
4956            let drawn = self.draw_path(&path, paint).map(|_| ());
4957            self.transform = held;
4958            drawn?;
4959            return Ok(self);
4960        }
4961        let mut b = PathBuilder::new();
4962        b.move_to(from).line_to(to);
4963        let path = b.build();
4964        self.draw_path(&path, paint)
4965    }
4966
4967    /// The device-space endpoints of a hairline worth snapping, or `None`.
4968    ///
4969    /// `None` covers every case that keeps the ordinary path: a stated width, a
4970    /// transform that is not a translation and a scale, a paint that wants a
4971    /// layer, and a line that is not axis-aligned once transformed.
4972    fn hairline_snap(&self, from: Vec2, to: Vec2, paint: &Paint) -> Option<(Vec2, Vec2)> {
4973        let Style::Stroke(stroke) = &paint.style else {
4974            return None;
4975        };
4976        if stroke.width != 0.0 {
4977            return None;
4978        }
4979        if paint.mask_blur > 0.0 || !paint.image_filter.is_identity() {
4980            return None;
4981        }
4982        // A solid color only, and this is the guard that matters most. Every
4983        // other shader is *mapped* by the transform -- a gradient's stops are
4984        // stated in the space the draw was recorded in, an image's source
4985        // rectangle likewise -- and this draw hands the batch an identity
4986        // transform, so the mapping would be taken without the scale. Measured
4987        // while writing this: a hairline under a doubling read its gradient at
4988        // sixteen per cent where the same line a pixel wide read it at four.
4989        //
4990        // Such a hairline keeps the ordinary path and stays half a pixel off
4991        // center, which is the right way round: a snap is worth half a pixel,
4992        // and a shader read in the wrong place is worth nothing.
4993        if !matches!(paint.shader, Shader::Solid(_)) {
4994            return None;
4995        }
4996        let affine = self.transform.to_affine()?;
4997        // A translation and a scale, which is what leaves an axis-aligned line
4998        // axis-aligned. Read off the matrix rather than tracked, so a caller who
4999        // arrived here through `concat` is judged on what they actually have.
5000        let m = affine.matrix2;
5001        if m.x_axis.y != 0.0 || m.y_axis.x != 0.0 {
5002            return None;
5003        }
5004        let (p0, p1) = (affine.transform_point2(from), affine.transform_point2(to));
5005        if !p0.is_finite() || !p1.is_finite() {
5006            return None;
5007        }
5008        // A pixel's middle, which is where a one-pixel line has to sit to cover
5009        // one pixel. Upstream's `RoundToHalf`.
5010        let to_half = |v: f32| v.floor() + 0.5;
5011        // Vertical first, matching upstream's order, and either branch leaves
5012        // the other coordinate alone -- the line's length is the caller's.
5013        if (p0.x - p1.x).abs() < CLOSE_ENOUGH {
5014            let x = to_half(p0.x);
5015            return Some((Vec2::new(x, p0.y), Vec2::new(x, p1.y)));
5016        }
5017        if (p0.y - p1.y).abs() < CLOSE_ENOUGH {
5018            let y = to_half(p0.y);
5019            return Some((Vec2::new(p0.x, y), Vec2::new(p1.x, y)));
5020        }
5021        None
5022    }
5023
5024    /// Close a layer: file its pass, and composite it onto the parent.
5025    /// The device-space rectangle a finished layer's draws actually cover.
5026    ///
5027    /// Read off the geometry rather than accumulated draw by draw, because the
5028    /// geometry is what gets rasterized. Anything covering the whole target --
5029    /// a `clear`, a fill the size of the frame -- has a quad that says so, so
5030    /// the cases upstream has to name as "unbounded content" need no special
5031    /// case here: their coverage comes out as the whole target and nothing is
5032    /// narrowed.
5033    ///
5034    /// Vertices are homogeneous clip positions for `target`, so this is the
5035    /// inverse of the projection they went through:
5036    /// `viewport_projection` maps a device offset `d` to `2d/extent - 1` in x
5037    /// and `1 - 2d/extent` in y.
5038    ///
5039    /// `None` where there is no geometry, and where any vertex sits at or past
5040    /// the vanishing line -- the first has nothing to bound, and the second has
5041    /// no finite bound to give.
5042    fn covered(batch: &Batch, target: Target) -> Option<Rect> {
5043        let (w, h) = (target.extent.width as f32, target.extent.height as f32);
5044        let (mut left, mut top) = (f32::INFINITY, f32::INFINITY);
5045        let (mut right, mut bottom) = (f32::NEG_INFINITY, f32::NEG_INFINITY);
5046        for vertex in batch.vertices() {
5047            let [x, y, w_clip] = vertex.position;
5048            // The rasterizer divides by this, so a vertex on the vanishing line
5049            // has no device position at all and one just past it has a position
5050            // on the wrong side. Neither can be bounded, and the honest answer
5051            // is to narrow nothing.
5052            if !w_clip.is_finite() || w_clip.abs() <= 1e-6 || !x.is_finite() || !y.is_finite() {
5053                return None;
5054            }
5055            let dx = (x / w_clip + 1.0) * 0.5 * w;
5056            let dy = (1.0 - y / w_clip) * 0.5 * h;
5057            left = left.min(dx);
5058            right = right.max(dx);
5059            top = top.min(dy);
5060            bottom = bottom.max(dy);
5061        }
5062        if !(right > left && bottom > top) {
5063            return None;
5064        }
5065        Some(Rect::new(
5066            target.origin.x + left,
5067            target.origin.y + top,
5068            target.origin.x + right,
5069            target.origin.y + bottom,
5070        ))
5071    }
5072
5073    /// The target a layer's content would fit in, if it is worth narrowing to.
5074    ///
5075    /// `None` keeps the target the layer was opened with, which is the whole
5076    /// parent. That is the answer whenever the content covers it anyway, and
5077    /// whenever the arithmetic cannot be trusted to cover everything the pass
5078    /// will draw.
5079    fn narrowed(
5080        &self,
5081        batch: &Batch,
5082        filter: Option<&ImageFilter>,
5083        paint: Layer,
5084        basis: BlurBasis,
5085        layer: Target,
5086    ) -> Option<Target> {
5087        // A layer composited with a mode that changes the destination where the
5088        // source drew nothing has to cover everything it might affect, not just
5089        // what it covered. `DstIn` is the one that shows it: narrowed to its
5090        // content, it stops taking the alpha out of everything outside that,
5091        // and a mask leaves the rest of the picture opaque.
5092        //
5093        // The same condition upstream spells `flood_output_coverage` in
5094        // `ComputeSaveLayerCoverage`, over the same nine modes.
5095        if paint.blend.is_destructive() {
5096            return None;
5097        }
5098        let content = Self::covered(batch, layer)?;
5099        // The same expansion the caller-bounded path makes, and for the same
5100        // reason: the content is where the draws are, and how far the layer's
5101        // own blur carries them past that is this renderer's arithmetic.
5102        let reach = paint.reach(basis);
5103        let (min, max) = (
5104            Vec2::new(content.left - reach.x, content.top - reach.y),
5105            Vec2::new(content.right + reach.x, content.bottom + reach.y),
5106        );
5107        // A filter given to the layer as a whole reaches past what it is
5108        // handed, and by how much is the filter's own arithmetic rather than
5109        // anything this can assume: a composition asks both halves, a matrix
5110        // carrying content across the vanishing line widens to everything, and
5111        // a color filter needs exactly what it was given. `covering` is the
5112        // same question a draw carrying an image filter already asks before it
5113        // opens its own layer.
5114        //
5115        // After the layer's own blur and morphology, because that is the order
5116        // `finish_layer` applies them in: the filter sees what those produced.
5117        let (min, max) = match filter {
5118            Some(filter) => filter.covering(min, max, &|sigma| basis.reach(sigma)),
5119            None => (min, max),
5120        };
5121        let left = min.x.floor().max(layer.origin.x);
5122        let top = min.y.floor().max(layer.origin.y);
5123        let right = max.x.ceil().min(layer.origin.x + layer.extent.width as f32);
5124        let bottom = max
5125            .y
5126            .ceil()
5127            .min(layer.origin.y + layer.extent.height as f32);
5128        if !(right > left && bottom > top) {
5129            return None;
5130        }
5131        let narrowed = Target {
5132            origin: Vec2::new(left, top),
5133            extent: Extent2D::new((right - left) as u32, (bottom - top) as u32),
5134        };
5135        // Nothing gained, and a viewport that offsets by zero into a target of
5136        // the same size is a slower way to spell what it already was.
5137        if narrowed.extent == layer.extent {
5138            return None;
5139        }
5140        Some(narrowed)
5141    }
5142
5143    fn finish_layer(&mut self, frame: LayerFrame) {
5144        // Read before the frame is taken apart below, and only these two are
5145        // needed: whether a whole-layer filter is in play, and the layer's own
5146        // blur and morphology, which decide how far past its content the pass
5147        // will draw.
5148        let (filter, paint) = (frame.filter.clone(), frame.paint);
5149        let mut batch = std::mem::replace(&mut self.batch, frame.batch);
5150        let sources = std::mem::replace(&mut self.sources, frame.sources);
5151
5152        // A layer clears to transparent rather than to the frame's background:
5153        // it is composited over what is already there, so anywhere it drew
5154        // nothing must contribute nothing. Clearing to the background instead
5155        // would paint an opaque rectangle over the parent.
5156        let opened = self.target;
5157        // What the layer's draws turned out to cover. A layer opened with no
5158        // bounds was given the whole parent, because nothing was known then
5159        // about where its content would land; now the content is recorded and
5160        // its own geometry says. Narrowing here rather than at `save_layer` is
5161        // what makes the derived bound possible at all.
5162        //
5163        // The geometry is *not* rewritten to suit the smaller target -- it is
5164        // in clip space, and so are the materials, and a radial gradient's
5165        // center among them. The pass keeps the space it was recorded against
5166        // and carries a viewport that lands it on the narrowed target, which
5167        // crops instead of scaling. See `PassViewport`.
5168        let layer = match self.narrowed(&batch, filter.as_ref(), paint, frame.blur_basis, opened) {
5169            Some(narrowed) => {
5170                let (dx, dy) = (
5171                    (narrowed.origin.x - opened.origin.x) as u32,
5172                    (narrowed.origin.y - opened.origin.y) as u32,
5173                );
5174                // The one recorded thing the move reaches: a scissor is in
5175                // target pixels where the geometry is in clip space.
5176                batch.rebase_scissors(dx, dy, narrowed.extent);
5177                narrowed
5178            }
5179            None => opened,
5180        };
5181        let viewport = (layer.extent != opened.extent).then(|| PassViewport {
5182            offset: [
5183                layer.origin.x - opened.origin.x,
5184                layer.origin.y - opened.origin.y,
5185            ]
5186            .map(|v| -v),
5187            extent: opened.extent,
5188        });
5189        self.aim_at(frame.parent);
5190
5191        self.finished.push(Pass {
5192            batch,
5193            descriptor: PassDescriptor {
5194                clear: Some([0.0; 4]),
5195                samples: self.pass_samples(),
5196                viewport,
5197            },
5198            sources,
5199            extent: layer.extent,
5200        });
5201        // After the pass above, which is the one the layer's own draws went
5202        // into, and before the composite below, which is an image quad that
5203        // multisampling cannot change.
5204        self.anti_alias = frame.anti_alias;
5205        let mut index = self.finished.len() - 1;
5206        if frame.paint.blur.max_element() > 0.0 {
5207            index = self.blur_passes(index, layer, frame.paint.blur, frame.blur_basis);
5208        }
5209        // After the blur, because that is the order the reach above assumes
5210        // when a layer asks for both: the blur softens the content and the
5211        // morphology then works on what the blur produced.
5212        if let Some(morphology) = frame.paint.morphology {
5213            index = self.morphology_passes(index, layer, morphology);
5214        }
5215        // Last, so a filter given to the layer as a whole sees whatever the
5216        // layer's own fields produced rather than the other way round. That is
5217        // the order a composition states: the outermost filter is peeled first
5218        // and becomes this layer, and anything inner was applied by the draw
5219        // inside it.
5220        //
5221        // The failure is swallowed rather than returned, and it is the one
5222        // place in this file that does so: `restore` has no result, and the
5223        // only filter that can refuse here is a matrix, which the two callers
5224        // that can supply one already refuse before opening the layer. So this
5225        // is unreachable rather than ignored.
5226        if let Some(filter) = frame.filter.clone() {
5227            if let Ok(filtered) = self.filter_passes(index, layer, &filter, frame.blur_basis) {
5228                index = filtered;
5229            }
5230        }
5231        let slot = self.slot_for(TextureSource::Layer(index));
5232
5233        // Where the layer sits in the parent's clip space, and how much of that
5234        // space one unit of the layer's texture spans. Clip space runs from -1
5235        // to 1 and upward, so the axes are halved and Y is negated; the ratio
5236        // of the two extents is what makes a layer smaller than its parent
5237        // sample across its own full width rather than a fraction of it.
5238        //
5239        // For a full-size layer this comes out to the identity mapping between
5240        // the target and the image — origin at the top-left of clip space,
5241        // axes halved — which is what it was before bounds existed.
5242        let parent = frame.parent;
5243        let offset = layer.origin - parent.origin;
5244        let anchor = Vec2::new(
5245            -1.0 + 2.0 * offset.x / parent.extent.width as f32,
5246            1.0 - 2.0 * offset.y / parent.extent.height as f32,
5247        );
5248        let scale = Mat2::from_diagonal(Vec2::new(
5249            0.5 * parent.extent.width as f32 / layer.extent.width as f32,
5250            -0.5 * parent.extent.height as f32 / layer.extent.height as f32,
5251        ));
5252        // Stated in the direction the shader reads it: a fragment's clip
5253        // position to the texel it samples, with the anchor inside the matrix
5254        // rather than packed beside it.
5255        let clip_to_texture = Affine2::from_mat2(scale) * Affine2::from_translation(-anchor);
5256        // A layer asking to be transformed on the way back needs both halves
5257        // moved, and moving only the geometry is the mistake worth naming: the
5258        // mapping below carries a fragment's clip position to a texel, so
5259        // geometry moved without it shows the layer through a window that
5260        // moved rather than showing a layer that moved.
5261        // A matrix that folds the plane has no inverse, so there is no mapping
5262        // saying which texel a fragment reads -- and no geometry either, since
5263        // the rectangle collapses to a line. Both halves are dropped together:
5264        // dropping only the mapping leaves a layer drawn through a singular
5265        // matrix, which is nothing at all, and the fallback would be invisible.
5266        //
5267        // The matrix is stated in device pixels and the mapping is in clip
5268        // space, so it is conjugated into clip space and composed ahead of the
5269        // mapping in inverse. Both are resolved here, once, rather than tested
5270        // in one place and taken in another -- the geometry below moves by the
5271        // first and the mapping by the second, and a pair either exists or
5272        // neither half does.
5273        let projection = self.target.projection();
5274        let placement = frame.paint.matrix.and_then(|matrix| {
5275            let in_clip = projection * matrix * projection.inverse()?;
5276            in_clip.is_finite().then_some(())?;
5277            Some((matrix, in_clip.inverse()?))
5278        });
5279        let clip_to_texture = Transform2D::from(clip_to_texture);
5280        let clip_to_texture = match placement {
5281            None => clip_to_texture,
5282            Some((_, undo)) => clip_to_texture * undo,
5283        };
5284        let material = Material::Image {
5285            to_local: to_local_columns(clip_to_texture),
5286            slot,
5287            alpha: frame.paint.alpha,
5288            tile: TileMode::Clamp,
5289            // A layer composites at its own size, so nothing is between texels
5290            // to choose between.
5291            sampling: Sampling::Linear,
5292            source: [0.0, 0.0, 1.0, 1.0],
5293            tint: [1.0, 1.0, 1.0, 1.0],
5294        };
5295        let paint = RenderPaint {
5296            material,
5297            filter: frame.paint.color_filter,
5298            blend: frame.paint.blend,
5299            clip: self.clip,
5300            stencil: ClipState::content(self.depth),
5301        };
5302        // Exactly the layer's own rectangle, stated in device pixels and drawn
5303        // through the identity: a layer's contents are already where they
5304        // belong, the transform having been applied to the shapes inside it
5305        // rather than again to the finished image. Covering more than the layer
5306        // would sample outside it, which the clamped sampler answers by
5307        // smearing the edge texels across the rest of the frame.
5308        let whole = layer.path();
5309        let _ = self.renderer.fill_into(
5310            &mut self.batch,
5311            &whole,
5312            placement.map_or(Transform2D::IDENTITY, |(matrix, _)| matrix),
5313            &paint,
5314        );
5315    }
5316
5317    /// Run an image filter over a pass, and answer which pass now holds it.
5318    ///
5319    /// The one place that knows how each kind of filter becomes passes, so a
5320    /// caller with an `ImageFilter` and a pass does not have to. A composition
5321    /// is its inner half then its outer, which is what composing means and is
5322    /// the order `peel` takes them apart in.
5323    ///
5324    /// A matrix does not become a pass. Every other kind reads its input where
5325    /// the fragment is and writes there; a matrix *moves* the image, so as a
5326    /// pass it would need a target sized for where the content went rather than
5327    /// where it was. A backdrop's matrix never gets here -- `backdrop_pass`
5328    /// peels it off and folds it into the seed, which draws into a target that
5329    /// was already the right size -- so what reaches this arm is a matrix
5330    /// somewhere inside a composition, where the seed is not what reads it, and
5331    /// that is refused rather than approximated.
5332    fn filter_passes(
5333        &mut self,
5334        source: usize,
5335        target: Target,
5336        filter: &ImageFilter,
5337        basis: BlurBasis,
5338    ) -> Result<usize> {
5339        Ok(match filter {
5340            ImageFilter::None => source,
5341            ImageFilter::Blur { sigma_x, sigma_y } => {
5342                self.blur_passes(source, target, Vec2::new(*sigma_x, *sigma_y), basis)
5343            }
5344            ImageFilter::Dilate { radius_x, radius_y } => {
5345                self.morphology_passes(source, target, Morphology::dilate(*radius_x, *radius_y))
5346            }
5347            ImageFilter::Erode { radius_x, radius_y } => {
5348                self.morphology_passes(source, target, Morphology::erode(*radius_x, *radius_y))
5349            }
5350            ImageFilter::Runtime { program, uniforms } => {
5351                self.runtime_pass(source, target, *program, uniforms)
5352            }
5353            ImageFilter::Color(recolor) => self.recolor_pass(source, target, *recolor),
5354            ImageFilter::Compose { outer, inner } => {
5355                let inner = self.filter_passes(source, target, inner, basis)?;
5356                self.filter_passes(inner, target, outer, basis)?
5357            }
5358            ImageFilter::Matrix { .. } => {
5359                return Err(Error::Unsupported(
5360                    "a matrix inside a composition is not available as a filter; it \
5361                     moves the image rather than recomputing it in place, and only \
5362                     the outermost one folds into a backdrop's seed",
5363                ))
5364            }
5365        })
5366    }
5367
5368    /// A color filter run over a pass, and the pass it landed at.
5369    ///
5370    /// The image material with the filter on the paint rather than on the
5371    /// material, which is where a color filter lives: it acts on whatever the
5372    /// fragment computed, and here what the fragment computed is the texel.
5373    fn recolor_pass(&mut self, source: usize, target: Target, recolor: ColorFilter) -> usize {
5374        let to_local = to_local_columns(Transform2D::from(
5375            Affine2::from_mat2(Mat2::from_diagonal(Vec2::new(0.5, -0.5)))
5376                * Affine2::from_translation(Vec2::new(1.0, -1.0)),
5377        ));
5378        self.filter_pass_recolored(
5379            source,
5380            target,
5381            Material::Image {
5382                to_local,
5383                slot: 0,
5384                alpha: 1.0,
5385                tile: TileMode::Clamp,
5386                sampling: Sampling::Linear,
5387                source: [0.0, 0.0, 1.0, 1.0],
5388                tint: [1.0, 1.0, 1.0, 1.0],
5389            },
5390            recolor,
5391        )
5392    }
5393
5394    /// A caller's program run over a finished layer, and the pass it landed at.
5395    ///
5396    /// The whole of what a runtime image filter is, and it is short for the
5397    /// reason recorded on [`ImageFilter::Runtime`]: a filter pass covers a
5398    /// target the size of its source, so a fragment's clip position is already
5399    /// its texture coordinate and the program needs no mapping handed to it.
5400    /// Upstream re-rasterizes its input to arrange the same thing.
5401    ///
5402    /// Slot zero, because `filter_pass` gives the pass one source and that
5403    /// source is the layer being filtered. A program declaring the binding
5404    /// every draw already fills reads it and nothing further is bound.
5405    fn runtime_pass(
5406        &mut self,
5407        source: usize,
5408        target: Target,
5409        program: u32,
5410        uniforms: &[f32],
5411    ) -> usize {
5412        let mut textures = [None; impeller_hal::MAX_EFFECT_TEXTURES];
5413        textures[0] = Some(0);
5414        self.filter_pass(
5415            source,
5416            target,
5417            Material::Runtime {
5418                program,
5419                uniforms: uniforms.to_vec(),
5420                textures,
5421            },
5422        )
5423    }
5424
5425    /// One pass covering `target`, drawing `material` over all of it, and the
5426    /// index it landed at.
5427    ///
5428    /// What every image filter is made of. The material carries the mapping
5429    /// from the quad's clip position to the sampled texture's coordinates --
5430    /// the same pair a layer composite uses at zero offset, since a filter's
5431    /// target is the same size as its source -- and the filtering itself. The
5432    /// composite that follows is untouched, and still applies the layer's alpha
5433    /// and blend: keeping the filter passes pure means neither has to know
5434    /// about compositing.
5435    fn filter_pass(&mut self, source: usize, target: Target, material: Material) -> usize {
5436        self.filter_pass_recolored(source, target, material, ColorFilter::None)
5437    }
5438
5439    /// The same, with a color filter applied to what the material produced.
5440    fn filter_pass_recolored(
5441        &mut self,
5442        source: usize,
5443        target: Target,
5444        material: Material,
5445        recolor: ColorFilter,
5446    ) -> usize {
5447        // A pass of its own, so its slot table starts empty and the one slot it
5448        // uses is the pass it samples.
5449        let mut batch = Batch::new();
5450        let sources = vec![TextureSource::Layer(source)];
5451        let paint = RenderPaint {
5452            material,
5453            filter: recolor,
5454            // Replaces rather than blends: the target is cleared and this
5455            // covers all of it, so anything else would blend against the clear
5456            // for no reason.
5457            blend: BlendMode::Src,
5458            clip: None,
5459            stencil: ClipState::UNCLIPPED,
5460        };
5461        let quad = target.path();
5462        // The canvas's own renderer, aimed at the filter target for the one
5463        // draw and put back afterward. A fresh `Renderer` here would build a
5464        // pair of tessellators per pass per filtered layer per frame, for a
5465        // quad -- and would be the kind of allocation that never shows up in a
5466        // profile as itself.
5467        self.renderer.set_viewport(target.origin, target.extent);
5468        let _ = self
5469            .renderer
5470            .fill_into(&mut batch, &quad, Affine2::IDENTITY, &paint);
5471        self.renderer
5472            .set_viewport(self.target.origin, self.target.extent);
5473
5474        self.finished.push(Pass {
5475            batch,
5476            descriptor: PassDescriptor {
5477                clear: Some([0.0; 4]),
5478                // One sample: this reads a resolved image and writes another,
5479                // so multisampling it would resolve twice for no difference.
5480                samples: 1,
5481                viewport: None,
5482            },
5483            sources,
5484            extent: target.extent,
5485        });
5486        self.finished.len() - 1
5487    }
5488
5489    /// Spread or shrink a finished layer, and answer which pass holds the
5490    /// result.
5491    ///
5492    /// One pass per axis where the radius fits the shader's tap budget, and
5493    /// more where it does not. Splitting is exact rather than an
5494    /// approximation: dilating by `a` and then by `b` dilates by `a + b`,
5495    /// because the structuring elements add, and the same holds for erosion.
5496    /// So a radius of eighty runs as thirty-two, thirty-two and sixteen, and
5497    /// the picture is the one a single pass of eighty would have given.
5498    ///
5499    /// An axis with no radius contributes no passes at all. A dilation of ten
5500    /// horizontally and none vertically is one pass, not two, and the second
5501    /// would only have copied the image.
5502    fn morphology_passes(
5503        &mut self,
5504        source: usize,
5505        target: Target,
5506        morphology: Morphology,
5507    ) -> usize {
5508        // A filter pass covers the whole target, so its mapping is the fixed
5509        // one from clip space to the unit square and carries no transform of
5510        // the caller's at all. That is what keeps `step` -- a constant offset
5511        // between taps, in texels -- meaning the same thing at every fragment,
5512        // which a mapping with perspective would not.
5513        let to_local = to_local_columns(Transform2D::from(
5514            Affine2::from_mat2(Mat2::from_diagonal(Vec2::new(0.5, -0.5)))
5515                * Affine2::from_translation(Vec2::new(1.0, -1.0)),
5516        ));
5517        let axes = [
5518            (
5519                [1.0 / target.extent.width as f32, 0.0],
5520                morphology.radius[0],
5521            ),
5522            (
5523                [0.0, 1.0 / target.extent.height as f32],
5524                morphology.radius[1],
5525            ),
5526        ];
5527
5528        let mut sampled = source;
5529        for (step, radius) in axes {
5530            let mut left = radius;
5531            while left > 0.0 {
5532                let taken = left.min(MORPHOLOGY_TAPS as f32);
5533                left -= taken;
5534                let material = Material::Morphology {
5535                    to_local,
5536                    slot: 0,
5537                    step,
5538                    radius: taken,
5539                    dilate: morphology.dilate,
5540                };
5541                sampled = self.filter_pass(sampled, target, material);
5542            }
5543        }
5544        sampled
5545    }
5546
5547    /// Blur a finished layer, and answer which pass now holds the result.
5548    ///
5549    /// Two passes, one per axis, because a two-dimensional Gaussian is the
5550    /// product of two one-dimensional ones: the same picture as a square of
5551    /// taps, at a fraction of the work. Each renders a quad covering its whole
5552    /// target and samples the pass before it.
5553    ///
5554    /// Both are the size of the layer, so a bounded layer's blur costs what the
5555    /// bound says and not what the frame is. The composite that follows is
5556    /// unchanged, and still applies the layer's alpha and blend -- keeping the
5557    /// blur passes pure means neither has to know about compositing.
5558    /// Halve a pass into a target of half its size, by sampling it.
5559    ///
5560    /// A linear sample taken at the center of a two-by-two block averages
5561    /// exactly those four texels, so halving with the sampler *is* a box
5562    /// filter and needs no kernel of its own. That is the whole reason the
5563    /// reduction below is a chain of halvings rather than one jump to the
5564    /// final size: a single bilinear tap spanning an eight-by-eight block
5565    /// reads four of its sixty-four texels and calls the rest absent, which is
5566    /// how a downsample turns a smooth image into a crawling one.
5567    fn halve_pass(&mut self, source: usize, into: Target) -> usize {
5568        // Clip space to the unit square, the same fixed mapping a blur pass
5569        // uses: a filter covers its whole target and samples the whole of what
5570        // it was given, whatever the two sizes are.
5571        let to_local = to_local_columns(Transform2D::from(
5572            Affine2::from_mat2(Mat2::from_diagonal(Vec2::new(0.5, -0.5)))
5573                * Affine2::from_translation(Vec2::new(1.0, -1.0)),
5574        ));
5575        self.filter_pass(
5576            source,
5577            into,
5578            Material::Image {
5579                to_local,
5580                slot: 0,
5581                alpha: 1.0,
5582                tile: TileMode::Clamp,
5583                sampling: Sampling::Linear,
5584                source: [0.0, 0.0, 1.0, 1.0],
5585                tint: [1.0, 1.0, 1.0, 1.0],
5586            },
5587        )
5588    }
5589
5590    /// How far to shrink a target before blurring it, as a power of two.
5591    ///
5592    /// The taps are one per texel while the radius fits in the budget and
5593    /// spread apart once it does not, which keeps a wide blur wide but samples
5594    /// it more and more coarsely -- past a deviation of about nineteen the gaps
5595    /// between taps open and a smooth ramp starts to show them. Upstream
5596    /// answers this by blurring a smaller copy, so the taps stay one per texel
5597    /// and it is the *image* that loses detail rather than the kernel. For a
5598    /// blur this wide that detail was leaving anyway.
5599    ///
5600    /// A power of two so that every step is an exact halving, which is what
5601    /// makes the sampler a box filter above.
5602    fn blur_downsample(sigma: f32, extent: u32) -> u32 {
5603        let mut scale = 1u32;
5604        // Stopped once the axis would round to nothing. The extent is floored
5605        // at one texel where it is built, so this is not what keeps a target
5606        // from having no pixels -- it is what keeps the reduction from spending
5607        // passes halving a single texel into itself, which a deviation of a few
5608        // hundred against a small layer will otherwise ask for.
5609        while blur_radius(sigma / scale as f32) > BLUR_MAX_TAPS && extent / (scale * 2) >= 1 {
5610            scale *= 2;
5611        }
5612        scale
5613    }
5614
5615    fn blur_passes(
5616        &mut self,
5617        source: usize,
5618        target: Target,
5619        sigma: Vec2,
5620        basis: BlurBasis,
5621    ) -> usize {
5622        // Clip space spans two units and runs upward, so this is the mapping
5623        // that turns a full-target quad's clip position into the texture
5624        // coordinates of the pass it samples -- the same pair a layer
5625        // composite uses at zero offset, since these targets are the same size.
5626        // A filter pass covers the whole target, so its mapping is the fixed
5627        // one from clip space to the unit square and carries no transform of
5628        // the caller's at all. That is what keeps `step` -- a constant offset
5629        // between taps, in texels -- meaning the same thing at every fragment,
5630        // which a mapping with perspective would not.
5631        let to_local = to_local_columns(Transform2D::from(
5632            Affine2::from_mat2(Mat2::from_diagonal(Vec2::new(0.5, -0.5)))
5633                * Affine2::from_translation(Vec2::new(1.0, -1.0)),
5634        ));
5635        // Past the tap budget the image is shrunk rather than the taps spread,
5636        // which is upstream's answer and keeps the taps one per texel. Halved
5637        // repeatedly, so each step is an exact box filter; the deviation shrinks
5638        // with the image, and the composite that puts the layer back scales it
5639        // up again -- it maps clip space to a normalized coordinate, so it does
5640        // not care what resolution answers.
5641        // Per axis, because the deviations are. An axis blurred hard needs its
5642        // reduction and an axis blurred not at all must not be reduced with it
5643        // -- shrinking and enlarging a sharp axis for the sake of the other one
5644        // would soften it, which is the whole difference between a directional
5645        // blur and a soft smear.
5646        //
5647        // Only where the passes run along the target's own axes, though. Turned
5648        // off them, reducing one axis and not the other would shear the
5649        // directions the steps are stated in: two perpendicular directions in a
5650        // stretched image are no longer perpendicular, and separability is
5651        // exactly the property that would be lost. So a turned blur reduces both
5652        // axes together, which is a similarity and leaves the angle alone.
5653        let scale = if basis.is_axis_aligned() {
5654            [
5655                Self::blur_downsample(sigma.x, target.extent.width),
5656                Self::blur_downsample(sigma.y, target.extent.height),
5657            ]
5658        } else {
5659            let smallest = target.extent.width.min(target.extent.height);
5660            let together = Self::blur_downsample(sigma.x, smallest)
5661                .max(Self::blur_downsample(sigma.y, smallest));
5662            [together, together]
5663        };
5664        let reduced = |by: [u32; 2]| Target {
5665            origin: target.origin,
5666            extent: Extent2D::new(
5667                (target.extent.width / by[0]).max(1),
5668                (target.extent.height / by[1]).max(1),
5669            ),
5670        };
5671        let blurred = reduced(scale);
5672        let sigma = Vec2::new(sigma.x / scale[0] as f32, sigma.y / scale[1] as f32);
5673
5674        let mut source = source;
5675        let mut step_scale = [1u32; 2];
5676        while step_scale[0] < scale[0] || step_scale[1] < scale[1] {
5677            // Each iteration halves whichever axes are still above their own
5678            // target, so every step remains at most a halving per axis and
5679            // therefore an exact box filter, which is the property the
5680            // reduction rests on.
5681            for axis in 0..2 {
5682                if step_scale[axis] < scale[axis] {
5683                    step_scale[axis] *= 2;
5684                }
5685            }
5686            source = self.halve_pass(source, reduced(step_scale));
5687        }
5688
5689        // A step of one texel along each axis, in the sampled texture's own
5690        // coordinates. The shader cannot derive this: it does not know the size
5691        // of what it is sampling.
5692        //
5693        // A unit device direction moves `d.x` texels across and `d.y` down, so
5694        // in normalized coordinates it is `d` divided componentwise by the
5695        // extent. That reduces to the pair of axis steps this used to state when
5696        // the directions are the axes, and it is the whole of what makes a blur
5697        // turn with its caller.
5698        let (w, h) = (blurred.extent.width as f32, blurred.extent.height as f32);
5699        let [u, v] = basis.directions;
5700        let steps = [(sigma.x, [u.x / w, u.y / h]), (sigma.y, [v.x / w, v.y / h])];
5701
5702        let mut sampled = source;
5703        for (sigma, step) in steps {
5704            // Skipped rather than run with a deviation of zero. A zero-sigma
5705            // pass is the identity in arithmetic and a resample in fact, and a
5706            // blur stated along one axis is exactly the case where the other
5707            // pass must leave the image alone.
5708            if !ImageFilter::blurs(sigma) {
5709                continue;
5710            }
5711            sampled = self.filter_pass(
5712                sampled,
5713                blurred,
5714                Material::Blur {
5715                    to_local,
5716                    slot: 0,
5717                    step,
5718                    sigma,
5719                },
5720            );
5721        }
5722        sampled
5723    }
5724
5725    /// Point both the canvas and its renderer at a target.
5726    ///
5727    /// Together rather than separately: the canvas derives a paint's mapping
5728    /// from the target and the renderer derives the geometry's, and the two
5729    /// have to agree or a shape lands in one place while its gradient runs
5730    /// through another.
5731    fn aim_at(&mut self, target: Target) {
5732        self.target = target;
5733        self.renderer.set_viewport(target.origin, target.extent);
5734    }
5735
5736    /// The sample count a pass renders at.
5737    fn pass_samples(&self) -> u32 {
5738        // One sample unless something asked for antialiasing, because
5739        // multisampling costs bandwidth and a frame of solid rectangles gains
5740        // nothing from it.
5741        if self.anti_alias {
5742            self.samples
5743        } else {
5744            1
5745        }
5746    }
5747
5748    /// Finish recording.
5749    pub fn finish(mut self) -> Recording {
5750        // A layer left open is a caller mistake, and the useful recovery is to
5751        // composite it anyway: the alternative is silently dropping everything
5752        // drawn since the unbalanced `save_layer`, which looks like a rendering
5753        // fault rather than like the missing `restore` it is.
5754        while self.stack.iter().any(|s| s.layer.is_some()) {
5755            self.restore();
5756        }
5757
5758        let samples = self.pass_samples();
5759        let root = Pass {
5760            batch: self.batch,
5761            descriptor: PassDescriptor {
5762                clear: self.background.map(|c| c.to_array()),
5763                samples,
5764                // The root pass renders into the caller's surface, which is the
5765                // space its geometry was recorded against.
5766                viewport: None,
5767            },
5768            sources: self.sources,
5769            extent: self.extent,
5770        };
5771        let mut passes = self.finished;
5772        passes.push(root);
5773        Recording {
5774            passes,
5775            extent: self.extent,
5776            ramps: self.ramps,
5777        }
5778    }
5779}
5780
5781/// How much a transform magnifies, for converting a length into device pixels.
5782///
5783/// The larger of the two basis lengths, which is what upstream's
5784/// `GetMaxBasisLengthXY` gives and what `thin_stroke` already uses for the same
5785/// kind of conversion. A projective transform has no single answer -- it
5786/// magnifies differently at every point -- so it converts by its affine part,
5787/// which is exact wherever the perspective is mild and is the same reading the
5788/// stroke widening takes.
5789fn max_scale_of(transform: Transform2D) -> f32 {
5790    match transform.to_affine() {
5791        Some(affine) => max_scale(&affine),
5792        None => 1.0,
5793    }
5794}
5795
5796/// The constant that makes four cubics approximate a circle.
5797const KAPPA: f32 = 0.552_284_8;
5798
5799/// The smallest a stroke is drawn, in device pixels.
5800///
5801/// Upstream's `kMinStrokeSize`, and the same value for the same reason: a
5802/// stroke narrower than a pixel has no way to cover one, so it either lands on
5803/// a sample or misses it. Widening to exactly one and paying for the width in
5804/// alpha is what makes a thin line fade instead of flicker.
5805const MIN_STROKE_PIXELS: f32 = 1.0;
5806
5807/// The width a stroke is drawn at, and the alpha that pays for the difference.
5808///
5809/// A stroke narrower than a device pixel cannot be drawn at the width it asks
5810/// for. Multisampling quantizes it: at four samples the only coverages a
5811/// sub-pixel line can have are none, a quarter, a half and three quarters, so
5812/// a line a fifth of a pixel wide draws at a quarter or vanishes outright
5813/// depending on where it falls. Measured here before this existed, a stroke of
5814/// 0.18 device pixels and one of 0.3 drew identically, and one of 0.15 drew
5815/// nothing at all.
5816///
5817/// So the geometry is widened to a whole pixel and the alpha is scaled down to
5818/// pay for it, which is what upstream does — `ComputePixelHalfWidth` takes
5819/// `max(width, kMinStrokeSize / max_basis)` and `ComputeStrokeAlphaCoverage`
5820/// returns `clamp(scaled_width * 2, 0, 1)`, a factor its own comment calls
5821/// eyeballed from Skia. Both are carried rather than improved on: the whole
5822/// point is that a Flutter app's hairlines look here as they look there, and a
5823/// different constant would be a different picture at every sub-pixel width.
5824///
5825/// **Zero is a hairline**, which is upstream's reading and `dart:ui`'s
5826/// documented one: "Defaults to 0.0, which correspond to a hairline width".
5827/// Upstream spells it as an explicit exception rather than letting it fall out
5828/// of the arithmetic — `ComputeStrokeAlphaCoverage` opens with
5829/// `if (scaled_stroke_width == 0.0 || scaled_stroke_width >= kMinStrokeSize)
5830/// return 1.0`, so zero takes the widening to a whole pixel and pays nothing in
5831/// alpha for it.
5832///
5833/// This renderer read zero as no stroke until 2026-09-17, on the reasoning that
5834/// a caller animating a width to nothing should dim continuously rather than
5835/// jump back to full at the end. That reasoning is sound and it is not
5836/// upstream's: the jump is in upstream on purpose, and zero is the *default*
5837/// value of `Paint.strokeWidth` rather than an edge a caller has to ask for --
5838/// so a Flutter app that strokes without setting a width drew a hairline there
5839/// and nothing here. Parity wins, and the discontinuity comes with it.
5840///
5841/// Which zero means which is what [`Zero`] carries. The analytic route spells a
5842/// *fill* as a stroke width of zero, and widening that one would put a
5843/// one-pixel outline around every filled shape, so the distinction cannot be
5844/// left to the number.
5845///
5846/// A projective transform is left alone too. The widening asks how much a
5847/// transform stretches, which for a homography is a different answer at every
5848/// point; upstream guards the same case with `HasPerspective2D`.
5849fn thin_stroke(transform: Transform2D, width: f32, zero: Zero) -> (f32, f32) {
5850    let Some(affine) = transform.to_affine() else {
5851        return (width, 1.0);
5852    };
5853    if !width.is_finite() || width < 0.0 {
5854        return (width, 1.0);
5855    }
5856    if width == 0.0 && zero == Zero::IsNotAStroke {
5857        return (width, 1.0);
5858    }
5859    let basis = max_scale(&affine);
5860    if !basis.is_finite() || basis <= 0.0 {
5861        return (width, 1.0);
5862    }
5863    let scaled = basis * width;
5864    if scaled >= MIN_STROKE_PIXELS {
5865        return (width, 1.0);
5866    }
5867    // Upstream's exception, and the reason it is one: the dimming below would
5868    // take a width of zero to an alpha of zero, which is a hairline nobody can
5869    // see rather than the thinnest line the device can draw.
5870    let coverage = match scaled == 0.0 {
5871        true => 1.0,
5872        false => (scaled * 2.0).clamp(0.0, 1.0),
5873    };
5874    (MIN_STROKE_PIXELS / basis, coverage)
5875}
5876
5877/// The radius a point is drawn at, which for a small one is wider than asked.
5878///
5879/// The same problem `thin_stroke` solves and a different constant, both taken
5880/// from upstream. A point smaller than a pixel lands on a sample or misses it,
5881/// so `PointFieldGeometry` widens the *radius* to `0.5f / max_basis` --
5882/// `std::max(radius_, min_size)`, half a pixel, so the point comes out one pixel
5883/// across. A stroke's constant is a whole pixel because a stroke width spans the
5884/// line rather than reaching from its middle.
5885///
5886/// Nothing is dimmed to pay for it, which is upstream's rule and not an omission
5887/// here: a stroke fades as it thins and a point does not. A point is a disc
5888/// whose area falls away as the square of its radius, so dimming it as well
5889/// would take it to nothing twice over, and a field of them is a scatter plot
5890/// whose small marks are the ones a reader has to see.
5891///
5892/// A projective transform is left alone, as it is for a stroke: the stretch a
5893/// homography applies is a different answer at every point.
5894fn point_radius(transform: Transform2D, width: f32) -> f32 {
5895    let radius = width / 2.0;
5896    let Some(affine) = transform.to_affine() else {
5897        return radius;
5898    };
5899    if !radius.is_finite() || radius < 0.0 {
5900        return radius;
5901    }
5902    let basis = max_scale(&affine);
5903    if !basis.is_finite() || basis <= 0.0 {
5904        return radius;
5905    }
5906    radius.max(MIN_POINT_RADIUS_PIXELS / basis)
5907}
5908
5909/// When two coordinates are the same coordinate, for deciding axis alignment.
5910///
5911/// Upstream's `kEhCloseEnough`, and the same value. A line off vertical by less
5912/// than this covers the same pixels a vertical one does, so treating it as
5913/// vertical moves nothing a reader could see.
5914const CLOSE_ENOUGH: f32 = 1e-3;
5915
5916/// Half a device pixel, so the smallest point drawn covers one.
5917///
5918/// Upstream's `min_size` in `PointFieldGeometry::GetPositionBuffer`, which is
5919/// `0.5f / max_basis` rather than [`MIN_STROKE_PIXELS`] over it, for the reason
5920/// [`point_radius`] gives: this one is a radius.
5921const MIN_POINT_RADIUS_PIXELS: f32 = 0.5;
5922
5923/// What a stroke width of zero means at a call site, which is not always the
5924/// same thing.
5925///
5926/// A caller asking for a zero-width stroke wants a hairline. The analytic route
5927/// reaches [`thin_stroke`] through `analytic_stroke`, which reports a *fill* as
5928/// a width of zero -- and a fill widened to a pixel would grow an outline it
5929/// never asked for. So the two are told apart here rather than by the number,
5930/// which they share.
5931#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5932enum Zero {
5933    /// A stroke of no width, which draws the thinnest line the device can.
5934    IsAHairline,
5935    /// Not a stroke at all, so there is no width to widen.
5936    IsNotAStroke,
5937}
5938
5939/// A color at `factor` of its opacity.
5940///
5941/// The analytic route's materials carry one color rather than a material this
5942/// can be asked of, so the two spellings of the same dimming sit beside each
5943/// other: [`Material::with_opacity`] and this.
5944fn dimmed(color: crate::Color, factor: f32) -> [f32; 4] {
5945    let mut c = color.to_array();
5946    c[3] *= factor;
5947    c
5948}
5949
5950/// The stroke width a paint implies for a fragment-evaluated shape, if one can
5951/// be drawn that way at all.
5952///
5953/// `Some(0.0)` fills, `Some(w)` traces an outline, and `None` means this paint
5954/// has to be tessellated: without antialiasing there is nothing a distance
5955/// field offers that triangles do not, and a width that is not finite and
5956/// positive is not a stroke.
5957///
5958/// The joins and caps a stroke style also carries are ignored rather than
5959/// refused. These shapes are closed curves with no corners to join and no ends
5960/// to cap, so every setting produces the same outline -- which is why a stroked
5961/// one can take this path at all.
5962fn analytic_stroke(paint: &Paint) -> Option<f32> {
5963    if !paint.anti_alias {
5964        return None;
5965    }
5966    // The shape is drawn on a quad larger than itself and emits a fragment
5967    // everywhere on it, so a mode that discards the destination where the
5968    // source is transparent would erase what is behind it in the gap between
5969    // the two -- and in the corner of a rounded rectangle that gap is most of
5970    // the corner. Tessellating covers only the shape and has no such gap.
5971    if !paint.blend.respects_coverage() {
5972        return None;
5973    }
5974    // A distance field describes a continuous outline and has no notion of a
5975    // position along it, so a dashed stroke cannot be expressed this way at
5976    // all. Falling back to tessellation is what makes the dash appear; without
5977    // this the rounded rectangle and the circle would draw a solid outline and
5978    // silently ignore the pattern, which is the shape of bug the analytic path
5979    // is most able to hide -- the result looks like a stroke, because it is
5980    // one.
5981    if paint.dash.as_ref().is_some_and(|dash| dash.is_usable()) {
5982        return None;
5983    }
5984    // A mask blur is a layer around the draw, and a distance field evaluated on
5985    // a quad is not a shape that can be put inside one and blurred: the quad is
5986    // larger than the shape and the layer would blur its edges too.
5987    if paint.mask_blur > 0.0 {
5988        return None;
5989    }
5990    // An image filter is a layer around the draw for the same reason, and this
5991    // guard was missing when image filters were added -- so a blurred or moved
5992    // circle drew as though neither had been asked for. Nothing failed: the
5993    // shape was still a shape, and only a test comparing where it landed said
5994    // otherwise. Every route that needs a layer has to be refused here, which
5995    // is worth stating as the rule rather than as three cases.
5996    if !paint.image_filter.is_identity() {
5997        return None;
5998    }
5999    match &paint.style {
6000        Style::Fill => Some(0.0),
6001        Style::Stroke(stroke) => {
6002            let width = stroke.width;
6003            (width.is_finite() && width > 0.0).then_some(width)
6004        }
6005    }
6006}
6007
6008/// An ellipse inscribed in a rectangle, as four cubics.
6009///
6010/// The same construction a circle uses, with a different radius on each axis:
6011/// the constant that makes a cubic approximate a quarter turn does not care
6012/// which, since it scales with the axis it belongs to.
6013fn oval_path(bounds: Rect) -> Path {
6014    let (cx, cy) = (
6015        (bounds.left + bounds.right) / 2.0,
6016        (bounds.top + bounds.bottom) / 2.0,
6017    );
6018    let (rx, ry) = (bounds.width() / 2.0, bounds.height() / 2.0);
6019    let (kx, ky) = (KAPPA * rx, KAPPA * ry);
6020    let mut b = PathBuilder::new();
6021    b.move_to(Vec2::new(cx + rx, cy))
6022        .cubic_to(
6023            Vec2::new(cx + rx, cy + ky),
6024            Vec2::new(cx + kx, cy + ry),
6025            Vec2::new(cx, cy + ry),
6026        )
6027        .cubic_to(
6028            Vec2::new(cx - kx, cy + ry),
6029            Vec2::new(cx - rx, cy + ky),
6030            Vec2::new(cx - rx, cy),
6031        )
6032        .cubic_to(
6033            Vec2::new(cx - rx, cy - ky),
6034            Vec2::new(cx - kx, cy - ry),
6035            Vec2::new(cx, cy - ry),
6036        )
6037        .cubic_to(
6038            Vec2::new(cx + kx, cy - ry),
6039            Vec2::new(cx + rx, cy - ky),
6040            Vec2::new(cx + rx, cy),
6041        )
6042        .close();
6043    b.build()
6044}
6045
6046fn circle_path(center: Vec2, radius: f32) -> Path {
6047    let (cx, cy) = (center.x, center.y);
6048    let r = radius;
6049    let k = KAPPA * r;
6050    let mut b = PathBuilder::new();
6051    // Recorded for the same reason a rounded rectangle's is: a shadow arrives
6052    // as a path, and a circle is the one oval the blurred-rectangle expression
6053    // describes exactly -- corners of half the side, on a square.
6054    b.as_rounded_rect(
6055        GeometryRect::new(Vec2::new(cx - r, cy - r), Vec2::new(cx + r, cy + r)),
6056        r,
6057    );
6058    b.move_to(Vec2::new(cx + r, cy))
6059        .cubic_to(
6060            Vec2::new(cx + r, cy + k),
6061            Vec2::new(cx + k, cy + r),
6062            Vec2::new(cx, cy + r),
6063        )
6064        .cubic_to(
6065            Vec2::new(cx - k, cy + r),
6066            Vec2::new(cx - r, cy + k),
6067            Vec2::new(cx - r, cy),
6068        )
6069        .cubic_to(
6070            Vec2::new(cx - r, cy - k),
6071            Vec2::new(cx - k, cy - r),
6072            Vec2::new(cx, cy - r),
6073        )
6074        .cubic_to(
6075            Vec2::new(cx + k, cy - r),
6076            Vec2::new(cx + r, cy - k),
6077            Vec2::new(cx + r, cy),
6078        )
6079        .close();
6080    b.build()
6081}
6082
6083/// How far a shadow's blur reaches per unit of elevation, as a kernel radius.
6084///
6085/// The light's radius over its height, which is the ratio that decides how
6086/// quickly a shadow softens as its caster rises. Upstream writes it
6087/// `constexpr Scalar kLightRadius = 800 / 600;` in the dispatcher that draws
6088/// the shadow, and those are integer literals: the division happens in `int`
6089/// and the constant is one, not the one and a third the comment beside it
6090/// describes.
6091///
6092/// One is what this uses, because parity is against what upstream does rather
6093/// than against what it appears to have meant. This did carry the ratio the
6094/// comment states, and combined with reading the result as a deviation instead
6095/// of a radius it made every shadow here about twice as soft as the same
6096/// elevation gives upstream.
6097///
6098/// Note that `DlCanvas` has a second pair of these, `kShadowLightRadius` over
6099/// `kShadowLightHeight`, which are `DlScalar` and so do divide to one and a
6100/// third. Those size the shadow's *bounds*; this one draws it. Reading the
6101/// wrong pair is easy and gives a shadow a third too wide.
6102const LIGHT_RADIUS: f32 = 1.0;
6103
6104/// Kernel radius per deviation, for turning one into the other.
6105///
6106/// The square root of three, matching upstream's `kKernelRadiusPerSigma`. A
6107/// shadow's elevation gives a radius, and the blur takes a deviation, so the
6108/// conversion is not optional -- and upstream's is affine rather than a plain
6109/// scale, adding half a pixel so that a radius shrinking to nothing still
6110/// leaves a deviation the blur can act on.
6111const KERNEL_RADIUS_PER_SIGMA: f32 = 1.732_050_8;
6112
6113/// The deviation upstream blurs a shadow of this radius by.
6114///
6115/// `Radius::operator Sigma` in `impeller/geometry/sigma.cc`, which is
6116/// `radius / kKernelRadiusPerSigma + 0.5` for a positive radius and zero
6117/// otherwise.
6118fn sigma_for_radius(radius: f32) -> f32 {
6119    if radius > 0.0 {
6120        radius / KERNEL_RADIUS_PER_SIGMA + 0.5
6121    } else {
6122        0.0
6123    }
6124}
6125
6126/// What fraction of the stated color's alpha a shadow starts from.
6127///
6128/// A quarter, matching Impeller. A shadow is a suggestion of occlusion rather
6129/// than an absence of light, and at full alpha it reads as a hole. This is what
6130/// the tonal remap below begins with rather than the alpha a shadow ends up
6131/// drawn at -- for anything but a gray it raises the alpha again.
6132const SHADOW_ALPHA: f32 = 0.25;
6133
6134/// A shadow's color, tonally adjusted the way upstream adjusts it.
6135///
6136/// Ported from `DlDispatcherBase::drawShadow`, which says it ports
6137/// `SkShadowUtils::ComputeTonalColors`. The rule is that a colored shadow does
6138/// not read as a gray one tinted: a saturated shadow needs more alpha and less
6139/// saturation than the number a caller gave it, or it looks like a colored
6140/// object lying on the surface rather than an absence of light.
6141///
6142/// The arithmetic runs on **sRGB-encoded** components rather than on light,
6143/// because upstream's do: its pipeline holds encoded values throughout, so the
6144/// luminance this keys on is a luminance of encoded numbers. Computing it on
6145/// linear light would be the same formula answering a different question, and
6146/// would part company with upstream by more than the remap is worth.
6147///
6148/// It is the identity for a black shadow, which is nearly every shadow: at zero
6149/// luminance the color term falls out and the alpha is left at the quarter
6150/// above. What it changes is a colored one, and by a lot -- a fully saturated
6151/// red at full alpha comes out at better than twice the alpha and about
6152/// five eighths of the red.
6153fn tonal_shadow_color(color: Color) -> Color {
6154    let [r, g, b, a] = color.to_srgb();
6155    let alpha = a * SHADOW_ALPHA;
6156
6157    let luminance = (r.min(g).min(b) + r.max(g).max(b)) * 0.5;
6158
6159    let alpha_adjust = (2.6 + (-2.666_67 + 1.066_67 * alpha) * alpha) * alpha;
6160    let color_alpha = (3.544_762 + (-4.891_428 + 2.346_6 * luminance) * luminance) * luminance;
6161    let color_alpha = (alpha_adjust * color_alpha).clamp(0.0, 1.0);
6162
6163    let grayscale_alpha = (alpha * (1.0 - 0.4 * luminance)).clamp(0.0, 1.0);
6164
6165    let color_scale = color_alpha * (1.0 - grayscale_alpha);
6166    let tonal_alpha = color_scale + grayscale_alpha;
6167    // Guarded because a fully transparent shadow leaves both terms at zero, and
6168    // the ratio below is the only place that could divide by it.
6169    let unpremul_scale = if tonal_alpha != 0.0 {
6170        color_scale / tonal_alpha
6171    } else {
6172        0.0
6173    };
6174
6175    Color::srgb(
6176        unpremul_scale * r,
6177        unpremul_scale * g,
6178        unpremul_scale * b,
6179        tonal_alpha,
6180    )
6181}
6182
6183/// How far past its content a blur of this deviation reaches.
6184///
6185/// `(sigma - 0.5) * sqrt(3)`, which is upstream's `CalculateBlurRadius` and is
6186/// also where the shader stops taking taps -- so a target sized by this covers
6187/// everything the blur will actually read, and no more. It was three
6188/// deviations, which covered more of the curve than upstream does and made
6189/// every blur here wider than the same request gives it.
6190///
6191/// Shared rather than written twice. A bounded layer sizes its target with it,
6192/// and a mask blur style that combines the blurred coverage with the shape's
6193/// own has to size the layer holding both by the same rule -- which it did not
6194/// at first, and the halo was cut off square at the shape's own bounds.
6195/// The widest deviation a blur is asked for, matching upstream's `kMaxSigma`.
6196///
6197/// Not a limitation so much as the end of the useful range: at five hundred a
6198/// blur of anything smaller than a wall is a flat wash, and the reduction that
6199/// keeps the taps affordable has long since taken the image down to a handful
6200/// of texels.
6201pub(crate) const MAX_SIGMA: f32 = 500.0;
6202
6203/// The most taps a blur takes each way from center.
6204///
6205/// Must match `max_taps` in the blur shader, which is where the budget is
6206/// actually spent; this is the copy that decides when to shrink the image
6207/// instead of spreading the taps across it.
6208pub(crate) const BLUR_MAX_TAPS: f32 = 32.0;
6209
6210/// The kernel radius a deviation gives, in texels of whatever it is blurring.
6211///
6212/// Upstream's `CalculateBlurRadius`, which is `Radius(Sigma(sigma))`. Shared
6213/// so that the rule deciding how far a blur reaches, the rule sizing a target
6214/// to hold it, and the rule deciding whether it needs shrinking first cannot
6215/// drift apart -- they are the same question asked by three callers.
6216/// The error function, to about seven digits.
6217///
6218/// The same rational approximation the shader evaluates, and it has to be the
6219/// same one: `scale` is computed here and the fade there, so two different
6220/// approximations would normalize the shape against a curve it is not drawn
6221/// with. Upstream keeps a copy on each side for the same reason.
6222pub(crate) fn erf7(value: f32) -> f32 {
6223    let x = value * std::f32::consts::FRAC_2_SQRT_PI;
6224    let xx = x * x;
6225    let series = x + (0.24295 + (0.03395 + 0.0104 * xx) * xx) * (x * xx);
6226    series / (1.0 + series * series).sqrt()
6227}
6228
6229pub(crate) fn blur_radius(sigma: f32) -> f32 {
6230    ((sigma - 0.5) * KERNEL_RADIUS_PER_SIGMA).max(0.0)
6231}
6232
6233pub(crate) fn blur_reach(sigma: f32) -> f32 {
6234    if sigma > 0.0 {
6235        blur_radius(sigma).ceil()
6236    } else {
6237        0.0
6238    }
6239}
6240
6241/// A color in the form a vertex carries one.
6242///
6243/// Premultiplied, because a vertex color is interpolated across a triangle and
6244/// straight color interpolated between differing alphas is wrong at every point
6245/// between the ends.
6246fn premultiplied(color: Color) -> [f32; 4] {
6247    let [r, g, b, a] = color.to_array();
6248    [r * a, g * a, b * a, a]
6249}
6250
6251#[cfg(test)]
6252mod tests {
6253    use super::*;
6254
6255    fn canvas() -> Canvas {
6256        Canvas::new(Extent2D::new(128, 128))
6257    }
6258
6259    /// A backdrop layer opened under a clip is the size of the clip.
6260    ///
6261    /// It used to be the size of the parent, whatever the clip said, so a
6262    /// frosted panel a fortieth of the frame's area recorded a frame-sized
6263    /// target. The clip binds the layer's content and the draw that composites
6264    /// it alike, so there is nothing outside it for the layer to hold.
6265    ///
6266    /// The extents are asserted rather than the pixels because the pixels do
6267    /// not change: the sibling test below is what says they do not.
6268    #[test]
6269    fn a_backdrop_layer_under_a_clip_is_no_larger_than_the_clip() {
6270        let mut canvas = canvas();
6271        canvas.clear(Color::srgb(0.2, 0.3, 0.4, 1.0));
6272        canvas
6273            .draw_rect(
6274                Rect::new(0.0, 0.0, 128.0, 128.0),
6275                &Paint::fill(Color::srgb(0.9, 0.2, 0.2, 1.0)),
6276            )
6277            .expect("the ground");
6278        canvas.save();
6279        canvas
6280            .clip_rect(Rect::new(40.0, 40.0, 80.0, 80.0))
6281            .expect("the clip");
6282        canvas.save_layer(Layer::opacity(1.0).with_backdrop_blur(4.0));
6283        canvas
6284            .draw_rect(
6285                Rect::new(40.0, 40.0, 80.0, 80.0),
6286                &Paint::fill(Color::srgb(1.0, 0.6, 0.0, 0.6)),
6287            )
6288            .expect("the panel");
6289        canvas.restore();
6290        canvas.restore();
6291
6292        let recording = canvas.finish();
6293        let frame = recording.extent;
6294        let narrowed: Vec<_> = recording
6295            .passes
6296            .iter()
6297            .map(|pass| pass.extent)
6298            .filter(|extent| *extent != frame)
6299            .collect();
6300        // The layer's own target and both passes of the backdrop's blur. What
6301        // stays frame-sized is the pass being captured and the one the frame
6302        // ends in, and neither is allocated for this layer.
6303        assert_eq!(
6304            narrowed.len(),
6305            3,
6306            "three passes should be sized to the clip, and {} are: {:?}",
6307            narrowed.len(),
6308            recording
6309                .passes
6310                .iter()
6311                .map(|pass| pass.extent)
6312                .collect::<Vec<_>>()
6313        );
6314        for extent in narrowed {
6315            assert!(
6316                extent.width <= 48 && extent.height <= 48,
6317                "a pass came out {}x{} for a clip forty pixels across",
6318                extent.width,
6319                extent.height
6320            );
6321        }
6322    }
6323
6324    /// A layer sharing a backdrop key is narrowed too, and its capture is not.
6325    ///
6326    /// These two are separate regions and were once excluded together. The
6327    /// layer's own target is bound by the clip exactly as an unshared one is.
6328    /// What the key shares is the capture and the filtered image over it, and
6329    /// those have to cover every group naming the key -- so they stay the
6330    /// parent's size, and a second group somewhere else reads the same pass.
6331    #[test]
6332    fn a_shared_backdrop_layer_is_narrowed_while_its_capture_is_not() {
6333        let mut canvas = canvas();
6334        canvas.clear(Color::srgb(0.2, 0.3, 0.4, 1.0));
6335        let panel = |canvas: &mut Canvas, clip: Rect| {
6336            canvas.save();
6337            canvas.clip_rect(clip).expect("the clip");
6338            canvas.save_layer(
6339                Layer::opacity(1.0)
6340                    .with_backdrop_blur(4.0)
6341                    .with_backdrop_id(7),
6342            );
6343            canvas
6344                .draw_rect(clip, &Paint::fill(Color::srgb(1.0, 0.6, 0.0, 0.6)))
6345                .expect("the panel");
6346            canvas.restore();
6347            canvas.restore();
6348        };
6349        // Opposite corners, so nothing can pass by covering one with the other.
6350        panel(&mut canvas, Rect::new(8.0, 8.0, 48.0, 48.0));
6351        panel(&mut canvas, Rect::new(80.0, 80.0, 120.0, 120.0));
6352
6353        let recording = canvas.finish();
6354        let frame = recording.extent;
6355        let extents: Vec<_> = recording.passes.iter().map(|pass| pass.extent).collect();
6356        let narrowed: Vec<_> = extents.iter().filter(|e| **e != frame).collect();
6357        // One target per panel, and nothing else: the capture and the two
6358        // passes its blur takes are shared, and all three cover the parent.
6359        assert_eq!(
6360            narrowed.len(),
6361            2,
6362            "one narrowed target per panel and no more, in {extents:?}"
6363        );
6364        for extent in narrowed {
6365            assert!(
6366                extent.width <= 40 && extent.height <= 40,
6367                "a panel target came out {}x{} for a clip forty pixels across",
6368                extent.width,
6369                extent.height
6370            );
6371        }
6372        // The second panel pays for its own target and nothing else: the cut,
6373        // and the two passes of the blur over it, are taken once between them.
6374        // Six in all -- the cut, the blur's two, a target each, and the pass
6375        // the frame ends in, which holds both composites.
6376        assert_eq!(
6377            extents.len(),
6378            6,
6379            "a cut, two blur passes, a target each, and the frame's own, in \
6380             {extents:?}"
6381        );
6382    }
6383
6384    /// And it draws what stating the clip as the layer's bounds would.
6385    ///
6386    /// The narrowing above is an optimization, so the thing to check is that it
6387    /// is only that. A caller who writes the bounds out has always taken the
6388    /// bounded path; a caller who relies on the clip now takes it too, and the
6389    /// two recordings have to describe the same picture.
6390    #[test]
6391    fn narrowing_to_the_clip_records_what_stating_it_would() {
6392        let build = |bounded: bool| {
6393            let mut canvas = canvas();
6394            canvas.clear(Color::srgb(0.1, 0.1, 0.1, 1.0));
6395            canvas
6396                .draw_rect(
6397                    Rect::new(0.0, 20.0, 128.0, 60.0),
6398                    &Paint::fill(Color::srgb(0.9, 0.4, 0.1, 1.0)),
6399                )
6400                .expect("the ground");
6401            let clip = Rect::new(24.0, 34.0, 104.0, 94.0);
6402            canvas.save();
6403            canvas.clip_rect(clip).expect("the clip");
6404            let layer = Layer::opacity(1.0).with_backdrop_blur(6.0);
6405            if bounded {
6406                canvas.save_layer_bounds(layer, clip);
6407            } else {
6408                canvas.save_layer(layer);
6409            }
6410            canvas.restore();
6411            canvas.restore();
6412            canvas.finish()
6413        };
6414        let from_clip = build(false);
6415        let from_bounds = build(true);
6416        assert_eq!(from_clip.passes.len(), from_bounds.passes.len());
6417        for (a, b) in from_clip.passes.iter().zip(from_bounds.passes.iter()) {
6418            assert_eq!(a.extent, b.extent, "the two spellings size a pass alike");
6419            assert_eq!(
6420                a.batch.vertices(),
6421                b.batch.vertices(),
6422                "the two spellings record the same geometry"
6423            );
6424        }
6425    }
6426
6427    /// How far a blur reaches, which four comments in this tree got wrong.
6428    ///
6429    /// The kernel is truncated at `(sigma - 0.5) * sqrt(3)` -- upstream's
6430    /// `kKernelRadiusPerSigma` -- and every layer sized to hold a blur asks
6431    /// `blur_reach` for it. Nothing pinned the arithmetic, so the prose drifted
6432    /// to "three deviations" in four places and stayed there: a test written
6433    /// against that phrase expected 88 where the renderer gives 68, and the
6434    /// phrase was wrong rather than the renderer.
6435    ///
6436    /// Pinned here because the number is not free to change. It decides how
6437    /// wide a halo is, so moving it moves every blurred picture away from
6438    /// upstream's -- and it decides a target's size, so getting it *smaller*
6439    /// cuts the halo off square, which reads as a shadow with a straight edge
6440    /// rather than as arithmetic.
6441    #[test]
6442    fn a_blur_reaches_the_radius_its_kernel_is_truncated_at() {
6443        // Sqrt three per deviation, less the half pixel upstream subtracts, and
6444        // rounded out to a whole texel because a target is whole texels.
6445        assert_eq!(blur_reach(8.0), 13.0, "ceil(7.5 * sqrt(3))");
6446        assert_eq!(blur_reach(2.0), 3.0, "ceil(1.5 * sqrt(3))");
6447
6448        // Not three deviations, which is what the prose used to say. Stated as
6449        // its own assertion because the two agree closely enough at small sigma
6450        // that a spot check would not tell them apart.
6451        assert!(
6452            blur_reach(50.0) < 3.0 * 50.0,
6453            "the kernel is truncated nearer one and three quarter deviations \
6454             than three"
6455        );
6456
6457        // A sigma that describes no blur reaches nowhere, and a layer asking
6458        // for one gets no widening rather than half a pixel of it.
6459        assert_eq!(blur_reach(0.0), 0.0);
6460        assert_eq!(blur_reach(0.5), 0.0, "the half pixel is subtracted first");
6461        assert_eq!(blur_reach(-1.0), 0.0);
6462
6463        // And it never shrinks as the blur widens, which is what makes a target
6464        // sized from it safe.
6465        let mut previous = 0.0;
6466        for tenth in 0..600 {
6467            let reach = blur_reach(tenth as f32 / 10.0);
6468            assert!(
6469                reach >= previous,
6470                "reach fell at sigma {}",
6471                tenth as f32 / 10.0
6472            );
6473            previous = reach;
6474        }
6475    }
6476
6477    #[test]
6478    fn drawing_accumulates_into_one_batch() {
6479        let mut canvas = canvas();
6480        let paint = Paint::fill(Color::WHITE);
6481        canvas
6482            .draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &paint)
6483            .unwrap();
6484        canvas
6485            .draw_rect(Rect::new(20.0, 20.0, 30.0, 30.0), &paint)
6486            .unwrap();
6487
6488        // Two shapes, one batch: describing the frame before submitting is what
6489        // lets them share a pass.
6490        let recording = canvas.finish();
6491        assert_eq!(recording.draw_count(), 2);
6492    }
6493
6494    #[test]
6495    fn an_invisible_paint_records_nothing() {
6496        let mut canvas = canvas();
6497        let invisible = Paint::fill(Color::WHITE.with_alpha(0.0));
6498        canvas
6499            .draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &invisible)
6500            .unwrap();
6501        canvas
6502            .draw_circle(Vec2::new(5.0, 5.0), 4.0, &invisible)
6503            .unwrap();
6504
6505        // Rejecting early keeps empty geometry out of the batch rather than
6506        // tessellating it and discovering it was empty.
6507        assert!(canvas.finish().is_empty());
6508    }
6509
6510    #[test]
6511    fn outsetting_grows_every_side_and_negating_it_shrinks_back() {
6512        let rect = Rect::new(10.0, 20.0, 40.0, 60.0);
6513        let grown = rect.outset(5.0);
6514        assert_eq!(
6515            (grown.left, grown.top, grown.right, grown.bottom),
6516            (5.0, 15.0, 45.0, 65.0)
6517        );
6518        // Symmetric, which is what lets a caller undo one: a blur's reach is
6519        // the same in both directions and a target sized by it has to be too.
6520        let back = grown.outset(-5.0);
6521        assert_eq!(
6522            (back.left, back.top, back.right, back.bottom),
6523            (rect.left, rect.top, rect.right, rect.bottom)
6524        );
6525    }
6526
6527    #[test]
6528    fn degenerate_shapes_record_nothing() {
6529        let mut canvas = canvas();
6530        let paint = Paint::fill(Color::WHITE);
6531        canvas
6532            .draw_rect(Rect::new(10.0, 10.0, 10.0, 20.0), &paint)
6533            .unwrap();
6534        canvas.draw_circle(Vec2::ZERO, 0.0, &paint).unwrap();
6535        assert!(canvas.finish().is_empty());
6536    }
6537
6538    #[test]
6539    fn save_and_restore_return_the_previous_transform() {
6540        let mut canvas = canvas();
6541        canvas.translate(10.0, 20.0);
6542        let outer = canvas.transform();
6543
6544        canvas.save();
6545        canvas.scale(3.0, 3.0).rotate(0.5);
6546        assert_ne!(canvas.transform(), outer);
6547
6548        canvas.restore();
6549        assert_eq!(canvas.transform(), outer);
6550        assert_eq!(canvas.save_depth(), 0);
6551    }
6552
6553    #[test]
6554    fn saves_nest() {
6555        let mut canvas = canvas();
6556        let identity = canvas.transform();
6557        canvas.save();
6558        canvas.translate(5.0, 0.0);
6559        canvas.save();
6560        canvas.translate(5.0, 0.0);
6561        assert_eq!(canvas.save_depth(), 2);
6562
6563        canvas.restore();
6564        assert_eq!(
6565            canvas.transform(),
6566            Transform2D::from(Affine2::from_translation(Vec2::new(5.0, 0.0)))
6567        );
6568        canvas.restore();
6569        assert_eq!(canvas.transform(), identity);
6570    }
6571
6572    #[test]
6573    fn restoring_too_often_leaves_the_transform_alone() {
6574        let mut canvas = canvas();
6575        canvas.translate(7.0, 3.0);
6576        let current = canvas.transform();
6577        // An unbalanced restore is a caller bug, but taking down a frame loop
6578        // for it is worse than continuing with a transform that is still right.
6579        canvas.restore().restore().restore();
6580        assert_eq!(canvas.transform(), current);
6581    }
6582
6583    #[test]
6584    fn transforms_compose_in_the_order_applied() {
6585        let mut canvas = canvas();
6586        canvas.translate(10.0, 0.0).scale(2.0, 2.0);
6587        // Scale then translate, or translate then scale, place a point very
6588        // differently; the later call applies in the frame the earlier set up.
6589        let mapped = canvas.transform().project_point2(Vec2::new(1.0, 0.0));
6590        assert!((mapped - Vec2::new(12.0, 0.0)).length() < 1e-5);
6591    }
6592
6593    #[test]
6594    fn antialiasing_is_off_unless_something_asks_for_it() {
6595        let mut canvas = canvas();
6596        let aliased = Paint::fill(Color::WHITE).with_anti_alias(false);
6597        canvas
6598            .draw_rect(Rect::from_size(10.0, 10.0), &aliased)
6599            .unwrap();
6600        // Multisampling costs bandwidth, and a frame of solid rectangles gains
6601        // nothing from it.
6602        assert_eq!(canvas.finish().root().descriptor.samples, 1);
6603    }
6604
6605    #[test]
6606    fn one_antialiased_shape_antialiases_the_frame() {
6607        let mut canvas = canvas();
6608        canvas
6609            .draw_rect(
6610                Rect::from_size(10.0, 10.0),
6611                &Paint::fill(Color::WHITE).with_anti_alias(false),
6612            )
6613            .unwrap();
6614        // A path, because it has to be tessellated. A circle or a rounded
6615        // rectangle would antialias itself from a distance field and would
6616        // rightly leave the pass alone -- which the test below is about.
6617        let mut triangle = PathBuilder::new();
6618        triangle
6619            .move_to(Vec2::new(20.0, 20.0))
6620            .line_to(Vec2::new(40.0, 20.0))
6621            .line_to(Vec2::new(20.0, 40.0))
6622            .close();
6623        canvas
6624            .draw_path(&triangle.build(), &Paint::fill(Color::WHITE))
6625            .unwrap();
6626
6627        // Sampling is a property of the pass, so it cannot vary per shape.
6628        // Honoring the request for the frame beats silently ignoring it.
6629        assert!(canvas.finish().root().descriptor.samples > 1);
6630    }
6631
6632    #[test]
6633    fn a_shape_that_antialiases_itself_leaves_the_pass_alone() {
6634        // The saving that comes with the distance field, and the reason it is
6635        // worth having beyond the vertex count: a frame whose only antialiased
6636        // shapes compute their own coverage does not have to multisample, which
6637        // is four times the fill and four times the bandwidth for an edge it
6638        // was already going to get right.
6639        let mut canvas = canvas();
6640        canvas
6641            .draw_circle(Vec2::splat(20.0), 8.0, &Paint::fill(Color::WHITE))
6642            .unwrap();
6643        canvas
6644            .draw_rrect(
6645                Rect::new(30.0, 30.0, 60.0, 50.0),
6646                6.0,
6647                &Paint::fill(Color::WHITE),
6648            )
6649            .unwrap();
6650        assert_eq!(
6651            canvas.finish().root().descriptor.samples,
6652            1,
6653            "an analytic shape should not multisample the pass"
6654        );
6655    }
6656
6657    /// A black shadow is untouched by the tonal remap; a colored one is not.
6658    ///
6659    /// Both halves matter. The remap exists for colored shadows, and if it
6660    /// moved a black one it would change nearly every shadow anybody draws --
6661    /// so the identity is the safety property and the change is the feature.
6662    ///
6663    /// The figures are upstream's formula evaluated by hand rather than
6664    /// recorded from this implementation, which is the only way a test of a
6665    /// port can fail when the port is wrong.
6666    #[test]
6667    fn a_colored_shadow_is_tonally_adjusted_and_a_black_one_is_not() {
6668        // Black: at zero luminance the color term vanishes and the alpha is
6669        // left at the quarter it started from.
6670        let black = super::tonal_shadow_color(Color::srgb(0.0, 0.0, 0.0, 1.0));
6671        let [r, g, b, a] = black.to_srgb();
6672        assert_eq!([r, g, b], [0.0, 0.0, 0.0], "a black shadow gained color");
6673        assert!(
6674            (a - 0.25).abs() < 1e-6,
6675            "a black shadow should keep the quarter alpha, got {a}"
6676        );
6677
6678        // Saturated red at full alpha. Luminance is a half, so:
6679        //   alpha_adjust    = (2.6 + (-2.66667 + 1.06667*0.25)*0.25)*0.25 = 0.5
6680        //   color_alpha     = (3.544762 + (-4.891428 + 2.3466*0.5)*0.5)*0.5
6681        //                   = 0.842849, times alpha_adjust  = 0.421424
6682        //   grayscale_alpha = 0.25 * (1 - 0.4*0.5)           = 0.2
6683        //   color_scale     = 0.421424 * 0.8                 = 0.337140
6684        //   tonal_alpha     = 0.537140
6685        //   unpremul_scale  = 0.337140 / 0.537140            = 0.627650
6686        let red = super::tonal_shadow_color(Color::srgb(1.0, 0.0, 0.0, 1.0));
6687        let [r, g, b, a] = red.to_srgb();
6688        assert!(
6689            (a - 0.537_140).abs() < 1e-4,
6690            "the tonal alpha for a red shadow is {a}, not 0.53714"
6691        );
6692        assert!(
6693            (r - 0.627_650).abs() < 1e-4,
6694            "the tonal red for a red shadow is {r}, not 0.62765"
6695        );
6696        assert_eq!([g, b], [0.0, 0.0], "a red shadow gained other channels");
6697
6698        // And it is emphatically not what taking a quarter of the alpha gives,
6699        // which is what this used to do.
6700        assert!(
6701            a > 0.5,
6702            "the remap left the alpha near the naive quarter: {a}"
6703        );
6704
6705        // A mid-tone, which is the case that says *which space* this runs in.
6706        // Neither of the colors above can: zero and one are fixed points of the
6707        // transfer function, so a saturated primary is the same number encoded
6708        // or linear and the two readings agree by accident. Here they do not --
6709        // the same formula on linear light gives (0.137, 0.032, 0.004) at an
6710        // alpha of 0.410, against upstream's (0.345, 0.173, 0.058) at 0.506.
6711        let midtone = super::tonal_shadow_color(Color::srgb(0.6, 0.3, 0.1, 1.0));
6712        let [r, g, b, a] = midtone.to_srgb();
6713        for (got, want, name) in [
6714            (a, 0.506_265, "alpha"),
6715            (r, 0.345_193, "red"),
6716            (g, 0.172_596, "green"),
6717            (b, 0.057_532, "blue"),
6718        ] {
6719            assert!(
6720                (got - want).abs() < 1e-4,
6721                "the tonal {name} for a mid-tone shadow is {got}, not {want} -- \
6722                 which is what running the remap on light rather than on encoded \
6723                 values would give"
6724            );
6725        }
6726    }
6727
6728    #[test]
6729    fn clearing_sets_the_background_rather_than_recording_a_draw() {
6730        let mut canvas = canvas();
6731        canvas.clear(Color::rgba8(20, 30, 40, 255));
6732        let recording = canvas.finish();
6733        // A clear is a pass property; recording it as a full-target rectangle
6734        // would cost a draw and defeat the load operation.
6735        assert!(recording.is_empty());
6736        assert!(recording.root().descriptor.clear.is_some());
6737    }
6738}