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}