frust-engine 0.6.2

Sparse-strip GPU render pipeline for Frust: compiles a scene display list into strips and records the draw passes on wgpu.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
//! Layer filters: a layer rendered in isolation, run through a sequence of
//! filter passes, and composited from the page the last of them wrote.
//!
//! An opacity layer needs one page and one extra pass ([`schedule`]); a
//! filtered layer needs a *second* page as well, because a filter pass reads a
//! whole image and writes a whole image and a render pass cannot do both to one
//! texture. The two pages the scheduler already ping-pongs between are exactly
//! that pair: the layer's contents land in one, each pass writes the one it did
//! not read, and the sequence is arranged so the result ends back in the page
//! the parent's composite samples.
//!
//! [`schedule`]: crate::schedule
//!
//! ## What is served
//!
//! Two filters: [`LayerFilter::Blur`], the Gaussian blur that the theme
//! layer's `GlassMaterial` backdrop needs, and [`LayerFilter::DropShadow`], a
//! shadow-only drop shadow (`vello_common`'s `DropShadowOnly`, never
//! compositing the layer's own unfiltered content back over the shadow — see
//! [`drop_shadow`]'s own doc for why). Every other filter a recording can
//! carry — a flood, a standalone offset, a drop shadow that composites the
//! original back over itself, an edge mode other than [`SERVED_EDGE_MODE`], or
//! a graph of more than one primitive — is refused by [`served_filter`] with a
//! reason naming what was found, and the frame is skipped rather than rendered
//! without its filter. That refusal is not a placeholder for a fallback: the
//! engine tier carries no second renderer (see the [`schedule`] module header).
//!
//! The scene seam for a *layer* filter is deliberately absent. `frust_scene`
//! has no layer-filter command and gains none here — [`push_filter_layer`] is
//! how the engine's own compiler will open a filtered layer once a later plan
//! gives the display list something to lower from.
//!
//! ## The backdrop blur
//!
//! One filter *does* reach the display list: `frust_scene::Command::BackdropBlur`,
//! a Gaussian blur of whatever the frame has already painted beneath a
//! rectangle (a frosted-glass panel). It is not a filter layer — there are no
//! contents of its own to isolate — but it runs the very same pass sequence,
//! on a page seeded with a region of the frame's own target rather than with
//! rendered contents. [`Backdrop`] is the compiler's record of one, and the
//! three halves split like a filter layer's:
//!
//! - the compiler ([`crate::compile`]) records the backdrop as a layer holding
//!   one draw — its rectangle's coverage under whatever clip is open — and
//!   lists it in `CompiledFrame::backdrops`, with the device [`region`
//!   ](backdrop_region) the blur reads and the device-space σ;
//! - the scheduler ([`crate::schedule`]) cuts the frame's surface round at it,
//!   so everything painted before it has landed, then emits a *region copy*
//!   round ([`crate::schedule::RegionCopy`]) seeding a pooled page with that
//!   region of the frame's target, the blur's filter rounds over the page, and
//!   leaves the result held for the surface round that draws the rectangle;
//! - the renderer draws the rectangle's own strip coverage sampling the
//!   blurred page at each pixel's own position, so a rounded or path clip
//!   masks the blurred region exactly as it masks a fill.
//!
//! The region copy is the reusable half: "the frame's target, read back into a
//! page a later pass samples" is what any effect reading the backdrop needs
//! (a blend mode composite is the next one), so it is a round kind of the
//! scheduler and a helper of the renderer rather than a special case of the
//! blur. It needs the frame's target to be readable — `COPY_SRC` in the
//! engine's own [`INTERMEDIATE_FORMAT`](crate::gpu::pipelines::INTERMEDIATE_FORMAT),
//! or `TEXTURE_BINDING` in any format — which a swapchain is not:
//! `frust-render` renders a frame carrying a backdrop into an intermediate of
//! its own and presents it with one copy pass. A target that is neither draws
//! every backdrop as nothing (logged once), the "draw less, never wrong"
//! answer.
//!
//! Bounded at [`MAX_BACKDROPS_PER_FRAME`]: each costs a surface cut, a copy and
//! a blur's whole sequence, and a frame recording more is refused with
//! [`EngineError::TooManyBackdrops`].
//!
//! ## What is pure, and what is not
//!
//! Everything here is a decision over plain values, in the crate's usual split:
//! which passes a blur costs and at what extents ([`blur::blur_passes`]), what
//! the fragment stage reads out of the filter-data texture
//! ([`blur::GpuGaussianBlur`]), and how one pass's instance is packed
//! ([`blur::FilterInstanceData`]). No device is touched and no texture is
//! allocated here.
//!
//! The three device-side halves live where every other engine pipeline's do:
//! the WGSL program is assembled by [`crate::gpu::shader_src::FILTER`], the
//! pipeline it is drawn through is
//! [`crate::gpu::pipelines::EnginePipeline::Filter`], and the texture holding
//! the frame's parameter blocks plus the bilinear sampler the kernels read
//! through are [`crate::gpu::targets`]'. The renderer binds the pages the
//! scheduler named and issues one instanced quad per pass
//! ([`crate::renderer::FilterResources`]).

pub mod blur;
pub mod drop_shadow;

use kurbo::Affine;
use peniko::color::{AlphaColor, Srgb};
use vello_common::filter::drop_shadow::DropShadow;
use vello_common::filter::gaussian_blur::GaussianBlur;
use vello_common::filter::{FilterData, PreparedFilter};
use vello_common::filter_effects::{EdgeMode, Filter, FilterPrimitive};
use vello_common::geometry::{RectU16, SizeU16};
use vello_common::record::{CommandRecorder, LayerProps, RecordedLayerKind};
use vello_common::util::extract_scales;

use crate::error::EngineError;

/// The most `Command::BackdropBlur`s one frame is served with; a frame
/// recording more is refused with [`EngineError::TooManyBackdrops`].
///
/// Four covers the glass surfaces a screen layers at once (an app bar, a tab
/// bar, a sheet and a dialog over it) with room to spare, while keeping a
/// frame's worst case to four surface cuts, four region copies and four blur
/// sequences.
pub const MAX_BACKDROPS_PER_FRAME: usize = 4;

/// One `Command::BackdropBlur` the compiler recorded, as the scheduler and the
/// renderer read it (see this module's *The backdrop blur*).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Backdrop {
    /// The recorded layer standing for the backdrop at its painter-order
    /// position, indexed into
    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
    /// It holds exactly one draw: the rectangle's coverage.
    pub layer: u32,
    /// The painter-order depth of that one draw — how the renderer tells the
    /// backdrop's draw apart from every other draw of a surface round.
    pub depth: u32,
    /// The device region of the frame's target the blur reads, tile-aligned
    /// on its near edges and clamped to the target (see [`backdrop_region`]).
    pub region: RectU16,
    /// The blur's standard deviation in device space, already clamped to
    /// [`MAX_BLUR_SIGMA`] and strictly positive.
    pub sigma: f32,
}

impl Backdrop {
    /// The Gaussian blur this backdrop runs, planned exactly as a filter
    /// layer's own blur is (`vello_common`'s decimation plan, read through
    /// [`blur::blur_passes`]).
    #[must_use]
    pub fn blur(&self) -> GaussianBlur {
        GaussianBlur::new(self.sigma, SERVED_EDGE_MODE)
    }
}

/// The device-space standard deviation a backdrop blur of `radius` runs at
/// under `transform`, or `None` for one that blurs nothing.
///
/// Scaled exactly as a filter layer's σ is ([`device_blur_sigma`]) and clamped
/// to [`MAX_BLUR_SIGMA`]: past it the region is the whole target anyway, and
/// the clamp keeps the planner's decimation loop bounded. A radius at or
/// below zero — or one that is not a number at all, which the compiler's
/// up-front walk refuses before this is ever asked — blurs nothing.
#[must_use]
pub fn backdrop_sigma(radius: f64, transform: Affine) -> Option<f32> {
    let sigma = device_blur_sigma(radius as f32, transform);
    (sigma > 0.0).then(|| sigma.min(MAX_BLUR_SIGMA))
}

/// The device region a backdrop blur over `device` (the rectangle's device
/// bounding box) at device σ `sigma` reads from a `target`-sized frame, or
/// `None` when none of it lies on the target.
///
/// The bounding box grown by the blur's own reach (3σ, rounded up — the
/// kernel's radius) so every pixel of the rectangle sees its whole kernel
/// wherever the frame has content, snapped *outward* to the tile grid on its
/// near edges and clamped to the target on all four. The near edges are
/// tile-aligned because a strip instance starts on the tile grid and samples
/// the blurred page from its own origin: a region starting inside a tile would
/// leave a strip's first rows reading texels the page does not have.
#[must_use]
pub fn backdrop_region(device: kurbo::Rect, sigma: f32, target: (u16, u16)) -> Option<RectU16> {
    let reach = f64::from((3.0 * sigma).ceil());
    let grown = device.inflate(reach, reach);
    let (width, height) = (f64::from(target.0), f64::from(target.1));
    let x0 = grown.x0.floor().clamp(0.0, width);
    let y0 = grown.y0.floor().clamp(0.0, height);
    let x1 = grown.x1.ceil().clamp(0.0, width);
    let y1 = grown.y1.ceil().clamp(0.0, height);
    if !(x0 < x1 && y0 < y1) {
        return None;
    }
    let tile_w = f64::from(vello_common::tile::Tile::WIDTH);
    let tile_h = f64::from(vello_common::tile::Tile::HEIGHT);
    let x0 = (x0 / tile_w).floor() * tile_w;
    let y0 = (y0 / tile_h).floor() * tile_h;
    Some(RectU16::new(x0 as u16, y0 as u16, x1 as u16, y1 as u16))
}

/// What [`served_filter`] says this engine renders when a recording's
/// primitive is neither of its two, so a refusal names the whole served set
/// rather than one filter's half of it.
const SERVED_FILTERS: &str = "the only filters it renders are a single Gaussian blur or a single, \
     shadow-only drop shadow (no original content composited back over it)";

/// The largest standard deviation a blur layer is served at, in the layer's
/// own space and in device space alike.
///
/// A blur expands its layer by 3σ on every side, snapped up to the tile grid
/// and carried as a `u16`; a σ past this bound would wrap that padding instead
/// of expanding the layer. It sits an order of magnitude above the largest page
/// the scheduler will ever size (the default ceiling is 4096 texels per axis),
/// so nothing a frame could have rendered is refused by it.
///
/// Both the recorded σ and the device-space σ the layer's transform scales it
/// into are held to it (see [`device_blur_sigma`]); the second is the one that
/// reaches `vello_common`'s planner, and the planner does not terminate on an
/// infinite one.
pub const MAX_BLUR_SIGMA: f32 = 4096.0;

/// The one edge mode this engine's filter kernels implement.
///
/// Every kernel bounds its taps against the region it is filtering and reads
/// transparent black outside it (`sample_region_bilinear` in
/// `shaders/filters_blur.wgsl`, `drop_shadow_load_checked` in
/// `shaders/filters_drop_shadow.wgsl`), which is exactly
/// [`EdgeMode::None`] — the mode
/// [`LayerFilter::filter_data`] records and the only one
/// [`served_filter`] serves.
pub const SERVED_EDGE_MODE: EdgeMode = EdgeMode::None;

/// A filter the engine can apply to an isolated layer.
///
/// Engine-internal on purpose: this is the vocabulary
/// [`push_filter_layer`] records with, not a `frust_scene` command.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LayerFilter {
    /// A Gaussian blur of standard deviation `sigma`, in the layer's own user
    /// space — the transform in force when the layer is opened scales it into
    /// device space.
    Blur {
        /// The blur's standard deviation. Zero blurs nothing.
        sigma: f32,
    },
    /// A shadow-only drop shadow: the layer's alpha, blurred by `sigma`,
    /// shifted by `offset`, and recoloured to `color` — never the layer's own
    /// unfiltered content composited back over it (see [`drop_shadow`]'s own
    /// doc for why).
    DropShadow {
        /// Horizontal, vertical offset of the shadow, in the layer's own user
        /// space — the transform in force when the layer is opened scales it
        /// into device space, exactly as `sigma` is.
        offset: (f32, f32),
        /// The shadow's blur standard deviation. Zero blurs nothing, leaving a
        /// hard-edged, offset silhouette.
        sigma: f32,
        /// The shadow's own colour. Its alpha scales the shadow's own opacity;
        /// the layer's colour never reaches the shadow.
        color: AlphaColor<Srgb>,
    },
}

impl LayerFilter {
    /// The recorder-side description of this filter under `transform`.
    ///
    /// This is what carries the filter's *expansion* into the recording: a
    /// blurred layer's tile-aligned bounds grow by 3σ on every side, because
    /// the blur paints outside the contents that produced it, and the page the
    /// layer is rendered into is sized from those grown bounds.
    ///
    /// # Errors
    ///
    /// [`EngineError::InvalidGeometry`] for a σ that is not finite, is
    /// negative, or is past [`MAX_BLUR_SIGMA`] — in the layer's own user space
    /// *or* in the device space `transform` scales it into (a
    /// [`Self::DropShadow`]'s own offset held to the same finiteness) — and
    /// [`EngineError::InvalidTransform`] for a transform that is not finite —
    /// the same terms the compiler already refuses a draw's own geometry and
    /// transform on, checked here because every quantity below is derived from
    /// both.
    pub fn filter_data(self, transform: Affine) -> Result<FilterData, EngineError> {
        if !transform.as_coeffs().iter().all(|coeff| coeff.is_finite()) {
            return Err(EngineError::InvalidTransform);
        }

        match self {
            Self::Blur { sigma } => {
                // Both spaces, through the same predicate the served gate
                // takes: what the planner is handed is σ scaled by the
                // transform, and only the recorded half of that is visible
                // here without asking.
                if !served_sigmas(sigma, transform) {
                    return Err(EngineError::InvalidGeometry);
                }

                let filter = Filter::from_primitive(FilterPrimitive::GaussianBlur {
                    std_deviation: sigma,
                    edge_mode: SERVED_EDGE_MODE,
                });

                Ok(FilterData::new(filter, transform))
            }
            Self::DropShadow {
                offset: (dx, dy),
                sigma,
                color,
            } => {
                if !served_sigmas(sigma, transform) || !dx.is_finite() || !dy.is_finite() {
                    return Err(EngineError::InvalidGeometry);
                }

                // `DropShadowOnly`, never `DropShadow`: the shadow-only shape
                // is the whole of what this engine serves (see this module's
                // own doc and `served_filter` below).
                let filter = Filter::from_primitive(FilterPrimitive::DropShadowOnly {
                    dx,
                    dy,
                    std_deviation: sigma,
                    color,
                    edge_mode: SERVED_EDGE_MODE,
                });

                Ok(FilterData::new(filter, transform))
            }
        }
    }
}

/// Opens a filtered layer on `recorder` — the engine-internal counterpart of
/// [`CommandRecorder::push_layer`] for a layer that carries a filter.
///
/// Close it with [`CommandRecorder::pop_layer`], exactly like a regular layer.
/// Popping is what computes the layer's bounds, and for a filtered layer those
/// bounds are the *expanded* ones the filter paints into, which is what the
/// scheduler sizes the layer's pages from.
///
/// # Errors
///
/// Whatever [`LayerFilter::filter_data`] refuses; nothing is recorded when it
/// does, so a refused filter leaves the recording exactly as it was rather than
/// opening a layer no `pop_layer` will balance.
pub fn push_filter_layer<D>(
    recorder: &mut CommandRecorder<D>,
    props: LayerProps,
    filter: LayerFilter,
    transform: Affine,
) -> Result<(), EngineError> {
    let filter_data = filter.filter_data(transform)?;
    recorder.push_layer(props, Some(filter_data));
    Ok(())
}

/// The device-space standard deviation a blur of `sigma` runs at under
/// `transform`.
///
/// The quantity `vello_common`'s own `transform_blur_params` computes and hands
/// its planner — recomputed here, from the same `extract_scales` singular-value
/// pair averaged the same way, because upstream keeps that function
/// `pub(crate)` and because the whole point of the bound it feeds is to be
/// taken *before* the value reaches the planner. Keep it in step with
/// `vello_common::filter::gaussian_blur::transform_blur_params`.
#[must_use]
pub fn device_blur_sigma(sigma: f32, transform: Affine) -> f32 {
    let (scale_x, scale_y) = extract_scales(&transform);
    let uniform_scale = (scale_x + scale_y) / 2.0;
    sigma * uniform_scale
}

/// Whether `sigma` is a standard deviation this engine blurs at both as
/// recorded and as `transform` scales it into device space.
///
/// A range check rather than a chain of comparisons, which also settles the
/// non-finite cases: neither a NaN nor an infinity is contained by it. Both
/// ends matter — the recorded σ is what the layer's bounds are expanded by, and
/// the device-space σ is what `vello_common`'s `plan_decimated_blur` halves
/// down (`while remaining_variance > 4.0 { (v - 1.5) * 0.25 }`), a loop an
/// infinite variance never leaves.
#[must_use]
fn served_sigmas(sigma: f32, transform: Affine) -> bool {
    let served = 0.0..=MAX_BLUR_SIGMA;

    served.contains(&sigma) && served.contains(&device_blur_sigma(sigma, transform))
}

/// One of the two filters this engine renders, read back off a recording.
///
/// The shared answer of [`served_filter`], so the three sites that have to know
/// which filter a layer carries — [`crate::schedule`]'s `layer_role` and
/// `filter_rounds`, and [`crate::renderer`]'s `filter_block` — dispatch on one
/// decision rather than each re-deriving it from a try-blur-then-drop-shadow
/// pair of their own.
#[derive(Debug)]
pub enum ServedFilter {
    /// A Gaussian blur, prepared into device space.
    Blur(GaussianBlur),
    /// A shadow-only drop shadow, prepared into device space.
    DropShadow(DropShadow),
}

/// The filter layer `id`'s recorded `kind` describes, or a reason naming what
/// was found instead.
///
/// The reason is prose rather than an error variant because it reaches a log
/// through [`EngineError::SchedulerEscalation`], where a caller reading it is
/// trying to find out which recorded shape froze a surface. It names the
/// primitive's *own* fault — an unserved edge mode, an unserved σ — rather than
/// the fault of whichever filter was tried second, which is what a pair of
/// independent gates tried in sequence can only report.
///
/// # Errors
///
/// A `kind` that is not a filter at all; a filter under a transform that is not
/// finite; a filter graph of anything but one Gaussian blur or one shadow-only
/// drop shadow (a `DropShadow` that composites its original content back over
/// the shadow included — this engine serves only `DropShadowOnly`, see this
/// module's own doc); an edge mode other than [`SERVED_EDGE_MODE`]; a σ outside
/// [`MAX_BLUR_SIGMA`] as recorded or as the transform scales it; or a primitive
/// whose prepared kernel is not the one its own tag named.
pub fn served_filter(id: u32, kind: &RecordedLayerKind) -> Result<ServedFilter, String> {
    let RecordedLayerKind::Filter { filter_data, .. } = kind else {
        return Err(format!("layer {id} carries no filter"));
    };

    // Checked before preparing rather than after: every quantity the reference
    // derives below is derived from the transform as well as from the
    // primitive's own parameters, and a recording is not trusted to hold only
    // the shapes the engine's own compiler records (E17).
    let transform = filter_data.transform;
    if !transform.as_coeffs().iter().all(|coeff| coeff.is_finite()) {
        return Err(format!(
            "layer {id} carries a filter under a non-finite transform ({:?}), which is what \
             scales its σ into the device space the blur is planned in",
            transform.as_coeffs()
        ));
    }

    // Checked before preparing rather than after for the second reason too:
    // `PreparedFilter::new` panics on a graph the reference has not
    // implemented.
    let primitives = filter_data.filter.graph.primitives.as_slice();
    match primitives {
        [
            FilterPrimitive::GaussianBlur {
                std_deviation,
                edge_mode,
            },
        ] => {
            served_edge_mode(id, "a Gaussian blur", *edge_mode)?;
            served_sigma(id, "a Gaussian blur", *std_deviation, transform)?;

            match PreparedFilter::new(&filter_data.filter, &transform) {
                PreparedFilter::GaussianBlur(blur) => Ok(ServedFilter::Blur(blur)),
                _ => Err(format!(
                    "layer {id} carries a Gaussian blur that prepared as another filter"
                )),
            }
        }
        [
            FilterPrimitive::DropShadowOnly {
                std_deviation,
                edge_mode,
                ..
            },
        ] => {
            served_edge_mode(id, "a drop shadow", *edge_mode)?;
            served_sigma(id, "a drop shadow", *std_deviation, transform)?;

            match PreparedFilter::new(&filter_data.filter, &transform) {
                PreparedFilter::DropShadow(shadow) => Ok(ServedFilter::DropShadow(shadow)),
                _ => Err(format!(
                    "layer {id} carries a drop shadow that prepared as another filter"
                )),
            }
        }
        _ => Err(format!(
            "layer {id} carries a filter graph of {} primitive(s) the engine serves as neither of \
             its two filters; {SERVED_FILTERS}",
            primitives.len()
        )),
    }
}

/// Refuses an edge mode this engine's kernels do not implement, by name.
///
/// Only [`SERVED_EDGE_MODE`] is implemented: `Wrap` and `Mirror` would have to
/// address the region's opposite or reflected texels, and `Duplicate` would
/// have to replicate its edge ones, while every kernel here reads transparent
/// black outside the region unconditionally. Serving a recording that asks for
/// one of the other three would render it as `None` and call it that filter,
/// which is the silent-wrong-pixels answer this tier does not give.
fn served_edge_mode(id: u32, filter: &str, edge_mode: EdgeMode) -> Result<(), String> {
    if edge_mode == SERVED_EDGE_MODE {
        return Ok(());
    }

    Err(format!(
        "layer {id} carries {filter} with edge mode {edge_mode:?}, which this engine does not \
         render; its filter kernels read transparent black outside the region they filter, which \
         is {SERVED_EDGE_MODE:?}, and no other mode"
    ))
}

/// Refuses a σ this engine does not blur at, by name, in both spaces.
fn served_sigma(id: u32, filter: &str, sigma: f32, transform: Affine) -> Result<(), String> {
    if served_sigmas(sigma, transform) {
        return Ok(());
    }

    let device = device_blur_sigma(sigma, transform);
    Err(format!(
        "layer {id} carries {filter} of σ {sigma}, which its own transform scales to a \
         device-space σ of {device}; this engine blurs at a standard deviation of 0 to \
         {MAX_BLUR_SIGMA} in both spaces"
    ))
}

/// The blur that layer `id`'s recorded filter describes, or a reason naming
/// what was found instead.
///
/// [`served_filter`] narrowed to its blur arm, for a caller that has no answer
/// for a drop shadow.
///
/// # Errors
///
/// Whatever [`served_filter`] refuses, and a layer that carries the
/// shadow-only drop shadow instead.
pub fn served_blur(id: u32, kind: &RecordedLayerKind) -> Result<GaussianBlur, String> {
    match served_filter(id, kind)? {
        ServedFilter::Blur(blur) => Ok(blur),
        ServedFilter::DropShadow(_) => Err(format!(
            "layer {id} carries a shadow-only drop shadow rather than a Gaussian blur"
        )),
    }
}

/// The drop shadow that layer `id`'s recorded filter describes, or a reason
/// naming what was found instead.
///
/// [`served_filter`] narrowed to its drop-shadow arm, the counterpart of
/// [`served_blur`].
///
/// # Errors
///
/// Whatever [`served_filter`] refuses, and a layer that carries the Gaussian
/// blur instead.
pub fn served_drop_shadow(id: u32, kind: &RecordedLayerKind) -> Result<DropShadow, String> {
    match served_filter(id, kind)? {
        ServedFilter::DropShadow(shadow) => Ok(shadow),
        ServedFilter::Blur(_) => Err(format!(
            "layer {id} carries a Gaussian blur rather than a shadow-only drop shadow"
        )),
    }
}

/// One pass of a filter's sequence.
///
/// The numbering is the wire format the fragment stage switches on; it matches
/// the reference renderer's own — flood (1) and the drop-shadow composite that
/// reads the layer's own unfiltered content back (7) are still reserved and
/// still unserved (see [`drop_shadow`]'s own doc), and can be added later
/// without renumbering.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FilterPassKind {
    /// Copy the source region through unchanged.
    Copy,
    /// Shift the source region by a drop shadow's own device-space offset.
    Offset,
    /// Halve both axes.
    Downscale,
    /// Convolve horizontally with the blur kernel.
    BlurH,
    /// Convolve vertically with the blur kernel.
    BlurV,
    /// Double both axes.
    Upscale,
    /// Recolour a blurred, offset alpha mask into a drop shadow's own
    /// premultiplied colour.
    Colorize,
}

impl FilterPassKind {
    /// The value `filter_pass_kind` carries into the fragment stage.
    #[must_use]
    pub const fn code(self) -> u32 {
        match self {
            Self::Copy => 0,
            Self::Offset => 2,
            Self::Downscale => 3,
            Self::BlurH => 4,
            Self::BlurV => 5,
            Self::Upscale => 6,
            Self::Colorize => 8,
        }
    }
}

/// One pass of a filter's sequence, with the extents it reads and writes.
///
/// A pass reads the page the pass before it wrote and writes the other one, so
/// `source` is the previous step's `dest`; the two differ only for the
/// rescaling passes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FilterStep {
    /// Which pass this is.
    pub kind: FilterPassKind,
    /// Extent of the region read, at the source page's origin.
    pub source: SizeU16,
    /// Extent of the region written, at the destination page's origin.
    pub dest: SizeU16,
}