Skip to main content

frust_engine/filters/
mod.rs

1//! Layer filters: a layer rendered in isolation, run through a sequence of
2//! filter passes, and composited from the page the last of them wrote.
3//!
4//! An opacity layer needs one page and one extra pass ([`schedule`]); a
5//! filtered layer needs a *second* page as well, because a filter pass reads a
6//! whole image and writes a whole image and a render pass cannot do both to one
7//! texture. The two pages the scheduler already ping-pongs between are exactly
8//! that pair: the layer's contents land in one, each pass writes the one it did
9//! not read, and the sequence is arranged so the result ends back in the page
10//! the parent's composite samples.
11//!
12//! [`schedule`]: crate::schedule
13//!
14//! ## What is served
15//!
16//! Two filters: [`LayerFilter::Blur`], the Gaussian blur that the theme
17//! layer's `GlassMaterial` backdrop needs, and [`LayerFilter::DropShadow`], a
18//! shadow-only drop shadow (`vello_common`'s `DropShadowOnly`, never
19//! compositing the layer's own unfiltered content back over the shadow — see
20//! [`drop_shadow`]'s own doc for why). Every other filter a recording can
21//! carry — a flood, a standalone offset, a drop shadow that composites the
22//! original back over itself, an edge mode other than [`SERVED_EDGE_MODE`], or
23//! a graph of more than one primitive — is refused by [`served_filter`] with a
24//! reason naming what was found, and the frame is skipped rather than rendered
25//! without its filter. That refusal is not a placeholder for a fallback: the
26//! engine tier carries no second renderer (see the [`schedule`] module header).
27//!
28//! The scene seam for a *layer* filter is deliberately absent. `frust_scene`
29//! has no layer-filter command and gains none here — [`push_filter_layer`] is
30//! how the engine's own compiler will open a filtered layer once a later plan
31//! gives the display list something to lower from.
32//!
33//! ## The backdrop blur
34//!
35//! One filter *does* reach the display list: `frust_scene::Command::BackdropBlur`,
36//! a Gaussian blur of whatever the frame has already painted beneath a
37//! rectangle (a frosted-glass panel). It is not a filter layer — there are no
38//! contents of its own to isolate — but it runs the very same pass sequence,
39//! on a page seeded with a region of the frame's own target rather than with
40//! rendered contents. [`Backdrop`] is the compiler's record of one, and the
41//! three halves split like a filter layer's:
42//!
43//! - the compiler ([`crate::compile`]) records the backdrop as a layer holding
44//!   one draw — its rectangle's coverage under whatever clip is open — and
45//!   lists it in `CompiledFrame::backdrops`, with the device [`region`
46//!   ](backdrop_region) the blur reads and the device-space σ;
47//! - the scheduler ([`crate::schedule`]) cuts the frame's surface round at it,
48//!   so everything painted before it has landed, then emits a *region copy*
49//!   round ([`crate::schedule::RegionCopy`]) seeding a pooled page with that
50//!   region of the frame's target, the blur's filter rounds over the page, and
51//!   leaves the result held for the surface round that draws the rectangle;
52//! - the renderer draws the rectangle's own strip coverage sampling the
53//!   blurred page at each pixel's own position, so a rounded or path clip
54//!   masks the blurred region exactly as it masks a fill.
55//!
56//! The region copy is the reusable half: "the frame's target, read back into a
57//! page a later pass samples" is what any effect reading the backdrop needs
58//! (a blend mode composite is the next one), so it is a round kind of the
59//! scheduler and a helper of the renderer rather than a special case of the
60//! blur. It needs the frame's target to be readable — `COPY_SRC` in the
61//! engine's own [`INTERMEDIATE_FORMAT`](crate::gpu::pipelines::INTERMEDIATE_FORMAT),
62//! or `TEXTURE_BINDING` in any format — which a swapchain is not:
63//! `frust-render` renders a frame carrying a backdrop into an intermediate of
64//! its own and presents it with one copy pass. A target that is neither draws
65//! every backdrop as nothing (logged once), the "draw less, never wrong"
66//! answer.
67//!
68//! Bounded at [`MAX_BACKDROPS_PER_FRAME`]: each costs a surface cut, a copy and
69//! a blur's whole sequence, and a frame recording more is refused with
70//! [`EngineError::TooManyBackdrops`].
71//!
72//! ## What is pure, and what is not
73//!
74//! Everything here is a decision over plain values, in the crate's usual split:
75//! which passes a blur costs and at what extents ([`blur::blur_passes`]), what
76//! the fragment stage reads out of the filter-data texture
77//! ([`blur::GpuGaussianBlur`]), and how one pass's instance is packed
78//! ([`blur::FilterInstanceData`]). No device is touched and no texture is
79//! allocated here.
80//!
81//! The three device-side halves live where every other engine pipeline's do:
82//! the WGSL program is assembled by [`crate::gpu::shader_src::FILTER`], the
83//! pipeline it is drawn through is
84//! [`crate::gpu::pipelines::EnginePipeline::Filter`], and the texture holding
85//! the frame's parameter blocks plus the bilinear sampler the kernels read
86//! through are [`crate::gpu::targets`]'. The renderer binds the pages the
87//! scheduler named and issues one instanced quad per pass
88//! ([`crate::renderer::FilterResources`]).
89
90pub mod blur;
91pub mod drop_shadow;
92
93use kurbo::Affine;
94use peniko::color::{AlphaColor, Srgb};
95use vello_common::filter::drop_shadow::DropShadow;
96use vello_common::filter::gaussian_blur::GaussianBlur;
97use vello_common::filter::{FilterData, PreparedFilter};
98use vello_common::filter_effects::{EdgeMode, Filter, FilterPrimitive};
99use vello_common::geometry::{RectU16, SizeU16};
100use vello_common::record::{CommandRecorder, LayerProps, RecordedLayerKind};
101use vello_common::util::extract_scales;
102
103use crate::error::EngineError;
104
105/// The most `Command::BackdropBlur`s one frame is served with; a frame
106/// recording more is refused with [`EngineError::TooManyBackdrops`].
107///
108/// Four covers the glass surfaces a screen layers at once (an app bar, a tab
109/// bar, a sheet and a dialog over it) with room to spare, while keeping a
110/// frame's worst case to four surface cuts, four region copies and four blur
111/// sequences.
112pub const MAX_BACKDROPS_PER_FRAME: usize = 4;
113
114/// One `Command::BackdropBlur` the compiler recorded, as the scheduler and the
115/// renderer read it (see this module's *The backdrop blur*).
116#[derive(Debug, Clone, Copy, PartialEq)]
117pub struct Backdrop {
118    /// The recorded layer standing for the backdrop at its painter-order
119    /// position, indexed into
120    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
121    /// It holds exactly one draw: the rectangle's coverage.
122    pub layer: u32,
123    /// The painter-order depth of that one draw — how the renderer tells the
124    /// backdrop's draw apart from every other draw of a surface round.
125    pub depth: u32,
126    /// The device region of the frame's target the blur reads, tile-aligned
127    /// on its near edges and clamped to the target (see [`backdrop_region`]).
128    pub region: RectU16,
129    /// The blur's standard deviation in device space, already clamped to
130    /// [`MAX_BLUR_SIGMA`] and strictly positive.
131    pub sigma: f32,
132}
133
134impl Backdrop {
135    /// The Gaussian blur this backdrop runs, planned exactly as a filter
136    /// layer's own blur is (`vello_common`'s decimation plan, read through
137    /// [`blur::blur_passes`]).
138    #[must_use]
139    pub fn blur(&self) -> GaussianBlur {
140        GaussianBlur::new(self.sigma, SERVED_EDGE_MODE)
141    }
142}
143
144/// The device-space standard deviation a backdrop blur of `radius` runs at
145/// under `transform`, or `None` for one that blurs nothing.
146///
147/// Scaled exactly as a filter layer's σ is ([`device_blur_sigma`]) and clamped
148/// to [`MAX_BLUR_SIGMA`]: past it the region is the whole target anyway, and
149/// the clamp keeps the planner's decimation loop bounded. A radius at or
150/// below zero — or one that is not a number at all, which the compiler's
151/// up-front walk refuses before this is ever asked — blurs nothing.
152#[must_use]
153pub fn backdrop_sigma(radius: f64, transform: Affine) -> Option<f32> {
154    let sigma = device_blur_sigma(radius as f32, transform);
155    (sigma > 0.0).then(|| sigma.min(MAX_BLUR_SIGMA))
156}
157
158/// The device region a backdrop blur over `device` (the rectangle's device
159/// bounding box) at device σ `sigma` reads from a `target`-sized frame, or
160/// `None` when none of it lies on the target.
161///
162/// The bounding box grown by the blur's own reach (3σ, rounded up — the
163/// kernel's radius) so every pixel of the rectangle sees its whole kernel
164/// wherever the frame has content, snapped *outward* to the tile grid on its
165/// near edges and clamped to the target on all four. The near edges are
166/// tile-aligned because a strip instance starts on the tile grid and samples
167/// the blurred page from its own origin: a region starting inside a tile would
168/// leave a strip's first rows reading texels the page does not have.
169#[must_use]
170pub fn backdrop_region(device: kurbo::Rect, sigma: f32, target: (u16, u16)) -> Option<RectU16> {
171    let reach = f64::from((3.0 * sigma).ceil());
172    let grown = device.inflate(reach, reach);
173    let (width, height) = (f64::from(target.0), f64::from(target.1));
174    let x0 = grown.x0.floor().clamp(0.0, width);
175    let y0 = grown.y0.floor().clamp(0.0, height);
176    let x1 = grown.x1.ceil().clamp(0.0, width);
177    let y1 = grown.y1.ceil().clamp(0.0, height);
178    if !(x0 < x1 && y0 < y1) {
179        return None;
180    }
181    let tile_w = f64::from(vello_common::tile::Tile::WIDTH);
182    let tile_h = f64::from(vello_common::tile::Tile::HEIGHT);
183    let x0 = (x0 / tile_w).floor() * tile_w;
184    let y0 = (y0 / tile_h).floor() * tile_h;
185    Some(RectU16::new(x0 as u16, y0 as u16, x1 as u16, y1 as u16))
186}
187
188/// What [`served_filter`] says this engine renders when a recording's
189/// primitive is neither of its two, so a refusal names the whole served set
190/// rather than one filter's half of it.
191const SERVED_FILTERS: &str = "the only filters it renders are a single Gaussian blur or a single, \
192     shadow-only drop shadow (no original content composited back over it)";
193
194/// The largest standard deviation a blur layer is served at, in the layer's
195/// own space and in device space alike.
196///
197/// A blur expands its layer by 3σ on every side, snapped up to the tile grid
198/// and carried as a `u16`; a σ past this bound would wrap that padding instead
199/// of expanding the layer. It sits an order of magnitude above the largest page
200/// the scheduler will ever size (the default ceiling is 4096 texels per axis),
201/// so nothing a frame could have rendered is refused by it.
202///
203/// Both the recorded σ and the device-space σ the layer's transform scales it
204/// into are held to it (see [`device_blur_sigma`]); the second is the one that
205/// reaches `vello_common`'s planner, and the planner does not terminate on an
206/// infinite one.
207pub const MAX_BLUR_SIGMA: f32 = 4096.0;
208
209/// The one edge mode this engine's filter kernels implement.
210///
211/// Every kernel bounds its taps against the region it is filtering and reads
212/// transparent black outside it (`sample_region_bilinear` in
213/// `shaders/filters_blur.wgsl`, `drop_shadow_load_checked` in
214/// `shaders/filters_drop_shadow.wgsl`), which is exactly
215/// [`EdgeMode::None`] — the mode
216/// [`LayerFilter::filter_data`] records and the only one
217/// [`served_filter`] serves.
218pub const SERVED_EDGE_MODE: EdgeMode = EdgeMode::None;
219
220/// A filter the engine can apply to an isolated layer.
221///
222/// Engine-internal on purpose: this is the vocabulary
223/// [`push_filter_layer`] records with, not a `frust_scene` command.
224#[derive(Debug, Clone, Copy, PartialEq)]
225pub enum LayerFilter {
226    /// A Gaussian blur of standard deviation `sigma`, in the layer's own user
227    /// space — the transform in force when the layer is opened scales it into
228    /// device space.
229    Blur {
230        /// The blur's standard deviation. Zero blurs nothing.
231        sigma: f32,
232    },
233    /// A shadow-only drop shadow: the layer's alpha, blurred by `sigma`,
234    /// shifted by `offset`, and recoloured to `color` — never the layer's own
235    /// unfiltered content composited back over it (see [`drop_shadow`]'s own
236    /// doc for why).
237    DropShadow {
238        /// Horizontal, vertical offset of the shadow, in the layer's own user
239        /// space — the transform in force when the layer is opened scales it
240        /// into device space, exactly as `sigma` is.
241        offset: (f32, f32),
242        /// The shadow's blur standard deviation. Zero blurs nothing, leaving a
243        /// hard-edged, offset silhouette.
244        sigma: f32,
245        /// The shadow's own colour. Its alpha scales the shadow's own opacity;
246        /// the layer's colour never reaches the shadow.
247        color: AlphaColor<Srgb>,
248    },
249}
250
251impl LayerFilter {
252    /// The recorder-side description of this filter under `transform`.
253    ///
254    /// This is what carries the filter's *expansion* into the recording: a
255    /// blurred layer's tile-aligned bounds grow by 3σ on every side, because
256    /// the blur paints outside the contents that produced it, and the page the
257    /// layer is rendered into is sized from those grown bounds.
258    ///
259    /// # Errors
260    ///
261    /// [`EngineError::InvalidGeometry`] for a σ that is not finite, is
262    /// negative, or is past [`MAX_BLUR_SIGMA`] — in the layer's own user space
263    /// *or* in the device space `transform` scales it into (a
264    /// [`Self::DropShadow`]'s own offset held to the same finiteness) — and
265    /// [`EngineError::InvalidTransform`] for a transform that is not finite —
266    /// the same terms the compiler already refuses a draw's own geometry and
267    /// transform on, checked here because every quantity below is derived from
268    /// both.
269    pub fn filter_data(self, transform: Affine) -> Result<FilterData, EngineError> {
270        if !transform.as_coeffs().iter().all(|coeff| coeff.is_finite()) {
271            return Err(EngineError::InvalidTransform);
272        }
273
274        match self {
275            Self::Blur { sigma } => {
276                // Both spaces, through the same predicate the served gate
277                // takes: what the planner is handed is σ scaled by the
278                // transform, and only the recorded half of that is visible
279                // here without asking.
280                if !served_sigmas(sigma, transform) {
281                    return Err(EngineError::InvalidGeometry);
282                }
283
284                let filter = Filter::from_primitive(FilterPrimitive::GaussianBlur {
285                    std_deviation: sigma,
286                    edge_mode: SERVED_EDGE_MODE,
287                });
288
289                Ok(FilterData::new(filter, transform))
290            }
291            Self::DropShadow {
292                offset: (dx, dy),
293                sigma,
294                color,
295            } => {
296                if !served_sigmas(sigma, transform) || !dx.is_finite() || !dy.is_finite() {
297                    return Err(EngineError::InvalidGeometry);
298                }
299
300                // `DropShadowOnly`, never `DropShadow`: the shadow-only shape
301                // is the whole of what this engine serves (see this module's
302                // own doc and `served_filter` below).
303                let filter = Filter::from_primitive(FilterPrimitive::DropShadowOnly {
304                    dx,
305                    dy,
306                    std_deviation: sigma,
307                    color,
308                    edge_mode: SERVED_EDGE_MODE,
309                });
310
311                Ok(FilterData::new(filter, transform))
312            }
313        }
314    }
315}
316
317/// Opens a filtered layer on `recorder` — the engine-internal counterpart of
318/// [`CommandRecorder::push_layer`] for a layer that carries a filter.
319///
320/// Close it with [`CommandRecorder::pop_layer`], exactly like a regular layer.
321/// Popping is what computes the layer's bounds, and for a filtered layer those
322/// bounds are the *expanded* ones the filter paints into, which is what the
323/// scheduler sizes the layer's pages from.
324///
325/// # Errors
326///
327/// Whatever [`LayerFilter::filter_data`] refuses; nothing is recorded when it
328/// does, so a refused filter leaves the recording exactly as it was rather than
329/// opening a layer no `pop_layer` will balance.
330pub fn push_filter_layer<D>(
331    recorder: &mut CommandRecorder<D>,
332    props: LayerProps,
333    filter: LayerFilter,
334    transform: Affine,
335) -> Result<(), EngineError> {
336    let filter_data = filter.filter_data(transform)?;
337    recorder.push_layer(props, Some(filter_data));
338    Ok(())
339}
340
341/// The device-space standard deviation a blur of `sigma` runs at under
342/// `transform`.
343///
344/// The quantity `vello_common`'s own `transform_blur_params` computes and hands
345/// its planner — recomputed here, from the same `extract_scales` singular-value
346/// pair averaged the same way, because upstream keeps that function
347/// `pub(crate)` and because the whole point of the bound it feeds is to be
348/// taken *before* the value reaches the planner. Keep it in step with
349/// `vello_common::filter::gaussian_blur::transform_blur_params`.
350#[must_use]
351pub fn device_blur_sigma(sigma: f32, transform: Affine) -> f32 {
352    let (scale_x, scale_y) = extract_scales(&transform);
353    let uniform_scale = (scale_x + scale_y) / 2.0;
354    sigma * uniform_scale
355}
356
357/// Whether `sigma` is a standard deviation this engine blurs at both as
358/// recorded and as `transform` scales it into device space.
359///
360/// A range check rather than a chain of comparisons, which also settles the
361/// non-finite cases: neither a NaN nor an infinity is contained by it. Both
362/// ends matter — the recorded σ is what the layer's bounds are expanded by, and
363/// the device-space σ is what `vello_common`'s `plan_decimated_blur` halves
364/// down (`while remaining_variance > 4.0 { (v - 1.5) * 0.25 }`), a loop an
365/// infinite variance never leaves.
366#[must_use]
367fn served_sigmas(sigma: f32, transform: Affine) -> bool {
368    let served = 0.0..=MAX_BLUR_SIGMA;
369
370    served.contains(&sigma) && served.contains(&device_blur_sigma(sigma, transform))
371}
372
373/// One of the two filters this engine renders, read back off a recording.
374///
375/// The shared answer of [`served_filter`], so the three sites that have to know
376/// which filter a layer carries — [`crate::schedule`]'s `layer_role` and
377/// `filter_rounds`, and [`crate::renderer`]'s `filter_block` — dispatch on one
378/// decision rather than each re-deriving it from a try-blur-then-drop-shadow
379/// pair of their own.
380#[derive(Debug)]
381pub enum ServedFilter {
382    /// A Gaussian blur, prepared into device space.
383    Blur(GaussianBlur),
384    /// A shadow-only drop shadow, prepared into device space.
385    DropShadow(DropShadow),
386}
387
388/// The filter layer `id`'s recorded `kind` describes, or a reason naming what
389/// was found instead.
390///
391/// The reason is prose rather than an error variant because it reaches a log
392/// through [`EngineError::SchedulerEscalation`], where a caller reading it is
393/// trying to find out which recorded shape froze a surface. It names the
394/// primitive's *own* fault — an unserved edge mode, an unserved σ — rather than
395/// the fault of whichever filter was tried second, which is what a pair of
396/// independent gates tried in sequence can only report.
397///
398/// # Errors
399///
400/// A `kind` that is not a filter at all; a filter under a transform that is not
401/// finite; a filter graph of anything but one Gaussian blur or one shadow-only
402/// drop shadow (a `DropShadow` that composites its original content back over
403/// the shadow included — this engine serves only `DropShadowOnly`, see this
404/// module's own doc); an edge mode other than [`SERVED_EDGE_MODE`]; a σ outside
405/// [`MAX_BLUR_SIGMA`] as recorded or as the transform scales it; or a primitive
406/// whose prepared kernel is not the one its own tag named.
407pub fn served_filter(id: u32, kind: &RecordedLayerKind) -> Result<ServedFilter, String> {
408    let RecordedLayerKind::Filter { filter_data, .. } = kind else {
409        return Err(format!("layer {id} carries no filter"));
410    };
411
412    // Checked before preparing rather than after: every quantity the reference
413    // derives below is derived from the transform as well as from the
414    // primitive's own parameters, and a recording is not trusted to hold only
415    // the shapes the engine's own compiler records (E17).
416    let transform = filter_data.transform;
417    if !transform.as_coeffs().iter().all(|coeff| coeff.is_finite()) {
418        return Err(format!(
419            "layer {id} carries a filter under a non-finite transform ({:?}), which is what \
420             scales its σ into the device space the blur is planned in",
421            transform.as_coeffs()
422        ));
423    }
424
425    // Checked before preparing rather than after for the second reason too:
426    // `PreparedFilter::new` panics on a graph the reference has not
427    // implemented.
428    let primitives = filter_data.filter.graph.primitives.as_slice();
429    match primitives {
430        [
431            FilterPrimitive::GaussianBlur {
432                std_deviation,
433                edge_mode,
434            },
435        ] => {
436            served_edge_mode(id, "a Gaussian blur", *edge_mode)?;
437            served_sigma(id, "a Gaussian blur", *std_deviation, transform)?;
438
439            match PreparedFilter::new(&filter_data.filter, &transform) {
440                PreparedFilter::GaussianBlur(blur) => Ok(ServedFilter::Blur(blur)),
441                _ => Err(format!(
442                    "layer {id} carries a Gaussian blur that prepared as another filter"
443                )),
444            }
445        }
446        [
447            FilterPrimitive::DropShadowOnly {
448                std_deviation,
449                edge_mode,
450                ..
451            },
452        ] => {
453            served_edge_mode(id, "a drop shadow", *edge_mode)?;
454            served_sigma(id, "a drop shadow", *std_deviation, transform)?;
455
456            match PreparedFilter::new(&filter_data.filter, &transform) {
457                PreparedFilter::DropShadow(shadow) => Ok(ServedFilter::DropShadow(shadow)),
458                _ => Err(format!(
459                    "layer {id} carries a drop shadow that prepared as another filter"
460                )),
461            }
462        }
463        _ => Err(format!(
464            "layer {id} carries a filter graph of {} primitive(s) the engine serves as neither of \
465             its two filters; {SERVED_FILTERS}",
466            primitives.len()
467        )),
468    }
469}
470
471/// Refuses an edge mode this engine's kernels do not implement, by name.
472///
473/// Only [`SERVED_EDGE_MODE`] is implemented: `Wrap` and `Mirror` would have to
474/// address the region's opposite or reflected texels, and `Duplicate` would
475/// have to replicate its edge ones, while every kernel here reads transparent
476/// black outside the region unconditionally. Serving a recording that asks for
477/// one of the other three would render it as `None` and call it that filter,
478/// which is the silent-wrong-pixels answer this tier does not give.
479fn served_edge_mode(id: u32, filter: &str, edge_mode: EdgeMode) -> Result<(), String> {
480    if edge_mode == SERVED_EDGE_MODE {
481        return Ok(());
482    }
483
484    Err(format!(
485        "layer {id} carries {filter} with edge mode {edge_mode:?}, which this engine does not \
486         render; its filter kernels read transparent black outside the region they filter, which \
487         is {SERVED_EDGE_MODE:?}, and no other mode"
488    ))
489}
490
491/// Refuses a σ this engine does not blur at, by name, in both spaces.
492fn served_sigma(id: u32, filter: &str, sigma: f32, transform: Affine) -> Result<(), String> {
493    if served_sigmas(sigma, transform) {
494        return Ok(());
495    }
496
497    let device = device_blur_sigma(sigma, transform);
498    Err(format!(
499        "layer {id} carries {filter} of σ {sigma}, which its own transform scales to a \
500         device-space σ of {device}; this engine blurs at a standard deviation of 0 to \
501         {MAX_BLUR_SIGMA} in both spaces"
502    ))
503}
504
505/// The blur that layer `id`'s recorded filter describes, or a reason naming
506/// what was found instead.
507///
508/// [`served_filter`] narrowed to its blur arm, for a caller that has no answer
509/// for a drop shadow.
510///
511/// # Errors
512///
513/// Whatever [`served_filter`] refuses, and a layer that carries the
514/// shadow-only drop shadow instead.
515pub fn served_blur(id: u32, kind: &RecordedLayerKind) -> Result<GaussianBlur, String> {
516    match served_filter(id, kind)? {
517        ServedFilter::Blur(blur) => Ok(blur),
518        ServedFilter::DropShadow(_) => Err(format!(
519            "layer {id} carries a shadow-only drop shadow rather than a Gaussian blur"
520        )),
521    }
522}
523
524/// The drop shadow that layer `id`'s recorded filter describes, or a reason
525/// naming what was found instead.
526///
527/// [`served_filter`] narrowed to its drop-shadow arm, the counterpart of
528/// [`served_blur`].
529///
530/// # Errors
531///
532/// Whatever [`served_filter`] refuses, and a layer that carries the Gaussian
533/// blur instead.
534pub fn served_drop_shadow(id: u32, kind: &RecordedLayerKind) -> Result<DropShadow, String> {
535    match served_filter(id, kind)? {
536        ServedFilter::DropShadow(shadow) => Ok(shadow),
537        ServedFilter::Blur(_) => Err(format!(
538            "layer {id} carries a Gaussian blur rather than a shadow-only drop shadow"
539        )),
540    }
541}
542
543/// One pass of a filter's sequence.
544///
545/// The numbering is the wire format the fragment stage switches on; it matches
546/// the reference renderer's own — flood (1) and the drop-shadow composite that
547/// reads the layer's own unfiltered content back (7) are still reserved and
548/// still unserved (see [`drop_shadow`]'s own doc), and can be added later
549/// without renumbering.
550#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
551pub enum FilterPassKind {
552    /// Copy the source region through unchanged.
553    Copy,
554    /// Shift the source region by a drop shadow's own device-space offset.
555    Offset,
556    /// Halve both axes.
557    Downscale,
558    /// Convolve horizontally with the blur kernel.
559    BlurH,
560    /// Convolve vertically with the blur kernel.
561    BlurV,
562    /// Double both axes.
563    Upscale,
564    /// Recolour a blurred, offset alpha mask into a drop shadow's own
565    /// premultiplied colour.
566    Colorize,
567}
568
569impl FilterPassKind {
570    /// The value `filter_pass_kind` carries into the fragment stage.
571    #[must_use]
572    pub const fn code(self) -> u32 {
573        match self {
574            Self::Copy => 0,
575            Self::Offset => 2,
576            Self::Downscale => 3,
577            Self::BlurH => 4,
578            Self::BlurV => 5,
579            Self::Upscale => 6,
580            Self::Colorize => 8,
581        }
582    }
583}
584
585/// One pass of a filter's sequence, with the extents it reads and writes.
586///
587/// A pass reads the page the pass before it wrote and writes the other one, so
588/// `source` is the previous step's `dest`; the two differ only for the
589/// rescaling passes.
590#[derive(Debug, Clone, Copy, PartialEq, Eq)]
591pub struct FilterStep {
592    /// Which pass this is.
593    pub kind: FilterPassKind,
594    /// Extent of the region read, at the source page's origin.
595    pub source: SizeU16,
596    /// Extent of the region written, at the destination page's origin.
597    pub dest: SizeU16,
598}