Skip to main content

impeller_hal/
material.rs

1//! What fills a shape.
2//!
3//! A material is the paint as a backend sees it: fully resolved, in clip space,
4//! and packed into the layout the shader expects. Resolving happens above,
5//! because gradient geometry has to travel through the same transform the shape
6//! did.
7
8use crate::blend::BlendMode;
9
10/// Floats in the packed representation.
11///
12/// This was 128 bytes for a long time because that is exactly what every
13/// device guarantees as push constants, and the material travelled as one.
14/// That guarantee was also a ceiling, and the material had grown to occupy it
15/// exactly -- so the mechanism was deciding what a paint could hold, which is
16/// the wrong way round. Materials travel in a uniform buffer now, and the
17/// limit that binds is `maxUniformBufferRange`, whose guaranteed minimum is
18/// sixteen kilobytes: two orders of magnitude of room rather than none.
19///
20/// The size has not changed with the mechanism, and should not change without
21/// a material that needs it. Every float here is read by a shader that
22/// branches on the kind, and floats nobody reads are bandwidth in the one
23/// place a renderer spends it per draw.
24pub const MATERIAL_FLOATS: usize = 64;
25
26/// Enforced at compile time rather than by a test, so a material that outgrew
27/// what every device guarantees could not be built at all.
28///
29/// The bound is `maxUniformBufferRange`'s guaranteed minimum. A material is
30/// nowhere near it; the assertion is here because the previous bound was
31/// reached, and the way that was noticed was this line failing to compile.
32const _: () = assert!(
33    MATERIAL_FLOATS * 4 <= 16384,
34    "a material must fit the 16 KiB of uniform buffer range every device guarantees"
35);
36
37/// The most stops a gradient carries in the material itself.
38///
39/// Four covers the overwhelming majority of real gradients, and covers them
40/// without a texture, an upload or a binding. It is not a limit on what can be
41/// drawn: beyond this the recorder tabulates the stops into a ramp and the
42/// shader samples it instead, which is what this constant used to say was
43/// waiting on the HAL learning to sample textures. It has since learned.
44pub const MAX_STOPS: usize = 4;
45
46/// Offsets into the packed layout, matching the shader's declaration.
47///
48/// Public because it is a contract between the shader and every backend, not an
49/// internal detail. Both backends copy the packed floats straight into a
50/// uniform buffer, so a member is found by its offset here rather than by a
51/// name -- and naming the offsets in one place keeps the layout from being
52/// written out again as bare indices that quietly go stale when it grows.
53pub mod layout {
54    /// Four stop colors.
55    pub const STOPS: usize = 0;
56    /// Stop positions.
57    pub const OFFSETS: usize = 16;
58    /// Endpoints, or center plus angles.
59    pub const GEOMETRY: usize = 20;
60    /// Clip space to the paint's own space: three columns of a three-by-three,
61    /// in column order, each padded to four floats.
62    ///
63    /// Twelve floats rather than nine because that is how a `mat3x3` sits in a
64    /// uniform block, and because every member here is a four-component vector
65    /// on purpose -- it is what lets both backends copy the packed material
66    /// straight in without writing padding around anything.
67    ///
68    /// The paint's origin is inside this matrix rather than beside it in
69    /// `GEOMETRY`, which is why the first two floats there are unclaimed for
70    /// every kind that carries a mapping. See `invert_to_local`.
71    pub const TO_LOCAL: usize = 24;
72    /// Stop count, material kind, and two floats whose meaning the kind
73    /// decides -- a corner radius, a stroke width, a blur's deviation, a
74    /// gradient's tile mode, a conical gradient's separation.
75    ///
76    /// A zero stop count means the colors are in a ramp texture; see
77    /// `stop_count_code`.
78    pub const PARAMS: usize = 36;
79    /// A color filter's matrix, by column.
80    pub const FILTER: usize = 40;
81    /// The constant a color filter adds.
82    pub const FILTER_OFFSET: usize = 56;
83    /// Which color filter, if any, and in which form its matrix is stated.
84    ///
85    /// The second float holds the tint blend, and the last two hold the dither;
86    /// see [`DITHER`]. Four unrelated things share a slot because the slot is a
87    /// four-component vector whether or not anything fills it, and a fifth
88    /// vector would cost every draw sixteen bytes to carry three unused floats.
89    pub const FILTER_PARAMS: usize = 60;
90    /// The dither amplitude, and whether the target encodes on write.
91    ///
92    /// Amplitude first, in the target's storage units, with zero meaning no
93    /// dithering; then a flag saying whether a step of that size is a step of
94    /// encoded value rather than of light. Both are written by the backend at
95    /// submission rather than by the recorder, because the target's format is
96    /// what decides them and a recording is made without one -- which is also
97    /// what keeps the color policy intact. The shader is handed two numbers and
98    /// still knows nothing about formats.
99    pub const DITHER: usize = 62;
100}
101
102/// The number the shader reads for a tile mode.
103///
104/// Written once rather than at each call site: an image and a gradient must
105/// agree about what `1.0` means, and two copies of a mapping eventually do not.
106/// The stop count the shader reads, which doubles as the ramp flag.
107///
108/// Zero means the colors are in a texture rather than in this material, and
109/// the shader samples them instead of walking the stops. A sentinel rather
110/// than a flag of its own because the two are mutually exclusive by
111/// construction -- a ramp exists only when the stops outnumbered what fits, so
112/// a material with a ramp has no count worth reporting -- and because the float
113/// this frees is the one a conical gradient needs for its second center. That
114/// is the whole reason the fourth parameter slot was available to it.
115fn stop_count_code(count: usize, ramp: &Option<u32>) -> f32 {
116    match ramp {
117        Some(_) => 0.0,
118        None => count.max(1) as f32,
119    }
120}
121
122fn tile_code(tile: TileMode) -> f32 {
123    match tile {
124        TileMode::Clamp => tile::CLAMP,
125        TileMode::Repeat => tile::REPEAT,
126        TileMode::Decal => tile::DECAL,
127        TileMode::Mirror => tile::MIRROR,
128    }
129}
130
131/// Kind selector shared with the shader.
132pub mod kind {
133    pub const SOLID: f32 = 0.0;
134    pub const LINEAR: f32 = 1.0;
135    pub const RADIAL: f32 = 2.0;
136    pub const SWEEP: f32 = 3.0;
137    pub const IMAGE: f32 = 4.0;
138    pub const GLYPH: f32 = 5.0;
139    pub const BLUR: f32 = 6.0;
140    pub const ROUNDED_RECT: f32 = 7.0;
141    pub const ELLIPSE: f32 = 8.0;
142    pub const CONICAL: f32 = 9.0;
143    pub const MESH: f32 = 10.0;
144    pub const MORPHOLOGY: f32 = 11.0;
145    pub const ROUNDED_RECT_BLUR: f32 = 12.0;
146    pub const POINT_FIELD: f32 = 13.0;
147}
148
149/// How many textures one runtime program may sample.
150///
151/// A fixed ceiling because the descriptor set layout is shared: every pipeline
152/// this renderer builds is built against one layout, so the bindings it
153/// declares are the same for a solid fill and for a caller's program. Raising
154/// this costs an image binding on every draw; removing the ceiling costs a
155/// layout per program.
156///
157/// Four covers what `dart:ui` shaders ask for in practice -- an image and a
158/// mask, an image and a gradient ramp, occasionally a third.
159pub const MAX_EFFECT_TEXTURES: usize = 4;
160
161/// How far one morphology pass reaches, in texels each way.
162///
163/// The shader's loop has to be bounded, and this is the bound. It is not a
164/// limit on the filter: a larger radius becomes more passes, since dilating
165/// twice dilates by the sum. Sixty-five samples per pixel per pass is already
166/// enough bandwidth that splitting is the cheaper answer anyway.
167pub const MORPHOLOGY_TAPS: u32 = 32;
168
169/// How many floats a runtime effect may take.
170///
171/// The whole material block, because an effect replaces the shader that would
172/// have read it as a material. A caller writing an effect declares the same
173/// std140 block and reads these as their own.
174pub const RUNTIME_FLOATS: usize = MATERIAL_FLOATS;
175
176/// How a texture is read between its texels.
177///
178/// Not two samplers. The same argument that keeps tile modes in the shader
179/// keeps this there: a sampler baked with a filter would mean one sampler per
180/// combination and a descriptor set per draw that used a different one. A
181/// linear sampler read exactly at a texel's center returns that texel and
182/// nothing else, so nearest sampling is the coordinate snapped to the nearest
183/// center before the read, which is one multiply-floor-divide and no bindings
184/// at all.
185///
186/// `dart:ui` offers four qualities. These are its first two; the other two are
187/// mipmapped and bicubic, and neither exists here to select.
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
189pub enum Sampling {
190    /// Blend the texels around the coordinate. The right default: an image
191    /// drawn at any size but its own is otherwise a mess of hard edges.
192    #[default]
193    Linear,
194    /// The one texel the coordinate falls in.
195    ///
196    /// What pixel art needs, and what a sprite drawn at exactly its own size
197    /// wants in order to be certain no neighbor bled in.
198    Nearest,
199    /// A bicubic reconstruction over the sixteen texels around the coordinate.
200    ///
201    /// What `dart:ui` calls `FilterQuality.high`. Sharper than linear under
202    /// magnification, because the curve through four texels along an axis has
203    /// a slope where a straight line between two has a corner, and it costs
204    /// sixteen reads per fragment to say so.
205    ///
206    /// The particular curve is Mitchell-Netravali with `B` and `C` both a
207    /// third, which is what Skia's high quality has always meant and so what a
208    /// caller porting from Flutter is expecting. It rings slightly -- the
209    /// weights go a little negative between one and two texels out -- and that
210    /// overshoot is the sharpening, not an error in it.
211    Cubic,
212    /// A linear read of the mip level that matches how far the image is being
213    /// minified, blended with the level either side of it.
214    ///
215    /// What `dart:ui` calls `FilterQuality.medium`, and the one quality that
216    /// answers minification rather than magnification. Drawn at half its size
217    /// an image read linearly skips every other texel and aliases; read from
218    /// the level built for that size, every texel of the original contributes.
219    ///
220    /// It needs a texture that was allocated with a chain, since a level that
221    /// was never made cannot be read. On a texture without one every read lands
222    /// on the image itself and this is linear -- which is a picture that is
223    /// merely worse rather than wrong, and is what a sampler does with a level
224    /// it does not have.
225    Mipmap,
226}
227
228/// The number the shader reads for a sampling mode.
229fn sampling_code(sampling: Sampling) -> f32 {
230    match sampling {
231        Sampling::Linear => sampling::LINEAR,
232        Sampling::Nearest => sampling::NEAREST,
233        Sampling::Cubic => sampling::CUBIC,
234        Sampling::Mipmap => sampling::MIPMAP,
235    }
236}
237
238/// Sampling selector shared with the shader.
239pub mod sampling {
240    pub const LINEAR: f32 = 0.0;
241    pub const NEAREST: f32 = 1.0;
242    pub const CUBIC: f32 = 2.0;
243    pub const MIPMAP: f32 = 3.0;
244}
245
246/// Tile mode selector shared with the shader.
247pub mod tile {
248    pub const CLAMP: f32 = 0.0;
249    pub const REPEAT: f32 = 1.0;
250    pub const DECAL: f32 = 2.0;
251    pub const MIRROR: f32 = 3.0;
252}
253
254/// What space a color filter's matrix expects its input in.
255///
256/// A `dart:ui` color matrix is defined on straight color, which is the form a
257/// person writes a saturation or a sepia matrix in. A blend against a constant
258/// color is defined on premultiplied color, which is the form the compositing
259/// rules are stated in. Both are affine, both are one matrix here, and this is
260/// which one -- because the shader works in premultiplied color and has to know
261/// whether to undo that before applying the matrix.
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
263pub enum ColorForm {
264    /// Premultiplied, which is what the shader already has.
265    #[default]
266    Premultiplied,
267    /// Straight, which the shader divides out first and restores after.
268    Straight,
269}
270
271/// A function applied to a material's color, after the material and before the
272/// blend.
273///
274/// Only affine functions, which is less of a restriction than it sounds: a
275/// `dart:ui` color matrix is affine by definition, and every separable
276/// Porter-Duff blend against a *constant* color is affine in the other operand.
277/// So one matrix in the shader serves both, and which blend modes are offered
278/// is decided on the processor rather than in the fragment.
279#[derive(Debug, Clone, Copy, PartialEq, Default)]
280pub enum ColorFilter {
281    #[default]
282    None,
283    /// `columns[j]` scales the input's `j`th channel, and `offset` is added.
284    ///
285    /// Stored by column rather than by row -- the transpose of how `dart:ui`
286    /// writes it -- because that is what makes the shader four multiply-adds
287    /// of vectors rather than four dot products, and because a column is a
288    /// `vec4` where a row of the five-wide form is not.
289    Matrix {
290        columns: [[f32; 4]; 4],
291        offset: [f32; 4],
292        form: ColorForm,
293    },
294    /// Blend a constant color against the material's own, in a mode a matrix
295    /// cannot state.
296    ///
297    /// `dart:ui`'s `ColorFilter.mode` takes any blend mode, and most of them
298    /// are affine in the destination once the source is fixed -- those become
299    /// a [`Self::Matrix`] and cost the shader nothing beyond the multiply it
300    /// was already doing. The advanced modes are not affine: they are
301    /// piecewise, or they exchange components between channels. This is where
302    /// they go.
303    ///
304    /// It is arithmetic against a constant rather than against the frame, so
305    /// it needs no framebuffer fetch and no extension -- unlike the same mode
306    /// set on the paint, which reaches the hardware's blending unit and is
307    /// gated on one. The shader evaluates it with the same function a mesh's
308    /// per-vertex tint uses.
309    ///
310    /// `color` is premultiplied, as [`Self::blend`] leaves it and as the
311    /// specification states its operands.
312    Blend { color: [f32; 4], mode: BlendMode },
313    /// The sRGB transfer function, one direction or the other.
314    ///
315    /// The one filter `dart:ui` offers that a matrix cannot express: the curve
316    /// is piecewise, and the piece that covers almost all of the range has an
317    /// exponent in it. Approximating it as a power of 2.2 -- which is the usual
318    /// shortcut -- is wrong by about a percent in the midtones and much more
319    /// near black, where the linear segment is doing the work, so the shader
320    /// evaluates the real thing instead.
321    ///
322    /// Applied to straight color, per channel, leaving alpha alone. Gamma on a
323    /// premultiplied channel would be encoding the alpha along with the color.
324    Gamma { direction: Gamma },
325}
326
327/// Which way through the sRGB transfer function a [`ColorFilter::Gamma`] goes.
328#[derive(Debug, Clone, Copy, PartialEq, Eq)]
329pub enum Gamma {
330    /// Linear light in, sRGB's encoding out. `ColorFilter.linearToSrgbGamma`.
331    LinearToSrgb,
332    /// sRGB's encoding in, linear light out. `ColorFilter.srgbToLinearGamma`.
333    SrgbToLinear,
334}
335
336impl ColorFilter {
337    /// A filter from the twenty numbers `dart:ui` states one in.
338    ///
339    /// Row-major and five wide: four scales and a constant per output channel,
340    /// red row first, applied to straight color with each channel from zero to
341    /// one. The constants are in the same units, so a matrix written against
342    /// the 0-255 convention has to have its last column divided by 255 first.
343    pub fn matrix(rows: [f32; 20]) -> Self {
344        let mut columns = [[0.0f32; 4]; 4];
345        let mut offset = [0.0f32; 4];
346        for (out, row) in rows.chunks_exact(5).enumerate() {
347            for (channel, value) in row[..4].iter().enumerate() {
348                columns[channel][out] = *value;
349            }
350            offset[out] = row[4];
351        }
352        Self::Matrix {
353            columns,
354            offset,
355            form: ColorForm::Straight,
356        }
357    }
358
359    /// Linear light encoded into sRGB, which is `ColorFilter.linearToSrgbGamma`.
360    pub fn linear_to_srgb() -> Self {
361        Self::Gamma {
362            direction: Gamma::LinearToSrgb,
363        }
364    }
365
366    /// sRGB decoded back to linear light, which is
367    /// `ColorFilter.srgbToLinearGamma`.
368    pub fn srgb_to_linear() -> Self {
369        Self::Gamma {
370            direction: Gamma::SrgbToLinear,
371        }
372    }
373
374    /// A filter that blends a constant color against the material's own.
375    ///
376    /// The material is the destination and `color` is the source, which is the
377    /// way round `dart:ui` states `ColorFilter.mode` and the way round that
378    /// makes an icon sheet tinted by `SrcIn` mean what everyone expects.
379    ///
380    /// Most modes are affine in the destination once the source is fixed, so
381    /// each becomes a matrix and the shader needs no blending arithmetic at
382    /// all. The advanced modes are not affine -- they are piecewise, or
383    /// exchange components between channels -- so those become
384    /// [`Self::Blend`] and are evaluated per fragment against the constant, by
385    /// the same function a mesh's per-vertex tint goes through.
386    ///
387    /// Every mode is therefore a filter and none of them is refused. This used
388    /// to answer a `Result` and say in its own documentation that the advanced
389    /// modes were "refused rather than approximated", which the code below has
390    /// never done. Eighteen call sites carried an `expect` that could not fire, and
391    /// several of their messages stated the opposite of what the branch they
392    /// were on does.
393    ///
394    /// `color` is straight, like every color a caller states.
395    pub fn blend(color: [f32; 4], mode: BlendMode) -> Self {
396        use BlendMode as B;
397        let alpha = color[3];
398        // Premultiplied, because the rules below are stated for premultiplied
399        // operands and the shader's own color is premultiplied too.
400        let s = [color[0] * alpha, color[1] * alpha, color[2] * alpha, alpha];
401        let zero = [[0.0f32; 4]; 4];
402        let scaled = |k: f32| {
403            let mut m = zero;
404            for (i, column) in m.iter_mut().enumerate() {
405                column[i] = k;
406            }
407            m
408        };
409        // The destination's alpha reaches every output channel through this
410        // column alone, which is what makes the modes that multiply by it --
411        // or by one minus it -- a matrix rather than a special case.
412        let with_alpha_column = |mut m: [[f32; 4]; 4], column: [f32; 4]| {
413            for (i, value) in column.iter().enumerate() {
414                m[3][i] += *value;
415            }
416            m
417        };
418        let negated = [-s[0], -s[1], -s[2], -s[3]];
419        let (columns, offset) = match mode {
420            B::Clear => (zero, [0.0; 4]),
421            B::Src => (zero, s),
422            B::Dst => (scaled(1.0), [0.0; 4]),
423            B::SrcOver => (scaled(1.0 - alpha), s),
424            B::DstOver => (with_alpha_column(scaled(1.0), negated), s),
425            B::SrcIn => (with_alpha_column(zero, s), [0.0; 4]),
426            B::DstIn => (scaled(alpha), [0.0; 4]),
427            B::SrcOut => (with_alpha_column(zero, negated), s),
428            B::DstOut => (scaled(1.0 - alpha), [0.0; 4]),
429            B::SrcATop => (with_alpha_column(scaled(1.0 - alpha), s), [0.0; 4]),
430            B::DstATop => (with_alpha_column(scaled(alpha), negated), s),
431            B::Xor => (with_alpha_column(scaled(1.0 - alpha), negated), s),
432            B::Plus => (scaled(1.0), s),
433            B::Modulate => {
434                let mut m = zero;
435                for (i, column) in m.iter_mut().enumerate() {
436                    column[i] = s[i];
437                }
438                (m, [0.0; 4])
439            }
440            // Not affine, so not a matrix. Evaluated per fragment instead,
441            // against the same constant, by the same function a mesh's
442            // per-vertex tint goes through.
443            mode => return Self::Blend { color: s, mode },
444        };
445        Self::Matrix {
446            columns,
447            offset,
448            form: ColorForm::Premultiplied,
449        }
450    }
451
452    /// Whether this changes anything.
453    pub fn is_identity(&self) -> bool {
454        match self {
455            Self::None => true,
456            Self::Matrix {
457                columns,
458                offset,
459                form: _,
460            } => {
461                offset.iter().all(|v| *v == 0.0)
462                    && columns.iter().enumerate().all(|(j, column)| {
463                        column
464                            .iter()
465                            .enumerate()
466                            .all(|(i, v)| *v == if i == j { 1.0 } else { 0.0 })
467                    })
468            }
469            // `Dst` returns the destination untouched, which is the identity
470            // and is the only mode here that is. It cannot arrive through
471            // `blend`, which sends every affine mode to a matrix, but it can be
472            // written by hand.
473            Self::Blend { mode, .. } => *mode == BlendMode::Dst,
474            // The curve is the identity at exactly three points -- zero, one,
475            // and nowhere else on the range -- so as a function it never is.
476            Self::Gamma { .. } => false,
477        }
478    }
479
480    /// The code the shader reads to choose a path.
481    fn code(&self) -> f32 {
482        match self {
483            Self::None => filter::NONE,
484            Self::Matrix {
485                form: ColorForm::Premultiplied,
486                ..
487            } => filter::PREMULTIPLIED,
488            Self::Matrix {
489                form: ColorForm::Straight,
490                ..
491            } => filter::STRAIGHT,
492            Self::Gamma {
493                direction: Gamma::LinearToSrgb,
494            } => filter::LINEAR_TO_SRGB,
495            Self::Gamma {
496                direction: Gamma::SrgbToLinear,
497            } => filter::SRGB_TO_LINEAR,
498            Self::Blend { .. } => filter::BLEND,
499        }
500    }
501
502    pub(crate) fn pack_into(&self, out: &mut [f32; MATERIAL_FLOATS]) {
503        out[layout::FILTER_PARAMS] = self.code();
504        match self {
505            Self::Matrix {
506                columns, offset, ..
507            } => {
508                for (j, column) in columns.iter().enumerate() {
509                    out[layout::FILTER + j * 4..layout::FILTER + j * 4 + 4].copy_from_slice(column);
510                }
511                out[layout::FILTER_OFFSET..layout::FILTER_OFFSET + 4].copy_from_slice(offset);
512            }
513            // The constant goes where a matrix's offset would, which is what it
514            // is: the term that does not depend on the material. The mode goes
515            // in the first float of the matrix itself, which this filter has no
516            // use for -- sixteen floats are already reserved for every draw,
517            // and spending a fifth vector to carry one number would cost every
518            // draw that does not blend.
519            Self::Blend { color, mode } => {
520                out[layout::FILTER_OFFSET..layout::FILTER_OFFSET + 4].copy_from_slice(color);
521                out[layout::FILTER] = mode.code();
522            }
523            Self::None | Self::Gamma { .. } => {}
524        }
525    }
526}
527
528/// Filter selector shared with the shader.
529pub mod filter {
530    pub const NONE: f32 = 0.0;
531    pub const PREMULTIPLIED: f32 = 1.0;
532    pub const STRAIGHT: f32 = 2.0;
533    pub const LINEAR_TO_SRGB: f32 = 3.0;
534    pub const SRGB_TO_LINEAR: f32 = 4.0;
535    /// A blend against a constant color, for the modes a matrix cannot state.
536    pub const BLEND: f32 = 5.0;
537
538    /// Whether a code names a filter that reads straight rather than
539    /// premultiplied color.
540    ///
541    /// The shader decides this by comparison rather than by equality, and gets
542    /// the same answer, so the boundary lives here where both can cite it: a
543    /// filter added above this line is straight unless it says otherwise.
544    ///
545    /// [`BLEND`] says otherwise, which is what the sentence above anticipated.
546    /// The compositing specification states every blend on premultiplied
547    /// operands and the shader's own blend function takes them that way, so
548    /// handing it straight color would be handing it the wrong numbers.
549    pub fn is_straight(code: f32) -> bool {
550        code > PREMULTIPLIED && code < BLEND
551    }
552}
553
554/// A color stop.
555#[derive(Debug, Clone, Copy, PartialEq)]
556pub struct Stop {
557    /// Linear color with straight alpha.
558    pub color: [f32; 4],
559    /// Position along the gradient, from zero to one.
560    pub offset: f32,
561}
562
563impl Stop {
564    pub fn new(color: [f32; 4], offset: f32) -> Self {
565        Self { color, offset }
566    }
567}
568
569/// Maps a clip-space offset into a gradient's own space, in column order.
570///
571/// Clip space is anisotropic whenever the target is not square, and a transform
572/// may rotate or skew as well, so a circle in user space is an ellipse there.
573/// Radial and sweep gradients measure distance and angle, both of which that
574/// distortion changes, so they map back before measuring. A linear gradient
575/// projects onto an axis, which distortion does not affect, and so does not
576/// need this.
577pub type ToLocal = [f32; 12];
578
579/// How a shape is filled.
580#[derive(Debug, Clone, PartialEq)]
581pub enum Material {
582    Solid([f32; 4]),
583    /// A gradient along an axis, starting at a point **in clip space**.
584    ///
585    /// Clip space for the start, because the fragment stage locates itself from
586    /// an interpolated clip position: the alternative, the fragment coordinate
587    /// builtin, has a different origin in each graphics API and would run the
588    /// gradient in opposite directions on the two backends.
589    ///
590    /// The axis is in the gradient's own space, and `to_local` maps a clip-space
591    /// offset into it — the same pairing radial and sweep use, and for the same
592    /// reason. Clip space is anisotropic whenever the target is not square, so
593    /// projecting onto an axis *there* weights the two axes by the target's
594    /// shape: on a target twice as wide as it is tall, a diagonal gradient runs
595    /// in the wrong direction.
596    LinearGradient {
597        /// End minus start, in the gradient's own space.
598        axis: [f32; 2],
599        to_local: ToLocal,
600        stops: Vec<Stop>,
601        /// Texture slot holding this gradient's colors, when they did not fit.
602        ///
603        /// `None` is the ordinary case: the stops travel in the material and
604        /// the shader walks them. `Some` means the recorder tabulated them into
605        /// an image instead, because there were more than [`MAX_STOPS`], and
606        /// the shader reads the color at the parameter rather than computing
607        /// it. The two must agree where both are possible, which is what makes
608        /// the choice invisible to a caller.
609        ramp: Option<u32>,
610        /// What happens beyond the two endpoints.
611        ///
612        /// The parameter a gradient is sampled by runs from zero at one end to
613        /// one at the other and is defined everywhere else too, so a shape
614        /// larger than its gradient asks a question the stops do not answer.
615        /// Clamping holds the end colors, which is the usual choice; repeating
616        /// tiles the ramp, which is what a stripe pattern is; decal draws
617        /// nothing outside, the same meaning it has for an image.
618        tile: TileMode,
619    },
620    /// A gradient outward from a center, **in clip space**, where `to_local`
621    /// carries the radius: it maps the clip-space offset so that the gradient's
622    /// edge lands at unit distance.
623    RadialGradient {
624        to_local: ToLocal,
625        stops: Vec<Stop>,
626        /// Texture slot holding this gradient's colors, when they did not fit.
627        ///
628        /// `None` is the ordinary case: the stops travel in the material and
629        /// the shader walks them. `Some` means the recorder tabulated them into
630        /// an image instead, because there were more than [`MAX_STOPS`], and
631        /// the shader reads the color at the parameter rather than computing
632        /// it. The two must agree where both are possible, which is what makes
633        /// the choice invisible to a caller.
634        ramp: Option<u32>,
635        /// What happens beyond the radius. See [`Material::LinearGradient`].
636        tile: TileMode,
637    },
638    /// A gradient around a center, **in clip space**, running from `start_angle`
639    /// to `end_angle` in radians.
640    SweepGradient {
641        to_local: ToLocal,
642        start_angle: f32,
643        end_angle: f32,
644        stops: Vec<Stop>,
645        /// Texture slot holding this gradient's colors, when they did not fit.
646        ///
647        /// `None` is the ordinary case: the stops travel in the material and
648        /// the shader walks them. `Some` means the recorder tabulated them into
649        /// an image instead, because there were more than [`MAX_STOPS`], and
650        /// the shader reads the color at the parameter rather than computing
651        /// it. The two must agree where both are possible, which is what makes
652        /// the choice invisible to a caller.
653        ramp: Option<u32>,
654        /// What happens outside the swept arc.
655        ///
656        /// Unlike the other two this can be a no-op: a sweep covering the whole
657        /// turn has no outside, and every direction lands within it.
658        tile: TileMode,
659    },
660    /// A gradient between two circles, the general form the other two are
661    /// special cases of.
662    ///
663    /// `center` is the first circle's center **in clip space**, and `to_local`
664    /// maps a clip-space offset into a space where that center is the origin
665    /// and the second circle's center lies at `(separation, 0)`. Putting the
666    /// separation on an axis costs nothing -- the rotation folds into a matrix
667    /// that has to be there anyway -- and buys the second center for one float
668    /// instead of two, which is what makes this fit at all.
669    ///
670    /// The radii are in that same space and are *not* normalized, unlike the
671    /// radial gradient's, because there are two of them and a scale can only
672    /// remove one. Carrying both plainly also means the degenerate cases need
673    /// no special handling: concentric circles are `separation == 0`, and a
674    /// cone rather than a tube is `radius_delta != 0`.
675    ConicalGradient {
676        to_local: ToLocal,
677        /// Radius of the first circle.
678        start_radius: f32,
679        /// Second radius minus the first.
680        radius_delta: f32,
681        /// Distance between the two centers.
682        separation: f32,
683        stops: Vec<Stop>,
684        /// Texture slot holding this gradient's colors, when they did not fit.
685        /// See [`Material::LinearGradient`].
686        ramp: Option<u32>,
687        /// What happens where the parameter leaves the unit interval. See
688        /// [`Material::LinearGradient`].
689        tile: TileMode,
690    },
691    /// A caller's own fragment program, with the floats it reads.
692    ///
693    /// The program is named rather than carried: registering one builds a
694    /// pipeline, which is expensive and outlives any draw, so a context holds
695    /// them and a material names which. The floats travel in the same uniform
696    /// block every other material uses, which is what lets an effect exist
697    /// without a second descriptor set.
698    Runtime {
699        program: u32,
700        uniforms: Vec<f32>,
701        /// The textures the program may sample, in the order it declares them.
702        ///
703        /// `None` in a position means the program does not read that binding,
704        /// and the backend binds its placeholder there -- a pipeline must have
705        /// every binding it declares bound, however unreachable the branch
706        /// reading it.
707        ///
708        /// The count is fixed rather than free because the descriptor set
709        /// layout every pipeline is built against has to be one layout. Four is
710        /// what that costs: three unused image bindings on a draw that samples
711        /// nothing, against a second set and a second layout for the draws that
712        /// want more than one.
713        textures: [Option<u32>; MAX_EFFECT_TEXTURES],
714    },
715    /// A texture, sampled at coordinates the vertices carry.
716    ///
717    /// Deliberately not [`Material::Image`] with a switch on where its
718    /// coordinates come from. An image mapped from clip space needs an origin,
719    /// a matrix and a source rectangle to say which part of a sheet it draws;
720    /// a mesh needs none of them, because a caller stating a coordinate per
721    /// vertex has already answered all three. What is left is small enough to
722    /// be its own thing, and keeping it separate means neither carries a field
723    /// that means nothing for it.
724    ///
725    /// This is what makes a sprite batch one draw: a hundred quads reading a
726    /// hundred different parts of one sheet differ only in their vertices.
727    Mesh {
728        /// Index into the texture table given at submission.
729        slot: u32,
730        /// Scales the sampled color, applied to premultiplied color like
731        /// [`Material::Image`]'s.
732        alpha: f32,
733        /// Straight color the sampled texel is multiplied by; white changes
734        /// nothing.
735        tint: [f32; 4],
736        /// What happens where a vertex names a coordinate outside the texture.
737        tile: TileMode,
738        /// How to read between texels.
739        sampling: Sampling,
740    },
741    /// A texture, sampled through a mapping from clip space.
742    ///
743    /// `origin` and `to_local` together are an affine: a clip-space position
744    /// maps to texture coordinates as `to_local * (clip - origin)`, which lands
745    /// the image's top-left corner at zero and its bottom-right at one. The
746    /// same pair a radial gradient uses, for the same reason — clip space is
747    /// anisotropic on a non-square target, so a mapping that ignored it would
748    /// stretch every image by the aspect ratio.
749    ///
750    /// Which texture is not named here. The material is data the recorder
751    /// produces without touching the device, so it carries a slot into the
752    /// table supplied at submission instead of a backend handle.
753    Image {
754        to_local: ToLocal,
755        /// Index into the texture table given at submission.
756        slot: u32,
757        /// Scales the sampled color, for drawing an image translucently.
758        ///
759        /// Applied to premultiplied color, so it scales the whole texel rather
760        /// than only its alpha. A texture holds premultiplied color whether it
761        /// was uploaded or rendered into, and treating a sample as straight
762        /// alpha would apply the alpha twice — invisible for an opaque image,
763        /// and plain the moment one translucent image is drawn into another.
764        alpha: f32,
765        tile: TileMode,
766        /// How to read between texels.
767        sampling: Sampling,
768        /// The part of the texture to draw, as `[u0, v0, u1, v1]` from zero to
769        /// one.
770        ///
771        /// The whole texture is `[0, 0, 1, 1]`, which is what every caller
772        /// wanted until sprite sheets. Normalized rather than in texels because
773        /// a material is built by a recorder that has never seen the texture
774        /// and cannot know how large it is; the caller who uploaded it does.
775        ///
776        /// Applied after tiling rather than before, so a repeat repeats the
777        /// selected piece rather than the whole sheet -- which is the only
778        /// reading of "tile this sprite" that means anything.
779        source: [f32; 4],
780        /// Straight color the sampled texel is multiplied by; white changes
781        /// nothing.
782        ///
783        /// What turns one monochrome icon sheet into every state a control has.
784        /// A generalization of `alpha` rather than a rival to it: a tint of
785        /// `[1, 1, 1, a]` is exactly that scaling, and both are applied because
786        /// removing the narrower one would break callers for no gain.
787        ///
788        /// Straight rather than premultiplied because that is how a caller
789        /// states a color, and the shader premultiplies it before multiplying a
790        /// texel that already is -- scaling color by the tint's alpha as well,
791        /// which is what keeps the result premultiplied rather than merely
792        /// close to it.
793        tint: [f32; 4],
794    },
795    /// A rounded rectangle evaluated per fragment rather than tessellated.
796    ///
797    /// The shape an interface is mostly made of, and the one where computing
798    /// coverage beats building triangles for it. A tessellated rounded
799    /// rectangle costs vertices proportional to how round it is and has hard
800    /// edges unless the whole pass is multisampled; this is two triangles
801    /// whatever the radius, and antialiases itself from the distance field it
802    /// already computes.
803    ///
804    /// The geometry is in the shape's own space, with `to_local` mapping a
805    /// clip-space position into it -- the same pairing the gradients use, and
806    /// for the same reason: clip space is anisotropic on a target that is not
807    /// square, and a distance measured there would round the corners by
808    /// different amounts on each axis.
809    RoundedRect {
810        color: [f32; 4],
811        /// Half the width and height, in the shape's own space.
812        half_size: [f32; 2],
813        to_local: ToLocal,
814        /// Corner radius, in the shape's own space.
815        radius: f32,
816        /// The radius the outline's *outer* edge turns through, which is not
817        /// always the radius grown by half the stroke.
818        ///
819        /// An outline is the difference of two offset shapes rather than a band
820        /// around one, and the outer offset is where a join shows. Grow a
821        /// rounded corner and you get a bigger rounded corner, so this is
822        /// `radius + stroke / 2` for anything with a radius, and for anything
823        /// with a round join. A *mitered* square corner is the exception: its
824        /// offset is still square, so this is zero and the field draws the
825        /// point the join asks for.
826        ///
827        /// Carried rather than derived because the shader cannot see the join,
828        /// and because deriving it wrongly is what made a square-cornered
829        /// stroke come out with rounded corners and be refused this route
830        /// altogether.
831        outer_radius: f32,
832        /// Trace the outline at this width rather than filling, in the shape's
833        /// own space. Zero fills.
834        ///
835        /// Costs a distance field nothing: the field already says how far every
836        /// fragment is from the edge, so an outline is the band where that is
837        /// small. Tessellating one instead means building a second shape --
838        /// offset inward and outward, with the corners resolved -- which is
839        /// where a stroked outline gets its vertex count and its joins.
840        stroke: f32,
841    },
842    /// A *blurred* rounded rectangle evaluated per fragment, with no blur pass.
843    ///
844    /// The sibling of [`Self::RoundedRect`], and the reason it is worth having
845    /// is that the general route costs three passes per shape -- one for the
846    /// content and two for a separable Gaussian -- where this costs one draw in
847    /// the pass already being recorded. On a Raspberry Pi 5 three shadows that
848    /// way are 7.8 ms of a 26.8 ms frame and nine of its fourteen passes.
849    ///
850    /// The method is Raph Levien's "Blurred rounded rectangles", which is what
851    /// upstream's `SolidRRectBlurContents` evaluates too: the exact convolution
852    /// of a Gaussian with a rounded rectangle has no closed form, and this
853    /// approximates it as a product of two error functions along an axis,
854    /// corrected for the corners by measuring distance with an exponent other
855    /// than two. Every field here is a number the CPU precomputed for that
856    /// expression rather than anything a caller stated -- see
857    /// `Canvas::rrect_blur_material`, which is the only place they are derived.
858    ///
859    /// Only where every corner shares one circular radius, which is upstream's
860    /// condition too. Anything else takes the general route.
861    RoundedRectBlur {
862        color: [f32; 4],
863        to_local: ToLocal,
864        /// Half the rectangle, less `r1`, in the shape's own space.
865        adjust: [f32; 2],
866        /// The corner radius the approximation uses, which is not the caller's:
867        /// it grows with the deviation, because a blurred corner is rounder
868        /// than a sharp one.
869        r1: f32,
870        /// The exponent the corner distance is measured with. Two is a circle;
871        /// this is larger, which is what makes the blurred corner's profile
872        /// match a Gaussian's rather than a circle's.
873        exponent: f32,
874        /// One over the deviation, which is what the error function takes.
875        s_inv: f32,
876        /// The shorter side, which bounds how far the fade can reach before the
877        /// two edges of the shape meet.
878        min_edge: f32,
879        /// Normalizes the fade so that the middle of a large shape reaches full
880        /// coverage.
881        scale: f32,
882    },
883    /// An ellipse evaluated per fragment.
884    ///
885    /// Its own variant rather than a rounded rectangle with a large radius,
886    /// which gives a stadium: past half the shorter side a rounded rectangle
887    /// stops changing, where an ellipse keeps curving along both axes.
888    ///
889    /// The same geometry a rounded rectangle carries, less the radius, which
890    /// the two axes already state.
891    Ellipse {
892        color: [f32; 4],
893        /// The two semi-axes, in the shape's own space.
894        half_size: [f32; 2],
895        to_local: ToLocal,
896        /// Trace the outline at this width rather than filling. Zero fills.
897        stroke: f32,
898    },
899    /// One axis of a separable Gaussian blur of a sampled texture.
900    ///
901    /// Separable because a two-dimensional Gaussian is the product of two
902    /// one-dimensional ones, so blurring along each axis in turn gives the
903    /// same result as a square of taps at a fraction of the cost: at a radius
904    /// of sixteen that is thirty-three taps against a thousand and eighty-nine.
905    /// Two passes are the price, which is why this names an axis rather than
906    /// describing the whole blur.
907    Blur {
908        to_local: ToLocal,
909        slot: u32,
910        /// One tap's step, in the sampled texture's own coordinates.
911        ///
912        /// The axis and the texel size together: a horizontal pass over a
913        /// target `w` wide steps `(1/w, 0)`. Stated here rather than derived in
914        /// the shader because the shader does not know the size of what it is
915        /// sampling.
916        step: [f32; 2],
917        /// Standard deviation, in taps.
918        ///
919        /// The tap count follows from it -- the kernel reaches
920        /// `(sigma - 0.5) * sqrt(3)` each way, which is upstream's radius for a
921        /// given deviation -- so a caller sets how soft the result is and
922        /// nothing else.
923        sigma: f32,
924    },
925    /// One axis of a morphological filter of a finished layer.
926    ///
927    /// The largest or smallest sample within a radius, per channel, which is
928    /// what `dart:ui` calls `ImageFilter.dilate` and `ImageFilter.erode`. Like
929    /// the blur it is separable -- a rectangular structuring element is the
930    /// product of two intervals -- so two passes give the square of taps.
931    ///
932    /// Unlike the blur it is also *decomposable*: dilating by `a` and then by
933    /// `b` is dilating by `a + b` exactly, because the structuring elements add
934    /// under the Minkowski sum. A radius past what one pass can reach is
935    /// therefore split across passes rather than approximated by sampling more
936    /// sparsely. Sparse taps work for a blur, where a missed sample costs a
937    /// little smoothness, and do not work here: the result is a maximum, so a
938    /// missed sample is a scallop in the edge.
939    Morphology {
940        to_local: ToLocal,
941        slot: u32,
942        /// One tap's step, in the sampled texture's own coordinates. As
943        /// [`Self::Blur::step`].
944        step: [f32; 2],
945        /// How many texels each way this pass reaches, at most
946        /// [`MORPHOLOGY_TAPS`].
947        ///
948        /// A whole number of texels, because the structuring element is a set
949        /// of samples rather than a weighting of them and there is no meaning
950        /// to half of one.
951        radius: f32,
952        /// The largest sample in reach rather than the smallest.
953        dilate: bool,
954    },
955    /// Coverage sampled from an atlas, tinting one color.
956    ///
957    /// Distinct from [`Self::Image`] in two ways that matter. The texture is
958    /// read as *coverage* rather than as color — one channel scaling a solid,
959    /// which is what antialiased text is — and the coordinates come from the
960    /// vertices rather than from a mapping in the paint, so a run of glyphs
961    /// reading different parts of one atlas is a single draw.
962    Glyph {
963        /// Linear color with straight alpha, as the text is painted.
964        color: [f32; 4],
965        /// Index into the texture table given at submission.
966        slot: u32,
967    },
968    /// Many discs of one color, evaluated from the vertices rather than the
969    /// paint, so that a field of them is one draw.
970    ///
971    /// Carries no mapping and no size, which is the whole point of it. Every
972    /// other fragment-evaluated shape here locates itself through `to_local`,
973    /// and `to_local` holds the shape's center -- so two of them at different
974    /// places are two materials and cannot share a draw. This one locates
975    /// itself from the interpolated texture coordinate, which the vertices
976    /// carry as the unit circle's corners, so any number of discs at any
977    /// centers are one material and one draw.
978    ///
979    /// The edge is the same edge. `disc_coverage` in the shader differentiates
980    /// the implicit function across the pixel rather than forming a distance,
981    /// and a derivative of an interpolated value is as available as that of a
982    /// computed one.
983    PointField {
984        /// One color for the field, premultiplied as everything here is.
985        color: [f32; 4],
986    },
987}
988
989/// What happens outside an image's own bounds.
990#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
991pub enum TileMode {
992    /// Hold the edge pixel. The usual choice for drawing an image once.
993    #[default]
994    Clamp,
995    /// Repeat the image, tiling the plane.
996    Repeat,
997    /// Draw nothing outside the image.
998    ///
999    /// Distinct from clamping in the only case that matters: a shape larger
1000    /// than the image it is filled with. Clamping smears the border across the
1001    /// remainder, which reads as a rendering fault rather than as a choice.
1002    Decal,
1003    /// Repeat, reversing every other copy.
1004    ///
1005    /// What repeating is for when the two ends do not match. A ramp tiled by
1006    /// [`TileMode::Repeat`] jumps from its last color back to its first at
1007    /// every period, and that discontinuity is a visible seam; reflecting each
1008    /// alternate copy joins end to end and leaves none. The period is twice as
1009    /// long, since a copy and its reflection make one.
1010    Mirror,
1011}
1012
1013impl Material {
1014    pub fn solid(color: [f32; 4]) -> Self {
1015        Self::Solid(color)
1016    }
1017
1018    /// The same material at `factor` of its opacity.
1019    ///
1020    /// Applied to every color a material carries, since a gradient's stops may
1021    /// differ in alpha and scaling them together is what keeps the ramp the
1022    /// same ramp. Straight alpha, so this happens before the premultiply the
1023    /// packing does rather than after it.
1024    ///
1025    /// Two variants are left alone and it is worth saying which. A blur or a
1026    /// morphology is a filter over a finished pass rather than a paint, so
1027    /// there is no color in it to dim -- and nothing asks this of one. A
1028    /// caller's program is the other: its output is whatever it computes, and
1029    /// this renderer has no uniform it may write to. That is a real limit
1030    /// wherever the caller wanted the dimming, and it is stated at the one
1031    /// place that asks for it.
1032    #[must_use]
1033    pub fn with_opacity(mut self, factor: f32) -> Self {
1034        let scale = |color: &mut [f32; 4]| color[3] *= factor;
1035        match &mut self {
1036            Self::Solid(color)
1037            | Self::RoundedRect { color, .. }
1038            | Self::RoundedRectBlur { color, .. }
1039            | Self::Ellipse { color, .. }
1040            | Self::PointField { color }
1041            | Self::Glyph { color, .. } => scale(color),
1042            Self::LinearGradient { stops, .. }
1043            | Self::RadialGradient { stops, .. }
1044            | Self::SweepGradient { stops, .. }
1045            | Self::ConicalGradient { stops, .. } => {
1046                for stop in stops.iter_mut() {
1047                    scale(&mut stop.color);
1048                }
1049            }
1050            Self::Mesh { alpha, .. } | Self::Image { alpha, .. } => *alpha *= factor,
1051            Self::Runtime { .. } | Self::Blur { .. } | Self::Morphology { .. } => {}
1052        }
1053        self
1054    }
1055
1056    /// Whether drawing with this would change anything.
1057    pub fn is_invisible(&self) -> bool {
1058        match self {
1059            Self::Solid(color) | Self::PointField { color } => color[3] <= 0.0,
1060            Self::LinearGradient { stops, .. }
1061            | Self::RadialGradient { stops, .. }
1062            | Self::SweepGradient { stops, .. }
1063            | Self::ConicalGradient { stops, .. } => {
1064                stops.is_empty() || stops.iter().all(|s| s.color[3] <= 0.0)
1065            }
1066            // What the texture holds is unknown here, so only a zero alpha
1067            // makes an image provably invisible.
1068            Self::Image { alpha, .. } | Self::Mesh { alpha, .. } => *alpha <= 0.0,
1069            // What a caller's program draws is unknowable from here, so the
1070            // only honest answer is that it might draw something.
1071            Self::Runtime { .. } => false,
1072            // A blur of nothing is nothing, but the pass still has to run: what
1073            // it samples is not knowable from here.
1074            Self::Blur { .. } | Self::Morphology { .. } => false,
1075            // No `half_size` to test: a blurred rectangle with no area still
1076            // draws, because the blur carries color past where the shape is.
1077            Self::RoundedRectBlur { color, .. } => color[3] <= 0.0,
1078            Self::RoundedRect {
1079                color, half_size, ..
1080            }
1081            | Self::Ellipse {
1082                color, half_size, ..
1083            } => color[3] <= 0.0 || half_size[0] <= 0.0 || half_size[1] <= 0.0,
1084            Self::Glyph { color, .. } => color[3] <= 0.0,
1085        }
1086    }
1087
1088    /// The texture slot this samples, for a backend building its bindings.
1089    ///
1090    /// Matched exhaustively rather than with a catch-all. A variant that
1091    /// samples something and is not listed here reports no slot, so the backend
1092    /// binds its placeholder and the draw comes out flat white -- a plausible
1093    /// picture rather than an error, and one nothing else would explain.
1094    /// Every texture slot this material samples, in binding order.
1095    ///
1096    /// One entry for everything but a runtime program, which may declare
1097    /// several. A backend building descriptors needs the whole tuple, since a
1098    /// set holds all of them at once; a backend asking only which slot to bind
1099    /// first has [`Self::texture_slot`].
1100    pub fn texture_slots(&self) -> [Option<u32>; MAX_EFFECT_TEXTURES] {
1101        match self {
1102            Self::Runtime { textures, .. } => *textures,
1103            other => {
1104                let mut slots = [None; MAX_EFFECT_TEXTURES];
1105                slots[0] = other.texture_slot();
1106                slots
1107            }
1108        }
1109    }
1110
1111    pub fn texture_slot(&self) -> Option<u32> {
1112        match self {
1113            Self::Image { slot, .. }
1114            | Self::Mesh { slot, .. }
1115            | Self::Glyph { slot, .. }
1116            | Self::Blur { slot, .. }
1117            | Self::Morphology { slot, .. } => Some(*slot),
1118            // A gradient names a texture only when its colors were too many to
1119            // carry, which is why this is an option rather than a slot.
1120            Self::LinearGradient { ramp, .. }
1121            | Self::RadialGradient { ramp, .. }
1122            | Self::SweepGradient { ramp, .. }
1123            | Self::ConicalGradient { ramp, .. } => *ramp,
1124            Self::Solid(_)
1125            | Self::PointField { .. }
1126            | Self::RoundedRect { .. }
1127            | Self::RoundedRectBlur { .. }
1128            | Self::Ellipse { .. } => None,
1129            // Whatever a caller named, and `None` where they named nothing --
1130            // in which case the placeholder is bound and a program that
1131            // samples anyway reads opaque white.
1132            // The first of them, for the callers that still ask a material for
1133            // one slot. `texture_slots` is what a backend binding several
1134            // should ask.
1135            Self::Runtime { textures, .. } => textures[0],
1136        }
1137    }
1138
1139    /// The stops, for any material that has them.
1140    fn stops(&self) -> &[Stop] {
1141        match self {
1142            Self::Solid(_)
1143            | Self::PointField { .. }
1144            | Self::Image { .. }
1145            | Self::Mesh { .. }
1146            | Self::Glyph { .. }
1147            | Self::Blur { .. }
1148            | Self::Morphology { .. }
1149            | Self::RoundedRect { .. }
1150            | Self::RoundedRectBlur { .. }
1151            | Self::Ellipse { .. }
1152            | Self::Runtime { .. } => &[],
1153            Self::LinearGradient { stops, .. }
1154            | Self::RadialGradient { stops, .. }
1155            | Self::SweepGradient { stops, .. }
1156            | Self::ConicalGradient { stops, .. } => stops,
1157        }
1158    }
1159
1160    /// Pack into the layout the shader declares.
1161    ///
1162    /// Every member is a four-component vector, which is what lets this be a
1163    /// flat array of floats copied straight into a uniform buffer: the std140
1164    /// rules the shader's block is declared with place a `vec4` and an array
1165    /// of them at exactly these offsets, so no member needs padding written
1166    /// around it.
1167    ///
1168    /// Stops beyond the limit are dropped rather than resampled, and the count
1169    /// travels alongside so the shader ignores unused entries instead of
1170    /// blending toward whatever happens to be in them.
1171    pub fn to_uniform(&self) -> [f32; MATERIAL_FLOATS] {
1172        let mut out = [0.0f32; MATERIAL_FLOATS];
1173
1174        if let Self::Solid(color) = self {
1175            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
1176            out[layout::PARAMS] = 1.0;
1177            out[layout::PARAMS + 1] = kind::SOLID;
1178            return out;
1179        }
1180
1181        // A glyph is a solid color plus a texture read; the coordinates come
1182        // from the vertices, so nothing about the mapping is packed here.
1183        if let Self::Glyph { color, .. } = self {
1184            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
1185            out[layout::PARAMS] = 1.0;
1186            out[layout::PARAMS + 1] = kind::GLYPH;
1187            return out;
1188        }
1189
1190        // An image carries no stops and no count, and must be packed before
1191        // the gradient path below decides it has too few to interpolate.
1192        if let Self::PointField { color } = self {
1193            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
1194            out[layout::PARAMS] = 1.0;
1195            out[layout::PARAMS + 1] = kind::POINT_FIELD;
1196            return out;
1197        }
1198
1199        if let Self::Ellipse {
1200            color,
1201            half_size,
1202            to_local,
1203            stroke,
1204        } = self
1205        {
1206            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
1207            out[layout::GEOMETRY + 2] = half_size[0];
1208            out[layout::GEOMETRY + 3] = half_size[1];
1209            out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1210            out[layout::PARAMS] = 1.0;
1211            out[layout::PARAMS + 1] = kind::ELLIPSE;
1212            out[layout::PARAMS + 3] = *stroke;
1213            return out;
1214        }
1215
1216        if let Self::RoundedRect {
1217            color,
1218            half_size,
1219            to_local,
1220            radius,
1221            outer_radius,
1222            stroke,
1223        } = self
1224        {
1225            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
1226            out[layout::GEOMETRY + 1] = *outer_radius;
1227            out[layout::GEOMETRY + 2] = half_size[0];
1228            out[layout::GEOMETRY + 3] = half_size[1];
1229            out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1230            out[layout::PARAMS] = 1.0;
1231            out[layout::PARAMS + 1] = kind::ROUNDED_RECT;
1232            out[layout::PARAMS + 2] = *radius;
1233            out[layout::PARAMS + 3] = *stroke;
1234            return out;
1235        }
1236
1237        if let Self::RoundedRectBlur {
1238            color,
1239            to_local,
1240            adjust,
1241            r1,
1242            exponent,
1243            s_inv,
1244            min_edge,
1245            scale,
1246        } = self
1247        {
1248            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
1249            out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1250            // The first two floats of `GEOMETRY` are unclaimed for every kind
1251            // that carries a mapping, which this does, so the two that follow
1252            // are what a rounded rectangle would have used for its half size --
1253            // and it needs none, because `adjust` already has the shape in it.
1254            out[layout::GEOMETRY] = adjust[0];
1255            out[layout::GEOMETRY + 1] = adjust[1];
1256            out[layout::GEOMETRY + 2] = *s_inv;
1257            out[layout::GEOMETRY + 3] = *min_edge;
1258            // `OFFSETS` holds stop positions for a gradient and nothing for
1259            // anything else, which is what makes it the seventh float this
1260            // needs and the only kind here that borrows it.
1261            out[layout::OFFSETS] = *scale;
1262            out[layout::PARAMS] = 1.0;
1263            out[layout::PARAMS + 1] = kind::ROUNDED_RECT_BLUR;
1264            out[layout::PARAMS + 2] = *r1;
1265            out[layout::PARAMS + 3] = *exponent;
1266            return out;
1267        }
1268
1269        if let Self::Blur {
1270            to_local,
1271            step,
1272            sigma,
1273            ..
1274        } = self
1275        {
1276            out[layout::GEOMETRY + 2] = step[0];
1277            out[layout::GEOMETRY + 3] = step[1];
1278            out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1279            out[layout::PARAMS] = 1.0;
1280            out[layout::PARAMS + 1] = kind::BLUR;
1281            out[layout::PARAMS + 2] = *sigma;
1282            return out;
1283        }
1284
1285        if let Self::Morphology {
1286            to_local,
1287            step,
1288            radius,
1289            dilate,
1290            ..
1291        } = self
1292        {
1293            out[layout::GEOMETRY + 2] = step[0];
1294            out[layout::GEOMETRY + 3] = step[1];
1295            out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1296            out[layout::PARAMS] = 1.0;
1297            out[layout::PARAMS + 1] = kind::MORPHOLOGY;
1298            out[layout::PARAMS + 2] = radius.clamp(0.0, MORPHOLOGY_TAPS as f32);
1299            out[layout::PARAMS + 3] = if *dilate { 1.0 } else { 0.0 };
1300            return out;
1301        }
1302
1303        if let Self::Runtime { uniforms, .. } = self {
1304            // Straight into the block, in the order the caller wrote them. No
1305            // kind is set: nothing in the shared shader will read this, and a
1306            // caller's program does not branch on one.
1307            let count = uniforms.len().min(RUNTIME_FLOATS);
1308            out[..count].copy_from_slice(&uniforms[..count]);
1309            return out;
1310        }
1311
1312        if let Self::Mesh {
1313            alpha,
1314            tint,
1315            tile,
1316            sampling,
1317            ..
1318        } = self
1319        {
1320            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(tint);
1321            out[layout::GEOMETRY] = *alpha;
1322            out[layout::GEOMETRY + 1] = tile_code(*tile);
1323            out[layout::PARAMS] = 1.0;
1324            out[layout::PARAMS + 1] = kind::MESH;
1325            out[layout::PARAMS + 2] = sampling_code(*sampling);
1326            return out;
1327        }
1328
1329        if let Self::Image {
1330            to_local,
1331            alpha,
1332            tile,
1333            sampling,
1334            source,
1335            tint,
1336            ..
1337        } = self
1338        {
1339            // Into the stop colors, which an image has none of. Eight floats
1340            // that would otherwise travel as zeros on every image draw.
1341            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(source);
1342            out[layout::STOPS + 4..layout::STOPS + 8].copy_from_slice(tint);
1343            out[layout::GEOMETRY + 2] = *alpha;
1344            out[layout::GEOMETRY + 3] = tile_code(*tile);
1345            out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1346            out[layout::PARAMS] = 1.0;
1347            out[layout::PARAMS + 1] = kind::IMAGE;
1348            out[layout::PARAMS + 2] = sampling_code(*sampling);
1349            return out;
1350        }
1351
1352        let stops = self.stops();
1353        let count = stops.len().min(MAX_STOPS);
1354        for (i, stop) in stops.iter().take(count).enumerate() {
1355            out[layout::STOPS + i * 4..layout::STOPS + i * 4 + 4].copy_from_slice(&stop.color);
1356            out[layout::OFFSETS + i] = stop.offset;
1357        }
1358        out[layout::PARAMS] = count.max(1) as f32;
1359
1360        // A gradient with fewer than two stops has nothing to interpolate
1361        // between, so it renders as its first color rather than sending the
1362        // shader down a path that would read an entry nothing wrote.
1363        if count < 2 {
1364            out[layout::PARAMS + 1] = kind::SOLID;
1365            return out;
1366        }
1367
1368        match self {
1369            Self::Solid(_)
1370            | Self::PointField { .. }
1371            | Self::Image { .. }
1372            | Self::Mesh { .. }
1373            | Self::Glyph { .. }
1374            | Self::Blur { .. }
1375            | Self::Morphology { .. }
1376            | Self::RoundedRect { .. }
1377            | Self::RoundedRectBlur { .. }
1378            | Self::Ellipse { .. }
1379            | Self::Runtime { .. } => {
1380                unreachable!("handled above")
1381            }
1382            Self::LinearGradient {
1383                ramp,
1384                axis,
1385                to_local,
1386                tile,
1387                ..
1388            } => {
1389                out[layout::PARAMS] = stop_count_code(count, ramp);
1390                out[layout::PARAMS + 2] = tile_code(*tile);
1391                out[layout::GEOMETRY + 2] = axis[0];
1392                out[layout::GEOMETRY + 3] = axis[1];
1393                out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1394                out[layout::PARAMS + 1] = kind::LINEAR;
1395            }
1396            Self::RadialGradient {
1397                ramp,
1398                to_local,
1399                tile,
1400                ..
1401            } => {
1402                out[layout::PARAMS] = stop_count_code(count, ramp);
1403                out[layout::PARAMS + 2] = tile_code(*tile);
1404                out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1405                out[layout::PARAMS + 1] = kind::RADIAL;
1406            }
1407            Self::SweepGradient {
1408                ramp,
1409                to_local,
1410                start_angle,
1411                end_angle,
1412                tile,
1413                ..
1414            } => {
1415                out[layout::PARAMS] = stop_count_code(count, ramp);
1416                out[layout::PARAMS + 2] = tile_code(*tile);
1417                out[layout::GEOMETRY + 2] = *start_angle;
1418                out[layout::GEOMETRY + 3] = *end_angle;
1419                out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1420                out[layout::PARAMS + 1] = kind::SWEEP;
1421            }
1422            Self::ConicalGradient {
1423                ramp,
1424                to_local,
1425                start_radius,
1426                radius_delta,
1427                separation,
1428                tile,
1429                ..
1430            } => {
1431                out[layout::PARAMS] = stop_count_code(count, ramp);
1432                out[layout::PARAMS + 2] = tile_code(*tile);
1433                out[layout::GEOMETRY + 2] = *start_radius;
1434                out[layout::GEOMETRY + 3] = *radius_delta;
1435                out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1436                out[layout::PARAMS + 1] = kind::CONICAL;
1437                out[layout::PARAMS + 3] = *separation;
1438            }
1439        }
1440        out
1441    }
1442
1443    /// The caller's program this draws with, where it uses one.
1444    ///
1445    /// `None` is every material the built-in shader draws, which is all of
1446    /// them but one.
1447    pub fn program(&self) -> Option<u32> {
1448        match self {
1449            Self::Runtime { program, .. } => Some(*program),
1450            _ => None,
1451        }
1452    }
1453
1454    /// Which shader variant this needs, for keying a pipeline.
1455    ///
1456    /// Every variant lives in one program today, selected by a uniform, so this
1457    /// exists for the moment a material needs its own pipeline rather than
1458    /// pretending that moment has arrived.
1459    pub fn variant(&self) -> MaterialVariant {
1460        match self {
1461            Self::Solid(_) => MaterialVariant::Solid,
1462            _ => MaterialVariant::Gradient,
1463        }
1464    }
1465}
1466
1467#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1468pub enum MaterialVariant {
1469    Solid,
1470    Gradient,
1471}
1472
1473#[cfg(test)]
1474mod tests {
1475    use super::*;
1476
1477    /// A mapping with the given two-by-two linear part and no translation.
1478    ///
1479    /// Most of these tests care that the mapping survives packing, not what it
1480    /// is, and said so in four floats before the paint's origin moved inside
1481    /// it. This keeps them saying that.
1482    fn linear_to_local(m: [f32; 4]) -> ToLocal {
1483        [
1484            m[0], m[1], 0.0, 0.0, //
1485            m[2], m[3], 0.0, 0.0, //
1486            0.0, 0.0, 1.0, 0.0,
1487        ]
1488    }
1489
1490    fn two_stops() -> Vec<Stop> {
1491        vec![
1492            Stop::new([1.0, 0.0, 0.0, 1.0], 0.0),
1493            Stop::new([0.0, 0.0, 1.0, 1.0], 1.0),
1494        ]
1495    }
1496
1497    #[test]
1498    fn a_solid_color_lands_in_the_first_stop_and_selects_the_solid_path() {
1499        let packed = Material::solid([0.25, 0.5, 0.75, 1.0]).to_uniform();
1500        assert_eq!(&packed[0..4], &[0.25, 0.5, 0.75, 1.0]);
1501        assert_eq!(packed[layout::PARAMS], 1.0, "stop count");
1502        assert_eq!(packed[layout::PARAMS + 1], kind::SOLID);
1503    }
1504
1505    #[test]
1506    fn a_linear_gradient_packs_its_stops_axis_and_count() {
1507        let packed = Material::LinearGradient {
1508            axis: [2.0, 0.0],
1509            to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1510            stops: two_stops(),
1511            tile: TileMode::Clamp,
1512            ramp: None,
1513        }
1514        .to_uniform();
1515
1516        assert_eq!(&packed[0..4], &[1.0, 0.0, 0.0, 1.0], "first stop");
1517        assert_eq!(&packed[4..8], &[0.0, 0.0, 1.0, 1.0], "second stop");
1518        assert_eq!(packed[layout::OFFSETS], 0.0);
1519        assert_eq!(packed[layout::OFFSETS + 1], 1.0);
1520        assert_eq!(
1521            &packed[layout::GEOMETRY..layout::GEOMETRY + 4],
1522            &[0.0, 0.0, 2.0, 0.0],
1523            "the axis rather than the end point, and nothing in the half the \
1524             start used to occupy before it moved inside the mapping"
1525        );
1526        assert_eq!(
1527            &packed[layout::TO_LOCAL..layout::TO_LOCAL + 12],
1528            &linear_to_local([1.0, 0.0, 0.0, 1.0])
1529        );
1530        assert_eq!(packed[layout::PARAMS + 1], kind::LINEAR);
1531    }
1532
1533    #[test]
1534    fn a_radial_gradient_packs_its_mapping() {
1535        let packed = Material::RadialGradient {
1536            to_local: linear_to_local([2.0, 0.0, 0.0, 4.0]),
1537            stops: two_stops(),
1538            tile: TileMode::Clamp,
1539            ramp: None,
1540        }
1541        .to_uniform();
1542
1543        // A radial gradient states nothing in the geometry slot at all now: its
1544        // center rode there, and rides inside the mapping instead.
1545        assert_eq!(&packed[layout::GEOMETRY..layout::GEOMETRY + 2], &[0.0, 0.0]);
1546        // The mapping is what makes a circle circular on a non-square target,
1547        // so it has to survive packing intact.
1548        assert_eq!(
1549            &packed[layout::TO_LOCAL..layout::TO_LOCAL + 12],
1550            &linear_to_local([2.0, 0.0, 0.0, 4.0])
1551        );
1552        assert_eq!(packed[layout::PARAMS + 1], kind::RADIAL);
1553    }
1554
1555    #[test]
1556    fn a_sweep_gradient_packs_its_angles_alongside_its_center() {
1557        let packed = Material::SweepGradient {
1558            to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1559            start_angle: 0.5,
1560            end_angle: 2.5,
1561            stops: two_stops(),
1562            tile: TileMode::Clamp,
1563            ramp: None,
1564        }
1565        .to_uniform();
1566
1567        // Angles share the geometry slot with the center, which is why a linear
1568        // gradient's endpoints and a sweep's angles cannot both be present.
1569        assert_eq!(
1570            &packed[layout::GEOMETRY..layout::GEOMETRY + 4],
1571            &[0.0, 0.0, 0.5, 2.5]
1572        );
1573        assert_eq!(packed[layout::PARAMS + 1], kind::SWEEP);
1574    }
1575
1576    #[test]
1577    fn every_gradient_kind_falls_back_to_solid_with_one_stop() {
1578        // Interpolating needs two points. Selecting a gradient path with one
1579        // would have the shader read an entry nothing wrote.
1580        let one = vec![Stop::new([1.0, 1.0, 1.0, 1.0], 0.0)];
1581        let materials = [
1582            Material::LinearGradient {
1583                axis: [1.0 - 0.0, 0.0 - 0.0],
1584                to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1585                stops: one.clone(),
1586                tile: TileMode::Clamp,
1587                ramp: None,
1588            },
1589            Material::RadialGradient {
1590                to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1591                stops: one.clone(),
1592                tile: TileMode::Clamp,
1593                ramp: None,
1594            },
1595            Material::SweepGradient {
1596                to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1597                start_angle: 0.0,
1598                end_angle: 1.0,
1599                stops: one,
1600                tile: TileMode::Clamp,
1601                ramp: None,
1602            },
1603        ];
1604        for material in materials {
1605            let packed = material.to_uniform();
1606            assert_eq!(packed[layout::PARAMS + 1], kind::SOLID, "{material:?}");
1607            assert_eq!(&packed[0..4], &[1.0, 1.0, 1.0, 1.0]);
1608        }
1609    }
1610
1611    #[test]
1612    fn stops_beyond_the_limit_are_dropped_rather_than_overflowing() {
1613        let stops: Vec<Stop> = (0..8)
1614            .map(|i| Stop::new([i as f32 / 8.0, 0.0, 0.0, 1.0], i as f32 / 7.0))
1615            .collect();
1616        let packed = Material::LinearGradient {
1617            axis: [1.0 - 0.0, 0.0 - 0.0],
1618            to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1619            stops,
1620            tile: TileMode::Clamp,
1621            ramp: None,
1622        }
1623        .to_uniform();
1624        // The count is what stops the shader reading past what was written.
1625        assert_eq!(packed[layout::PARAMS], MAX_STOPS as f32);
1626    }
1627
1628    #[test]
1629    fn visibility_accounts_for_every_stop_of_every_kind() {
1630        assert!(Material::solid([1.0, 1.0, 1.0, 0.0]).is_invisible());
1631        assert!(!Material::solid([0.0, 0.0, 0.0, 1.0]).is_invisible());
1632
1633        let clear = vec![
1634            Stop::new([1.0, 0.0, 0.0, 0.0], 0.0),
1635            Stop::new([0.0, 0.0, 1.0, 0.0], 1.0),
1636        ];
1637        assert!(Material::RadialGradient {
1638            to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1639            stops: clear,
1640            tile: TileMode::Clamp,
1641            ramp: None,
1642        }
1643        .is_invisible());
1644
1645        let partly = vec![
1646            Stop::new([1.0, 0.0, 0.0, 0.0], 0.0),
1647            Stop::new([0.0, 0.0, 1.0, 1.0], 1.0),
1648        ];
1649        assert!(
1650            !Material::SweepGradient {
1651                to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1652                start_angle: 0.0,
1653                end_angle: 1.0,
1654                stops: partly,
1655                tile: TileMode::Clamp,
1656                ramp: None,
1657            }
1658            .is_invisible(),
1659            "one visible stop is enough"
1660        );
1661    }
1662
1663    #[test]
1664    fn every_gradient_kind_reports_the_same_variant() {
1665        // They share one program, selected by a uniform, so keying a pipeline
1666        // on the kind would create three identical pipelines.
1667        assert_eq!(Material::solid([0.0; 4]).variant(), MaterialVariant::Solid);
1668        for material in [
1669            Material::LinearGradient {
1670                axis: [1.0, 0.0],
1671                to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1672                stops: two_stops(),
1673                tile: TileMode::Clamp,
1674                ramp: None,
1675            },
1676            Material::RadialGradient {
1677                to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1678                stops: two_stops(),
1679                tile: TileMode::Clamp,
1680                ramp: None,
1681            },
1682        ] {
1683            assert_eq!(material.variant(), MaterialVariant::Gradient);
1684        }
1685    }
1686}