pub enum Material {
Show 16 variants
Solid([f32; 4]),
VertexGradient,
LinearGradient {
axis: [f32; 2],
to_local: ToLocal,
stops: Vec<Stop>,
ramp: Option<u32>,
tile: TileMode,
},
RadialGradient {
to_local: ToLocal,
stops: Vec<Stop>,
ramp: Option<u32>,
tile: TileMode,
},
SweepGradient {
to_local: ToLocal,
start_angle: f32,
end_angle: f32,
stops: Vec<Stop>,
ramp: Option<u32>,
tile: TileMode,
},
ConicalGradient {
to_local: ToLocal,
start_radius: f32,
radius_delta: f32,
separation: f32,
stops: Vec<Stop>,
ramp: Option<u32>,
tile: TileMode,
},
Runtime {
program: u32,
uniforms: Vec<f32>,
textures: [Option<u32>; 4],
},
Mesh {
slot: u32,
alpha: f32,
tint: [f32; 4],
tile: TileMode,
sampling: Sampling,
},
Image {
to_local: ToLocal,
slot: u32,
alpha: f32,
tile: TileMode,
sampling: Sampling,
source: [f32; 4],
tint: [f32; 4],
},
RoundedRect {
color: [f32; 4],
half_size: [f32; 2],
to_local: ToLocal,
radius: f32,
outer_radius: f32,
stroke: f32,
},
RoundedRectBlur {
color: [f32; 4],
to_local: ToLocal,
adjust: [f32; 2],
r1: f32,
exponent: f32,
s_inv: f32,
min_edge: f32,
scale: f32,
},
Ellipse {
color: [f32; 4],
half_size: [f32; 2],
to_local: ToLocal,
stroke: f32,
},
Blur {
to_local: ToLocal,
slot: u32,
step: [f32; 2],
sigma: f32,
},
Morphology {
to_local: ToLocal,
slot: u32,
step: [f32; 2],
radius: f32,
dilate: bool,
},
Glyph {
color: [f32; 4],
slot: u32,
},
PointField {
color: [f32; 4],
},
}Expand description
How a shape is filled.
Variants§
Solid([f32; 4])
VertexGradient
Color comes from the vertices, and the result is dithered.
Shades as opaque white, so the per-vertex color under Modulate – which
is the identity against white – is the whole result. That is the
arrangement draw_vertices already uses for a caller’s mesh; what this
adds is that it dithers, which Solid must not.
It exists for a gradient the rasterizer interpolates instead of the
fragment stage evaluating. A long run of nearly equal values is what
bands on an eight-bit target whichever stage produced it, and a route
that could not say so would drop the dither silently – which is what
§19 of docs/non-parity.md records killing the first attempt at it.
Carries no data: a section’s colors ride on its vertices, and the premultiplied alpha rides with them.
LinearGradient
A gradient along an axis, starting at a point in clip space.
Clip space for the start, because the fragment stage locates itself from an interpolated clip position: the alternative, the fragment coordinate builtin, has a different origin in each graphics API and would run the gradient in opposite directions on the two backends.
The axis is in the gradient’s own space, and to_local maps a clip-space
offset into it — the same pairing radial and sweep use, and for the same
reason. Clip space is anisotropic whenever the target is not square, so
projecting onto an axis there weights the two axes by the target’s
shape: on a target twice as wide as it is tall, a diagonal gradient runs
in the wrong direction.
Fields
ramp: Option<u32>Texture slot holding this gradient’s colors, when they did not fit.
None is the ordinary case: the stops travel in the material and
the shader walks them. Some means the recorder tabulated them into
an image instead, because there were more than MAX_STOPS, and
the shader reads the color at the parameter rather than computing
it. The two must agree where both are possible, which is what makes
the choice invisible to a caller.
tile: TileModeWhat happens beyond the two endpoints.
The parameter a gradient is sampled by runs from zero at one end to one at the other and is defined everywhere else too, so a shape larger than its gradient asks a question the stops do not answer. Clamping holds the end colors, which is the usual choice; repeating tiles the ramp, which is what a stripe pattern is; decal draws nothing outside, the same meaning it has for an image.
RadialGradient
A gradient outward from a center, in clip space, where to_local
carries the radius: it maps the clip-space offset so that the gradient’s
edge lands at unit distance.
Fields
ramp: Option<u32>Texture slot holding this gradient’s colors, when they did not fit.
None is the ordinary case: the stops travel in the material and
the shader walks them. Some means the recorder tabulated them into
an image instead, because there were more than MAX_STOPS, and
the shader reads the color at the parameter rather than computing
it. The two must agree where both are possible, which is what makes
the choice invisible to a caller.
tile: TileModeWhat happens beyond the radius. See Material::LinearGradient.
SweepGradient
A gradient around a center, in clip space, running from start_angle
to end_angle in radians.
Fields
ramp: Option<u32>Texture slot holding this gradient’s colors, when they did not fit.
None is the ordinary case: the stops travel in the material and
the shader walks them. Some means the recorder tabulated them into
an image instead, because there were more than MAX_STOPS, and
the shader reads the color at the parameter rather than computing
it. The two must agree where both are possible, which is what makes
the choice invisible to a caller.
ConicalGradient
A gradient between two circles, the general form the other two are special cases of.
center is the first circle’s center in clip space, and to_local
maps a clip-space offset into a space where that center is the origin
and the second circle’s center lies at (separation, 0). Putting the
separation on an axis costs nothing – the rotation folds into a matrix
that has to be there anyway – and buys the second center for one float
instead of two, which is what makes this fit at all.
The radii are in that same space and are not normalized, unlike the
radial gradient’s, because there are two of them and a scale can only
remove one. Carrying both plainly also means the degenerate cases need
no special handling: concentric circles are separation == 0, and a
cone rather than a tube is radius_delta != 0.
Fields
ramp: Option<u32>Texture slot holding this gradient’s colors, when they did not fit.
See Material::LinearGradient.
tile: TileModeWhat happens where the parameter leaves the unit interval. See
Material::LinearGradient.
Runtime
A caller’s own fragment program, with the floats it reads.
The program is named rather than carried: registering one builds a pipeline, which is expensive and outlives any draw, so a context holds them and a material names which. The floats travel in the same uniform block every other material uses, which is what lets an effect exist without a second descriptor set.
Fields
textures: [Option<u32>; 4]The textures the program may sample, in the order it declares them.
None in a position means the program does not read that binding,
and the backend binds its placeholder there – a pipeline must have
every binding it declares bound, however unreachable the branch
reading it.
The count is fixed rather than free because the descriptor set layout every pipeline is built against has to be one layout. Four is what that costs: three unused image bindings on a draw that samples nothing, against a second set and a second layout for the draws that want more than one.
Mesh
A texture, sampled at coordinates the vertices carry.
Deliberately not Material::Image with a switch on where its
coordinates come from. An image mapped from clip space needs an origin,
a matrix and a source rectangle to say which part of a sheet it draws;
a mesh needs none of them, because a caller stating a coordinate per
vertex has already answered all three. What is left is small enough to
be its own thing, and keeping it separate means neither carries a field
that means nothing for it.
This is what makes a sprite batch one draw: a hundred quads reading a hundred different parts of one sheet differ only in their vertices.
Fields
alpha: f32Scales the sampled color, applied to premultiplied color like
Material::Image’s.
Image
A texture, sampled through a mapping from clip space.
origin and to_local together are an affine: a clip-space position
maps to texture coordinates as to_local * (clip - origin), which lands
the image’s top-left corner at zero and its bottom-right at one. The
same pair a radial gradient uses, for the same reason — clip space is
anisotropic on a non-square target, so a mapping that ignored it would
stretch every image by the aspect ratio.
Which texture is not named here. The material is data the recorder produces without touching the device, so it carries a slot into the table supplied at submission instead of a backend handle.
Fields
alpha: f32Scales the sampled color, for drawing an image translucently.
Applied to premultiplied color, so it scales the whole texel rather than only its alpha. A texture holds premultiplied color whether it was uploaded or rendered into, and treating a sample as straight alpha would apply the alpha twice — invisible for an opaque image, and plain the moment one translucent image is drawn into another.
source: [f32; 4]The part of the texture to draw, as [u0, v0, u1, v1] from zero to
one.
The whole texture is [0, 0, 1, 1], which is what every caller
wanted until sprite sheets. Normalized rather than in texels because
a material is built by a recorder that has never seen the texture
and cannot know how large it is; the caller who uploaded it does.
Applied after tiling rather than before, so a repeat repeats the selected piece rather than the whole sheet – which is the only reading of “tile this sprite” that means anything.
tint: [f32; 4]Straight color the sampled texel is multiplied by; white changes nothing.
What turns one monochrome icon sheet into every state a control has.
A generalization of alpha rather than a rival to it: a tint of
[1, 1, 1, a] is exactly that scaling, and both are applied because
removing the narrower one would break callers for no gain.
Straight rather than premultiplied because that is how a caller states a color, and the shader premultiplies it before multiplying a texel that already is – scaling color by the tint’s alpha as well, which is what keeps the result premultiplied rather than merely close to it.
RoundedRect
A rounded rectangle evaluated per fragment rather than tessellated.
The shape an interface is mostly made of, and the one where computing coverage beats building triangles for it. A tessellated rounded rectangle costs vertices proportional to how round it is and has hard edges unless the whole pass is multisampled; this is two triangles whatever the radius, and antialiases itself from the distance field it already computes.
The geometry is in the shape’s own space, with to_local mapping a
clip-space position into it – the same pairing the gradients use, and
for the same reason: clip space is anisotropic on a target that is not
square, and a distance measured there would round the corners by
different amounts on each axis.
Fields
outer_radius: f32The radius the outline’s outer edge turns through, which is not always the radius grown by half the stroke.
An outline is the difference of two offset shapes rather than a band
around one, and the outer offset is where a join shows. Grow a
rounded corner and you get a bigger rounded corner, so this is
radius + stroke / 2 for anything with a radius, and for anything
with a round join. A mitered square corner is the exception: its
offset is still square, so this is zero and the field draws the
point the join asks for.
Carried rather than derived because the shader cannot see the join, and because deriving it wrongly is what made a square-cornered stroke come out with rounded corners and be refused this route altogether.
stroke: f32Trace the outline at this width rather than filling, in the shape’s own space. Zero fills.
Costs a distance field nothing: the field already says how far every fragment is from the edge, so an outline is the band where that is small. Tessellating one instead means building a second shape – offset inward and outward, with the corners resolved – which is where a stroked outline gets its vertex count and its joins.
RoundedRectBlur
A blurred rounded rectangle evaluated per fragment, with no blur pass.
The sibling of Self::RoundedRect, and the reason it is worth having
is that the general route costs three passes per shape – one for the
content and two for a separable Gaussian – where this costs one draw in
the pass already being recorded. On a Raspberry Pi 5 three shadows that
way are 7.8 ms of a 26.8 ms frame and nine of its fourteen passes.
The method is Raph Levien’s “Blurred rounded rectangles”, which is what
upstream’s SolidRRectBlurContents evaluates too: the exact convolution
of a Gaussian with a rounded rectangle has no closed form, and this
approximates it as a product of two error functions along an axis,
corrected for the corners by measuring distance with an exponent other
than two. Every field here is a number the CPU precomputed for that
expression rather than anything a caller stated – see
Canvas::rrect_blur_material, which is the only place they are derived.
Only where every corner shares one circular radius, which is upstream’s condition too. Anything else takes the general route.
Fields
r1: f32The corner radius the approximation uses, which is not the caller’s: it grows with the deviation, because a blurred corner is rounder than a sharp one.
exponent: f32The exponent the corner distance is measured with. Two is a circle; this is larger, which is what makes the blurred corner’s profile match a Gaussian’s rather than a circle’s.
Ellipse
An ellipse evaluated per fragment.
Its own variant rather than a rounded rectangle with a large radius, which gives a stadium: past half the shorter side a rounded rectangle stops changing, where an ellipse keeps curving along both axes.
The same geometry a rounded rectangle carries, less the radius, which the two axes already state.
Fields
Blur
One axis of a separable Gaussian blur of a sampled texture.
Separable because a two-dimensional Gaussian is the product of two one-dimensional ones, so blurring along each axis in turn gives the same result as a square of taps at a fraction of the cost: at a radius of sixteen that is thirty-three taps against a thousand and eighty-nine. Two passes are the price, which is why this names an axis rather than describing the whole blur.
Fields
Morphology
One axis of a morphological filter of a finished layer.
The largest or smallest sample within a radius, per channel, which is
what dart:ui calls ImageFilter.dilate and ImageFilter.erode. Like
the blur it is separable – a rectangular structuring element is the
product of two intervals – so two passes give the square of taps.
Unlike the blur it is also decomposable: dilating by a and then by
b is dilating by a + b exactly, because the structuring elements add
under the Minkowski sum. A radius past what one pass can reach is
therefore split across passes rather than approximated by sampling more
sparsely. Sparse taps work for a blur, where a missed sample costs a
little smoothness, and do not work here: the result is a maximum, so a
missed sample is a scallop in the edge.
Fields
step: [f32; 2]One tap’s step, in the sampled texture’s own coordinates. As
Self::Blur::step.
radius: f32How many texels each way this pass reaches, at most
MORPHOLOGY_TAPS.
A whole number of texels, because the structuring element is a set of samples rather than a weighting of them and there is no meaning to half of one.
Glyph
Coverage sampled from an atlas, tinting one color.
Distinct from Self::Image in two ways that matter. The texture is
read as coverage rather than as color — one channel scaling a solid,
which is what antialiased text is — and the coordinates come from the
vertices rather than from a mapping in the paint, so a run of glyphs
reading different parts of one atlas is a single draw.
Fields
PointField
Many discs of one color, evaluated from the vertices rather than the paint, so that a field of them is one draw.
Carries no mapping and no size, which is the whole point of it. Every
other fragment-evaluated shape here locates itself through to_local,
and to_local holds the shape’s center – so two of them at different
places are two materials and cannot share a draw. This one locates
itself from the interpolated texture coordinate, which the vertices
carry as the unit circle’s corners, so any number of discs at any
centers are one material and one draw.
The edge is the same edge. disc_coverage in the shader differentiates
the implicit function across the pixel rather than forming a distance,
and a derivative of an interpolated value is as available as that of a
computed one.
Implementations§
Source§impl Material
impl Material
pub fn solid(color: [f32; 4]) -> Self
Sourcepub fn sampling(&self) -> Option<Sampling>
pub fn sampling(&self) -> Option<Sampling>
How this material reads a texture, if it reads one.
None for a material that samples nothing, which is not the same answer
as Sampling::Nearest: one says there is no texture and the other says
there is and it is read at a texel’s center.
Here because a caller above the HAL may need to know whether a draw interpolates between texels, and only this type knows. The test harness asks it to size a cross-device tolerance: a filter that forms a weighted sum of texels does so with weights of implementation-defined precision, so two devices disagree on every interpolated pixel by more than the last bit of one store. Deriving that from a recording rather than from the scene that produced it is the point – the scene model has four separate places a sampled texture can come from and a comment recording four occasions on which enumerating them missed one.
Sourcepub fn reads_at_computed_offsets(&self) -> bool
pub fn reads_at_computed_offsets(&self) -> bool
Whether this material reads between texels rather than at one.
Every mode but Sampling::Nearest forms a weighted sum: linear over
four texels, bicubic over sixteen, mipmapped over two levels of the
first. Measured on a Raspberry Pi 5, one scene rendered twice differing
only in this: nearest is byte for byte identical between v3d and llvmpipe
over the whole frame, and linear reaches a delta of three across seventy
per cent of it.
Whether this material reads its texture at offsets it computes, rather
than at the coordinate it was handed.
True for the two filters that walk a neighborhood: a blur steps out by a
sigma, a morphology by a radius, and neither need land on a texel center
whatever size the texture is. So this is the one case where a material
can say that a read interpolates without knowing the texture – which is
why it is a separate question from Self::sampling, and why a layer
dilated by a morphology is one of the scenes that put two devices three
levels apart.
Sourcepub fn with_opacity(self, factor: f32) -> Self
pub fn with_opacity(self, factor: f32) -> Self
The same material at factor of its opacity.
Applied to every color a material carries, since a gradient’s stops may differ in alpha and scaling them together is what keeps the ramp the same ramp. Straight alpha, so this happens before the premultiply the packing does rather than after it.
Two variants are left alone and it is worth saying which. A blur or a morphology is a filter over a finished pass rather than a paint, so there is no color in it to dim – and nothing asks this of one. A caller’s program is the other: its output is whatever it computes, and this renderer has no uniform it may write to. That is a real limit wherever the caller wanted the dimming, and it is stated at the one place that asks for it.
Sourcepub fn is_invisible(&self) -> bool
pub fn is_invisible(&self) -> bool
Whether drawing with this would change anything.
Sourcepub fn texture_slots(&self) -> [Option<u32>; 4]
pub fn texture_slots(&self) -> [Option<u32>; 4]
The texture slot this samples, for a backend building its bindings.
Matched exhaustively rather than with a catch-all. A variant that samples something and is not listed here reports no slot, so the backend binds its placeholder and the draw comes out flat white – a plausible picture rather than an error, and one nothing else would explain. Every texture slot this material samples, in binding order.
One entry for everything but a runtime program, which may declare
several. A backend building descriptors needs the whole tuple, since a
set holds all of them at once; a backend asking only which slot to bind
first has Self::texture_slot.
pub fn texture_slot(&self) -> Option<u32>
Sourcepub fn is_opaque(&self) -> bool
pub fn is_opaque(&self) -> bool
Pack into the layout the shader declares.
Every member is a four-component vector, which is what lets this be a
flat array of floats copied straight into a uniform buffer: the std140
rules the shader’s block is declared with place a vec4 and an array
of them at exactly these offsets, so no member needs padding written
around it.
Stops beyond the limit are dropped rather than resampled, and the count travels alongside so the shader ignores unused entries instead of blending toward whatever happens to be in them. Whether every pixel this material writes comes out fully opaque.
Used to decide whether a draw may be split into several, which is only safe when
writing a pixel twice gives what writing it once does. Under SrcOver that holds
exactly when the source is opaque.
False unless shown otherwise. A solid fill carries its alpha and a gradient’s stops carry theirs. Everything else – images, the analytic shapes, blurs, a caller’s own program – is refused outright.
A tabulated gradient still answers from its stops, and that is worth saying because
the first version of this refused one. ramp being Some means the shader reads
a texture instead of walking the list, not that the list is gone: the recorder maps
every stop into the material either way and bakes the ramp from the same list, by
interpolating between them. Interpolating between opaque colors gives an opaque
one, so the texels are opaque exactly when the stops are. Refusing a ramp cost the
whole of occlusion culling’s benefit on the bench’s frame, whose wash is five stops
against a MAX_STOPS of four.
Sourcepub fn needs_screen_derivatives(&self) -> bool
pub fn needs_screen_derivatives(&self) -> bool
Whether this material’s shading reads screen-space derivatives.
dpdx and dpdy are computed across a two-by-two quad of fragments, using helper
invocations outside the primitive. Whether a scissor keeps those helpers alive is
not something either specification settles, and implementations differ: Mesa
26.2.3 keeps them and 25.2.8 does not. So a draw whose shading depends on them
gives a different answer at a scissor’s edge than away from it, and splitting such
a draw by scissor changes the picture along the seam.
Measured rather than reasoned about: an analytic shadow drawn once reads 217 where
the same shadow drawn in four scissored strips reads 206, at the pixel on the
strip boundary, on Mesa 25.2.8. See a_scissor_does_not_change_what_a_draw_paints.
True unless shown otherwise, which is the only safe direction: a material wrongly called derivative-free leaves a seam in the frame. The two groups that are shown otherwise are solid fills, whose coverage comes from the rasterizer, and the gradients, which sample their ramp at an explicit level of zero. The rest either take a coverage from the gradient of a distance field – the rounded rectangle, the ellipse and the rounded-rectangle blur – or a texture level from the rate their coordinates change, which is every image fill, or are a caller’s own program.
Sourcepub fn dithers(&self) -> bool
pub fn dithers(&self) -> bool
Whether this material asks the target for a long run of nearly equal values, and so wants a dither.
The gradients, and nothing else – the same set the shader used to test
for itself. Asked here because the shader cannot be the place that knows:
it tests the material kind, so a route that draws a gradient under a
different kind silently stops dithering. §19 of docs/non-parity.md
records that happening, in the reverted attempt at upstream’s
vertex-interpolated fast gradient.
False unless shown otherwise. Dithering something with no band to break adds noise to a flat color.
pub fn to_uniform(&self) -> [f32; 64]
Sourcepub fn program(&self) -> Option<u32>
pub fn program(&self) -> Option<u32>
The caller’s program this draws with, where it uses one.
None is every material the built-in shader draws, which is all of
them but one.
Sourcepub fn variant(&self) -> MaterialVariant
pub fn variant(&self) -> MaterialVariant
Which shader variant this needs, for keying a pipeline.
Every variant lives in one program today, selected by a uniform, so this exists for the moment a material needs its own pipeline rather than pretending that moment has arrived.