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}