Skip to main content

emblema_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 traveled 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    /// Color comes from the vertices, and the result is dithered.
584    ///
585    /// Shades as opaque white, so the per-vertex color under `Modulate` -- which
586    /// is the identity against white -- is the whole result. That is the
587    /// arrangement `draw_vertices` already uses for a caller's mesh; what this
588    /// adds is that it dithers, which `Solid` must not.
589    ///
590    /// It exists for a gradient the rasterizer interpolates instead of the
591    /// fragment stage evaluating. A long run of nearly equal values is what
592    /// bands on an eight-bit target whichever stage produced it, and a route
593    /// that could not say so would drop the dither silently -- which is what
594    /// §19 of `docs/non-parity.md` records killing the first attempt at it.
595    ///
596    /// Carries no data: a section's colors ride on its vertices, and the
597    /// premultiplied alpha rides with them.
598    VertexGradient,
599    /// A gradient along an axis, starting at a point **in clip space**.
600    ///
601    /// Clip space for the start, because the fragment stage locates itself from
602    /// an interpolated clip position: the alternative, the fragment coordinate
603    /// builtin, has a different origin in each graphics API and would run the
604    /// gradient in opposite directions on the two backends.
605    ///
606    /// The axis is in the gradient's own space, and `to_local` maps a clip-space
607    /// offset into it — the same pairing radial and sweep use, and for the same
608    /// reason. Clip space is anisotropic whenever the target is not square, so
609    /// projecting onto an axis *there* weights the two axes by the target's
610    /// shape: on a target twice as wide as it is tall, a diagonal gradient runs
611    /// in the wrong direction.
612    LinearGradient {
613        /// End minus start, in the gradient's own space.
614        axis: [f32; 2],
615        to_local: ToLocal,
616        stops: Vec<Stop>,
617        /// Texture slot holding this gradient's colors, when they did not fit.
618        ///
619        /// `None` is the ordinary case: the stops travel in the material and
620        /// the shader walks them. `Some` means the recorder tabulated them into
621        /// an image instead, because there were more than [`MAX_STOPS`], and
622        /// the shader reads the color at the parameter rather than computing
623        /// it. The two must agree where both are possible, which is what makes
624        /// the choice invisible to a caller.
625        ramp: Option<u32>,
626        /// What happens beyond the two endpoints.
627        ///
628        /// The parameter a gradient is sampled by runs from zero at one end to
629        /// one at the other and is defined everywhere else too, so a shape
630        /// larger than its gradient asks a question the stops do not answer.
631        /// Clamping holds the end colors, which is the usual choice; repeating
632        /// tiles the ramp, which is what a stripe pattern is; decal draws
633        /// nothing outside, the same meaning it has for an image.
634        tile: TileMode,
635    },
636    /// A gradient outward from a center, **in clip space**, where `to_local`
637    /// carries the radius: it maps the clip-space offset so that the gradient's
638    /// edge lands at unit distance.
639    RadialGradient {
640        to_local: ToLocal,
641        stops: Vec<Stop>,
642        /// Texture slot holding this gradient's colors, when they did not fit.
643        ///
644        /// `None` is the ordinary case: the stops travel in the material and
645        /// the shader walks them. `Some` means the recorder tabulated them into
646        /// an image instead, because there were more than [`MAX_STOPS`], and
647        /// the shader reads the color at the parameter rather than computing
648        /// it. The two must agree where both are possible, which is what makes
649        /// the choice invisible to a caller.
650        ramp: Option<u32>,
651        /// What happens beyond the radius. See [`Material::LinearGradient`].
652        tile: TileMode,
653    },
654    /// A gradient around a center, **in clip space**, running from `start_angle`
655    /// to `end_angle` in radians.
656    SweepGradient {
657        to_local: ToLocal,
658        start_angle: f32,
659        end_angle: f32,
660        stops: Vec<Stop>,
661        /// Texture slot holding this gradient's colors, when they did not fit.
662        ///
663        /// `None` is the ordinary case: the stops travel in the material and
664        /// the shader walks them. `Some` means the recorder tabulated them into
665        /// an image instead, because there were more than [`MAX_STOPS`], and
666        /// the shader reads the color at the parameter rather than computing
667        /// it. The two must agree where both are possible, which is what makes
668        /// the choice invisible to a caller.
669        ramp: Option<u32>,
670        /// What happens outside the swept arc.
671        ///
672        /// Unlike the other two this can be a no-op: a sweep covering the whole
673        /// turn has no outside, and every direction lands within it.
674        tile: TileMode,
675    },
676    /// A gradient between two circles, the general form the other two are
677    /// special cases of.
678    ///
679    /// `center` is the first circle's center **in clip space**, and `to_local`
680    /// maps a clip-space offset into a space where that center is the origin
681    /// and the second circle's center lies at `(separation, 0)`. Putting the
682    /// separation on an axis costs nothing -- the rotation folds into a matrix
683    /// that has to be there anyway -- and buys the second center for one float
684    /// instead of two, which is what makes this fit at all.
685    ///
686    /// The radii are in that same space and are *not* normalized, unlike the
687    /// radial gradient's, because there are two of them and a scale can only
688    /// remove one. Carrying both plainly also means the degenerate cases need
689    /// no special handling: concentric circles are `separation == 0`, and a
690    /// cone rather than a tube is `radius_delta != 0`.
691    ConicalGradient {
692        to_local: ToLocal,
693        /// Radius of the first circle.
694        start_radius: f32,
695        /// Second radius minus the first.
696        radius_delta: f32,
697        /// Distance between the two centers.
698        separation: f32,
699        stops: Vec<Stop>,
700        /// Texture slot holding this gradient's colors, when they did not fit.
701        /// See [`Material::LinearGradient`].
702        ramp: Option<u32>,
703        /// What happens where the parameter leaves the unit interval. See
704        /// [`Material::LinearGradient`].
705        tile: TileMode,
706    },
707    /// A caller's own fragment program, with the floats it reads.
708    ///
709    /// The program is named rather than carried: registering one builds a
710    /// pipeline, which is expensive and outlives any draw, so a context holds
711    /// them and a material names which. The floats travel in the same uniform
712    /// block every other material uses, which is what lets an effect exist
713    /// without a second descriptor set.
714    Runtime {
715        program: u32,
716        uniforms: Vec<f32>,
717        /// The textures the program may sample, in the order it declares them.
718        ///
719        /// `None` in a position means the program does not read that binding,
720        /// and the backend binds its placeholder there -- a pipeline must have
721        /// every binding it declares bound, however unreachable the branch
722        /// reading it.
723        ///
724        /// The count is fixed rather than free because the descriptor set
725        /// layout every pipeline is built against has to be one layout. Four is
726        /// what that costs: three unused image bindings on a draw that samples
727        /// nothing, against a second set and a second layout for the draws that
728        /// want more than one.
729        textures: [Option<u32>; MAX_EFFECT_TEXTURES],
730    },
731    /// A texture, sampled at coordinates the vertices carry.
732    ///
733    /// Deliberately not [`Material::Image`] with a switch on where its
734    /// coordinates come from. An image mapped from clip space needs an origin,
735    /// a matrix and a source rectangle to say which part of a sheet it draws;
736    /// a mesh needs none of them, because a caller stating a coordinate per
737    /// vertex has already answered all three. What is left is small enough to
738    /// be its own thing, and keeping it separate means neither carries a field
739    /// that means nothing for it.
740    ///
741    /// This is what makes a sprite batch one draw: a hundred quads reading a
742    /// hundred different parts of one sheet differ only in their vertices.
743    Mesh {
744        /// Index into the texture table given at submission.
745        slot: u32,
746        /// Scales the sampled color, applied to premultiplied color like
747        /// [`Material::Image`]'s.
748        alpha: f32,
749        /// Straight color the sampled texel is multiplied by; white changes
750        /// nothing.
751        tint: [f32; 4],
752        /// What happens where a vertex names a coordinate outside the texture.
753        tile: TileMode,
754        /// How to read between texels.
755        sampling: Sampling,
756    },
757    /// A texture, sampled through a mapping from clip space.
758    ///
759    /// `origin` and `to_local` together are an affine: a clip-space position
760    /// maps to texture coordinates as `to_local * (clip - origin)`, which lands
761    /// the image's top-left corner at zero and its bottom-right at one. The
762    /// same pair a radial gradient uses, for the same reason — clip space is
763    /// anisotropic on a non-square target, so a mapping that ignored it would
764    /// stretch every image by the aspect ratio.
765    ///
766    /// Which texture is not named here. The material is data the recorder
767    /// produces without touching the device, so it carries a slot into the
768    /// table supplied at submission instead of a backend handle.
769    Image {
770        to_local: ToLocal,
771        /// Index into the texture table given at submission.
772        slot: u32,
773        /// Scales the sampled color, for drawing an image translucently.
774        ///
775        /// Applied to premultiplied color, so it scales the whole texel rather
776        /// than only its alpha. A texture holds premultiplied color whether it
777        /// was uploaded or rendered into, and treating a sample as straight
778        /// alpha would apply the alpha twice — invisible for an opaque image,
779        /// and plain the moment one translucent image is drawn into another.
780        alpha: f32,
781        tile: TileMode,
782        /// How to read between texels.
783        sampling: Sampling,
784        /// The part of the texture to draw, as `[u0, v0, u1, v1]` from zero to
785        /// one.
786        ///
787        /// The whole texture is `[0, 0, 1, 1]`, which is what every caller
788        /// wanted until sprite sheets. Normalized rather than in texels because
789        /// a material is built by a recorder that has never seen the texture
790        /// and cannot know how large it is; the caller who uploaded it does.
791        ///
792        /// Applied after tiling rather than before, so a repeat repeats the
793        /// selected piece rather than the whole sheet -- which is the only
794        /// reading of "tile this sprite" that means anything.
795        source: [f32; 4],
796        /// Straight color the sampled texel is multiplied by; white changes
797        /// nothing.
798        ///
799        /// What turns one monochrome icon sheet into every state a control has.
800        /// A generalization of `alpha` rather than a rival to it: a tint of
801        /// `[1, 1, 1, a]` is exactly that scaling, and both are applied because
802        /// removing the narrower one would break callers for no gain.
803        ///
804        /// Straight rather than premultiplied because that is how a caller
805        /// states a color, and the shader premultiplies it before multiplying a
806        /// texel that already is -- scaling color by the tint's alpha as well,
807        /// which is what keeps the result premultiplied rather than merely
808        /// close to it.
809        tint: [f32; 4],
810    },
811    /// A rounded rectangle evaluated per fragment rather than tessellated.
812    ///
813    /// The shape an interface is mostly made of, and the one where computing
814    /// coverage beats building triangles for it. A tessellated rounded
815    /// rectangle costs vertices proportional to how round it is and has hard
816    /// edges unless the whole pass is multisampled; this is two triangles
817    /// whatever the radius, and antialiases itself from the distance field it
818    /// already computes.
819    ///
820    /// The geometry is in the shape's own space, with `to_local` mapping a
821    /// clip-space position into it -- the same pairing the gradients use, and
822    /// for the same reason: clip space is anisotropic on a target that is not
823    /// square, and a distance measured there would round the corners by
824    /// different amounts on each axis.
825    RoundedRect {
826        color: [f32; 4],
827        /// Half the width and height, in the shape's own space.
828        half_size: [f32; 2],
829        to_local: ToLocal,
830        /// Corner radius, in the shape's own space.
831        radius: f32,
832        /// The radius the outline's *outer* edge turns through, which is not
833        /// always the radius grown by half the stroke.
834        ///
835        /// An outline is the difference of two offset shapes rather than a band
836        /// around one, and the outer offset is where a join shows. Grow a
837        /// rounded corner and you get a bigger rounded corner, so this is
838        /// `radius + stroke / 2` for anything with a radius, and for anything
839        /// with a round join. A *mitered* square corner is the exception: its
840        /// offset is still square, so this is zero and the field draws the
841        /// point the join asks for.
842        ///
843        /// Carried rather than derived because the shader cannot see the join,
844        /// and because deriving it wrongly is what made a square-cornered
845        /// stroke come out with rounded corners and be refused this route
846        /// altogether.
847        outer_radius: f32,
848        /// Trace the outline at this width rather than filling, in the shape's
849        /// own space. Zero fills.
850        ///
851        /// Costs a distance field nothing: the field already says how far every
852        /// fragment is from the edge, so an outline is the band where that is
853        /// small. Tessellating one instead means building a second shape --
854        /// offset inward and outward, with the corners resolved -- which is
855        /// where a stroked outline gets its vertex count and its joins.
856        stroke: f32,
857    },
858    /// A *blurred* rounded rectangle evaluated per fragment, with no blur pass.
859    ///
860    /// The sibling of [`Self::RoundedRect`], and the reason it is worth having
861    /// is that the general route costs three passes per shape -- one for the
862    /// content and two for a separable Gaussian -- where this costs one draw in
863    /// the pass already being recorded. On a Raspberry Pi 5 three shadows that
864    /// way are 7.8 ms of a 26.8 ms frame and nine of its fourteen passes.
865    ///
866    /// The method is Raph Levien's "Blurred rounded rectangles", which is what
867    /// upstream's `SolidRRectBlurContents` evaluates too: the exact convolution
868    /// of a Gaussian with a rounded rectangle has no closed form, and this
869    /// approximates it as a product of two error functions along an axis,
870    /// corrected for the corners by measuring distance with an exponent other
871    /// than two. Every field here is a number the CPU precomputed for that
872    /// expression rather than anything a caller stated -- see
873    /// `Canvas::rrect_blur_material`, which is the only place they are derived.
874    ///
875    /// Only where every corner shares one circular radius, which is upstream's
876    /// condition too. Anything else takes the general route.
877    RoundedRectBlur {
878        color: [f32; 4],
879        to_local: ToLocal,
880        /// Half the rectangle, less `r1`, in the shape's own space.
881        adjust: [f32; 2],
882        /// The corner radius the approximation uses, which is not the caller's:
883        /// it grows with the deviation, because a blurred corner is rounder
884        /// than a sharp one.
885        r1: f32,
886        /// The exponent the corner distance is measured with. Two is a circle;
887        /// this is larger, which is what makes the blurred corner's profile
888        /// match a Gaussian's rather than a circle's.
889        exponent: f32,
890        /// One over the deviation, which is what the error function takes.
891        s_inv: f32,
892        /// The shorter side, which bounds how far the fade can reach before the
893        /// two edges of the shape meet.
894        min_edge: f32,
895        /// Normalizes the fade so that the middle of a large shape reaches full
896        /// coverage.
897        scale: f32,
898    },
899    /// An ellipse evaluated per fragment.
900    ///
901    /// Its own variant rather than a rounded rectangle with a large radius,
902    /// which gives a stadium: past half the shorter side a rounded rectangle
903    /// stops changing, where an ellipse keeps curving along both axes.
904    ///
905    /// The same geometry a rounded rectangle carries, less the radius, which
906    /// the two axes already state.
907    Ellipse {
908        color: [f32; 4],
909        /// The two semi-axes, in the shape's own space.
910        half_size: [f32; 2],
911        to_local: ToLocal,
912        /// Trace the outline at this width rather than filling. Zero fills.
913        stroke: f32,
914    },
915    /// One axis of a separable Gaussian blur of a sampled texture.
916    ///
917    /// Separable because a two-dimensional Gaussian is the product of two
918    /// one-dimensional ones, so blurring along each axis in turn gives the
919    /// same result as a square of taps at a fraction of the cost: at a radius
920    /// of sixteen that is thirty-three taps against a thousand and eighty-nine.
921    /// Two passes are the price, which is why this names an axis rather than
922    /// describing the whole blur.
923    Blur {
924        to_local: ToLocal,
925        slot: u32,
926        /// One tap's step, in the sampled texture's own coordinates.
927        ///
928        /// The axis and the texel size together: a horizontal pass over a
929        /// target `w` wide steps `(1/w, 0)`. Stated here rather than derived in
930        /// the shader because the shader does not know the size of what it is
931        /// sampling.
932        step: [f32; 2],
933        /// Standard deviation, in taps.
934        ///
935        /// The tap count follows from it -- the kernel reaches
936        /// `(sigma - 0.5) * sqrt(3)` each way, which is upstream's radius for a
937        /// given deviation -- so a caller sets how soft the result is and
938        /// nothing else.
939        sigma: f32,
940    },
941    /// One axis of a morphological filter of a finished layer.
942    ///
943    /// The largest or smallest sample within a radius, per channel, which is
944    /// what `dart:ui` calls `ImageFilter.dilate` and `ImageFilter.erode`. Like
945    /// the blur it is separable -- a rectangular structuring element is the
946    /// product of two intervals -- so two passes give the square of taps.
947    ///
948    /// Unlike the blur it is also *decomposable*: dilating by `a` and then by
949    /// `b` is dilating by `a + b` exactly, because the structuring elements add
950    /// under the Minkowski sum. A radius past what one pass can reach is
951    /// therefore split across passes rather than approximated by sampling more
952    /// sparsely. Sparse taps work for a blur, where a missed sample costs a
953    /// little smoothness, and do not work here: the result is a maximum, so a
954    /// missed sample is a scallop in the edge.
955    Morphology {
956        to_local: ToLocal,
957        slot: u32,
958        /// One tap's step, in the sampled texture's own coordinates. As
959        /// [`Self::Blur::step`].
960        step: [f32; 2],
961        /// How many texels each way this pass reaches, at most
962        /// [`MORPHOLOGY_TAPS`].
963        ///
964        /// A whole number of texels, because the structuring element is a set
965        /// of samples rather than a weighting of them and there is no meaning
966        /// to half of one.
967        radius: f32,
968        /// The largest sample in reach rather than the smallest.
969        dilate: bool,
970    },
971    /// Coverage sampled from an atlas, tinting one color.
972    ///
973    /// Distinct from [`Self::Image`] in two ways that matter. The texture is
974    /// read as *coverage* rather than as color — one channel scaling a solid,
975    /// which is what antialiased text is — and the coordinates come from the
976    /// vertices rather than from a mapping in the paint, so a run of glyphs
977    /// reading different parts of one atlas is a single draw.
978    Glyph {
979        /// Linear color with straight alpha, as the text is painted.
980        color: [f32; 4],
981        /// Index into the texture table given at submission.
982        slot: u32,
983    },
984    /// Many discs of one color, evaluated from the vertices rather than the
985    /// paint, so that a field of them is one draw.
986    ///
987    /// Carries no mapping and no size, which is the whole point of it. Every
988    /// other fragment-evaluated shape here locates itself through `to_local`,
989    /// and `to_local` holds the shape's center -- so two of them at different
990    /// places are two materials and cannot share a draw. This one locates
991    /// itself from the interpolated texture coordinate, which the vertices
992    /// carry as the unit circle's corners, so any number of discs at any
993    /// centers are one material and one draw.
994    ///
995    /// The edge is the same edge. `disc_coverage` in the shader differentiates
996    /// the implicit function across the pixel rather than forming a distance,
997    /// and a derivative of an interpolated value is as available as that of a
998    /// computed one.
999    PointField {
1000        /// One color for the field, premultiplied as everything here is.
1001        color: [f32; 4],
1002    },
1003}
1004
1005/// What happens outside an image's own bounds.
1006#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1007pub enum TileMode {
1008    /// Hold the edge pixel. The usual choice for drawing an image once.
1009    #[default]
1010    Clamp,
1011    /// Repeat the image, tiling the plane.
1012    Repeat,
1013    /// Draw nothing outside the image.
1014    ///
1015    /// Distinct from clamping in the only case that matters: a shape larger
1016    /// than the image it is filled with. Clamping smears the border across the
1017    /// remainder, which reads as a rendering fault rather than as a choice.
1018    Decal,
1019    /// Repeat, reversing every other copy.
1020    ///
1021    /// What repeating is for when the two ends do not match. A ramp tiled by
1022    /// [`TileMode::Repeat`] jumps from its last color back to its first at
1023    /// every period, and that discontinuity is a visible seam; reflecting each
1024    /// alternate copy joins end to end and leaves none. The period is twice as
1025    /// long, since a copy and its reflection make one.
1026    Mirror,
1027}
1028
1029impl Material {
1030    pub fn solid(color: [f32; 4]) -> Self {
1031        Self::Solid(color)
1032    }
1033
1034    /// How this material reads a texture, if it reads one.
1035    ///
1036    /// `None` for a material that samples nothing, which is not the same answer
1037    /// as [`Sampling::Nearest`]: one says there is no texture and the other says
1038    /// there is and it is read at a texel's center.
1039    ///
1040    /// Here because a caller above the HAL may need to know whether a draw
1041    /// interpolates between texels, and only this type knows. The test harness
1042    /// asks it to size a cross-device tolerance: a filter that forms a weighted
1043    /// sum of texels does so with weights of implementation-defined precision,
1044    /// so two devices disagree on every interpolated pixel by more than the last
1045    /// bit of one store. Deriving that from a recording rather than from the
1046    /// scene that produced it is the point -- the scene model has four separate
1047    /// places a sampled texture can come from and a comment recording four
1048    /// occasions on which enumerating them missed one.
1049    pub fn sampling(&self) -> Option<Sampling> {
1050        match self {
1051            // Samples nothing: the color arrives on the vertices.
1052            Self::VertexGradient => None,
1053            Self::Image { sampling, .. } | Self::Mesh { sampling, .. } => Some(*sampling),
1054            // Reads a texture and states no filter: a blur and a morphology
1055            // sample their own target at offsets they compute, and a glyph reads
1056            // coverage out of the atlas. What they do between texels is the
1057            // shader's, not a field's, so there is no mode to report here.
1058            Self::Blur { .. }
1059            | Self::Morphology { .. }
1060            | Self::Glyph { .. }
1061            | Self::Runtime { .. } => None,
1062            // Reads no texture at all.
1063            Self::Solid(_)
1064            | Self::LinearGradient { .. }
1065            | Self::RadialGradient { .. }
1066            | Self::SweepGradient { .. }
1067            | Self::ConicalGradient { .. }
1068            | Self::RoundedRect { .. }
1069            | Self::RoundedRectBlur { .. }
1070            | Self::Ellipse { .. }
1071            | Self::PointField { .. } => None,
1072        }
1073    }
1074
1075    /// Whether this material reads between texels rather than at one.
1076    ///
1077    /// Every mode but [`Sampling::Nearest`] forms a weighted sum: linear over
1078    /// four texels, bicubic over sixteen, mipmapped over two levels of the
1079    /// first. Measured on a Raspberry Pi 5, one scene rendered twice differing
1080    /// only in this: nearest is byte for byte identical between v3d and llvmpipe
1081    /// over the whole frame, and linear reaches a delta of three across seventy
1082    /// per cent of it.
1083    /// Whether this material reads its texture at offsets it computes, rather
1084    /// than at the coordinate it was handed.
1085    ///
1086    /// True for the two filters that walk a neighborhood: a blur steps out by a
1087    /// sigma, a morphology by a radius, and neither need land on a texel center
1088    /// whatever size the texture is. So this is the one case where a material
1089    /// can say that a read interpolates without knowing the texture -- which is
1090    /// why it is a separate question from [`Self::sampling`], and why a layer
1091    /// dilated by a morphology is one of the scenes that put two devices three
1092    /// levels apart.
1093    pub fn reads_at_computed_offsets(&self) -> bool {
1094        matches!(self, Self::Blur { .. } | Self::Morphology { .. })
1095    }
1096
1097    /// The same material at `factor` of its opacity.
1098    ///
1099    /// Applied to every color a material carries, since a gradient's stops may
1100    /// differ in alpha and scaling them together is what keeps the ramp the
1101    /// same ramp. Straight alpha, so this happens before the premultiply the
1102    /// packing does rather than after it.
1103    ///
1104    /// Two variants are left alone and it is worth saying which. A blur or a
1105    /// morphology is a filter over a finished pass rather than a paint, so
1106    /// there is no color in it to dim -- and nothing asks this of one. A
1107    /// caller's program is the other: its output is whatever it computes, and
1108    /// this renderer has no uniform it may write to. That is a real limit
1109    /// wherever the caller wanted the dimming, and it is stated at the one
1110    /// place that asks for it.
1111    #[must_use]
1112    pub fn with_opacity(mut self, factor: f32) -> Self {
1113        let scale = |color: &mut [f32; 4]| color[3] *= factor;
1114        match &mut self {
1115            Self::Solid(color)
1116            | Self::RoundedRect { color, .. }
1117            | Self::RoundedRectBlur { color, .. }
1118            | Self::Ellipse { color, .. }
1119            | Self::PointField { color }
1120            | Self::Glyph { color, .. } => scale(color),
1121            Self::LinearGradient { stops, .. }
1122            | Self::RadialGradient { stops, .. }
1123            | Self::SweepGradient { stops, .. }
1124            | Self::ConicalGradient { stops, .. } => {
1125                for stop in stops.iter_mut() {
1126                    scale(&mut stop.color);
1127                }
1128            }
1129            Self::Mesh { alpha, .. } | Self::Image { alpha, .. } => *alpha *= factor,
1130            Self::Runtime { .. } | Self::Blur { .. } | Self::Morphology { .. } => {}
1131            // Nothing here to scale -- the alpha is premultiplied into the
1132            // vertex colors, which this cannot see. The one caller is the thin
1133            // stroke above, and a fill passes a factor of one, so the route
1134            // that uses this material never asks. A route that did would have
1135            // to bake the factor in when it builds the sections.
1136            Self::VertexGradient => {}
1137        }
1138        self
1139    }
1140
1141    /// Whether drawing with this would change anything.
1142    pub fn is_invisible(&self) -> bool {
1143        match self {
1144            Self::Solid(color) | Self::PointField { color } => color[3] <= 0.0,
1145            // The colors are on the vertices, so the only honest answer is
1146            // that it might draw something.
1147            Self::VertexGradient => false,
1148            Self::LinearGradient { stops, .. }
1149            | Self::RadialGradient { stops, .. }
1150            | Self::SweepGradient { stops, .. }
1151            | Self::ConicalGradient { stops, .. } => {
1152                stops.is_empty() || stops.iter().all(|s| s.color[3] <= 0.0)
1153            }
1154            // What the texture holds is unknown here, so only a zero alpha
1155            // makes an image provably invisible.
1156            Self::Image { alpha, .. } | Self::Mesh { alpha, .. } => *alpha <= 0.0,
1157            // What a caller's program draws is unknowable from here, so the
1158            // only honest answer is that it might draw something.
1159            Self::Runtime { .. } => false,
1160            // A blur of nothing is nothing, but the pass still has to run: what
1161            // it samples is not knowable from here.
1162            Self::Blur { .. } | Self::Morphology { .. } => false,
1163            // No `half_size` to test: a blurred rectangle with no area still
1164            // draws, because the blur carries color past where the shape is.
1165            Self::RoundedRectBlur { color, .. } => color[3] <= 0.0,
1166            Self::RoundedRect {
1167                color, half_size, ..
1168            }
1169            | Self::Ellipse {
1170                color, half_size, ..
1171            } => color[3] <= 0.0 || half_size[0] <= 0.0 || half_size[1] <= 0.0,
1172            Self::Glyph { color, .. } => color[3] <= 0.0,
1173        }
1174    }
1175
1176    /// The texture slot this samples, for a backend building its bindings.
1177    ///
1178    /// Matched exhaustively rather than with a catch-all. A variant that
1179    /// samples something and is not listed here reports no slot, so the backend
1180    /// binds its placeholder and the draw comes out flat white -- a plausible
1181    /// picture rather than an error, and one nothing else would explain.
1182    /// Every texture slot this material samples, in binding order.
1183    ///
1184    /// One entry for everything but a runtime program, which may declare
1185    /// several. A backend building descriptors needs the whole tuple, since a
1186    /// set holds all of them at once; a backend asking only which slot to bind
1187    /// first has [`Self::texture_slot`].
1188    pub fn texture_slots(&self) -> [Option<u32>; MAX_EFFECT_TEXTURES] {
1189        match self {
1190            Self::Runtime { textures, .. } => *textures,
1191            other => {
1192                let mut slots = [None; MAX_EFFECT_TEXTURES];
1193                slots[0] = other.texture_slot();
1194                slots
1195            }
1196        }
1197    }
1198
1199    pub fn texture_slot(&self) -> Option<u32> {
1200        match self {
1201            Self::VertexGradient => None,
1202            Self::Image { slot, .. }
1203            | Self::Mesh { slot, .. }
1204            | Self::Glyph { slot, .. }
1205            | Self::Blur { slot, .. }
1206            | Self::Morphology { slot, .. } => Some(*slot),
1207            // A gradient names a texture only when its colors were too many to
1208            // carry, which is why this is an option rather than a slot.
1209            Self::LinearGradient { ramp, .. }
1210            | Self::RadialGradient { ramp, .. }
1211            | Self::SweepGradient { ramp, .. }
1212            | Self::ConicalGradient { ramp, .. } => *ramp,
1213            Self::Solid(_)
1214            | Self::PointField { .. }
1215            | Self::RoundedRect { .. }
1216            | Self::RoundedRectBlur { .. }
1217            | Self::Ellipse { .. } => None,
1218            // Whatever a caller named, and `None` where they named nothing --
1219            // in which case the placeholder is bound and a program that
1220            // samples anyway reads opaque white.
1221            // The first of them, for the callers that still ask a material for
1222            // one slot. `texture_slots` is what a backend binding several
1223            // should ask.
1224            Self::Runtime { textures, .. } => textures[0],
1225        }
1226    }
1227
1228    /// The stops, for any material that has them.
1229    fn stops(&self) -> &[Stop] {
1230        match self {
1231            // Its stops became vertex colors when the sections were built.
1232            Self::VertexGradient => &[],
1233            Self::Solid(_)
1234            | Self::PointField { .. }
1235            | Self::Image { .. }
1236            | Self::Mesh { .. }
1237            | Self::Glyph { .. }
1238            | Self::Blur { .. }
1239            | Self::Morphology { .. }
1240            | Self::RoundedRect { .. }
1241            | Self::RoundedRectBlur { .. }
1242            | Self::Ellipse { .. }
1243            | Self::Runtime { .. } => &[],
1244            Self::LinearGradient { stops, .. }
1245            | Self::RadialGradient { stops, .. }
1246            | Self::SweepGradient { stops, .. }
1247            | Self::ConicalGradient { stops, .. } => stops,
1248        }
1249    }
1250
1251    /// Pack into the layout the shader declares.
1252    ///
1253    /// Every member is a four-component vector, which is what lets this be a
1254    /// flat array of floats copied straight into a uniform buffer: the std140
1255    /// rules the shader's block is declared with place a `vec4` and an array
1256    /// of them at exactly these offsets, so no member needs padding written
1257    /// around it.
1258    ///
1259    /// Stops beyond the limit are dropped rather than resampled, and the count
1260    /// travels alongside so the shader ignores unused entries instead of
1261    /// blending toward whatever happens to be in them.
1262    /// Whether every pixel this material writes comes out fully opaque.
1263    ///
1264    /// Used to decide whether a draw may be split into several, which is only safe when
1265    /// writing a pixel twice gives what writing it once does. Under `SrcOver` that holds
1266    /// exactly when the source is opaque.
1267    ///
1268    /// **False unless shown otherwise.** A solid fill carries its alpha and a gradient's
1269    /// stops carry theirs. Everything else -- images, the analytic shapes, blurs, a
1270    /// caller's own program -- is refused outright.
1271    ///
1272    /// A tabulated gradient still answers from its stops, and that is worth saying because
1273    /// the first version of this refused one. `ramp` being `Some` means the *shader* reads
1274    /// a texture instead of walking the list, not that the list is gone: the recorder maps
1275    /// every stop into the material either way and bakes the ramp from the same list, by
1276    /// interpolating between them. Interpolating between opaque colors gives an opaque
1277    /// one, so the texels are opaque exactly when the stops are. Refusing a ramp cost the
1278    /// whole of occlusion culling's benefit on the bench's frame, whose wash is five stops
1279    /// against a `MAX_STOPS` of four.
1280    pub fn is_opaque(&self) -> bool {
1281        let stops_opaque =
1282            |stops: &[Stop]| !stops.is_empty() && stops.iter().all(|s| s.color[3] >= 1.0);
1283        match self {
1284            Material::Solid(color) => color[3] >= 1.0,
1285            Material::LinearGradient { stops, .. }
1286            | Material::RadialGradient { stops, .. }
1287            | Material::SweepGradient { stops, .. }
1288            | Material::ConicalGradient { stops, .. } => stops_opaque(stops),
1289            _ => false,
1290        }
1291    }
1292
1293    /// Whether this material's shading reads screen-space derivatives.
1294    ///
1295    /// `dpdx` and `dpdy` are computed across a two-by-two quad of fragments, using helper
1296    /// invocations outside the primitive. Whether a scissor keeps those helpers alive is
1297    /// not something either specification settles, and implementations differ: Mesa
1298    /// 26.2.3 keeps them and 25.2.8 does not. So a draw whose shading depends on them
1299    /// gives a different answer at a scissor's edge than away from it, and splitting such
1300    /// a draw by scissor changes the picture along the seam.
1301    ///
1302    /// Measured rather than reasoned about: an analytic shadow drawn once reads 217 where
1303    /// the same shadow drawn in four scissored strips reads 206, at the pixel on the
1304    /// strip boundary, on Mesa 25.2.8. See `a_scissor_does_not_change_what_a_draw_paints`.
1305    ///
1306    /// **True unless shown otherwise**, which is the only safe direction: a material
1307    /// wrongly called derivative-free leaves a seam in the frame. The two groups that are
1308    /// shown otherwise are solid fills, whose coverage comes from the rasterizer, and the
1309    /// gradients, which sample their ramp at an explicit level of zero. The rest either
1310    /// take a coverage from the gradient of a distance field -- the rounded rectangle, the
1311    /// ellipse and the rounded-rectangle blur -- or a texture level from the rate their
1312    /// coordinates change, which is every image fill, or are a caller's own program.
1313    pub fn needs_screen_derivatives(&self) -> bool {
1314        !matches!(
1315            self,
1316            Material::Solid(_)
1317                | Material::VertexGradient
1318                | Material::LinearGradient { .. }
1319                | Material::RadialGradient { .. }
1320                | Material::SweepGradient { .. }
1321                | Material::ConicalGradient { .. }
1322        )
1323    }
1324
1325    /// Whether this material asks the target for a long run of nearly equal
1326    /// values, and so wants a dither.
1327    ///
1328    /// The gradients, and nothing else -- the same set the shader used to test
1329    /// for itself. Asked here because the shader cannot be the place that knows:
1330    /// it tests the material *kind*, so a route that draws a gradient under a
1331    /// different kind silently stops dithering. §19 of `docs/non-parity.md`
1332    /// records that happening, in the reverted attempt at upstream's
1333    /// vertex-interpolated fast gradient.
1334    ///
1335    /// **False unless shown otherwise.** Dithering something with no band to
1336    /// break adds noise to a flat color.
1337    pub fn dithers(&self) -> bool {
1338        matches!(
1339            self,
1340            // The one non-gradient here, and the reason this predicate is not
1341            // simply "is a gradient": what it draws is a gradient, with the
1342            // rasterizer interpolating it instead of the fragment stage.
1343            Material::VertexGradient
1344                | Material::LinearGradient { .. }
1345                | Material::RadialGradient { .. }
1346                | Material::SweepGradient { .. }
1347                | Material::ConicalGradient { .. }
1348        )
1349    }
1350
1351    pub fn to_uniform(&self) -> [f32; MATERIAL_FLOATS] {
1352        let mut out = [0.0f32; MATERIAL_FLOATS];
1353
1354        if let Self::Solid(color) = self {
1355            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
1356            out[layout::PARAMS] = 1.0;
1357            out[layout::PARAMS + 1] = kind::SOLID;
1358            return out;
1359        }
1360
1361        // Opaque white under the solid kind, which is the identity for the
1362        // `Modulate` tint that carries the vertex color. Packed here rather
1363        // than falling through, because the machinery below reads `stops` and
1364        // this material has none -- a gradient with no stops packs as
1365        // transparent black, which is the opposite of an identity.
1366        if matches!(self, Self::VertexGradient) {
1367            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(&[1.0, 1.0, 1.0, 1.0]);
1368            out[layout::PARAMS] = 1.0;
1369            out[layout::PARAMS + 1] = kind::SOLID;
1370            return out;
1371        }
1372
1373        // A glyph is a solid color plus a texture read; the coordinates come
1374        // from the vertices, so nothing about the mapping is packed here.
1375        if let Self::Glyph { color, .. } = self {
1376            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
1377            out[layout::PARAMS] = 1.0;
1378            out[layout::PARAMS + 1] = kind::GLYPH;
1379            return out;
1380        }
1381
1382        // An image carries no stops and no count, and must be packed before
1383        // the gradient path below decides it has too few to interpolate.
1384        if let Self::PointField { color } = self {
1385            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
1386            out[layout::PARAMS] = 1.0;
1387            out[layout::PARAMS + 1] = kind::POINT_FIELD;
1388            return out;
1389        }
1390
1391        if let Self::Ellipse {
1392            color,
1393            half_size,
1394            to_local,
1395            stroke,
1396        } = self
1397        {
1398            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
1399            out[layout::GEOMETRY + 2] = half_size[0];
1400            out[layout::GEOMETRY + 3] = half_size[1];
1401            out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1402            out[layout::PARAMS] = 1.0;
1403            out[layout::PARAMS + 1] = kind::ELLIPSE;
1404            out[layout::PARAMS + 3] = *stroke;
1405            return out;
1406        }
1407
1408        if let Self::RoundedRect {
1409            color,
1410            half_size,
1411            to_local,
1412            radius,
1413            outer_radius,
1414            stroke,
1415        } = self
1416        {
1417            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
1418            out[layout::GEOMETRY + 1] = *outer_radius;
1419            out[layout::GEOMETRY + 2] = half_size[0];
1420            out[layout::GEOMETRY + 3] = half_size[1];
1421            out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1422            out[layout::PARAMS] = 1.0;
1423            out[layout::PARAMS + 1] = kind::ROUNDED_RECT;
1424            out[layout::PARAMS + 2] = *radius;
1425            out[layout::PARAMS + 3] = *stroke;
1426            return out;
1427        }
1428
1429        if let Self::RoundedRectBlur {
1430            color,
1431            to_local,
1432            adjust,
1433            r1,
1434            exponent,
1435            s_inv,
1436            min_edge,
1437            scale,
1438        } = self
1439        {
1440            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(color);
1441            out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1442            // The first two floats of `GEOMETRY` are unclaimed for every kind
1443            // that carries a mapping, which this does, so the two that follow
1444            // are what a rounded rectangle would have used for its half size --
1445            // and it needs none, because `adjust` already has the shape in it.
1446            out[layout::GEOMETRY] = adjust[0];
1447            out[layout::GEOMETRY + 1] = adjust[1];
1448            out[layout::GEOMETRY + 2] = *s_inv;
1449            out[layout::GEOMETRY + 3] = *min_edge;
1450            // `OFFSETS` holds stop positions for a gradient and nothing for
1451            // anything else, which is what makes it the seventh float this
1452            // needs and the only kind here that borrows it.
1453            out[layout::OFFSETS] = *scale;
1454            out[layout::PARAMS] = 1.0;
1455            out[layout::PARAMS + 1] = kind::ROUNDED_RECT_BLUR;
1456            out[layout::PARAMS + 2] = *r1;
1457            out[layout::PARAMS + 3] = *exponent;
1458            return out;
1459        }
1460
1461        if let Self::Blur {
1462            to_local,
1463            step,
1464            sigma,
1465            ..
1466        } = self
1467        {
1468            out[layout::GEOMETRY + 2] = step[0];
1469            out[layout::GEOMETRY + 3] = step[1];
1470            out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1471            out[layout::PARAMS] = 1.0;
1472            out[layout::PARAMS + 1] = kind::BLUR;
1473            out[layout::PARAMS + 2] = *sigma;
1474            return out;
1475        }
1476
1477        if let Self::Morphology {
1478            to_local,
1479            step,
1480            radius,
1481            dilate,
1482            ..
1483        } = self
1484        {
1485            out[layout::GEOMETRY + 2] = step[0];
1486            out[layout::GEOMETRY + 3] = step[1];
1487            out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1488            out[layout::PARAMS] = 1.0;
1489            out[layout::PARAMS + 1] = kind::MORPHOLOGY;
1490            out[layout::PARAMS + 2] = radius.clamp(0.0, MORPHOLOGY_TAPS as f32);
1491            out[layout::PARAMS + 3] = if *dilate { 1.0 } else { 0.0 };
1492            return out;
1493        }
1494
1495        if let Self::Runtime { uniforms, .. } = self {
1496            // Straight into the block, in the order the caller wrote them. No
1497            // kind is set: nothing in the shared shader will read this, and a
1498            // caller's program does not branch on one.
1499            let count = uniforms.len().min(RUNTIME_FLOATS);
1500            out[..count].copy_from_slice(&uniforms[..count]);
1501            return out;
1502        }
1503
1504        if let Self::Mesh {
1505            alpha,
1506            tint,
1507            tile,
1508            sampling,
1509            ..
1510        } = self
1511        {
1512            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(tint);
1513            out[layout::GEOMETRY] = *alpha;
1514            out[layout::GEOMETRY + 1] = tile_code(*tile);
1515            out[layout::PARAMS] = 1.0;
1516            out[layout::PARAMS + 1] = kind::MESH;
1517            out[layout::PARAMS + 2] = sampling_code(*sampling);
1518            return out;
1519        }
1520
1521        if let Self::Image {
1522            to_local,
1523            alpha,
1524            tile,
1525            sampling,
1526            source,
1527            tint,
1528            ..
1529        } = self
1530        {
1531            // Into the stop colors, which an image has none of. Eight floats
1532            // that would otherwise travel as zeros on every image draw.
1533            out[layout::STOPS..layout::STOPS + 4].copy_from_slice(source);
1534            out[layout::STOPS + 4..layout::STOPS + 8].copy_from_slice(tint);
1535            out[layout::GEOMETRY + 2] = *alpha;
1536            out[layout::GEOMETRY + 3] = tile_code(*tile);
1537            out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1538            out[layout::PARAMS] = 1.0;
1539            out[layout::PARAMS + 1] = kind::IMAGE;
1540            out[layout::PARAMS + 2] = sampling_code(*sampling);
1541            return out;
1542        }
1543
1544        let stops = self.stops();
1545        let count = stops.len().min(MAX_STOPS);
1546        for (i, stop) in stops.iter().take(count).enumerate() {
1547            out[layout::STOPS + i * 4..layout::STOPS + i * 4 + 4].copy_from_slice(&stop.color);
1548            out[layout::OFFSETS + i] = stop.offset;
1549        }
1550        out[layout::PARAMS] = count.max(1) as f32;
1551
1552        // A gradient with fewer than two stops has nothing to interpolate
1553        // between, so it renders as its first color rather than sending the
1554        // shader down a path that would read an entry nothing wrote.
1555        if count < 2 {
1556            out[layout::PARAMS + 1] = kind::SOLID;
1557            return out;
1558        }
1559
1560        match self {
1561            Self::Solid(_)
1562            | Self::VertexGradient
1563            | Self::PointField { .. }
1564            | Self::Image { .. }
1565            | Self::Mesh { .. }
1566            | Self::Glyph { .. }
1567            | Self::Blur { .. }
1568            | Self::Morphology { .. }
1569            | Self::RoundedRect { .. }
1570            | Self::RoundedRectBlur { .. }
1571            | Self::Ellipse { .. }
1572            | Self::Runtime { .. } => {
1573                unreachable!("handled above")
1574            }
1575            Self::LinearGradient {
1576                ramp,
1577                axis,
1578                to_local,
1579                tile,
1580                ..
1581            } => {
1582                out[layout::PARAMS] = stop_count_code(count, ramp);
1583                out[layout::PARAMS + 2] = tile_code(*tile);
1584                out[layout::GEOMETRY + 2] = axis[0];
1585                out[layout::GEOMETRY + 3] = axis[1];
1586                out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1587                out[layout::PARAMS + 1] = kind::LINEAR;
1588            }
1589            Self::RadialGradient {
1590                ramp,
1591                to_local,
1592                tile,
1593                ..
1594            } => {
1595                out[layout::PARAMS] = stop_count_code(count, ramp);
1596                out[layout::PARAMS + 2] = tile_code(*tile);
1597                out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1598                out[layout::PARAMS + 1] = kind::RADIAL;
1599            }
1600            Self::SweepGradient {
1601                ramp,
1602                to_local,
1603                start_angle,
1604                end_angle,
1605                tile,
1606                ..
1607            } => {
1608                out[layout::PARAMS] = stop_count_code(count, ramp);
1609                out[layout::PARAMS + 2] = tile_code(*tile);
1610                out[layout::GEOMETRY + 2] = *start_angle;
1611                out[layout::GEOMETRY + 3] = *end_angle;
1612                out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1613                out[layout::PARAMS + 1] = kind::SWEEP;
1614            }
1615            Self::ConicalGradient {
1616                ramp,
1617                to_local,
1618                start_radius,
1619                radius_delta,
1620                separation,
1621                tile,
1622                ..
1623            } => {
1624                out[layout::PARAMS] = stop_count_code(count, ramp);
1625                out[layout::PARAMS + 2] = tile_code(*tile);
1626                out[layout::GEOMETRY + 2] = *start_radius;
1627                out[layout::GEOMETRY + 3] = *radius_delta;
1628                out[layout::TO_LOCAL..layout::TO_LOCAL + 12].copy_from_slice(to_local);
1629                out[layout::PARAMS + 1] = kind::CONICAL;
1630                out[layout::PARAMS + 3] = *separation;
1631            }
1632        }
1633        out
1634    }
1635
1636    /// The caller's program this draws with, where it uses one.
1637    ///
1638    /// `None` is every material the built-in shader draws, which is all of
1639    /// them but one.
1640    pub fn program(&self) -> Option<u32> {
1641        match self {
1642            Self::Runtime { program, .. } => Some(*program),
1643            _ => None,
1644        }
1645    }
1646
1647    /// Which shader variant this needs, for keying a pipeline.
1648    ///
1649    /// Every variant lives in one program today, selected by a uniform, so this
1650    /// exists for the moment a material needs its own pipeline rather than
1651    /// pretending that moment has arrived.
1652    pub fn variant(&self) -> MaterialVariant {
1653        match self {
1654            Self::Solid(_) => MaterialVariant::Solid,
1655            _ => MaterialVariant::Gradient,
1656        }
1657    }
1658}
1659
1660#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1661pub enum MaterialVariant {
1662    Solid,
1663    Gradient,
1664}
1665
1666#[cfg(test)]
1667mod tests {
1668
1669    /// `VertexGradient` packs as the identity for the tint that colors it, and
1670    /// asks for a dither where `Solid` does not.
1671    #[test]
1672    fn a_vertex_gradient_packs_as_white_and_dithers() {
1673        let packed = Material::VertexGradient.to_uniform();
1674        assert_eq!(
1675            &packed[layout::STOPS..layout::STOPS + 4],
1676            &[1.0, 1.0, 1.0, 1.0],
1677            "opaque white is the identity for a `Modulate` tint"
1678        );
1679        assert_eq!(packed[layout::PARAMS + 1], kind::SOLID);
1680        assert_eq!(packed[layout::PARAMS], 1.0);
1681
1682        assert!(Material::VertexGradient.dithers());
1683        assert!(!Material::solid([1.0; 4]).dithers());
1684    }
1685
1686    /// And answers the rest of the predicates the way its shading does: no
1687    /// texture, no derivatives, and nothing knowable about its coverage.
1688    #[test]
1689    fn a_vertex_gradient_answers_the_other_predicates() {
1690        let m = Material::VertexGradient;
1691        assert!(!m.needs_screen_derivatives(), "a vertex color reads none");
1692        assert_eq!(m.sampling(), None);
1693        assert_eq!(m.texture_slot(), None);
1694        assert!(!m.is_invisible(), "the colors are on the vertices");
1695        assert!(
1696            !m.is_opaque(),
1697            "the vertex alphas are not visible from here"
1698        );
1699    }
1700    use super::*;
1701
1702    /// What a material can say about a texture read, and what it cannot.
1703    ///
1704    /// The second half is the point. A material has never seen the texture it
1705    /// names -- `Image::source` says so in as many words -- so it can report the
1706    /// filter that is bound and never whether that filter interpolates, which
1707    /// depends on the texture's size against the size it is drawn at. Only the
1708    /// two neighborhood filters escape that, because they step out by a sigma or
1709    /// a radius whatever the size is.
1710    #[test]
1711    fn a_material_reports_its_filter_and_not_what_the_filter_does() {
1712        let image = |sampling| Material::Image {
1713            to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1714            slot: 3,
1715            alpha: 1.0,
1716            tile: TileMode::Clamp,
1717            sampling,
1718            source: [0.0, 0.0, 1.0, 1.0],
1719            tint: [1.0; 4],
1720        };
1721
1722        for sampling in [
1723            Sampling::Nearest,
1724            Sampling::Linear,
1725            Sampling::Cubic,
1726            Sampling::Mipmap,
1727        ] {
1728            assert_eq!(image(sampling).sampling(), Some(sampling));
1729            assert_eq!(image(sampling).texture_slot(), Some(3));
1730            // Even a linear one: whether it lands between texels is the
1731            // caller's geometry against a size this type does not hold.
1732            assert!(
1733                !image(sampling).reads_at_computed_offsets(),
1734                "an image is read where it is drawn, not at an offset of its own"
1735            );
1736        }
1737
1738        // The one case a material can settle by itself.
1739        let morphology = Material::Morphology {
1740            to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1741            slot: 0,
1742            step: [1.0, 0.0],
1743            radius: 2.0,
1744            dilate: true,
1745        };
1746        assert_eq!(
1747            morphology.sampling(),
1748            None,
1749            "it carries no filter to report"
1750        );
1751        assert!(morphology.reads_at_computed_offsets());
1752
1753        // Reads a texture, states no filter, and walks nothing: the pair of
1754        // answers is why `sampling` returning `None` cannot be read as "no
1755        // texture".
1756        let glyph = Material::Glyph {
1757            color: [1.0; 4],
1758            slot: 1,
1759        };
1760        assert_eq!(glyph.sampling(), None);
1761        assert_eq!(glyph.texture_slot(), Some(1));
1762        assert!(!glyph.reads_at_computed_offsets());
1763
1764        // Reads nothing at all.
1765        let solid = Material::solid([1.0; 4]);
1766        assert_eq!(solid.sampling(), None);
1767        assert_eq!(solid.texture_slot(), None);
1768        assert!(!solid.reads_at_computed_offsets());
1769    }
1770
1771    /// A mapping with the given two-by-two linear part and no translation.
1772    ///
1773    /// Most of these tests care that the mapping survives packing, not what it
1774    /// is, and said so in four floats before the paint's origin moved inside
1775    /// it. This keeps them saying that.
1776    fn linear_to_local(m: [f32; 4]) -> ToLocal {
1777        [
1778            m[0], m[1], 0.0, 0.0, //
1779            m[2], m[3], 0.0, 0.0, //
1780            0.0, 0.0, 1.0, 0.0,
1781        ]
1782    }
1783
1784    fn two_stops() -> Vec<Stop> {
1785        vec![
1786            Stop::new([1.0, 0.0, 0.0, 1.0], 0.0),
1787            Stop::new([0.0, 0.0, 1.0, 1.0], 1.0),
1788        ]
1789    }
1790
1791    #[test]
1792    fn a_solid_color_lands_in_the_first_stop_and_selects_the_solid_path() {
1793        let packed = Material::solid([0.25, 0.5, 0.75, 1.0]).to_uniform();
1794        assert_eq!(&packed[0..4], &[0.25, 0.5, 0.75, 1.0]);
1795        assert_eq!(packed[layout::PARAMS], 1.0, "stop count");
1796        assert_eq!(packed[layout::PARAMS + 1], kind::SOLID);
1797    }
1798
1799    #[test]
1800    fn a_linear_gradient_packs_its_stops_axis_and_count() {
1801        let packed = Material::LinearGradient {
1802            axis: [2.0, 0.0],
1803            to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1804            stops: two_stops(),
1805            tile: TileMode::Clamp,
1806            ramp: None,
1807        }
1808        .to_uniform();
1809
1810        assert_eq!(&packed[0..4], &[1.0, 0.0, 0.0, 1.0], "first stop");
1811        assert_eq!(&packed[4..8], &[0.0, 0.0, 1.0, 1.0], "second stop");
1812        assert_eq!(packed[layout::OFFSETS], 0.0);
1813        assert_eq!(packed[layout::OFFSETS + 1], 1.0);
1814        assert_eq!(
1815            &packed[layout::GEOMETRY..layout::GEOMETRY + 4],
1816            &[0.0, 0.0, 2.0, 0.0],
1817            "the axis rather than the end point, and nothing in the half the \
1818             start used to occupy before it moved inside the mapping"
1819        );
1820        assert_eq!(
1821            &packed[layout::TO_LOCAL..layout::TO_LOCAL + 12],
1822            &linear_to_local([1.0, 0.0, 0.0, 1.0])
1823        );
1824        assert_eq!(packed[layout::PARAMS + 1], kind::LINEAR);
1825    }
1826
1827    #[test]
1828    fn a_radial_gradient_packs_its_mapping() {
1829        let packed = Material::RadialGradient {
1830            to_local: linear_to_local([2.0, 0.0, 0.0, 4.0]),
1831            stops: two_stops(),
1832            tile: TileMode::Clamp,
1833            ramp: None,
1834        }
1835        .to_uniform();
1836
1837        // A radial gradient states nothing in the geometry slot at all now: its
1838        // center rode there, and rides inside the mapping instead.
1839        assert_eq!(&packed[layout::GEOMETRY..layout::GEOMETRY + 2], &[0.0, 0.0]);
1840        // The mapping is what makes a circle circular on a non-square target,
1841        // so it has to survive packing intact.
1842        assert_eq!(
1843            &packed[layout::TO_LOCAL..layout::TO_LOCAL + 12],
1844            &linear_to_local([2.0, 0.0, 0.0, 4.0])
1845        );
1846        assert_eq!(packed[layout::PARAMS + 1], kind::RADIAL);
1847    }
1848
1849    #[test]
1850    fn a_sweep_gradient_packs_its_angles_alongside_its_center() {
1851        let packed = Material::SweepGradient {
1852            to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1853            start_angle: 0.5,
1854            end_angle: 2.5,
1855            stops: two_stops(),
1856            tile: TileMode::Clamp,
1857            ramp: None,
1858        }
1859        .to_uniform();
1860
1861        // Angles share the geometry slot with the center, which is why a linear
1862        // gradient's endpoints and a sweep's angles cannot both be present.
1863        assert_eq!(
1864            &packed[layout::GEOMETRY..layout::GEOMETRY + 4],
1865            &[0.0, 0.0, 0.5, 2.5]
1866        );
1867        assert_eq!(packed[layout::PARAMS + 1], kind::SWEEP);
1868    }
1869
1870    #[test]
1871    fn every_gradient_kind_falls_back_to_solid_with_one_stop() {
1872        // Interpolating needs two points. Selecting a gradient path with one
1873        // would have the shader read an entry nothing wrote.
1874        let one = vec![Stop::new([1.0, 1.0, 1.0, 1.0], 0.0)];
1875        let materials = [
1876            Material::LinearGradient {
1877                axis: [1.0 - 0.0, 0.0 - 0.0],
1878                to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1879                stops: one.clone(),
1880                tile: TileMode::Clamp,
1881                ramp: None,
1882            },
1883            Material::RadialGradient {
1884                to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1885                stops: one.clone(),
1886                tile: TileMode::Clamp,
1887                ramp: None,
1888            },
1889            Material::SweepGradient {
1890                to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1891                start_angle: 0.0,
1892                end_angle: 1.0,
1893                stops: one,
1894                tile: TileMode::Clamp,
1895                ramp: None,
1896            },
1897        ];
1898        for material in materials {
1899            let packed = material.to_uniform();
1900            assert_eq!(packed[layout::PARAMS + 1], kind::SOLID, "{material:?}");
1901            assert_eq!(&packed[0..4], &[1.0, 1.0, 1.0, 1.0]);
1902        }
1903    }
1904
1905    #[test]
1906    fn stops_beyond_the_limit_are_dropped_rather_than_overflowing() {
1907        let stops: Vec<Stop> = (0..8)
1908            .map(|i| Stop::new([i as f32 / 8.0, 0.0, 0.0, 1.0], i as f32 / 7.0))
1909            .collect();
1910        let packed = Material::LinearGradient {
1911            axis: [1.0 - 0.0, 0.0 - 0.0],
1912            to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1913            stops,
1914            tile: TileMode::Clamp,
1915            ramp: None,
1916        }
1917        .to_uniform();
1918        // The count is what stops the shader reading past what was written.
1919        assert_eq!(packed[layout::PARAMS], MAX_STOPS as f32);
1920    }
1921
1922    #[test]
1923    fn visibility_accounts_for_every_stop_of_every_kind() {
1924        assert!(Material::solid([1.0, 1.0, 1.0, 0.0]).is_invisible());
1925        assert!(!Material::solid([0.0, 0.0, 0.0, 1.0]).is_invisible());
1926
1927        let clear = vec![
1928            Stop::new([1.0, 0.0, 0.0, 0.0], 0.0),
1929            Stop::new([0.0, 0.0, 1.0, 0.0], 1.0),
1930        ];
1931        assert!(Material::RadialGradient {
1932            to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1933            stops: clear,
1934            tile: TileMode::Clamp,
1935            ramp: None,
1936        }
1937        .is_invisible());
1938
1939        let partly = vec![
1940            Stop::new([1.0, 0.0, 0.0, 0.0], 0.0),
1941            Stop::new([0.0, 0.0, 1.0, 1.0], 1.0),
1942        ];
1943        assert!(
1944            !Material::SweepGradient {
1945                to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1946                start_angle: 0.0,
1947                end_angle: 1.0,
1948                stops: partly,
1949                tile: TileMode::Clamp,
1950                ramp: None,
1951            }
1952            .is_invisible(),
1953            "one visible stop is enough"
1954        );
1955    }
1956
1957    #[test]
1958    fn every_gradient_kind_reports_the_same_variant() {
1959        // They share one program, selected by a uniform, so keying a pipeline
1960        // on the kind would create three identical pipelines.
1961        assert_eq!(Material::solid([0.0; 4]).variant(), MaterialVariant::Solid);
1962        for material in [
1963            Material::LinearGradient {
1964                axis: [1.0, 0.0],
1965                to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1966                stops: two_stops(),
1967                tile: TileMode::Clamp,
1968                ramp: None,
1969            },
1970            Material::RadialGradient {
1971                to_local: linear_to_local([1.0, 0.0, 0.0, 1.0]),
1972                stops: two_stops(),
1973                tile: TileMode::Clamp,
1974                ramp: None,
1975            },
1976        ] {
1977            assert_eq!(material.variant(), MaterialVariant::Gradient);
1978        }
1979    }
1980}