Skip to main content

alpha2_render/
composite.rs

1use crate::{
2    BlendMode, Color, GraphError, GraphLayer, RenderError, RenderGraph, RenderSize,
3    RenderedTexture, Renderer, SourceView,
4    pipeline::CachedCompositePipeline,
5    uniform::{BATCH_UNIFORM_FLOATS, UniformArenaLease, f32_bytes, float_uniform_bytes},
6};
7use alpha2_shaders::COMPOSITE_WGSL;
8
9/// The most layers one composite pass takes (`BATCH_LAYERS` in
10/// composite.wesl). Each has a texture binding of its own, and with the
11/// backdrop's that stays inside the 16 sampled textures a WebGL2-class
12/// device offers a shader stage.
13pub(crate) const COMPOSITE_BATCH_LAYERS: usize = 8;
14
15/// Placement and opacity of one composited layer.
16#[derive(Copy, Clone, Debug, PartialEq)]
17pub struct LayerTransform {
18    /// Normalized output translation, as `[x, y]`.
19    pub position: [f32; 2],
20    /// Normalized source anchor, as `[x, y]`.
21    pub anchor: [f32; 2],
22    /// Per-axis scale, as `[x, y]`.
23    pub scale: [f32; 2],
24    /// Clockwise rotation in degrees.
25    pub rotation_degrees: f32,
26    /// Opacity from zero to one.
27    pub opacity: f32,
28}
29
30/// One decoded, premultiplied sRGB layer and its presentation controls.
31#[derive(Copy, Clone, Debug)]
32#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
33pub struct CompositeLayer<'a> {
34    /// Source texture sampled by this layer.
35    pub texture: &'a wgpu::TextureView,
36    /// Layer transform and opacity.
37    pub visual: LayerTransform,
38    /// Artistic blend operation evaluated before source-over coverage.
39    pub blend_mode: BlendMode,
40}
41
42/// Where a composite pass draws and how it blends.
43#[derive(Clone, Copy)]
44pub(crate) struct CompositeTarget<'a> {
45    /// What the first layer composites over; transparent when `None`.
46    pub(crate) background: Option<Color>,
47    /// The space blend modes are evaluated in.
48    pub(crate) blend_space: crate::BlendSpace,
49    /// The texture the pass writes.
50    pub(crate) output: &'a wgpu::Texture,
51    /// The default view of `output`.
52    pub(crate) output_view: &'a wgpu::TextureView,
53}
54
55impl Renderer {
56    /// Composites layers in slice order onto a transparent RGBA target.
57    ///
58    /// Inputs and output use premultiplied, sRGB-encoded RGBA storage. Blend
59    /// arithmetic uses the renderer's configured [`crate::BlendSpace`].
60    ///
61    /// # Errors
62    /// Returns [`RenderError::InvalidVisual`] for non-finite or out-of-range
63    /// layer presentation values. A zero-scale layer is valid and draws
64    /// nothing.
65    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
66    pub fn render_layers(
67        &self,
68        size: RenderSize,
69        layers: &[CompositeLayer<'_>],
70    ) -> Result<RenderedTexture, RenderError> {
71        let mut graph = RenderGraph::new(size);
72        let mut graph_layers = Vec::with_capacity(layers.len());
73        for layer in layers {
74            graph_layers.push(GraphLayer {
75                source: graph.add_source(SourceView::from_wgpu(layer.texture)),
76                visual: layer.visual,
77                blend_mode: layer.blend_mode,
78            });
79        }
80        let output = graph
81            .add_composite(graph_layers)
82            .map_err(GraphError::into_render_error)?;
83        graph
84            .set_output(output)
85            .map_err(GraphError::into_render_error)?;
86        self.execute_graph(&graph)
87            .map_err(GraphError::into_render_error)
88    }
89
90    /// Composites `layers` over `background` (transparent when `None`) into
91    /// `output`, returning the render passes encoded.
92    ///
93    /// Layers whose pixels cannot reach the target, or that have no opacity,
94    /// are skipped. Consecutive small layers composite in batches of up to
95    /// [`Self::composite_batch_layers`], one pass per batch (see
96    /// [`composite_batches`]). Which pipeline draws a pass follows from its
97    /// layers' [`LayerTier`] alone (see [`Self::composite_program`]): every
98    /// small layer, alone or in a batch, draws with the loop pipeline, the
99    /// batch's layer count a uniform, so a small layer drawn alone runs the
100    /// same compiled code as a batch: each pixel runs the batch's layers in
101    /// order, each result as the intermediate texture holds it, and the result
102    /// is what a pass per layer stores. A large layer, which never shares a
103    /// pass, draws with the one-layer pipeline. The first batch composites
104    /// over the background in a pass that clears the target to it as stored
105    /// (what the shader writes where no layer reaches), drawing only inside
106    /// its layers' bounds when they cover less than half the target. A
107    /// later batch whose layers cover half the target or more draws the whole
108    /// frame into the other of two textures, over the one holding the
109    /// composite so far; a smaller one draws in place, only inside its
110    /// layers' bounds, over a copy of the region it covers.
111    #[allow(clippy::too_many_lines)]
112    pub(crate) fn encode_layers_pass(
113        &self,
114        encoder: &mut wgpu::CommandEncoder,
115        size: RenderSize,
116        layers: &[CompositeLayer<'_>],
117        target: CompositeTarget<'_>,
118        uniforms: &mut UniformArenaLease,
119    ) -> Result<u64, RenderError> {
120        let CompositeTarget {
121            background,
122            blend_space,
123            output,
124            output_view,
125        } = target;
126        for layer in layers {
127            validate_visual(layer.visual)?;
128        }
129        let format = self.intermediate_format();
130        let drawn = self.drawn_layers(layers.iter().map(|layer| layer.visual), size);
131        let drawn = drawn
132            .iter()
133            .map(|(index, bounds, tier)| (&layers[*index], *bounds, *tier))
134            .collect::<Vec<_>>();
135        let background = stored_background(background, format);
136        if drawn.is_empty() {
137            // The value a first layer writes where it does not reach, so a canvas with
138            // nothing on it matches the canvas beside a layer.
139            clear_target(encoder, output_view, background);
140            return Ok(1);
141        }
142
143        let cached = self.composite_pipeline();
144        let preparation = BatchPreparationContext {
145            device: self.gpu.device(),
146            layout: &cached.bind_group_layout,
147            sampler: &cached.sampler,
148            no_backdrop: &self.empty_input,
149            blend_space,
150            max_filter_samples: self.quality_controls().max_filter_samples,
151            output_size: size,
152            background,
153        };
154        let batches = composite_batches(&drawn, self.composite_batch_layers())
155            .into_iter()
156            .map(|(range, bounds)| (&drawn[range], bounds))
157            .collect::<Vec<_>>();
158        let bounded = |bounds: &PixelRect| under_half(*bounds, size);
159        let ((first, first_bounds), rest) = batches.split_first().expect("a layer is drawn");
160        let first_program = self.batch_program(first);
161        let first = prepare_batch(&preparation, None, first, uniforms);
162        // The first pass clears to the stored background, which the shader writes wherever layers
163        // do not reach, so a first batch under half the target draws only inside its bounds.
164        let first_pass = PassTarget {
165            load: wgpu::LoadOp::Clear(stored_color(background)),
166            scissor: bounded(first_bounds).then_some(*first_bounds),
167        };
168        if rest.is_empty() {
169            self.draw_batch(
170                encoder,
171                cached,
172                first_program,
173                &first,
174                output_view,
175                first_pass,
176            );
177            return Ok(1);
178        }
179
180        let scratch = self.textures.acquire(
181            self.gpu.device(),
182            size,
183            format,
184            "alpha2 layer composition scratch",
185        );
186        let textures = [output, scratch.texture()];
187        let views = [output_view, scratch.view()];
188        // Each full-frame batch flips the target, so start where the last lands in `output`.
189        let mut current = rest.iter().filter(|(_, bounds)| !bounded(bounds)).count() % 2;
190        self.draw_batch(
191            encoder,
192            cached,
193            first_program,
194            &first,
195            views[current],
196            first_pass,
197        );
198        for (batch, bounds) in rest {
199            let other = 1 - current;
200            let program = self.batch_program(batch);
201            if bounded(bounds) {
202                // What the batch composites over, set aside where it draws: a pass
203                // cannot read the texture it writes.
204                copy_region(encoder, textures[current], textures[other], *bounds);
205                let prepared = prepare_batch(&preparation, Some(views[other]), batch, uniforms);
206                self.draw_batch(
207                    encoder,
208                    cached,
209                    program,
210                    &prepared,
211                    views[current],
212                    PassTarget {
213                        load: wgpu::LoadOp::Load,
214                        scissor: Some(*bounds),
215                    },
216                );
217            } else {
218                let prepared = prepare_batch(&preparation, Some(views[current]), batch, uniforms);
219                self.draw_batch(
220                    encoder,
221                    cached,
222                    program,
223                    &prepared,
224                    views[other],
225                    PassTarget::WHOLE,
226                );
227                current = other;
228            }
229        }
230        debug_assert_eq!(current, 0);
231        Ok(u64::try_from(batches.len()).unwrap_or(u64::MAX))
232    }
233
234    /// The program a batch's pass draws with: its layers share a tier, and a
235    /// batch of several is drawn only with the loop.
236    fn batch_program(
237        &self,
238        batch: &[(&CompositeLayer<'_>, PixelRect, LayerTier)],
239    ) -> (CompositeProgram, LayerTier, usize) {
240        let tier = batch[0].2;
241        debug_assert!(batch.iter().all(|(_, _, other)| *other == tier));
242        let program = self.composite_program(tier);
243        debug_assert!(batch.len() == 1 || program == CompositeProgram::Loop);
244        (program, tier, batch.len())
245    }
246
247    /// Draws one composite pass with the pipeline of `program`.
248    fn draw_batch(
249        &self,
250        encoder: &mut wgpu::CommandEncoder,
251        cached: &CachedCompositePipeline,
252        (program, tier, layers): (CompositeProgram, LayerTier, usize),
253        bind_group: &wgpu::BindGroup,
254        target: &wgpu::TextureView,
255        pass: PassTarget,
256    ) {
257        let pipeline = self.composite_program_pipeline(cached, program);
258        #[cfg(test)]
259        {
260            let mut log = self
261                .composite_pipelines
262                .lock()
263                .unwrap_or_else(std::sync::PoisonError::into_inner);
264            log.passes += 1;
265            log.drawn
266                .insert((program, pipeline.clone(), tier, layers > 1));
267        }
268        #[cfg(not(test))]
269        let _ = (tier, layers);
270        encode_batch(encoder, pipeline, bind_group, target, pass);
271    }
272
273    /// Whether this renderer composites small layers in batches, drawing every
274    /// small layer with the loop pipeline: on a GPU, and in tests that set
275    /// `composite_batch_limit`, which composite as a GPU does on any device
276    /// (a limit of one draws a pass per layer with the loop pipeline).
277    pub(crate) fn batches_small_layers(&self) -> bool {
278        #[cfg(test)]
279        {
280            if self
281                .composite_batch_limit
282                .load(std::sync::atomic::Ordering::Relaxed)
283                > 0
284            {
285                return true;
286            }
287        }
288        !self.gpu.capabilities().software_rasterizer
289    }
290
291    /// The program that composites a layer of `tier`: the loop for a small
292    /// layer on a renderer that batches small layers, whether its pass holds
293    /// one layer or several, and the one-layer program otherwise.
294    ///
295    /// A pass of one small layer and a batch therefore run one compiled
296    /// program and store the same bytes on every device; two programs, one
297    /// for each, were optimized apart (contraction) by Metal, D3D12 and GL
298    /// compilers, a last bit apart. A large layer never shares a pass, so no
299    /// batch is compared with it, and the one-layer program spares it the
300    /// loop. A software rasterizer, which runs every step of a shader whether
301    /// it is taken or not, never batches and composites every layer with the
302    /// one-layer program.
303    pub(crate) fn composite_program(&self, tier: LayerTier) -> CompositeProgram {
304        if tier == LayerTier::Small && self.batches_small_layers() {
305            CompositeProgram::Loop
306        } else {
307            CompositeProgram::OneLayer
308        }
309    }
310
311    /// The most layers one composite pass takes: [`COMPOSITE_BATCH_LAYERS`]
312    /// on a GPU, one on a software rasterizer, or in tests what
313    /// `composite_batch_limit` sets.
314    ///
315    /// A GPU runs a batch's step for a layer only on the 2 x 2 quads that
316    /// overlap the layer, and saves the pass, the barrier and the backdrop
317    /// copy each layer would cost. A software rasterizer runs every step of a
318    /// shader on every pixel of the pass whatever it skips, so a small layer
319    /// in a batch costs it the batch's whole area; a pass per layer, drawn
320    /// only inside the layer with the one-layer program, costs it least.
321    pub(crate) fn composite_batch_layers(&self) -> usize {
322        #[cfg(test)]
323        {
324            let limit = self
325                .composite_batch_limit
326                .load(std::sync::atomic::Ordering::Relaxed);
327            if limit > 0 {
328                return limit.min(COMPOSITE_BATCH_LAYERS);
329            }
330        }
331        if self.batches_small_layers() {
332            COMPOSITE_BATCH_LAYERS
333        } else {
334            1
335        }
336    }
337
338    /// The composite shader, layout and sampler both composite pipelines
339    /// share, created on first use.
340    pub(crate) fn composite_pipeline(&self) -> &CachedCompositePipeline {
341        self.pipelines.composite.get_or_init(|| {
342            let device = self.gpu.device();
343            let bind_group_layout = batch_layout(device);
344            let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
345                label: Some("alpha2 layer composite shader"),
346                source: wgpu::ShaderSource::Wgsl(COMPOSITE_WGSL.into()),
347            });
348            let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
349                label: Some("alpha2 layer sampler"),
350                address_mode_u: wgpu::AddressMode::ClampToEdge,
351                address_mode_v: wgpu::AddressMode::ClampToEdge,
352                mag_filter: wgpu::FilterMode::Linear,
353                min_filter: wgpu::FilterMode::Linear,
354                ..Default::default()
355            });
356            CachedCompositePipeline {
357                bind_group_layout,
358                sampler,
359                shader,
360                programs: Default::default(),
361            }
362        })
363    }
364
365    /// The pipeline of `program`, created the first time a pass draws with it.
366    pub(crate) fn composite_program_pipeline<'a>(
367        &self,
368        cached: &'a CachedCompositePipeline,
369        program: CompositeProgram,
370    ) -> &'a wgpu::RenderPipeline {
371        let (slot, entry, label) = match program {
372            CompositeProgram::Loop => (0, "fs", "alpha2 small layer composite pipeline"),
373            CompositeProgram::OneLayer => (1, "fs_layer", "alpha2 large layer composite pipeline"),
374        };
375        cached.programs[slot].get_or_init(|| {
376            #[cfg(test)]
377            {
378                self.composite_pipelines
379                    .lock()
380                    .unwrap_or_else(std::sync::PoisonError::into_inner)
381                    .created
382                    .push(program);
383            }
384            composite_pipeline(
385                self.gpu.device(),
386                &cached.bind_group_layout,
387                &cached.shader,
388                self.intermediate_format(),
389                entry,
390                label,
391            )
392        })
393    }
394
395    /// The layers a composite draws, by index, with the target pixels each
396    /// can change and its tier: those that reach the target and have opacity.
397    #[cfg_attr(not(test), allow(clippy::unused_self))]
398    pub(crate) fn drawn_layers(
399        &self,
400        visuals: impl Iterator<Item = LayerTransform>,
401        size: RenderSize,
402    ) -> Vec<(usize, PixelRect, LayerTier)> {
403        #[cfg(test)]
404        let full_frame = self
405            .full_frame_layers
406            .load(std::sync::atomic::Ordering::Relaxed);
407        #[cfg(not(test))]
408        let full_frame = false;
409        visuals
410            .enumerate()
411            .filter_map(|(index, visual)| {
412                if is_degenerate(visual) || is_invisible(visual) {
413                    return None;
414                }
415                let bounds = layer_bounds(visual, size);
416                // Drawn over the whole target in tests, a layer keeps the tier of
417                // its bounds, so it draws with the same pipeline as drawn inside them.
418                let tier = LayerTier::of(bounds.unwrap_or(PixelRect::EMPTY), size);
419                if full_frame {
420                    Some((index, PixelRect::whole(size), tier))
421                } else {
422                    bounds.map(|bounds| (index, bounds, tier))
423                }
424            })
425            .collect()
426    }
427
428    /// The passes, and so the batch uniforms, a composite of these layers
429    /// encodes, a clear counted as none.
430    pub(crate) fn composite_batch_count(
431        &self,
432        visuals: impl Iterator<Item = LayerTransform>,
433        size: RenderSize,
434    ) -> usize {
435        let drawn = self.drawn_layers(visuals, size);
436        composite_batches(&drawn, self.composite_batch_layers()).len()
437    }
438
439    /// Compiles the pipelines a composite of `layers` layers can draw with.
440    pub(crate) fn warm_composite(&self, layers: usize) {
441        if layers == 0 {
442            return;
443        }
444        let cached = self.composite_pipeline();
445        self.composite_program_pipeline(cached, CompositeProgram::OneLayer);
446        if self.batches_small_layers() {
447            self.composite_program_pipeline(cached, CompositeProgram::Loop);
448        }
449    }
450}
451
452/// The size class of a drawn layer, which alone decides both whether it may
453/// share a pass ([`composite_batches`]) and which composite program draws it
454/// ([`Renderer::composite_program`]), so the two cannot disagree.
455#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
456pub(crate) enum LayerTier {
457    /// Bounds under half the target: may share a pass.
458    Small,
459    /// Bounds of half the target or more: always a pass of its own.
460    Large,
461}
462
463impl LayerTier {
464    /// The tier of a layer that can change the target pixels `bounds`.
465    pub(crate) fn of(bounds: PixelRect, size: RenderSize) -> Self {
466        if under_half(bounds, size) {
467            Self::Small
468        } else {
469            Self::Large
470        }
471    }
472}
473
474/// Whether `bounds` covers less than half the target: a layer's tier, and
475/// whether a pass draws only inside its layers' bounds.
476fn under_half(bounds: PixelRect, size: RenderSize) -> bool {
477    bounds.area() * 2 < PixelRect::whole(size).area()
478}
479
480/// The two composite programs, entry points of one shader that share its
481/// composite function (composite.wesl `composite_layer`).
482#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
483pub(crate) enum CompositeProgram {
484    /// `fs`: the pass's layers in a loop, their count a uniform.
485    Loop,
486    /// `fs_layer`: one layer, no loop.
487    OneLayer,
488}
489
490/// Test-only record of the composite pipelines a renderer created and drew
491/// with.
492#[cfg(test)]
493#[derive(Debug, Default)]
494pub(crate) struct CompositePipelineLog {
495    /// The program of each composite pipeline created, in order.
496    pub(crate) created: Vec<CompositeProgram>,
497    /// Composite passes drawn.
498    pub(crate) passes: usize,
499    /// Every distinct program, pipeline, tier and whether the pass held
500    /// several layers, of those passes.
501    pub(crate) drawn:
502        std::collections::HashSet<(CompositeProgram, wgpu::RenderPipeline, LayerTier, bool)>,
503}
504
505/// Groups drawn layers, in order, into the passes that composite them, each
506/// with the union of its layers' bounds.
507///
508/// Only small layers ([`LayerTier::Small`], under half the target) share a
509/// pass, up to `limit` of them. Such a pass saves a small layer what
510/// dominates its cost, the pass itself, its barrier and its backdrop copy; a
511/// large layer's pass costs its pixels, and in a batch every step runs over
512/// the batch's whole area, so it keeps a pass of its own.
513pub(crate) fn composite_batches<T>(
514    drawn: &[(T, PixelRect, LayerTier)],
515    limit: usize,
516) -> Vec<(std::ops::Range<usize>, PixelRect)> {
517    let mut batches: Vec<(std::ops::Range<usize>, PixelRect)> = Vec::new();
518    for (index, (_, bounds, tier)) in drawn.iter().enumerate() {
519        match batches.last_mut() {
520            Some((range, union))
521                if range.len() < limit
522                    && *tier == LayerTier::Small
523                    && drawn[range.start].2 == LayerTier::Small =>
524            {
525                range.end = index + 1;
526                *union = union.union(*bounds);
527            }
528            _ => batches.push((index..index + 1, *bounds)),
529        }
530    }
531    batches
532}
533
534/// How the intermediate texture stores a value, which the composite shader
535/// rounds to itself (its `STORED_FORMAT`, fixed by the renderer's
536/// intermediate format).
537#[derive(Copy, Clone, Debug)]
538enum StoredValue {
539    /// Kept as computed.
540    AsIs,
541    /// Rounded to a half float.
542    Half,
543    /// Rounded to an 8-bit unorm code.
544    Unorm8,
545}
546
547impl StoredValue {
548    fn of(format: wgpu::TextureFormat) -> Self {
549        match format {
550            wgpu::TextureFormat::Rgba16Float => Self::Half,
551            wgpu::TextureFormat::Rgba8Unorm => Self::Unorm8,
552            _ => Self::AsIs,
553        }
554    }
555
556    const fn shader_value(self) -> f64 {
557        match self {
558            Self::AsIs => 0.0,
559            Self::Half => 1.0,
560            Self::Unorm8 => 2.0,
561        }
562    }
563}
564
565struct BatchPreparationContext<'a> {
566    device: &'a wgpu::Device,
567    layout: &'a wgpu::BindGroupLayout,
568    sampler: &'a wgpu::Sampler,
569    /// Bound as the backdrop of the first pass, which composites over
570    /// `background` and reads no texture.
571    no_backdrop: &'a wgpu::TextureView,
572    blend_space: crate::BlendSpace,
573    max_filter_samples: u32,
574    output_size: RenderSize,
575    /// The first layer's backdrop, as [`stored_background`] gives it.
576    background: [f32; 4],
577}
578
579/// Writes one batch's `BatchParams` and binds its layers' textures, the
580/// unused texture slots repeating the first layer's. Without a `backdrop` the
581/// batch composites over the background.
582fn prepare_batch(
583    context: &BatchPreparationContext<'_>,
584    backdrop: Option<&wgpu::TextureView>,
585    batch: &[(&CompositeLayer<'_>, PixelRect, LayerTier)],
586    uniforms: &mut UniformArenaLease,
587) -> wgpu::BindGroup {
588    debug_assert!(!batch.is_empty() && batch.len() <= COMPOSITE_BATCH_LAYERS);
589    let mut values = [0.0_f32; BATCH_UNIFORM_FLOATS];
590    #[allow(clippy::cast_precision_loss)]
591    let controls = [
592        context.blend_space.shader_value(),
593        context.max_filter_samples as f32,
594        batch.len() as f32,
595        if backdrop.is_some() { 0.0 } else { 1.0 },
596        context.output_size.width as f32,
597        context.output_size.height as f32,
598        0.0,
599        0.0,
600        context.background[0],
601        context.background[1],
602        context.background[2],
603        context.background[3],
604    ];
605    values[..controls.len()].copy_from_slice(&controls);
606    for ((layer, bounds, _), slot) in batch
607        .iter()
608        .zip(values[controls.len()..].as_chunks_mut::<16>().0)
609    {
610        let visual = layer.visual;
611        #[allow(clippy::cast_precision_loss)]
612        slot.copy_from_slice(&[
613            visual.position[0],
614            visual.position[1],
615            visual.anchor[0],
616            visual.anchor[1],
617            visual.scale[0],
618            visual.scale[1],
619            visual.rotation_degrees.to_radians(),
620            visual.opacity,
621            layer.blend_mode.shader_value(),
622            0.0,
623            0.0,
624            0.0,
625            bounds.x as f32,
626            bounds.y as f32,
627            (bounds.x + bounds.width) as f32,
628            (bounds.y + bounds.height) as f32,
629        ]);
630    }
631    let uniform = uniforms.write(&f32_bytes(values));
632    let texture = |slot: usize| {
633        let (layer, _, _) = batch.get(slot).unwrap_or(&batch[0]);
634        layer.texture
635    };
636    let mut entries = (0..COMPOSITE_BATCH_LAYERS)
637        .map(|slot| wgpu::BindGroupEntry {
638            binding: u32::try_from(slot).expect("few layer slots"),
639            resource: wgpu::BindingResource::TextureView(texture(slot)),
640        })
641        .collect::<Vec<_>>();
642    entries.push(wgpu::BindGroupEntry {
643        binding: SAMPLER_BINDING,
644        resource: wgpu::BindingResource::Sampler(context.sampler),
645    });
646    entries.push(wgpu::BindGroupEntry {
647        binding: BACKDROP_BINDING,
648        resource: wgpu::BindingResource::TextureView(backdrop.unwrap_or(context.no_backdrop)),
649    });
650    entries.push(wgpu::BindGroupEntry {
651        binding: UNIFORM_BINDING,
652        resource: uniforms.binding(uniform),
653    });
654    context
655        .device
656        .create_bind_group(&wgpu::BindGroupDescriptor {
657            label: Some("alpha2 layer batch bind group"),
658            layout: context.layout,
659            entries: &entries,
660        })
661}
662
663/// Clears `target` to `stored`, a value the target's format holds exactly
664/// ([`stored_background`]), so no device rounds it: the clear stores what a
665/// layer pass writes for the same background.
666fn clear_target(encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, stored: [f32; 4]) {
667    let color = stored_color(stored);
668    let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
669        label: Some("alpha2 layer clear pass"),
670        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
671            view: target,
672            depth_slice: None,
673            resolve_target: None,
674            ops: wgpu::Operations {
675                load: wgpu::LoadOp::Clear(color),
676                store: wgpu::StoreOp::Store,
677            },
678        })],
679        depth_stencil_attachment: None,
680        timestamp_writes: None,
681        occlusion_query_set: None,
682        multiview_mask: None,
683    });
684}
685
686/// How a composite pass begins and where it draws.
687#[derive(Copy, Clone)]
688struct PassTarget {
689    /// What the target holds before the pass: kept, or cleared.
690    load: wgpu::LoadOp<wgpu::Color>,
691    /// Only these pixels are drawn; the whole target when `None`.
692    scissor: Option<PixelRect>,
693}
694
695impl PassTarget {
696    /// A pass that writes every pixel, so what the target held does not matter.
697    const WHOLE: Self = Self {
698        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
699        scissor: None,
700    };
701}
702
703/// A stored value as a clear color: a value the target holds exactly, so the
704/// device converts it unchanged.
705fn stored_color(stored: [f32; 4]) -> wgpu::Color {
706    let [r, g, b, a] = stored.map(f64::from);
707    wgpu::Color { r, g, b, a }
708}
709
710/// Draws one batch into `target`, over the whole frame, or with a scissor
711/// only inside it, the target loaded or cleared first.
712fn encode_batch(
713    encoder: &mut wgpu::CommandEncoder,
714    pipeline: &wgpu::RenderPipeline,
715    bind_group: &wgpu::BindGroup,
716    target: &wgpu::TextureView,
717    PassTarget { load, scissor }: PassTarget,
718) {
719    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
720        label: Some("alpha2 layer composition pass"),
721        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
722            view: target,
723            depth_slice: None,
724            resolve_target: None,
725            ops: wgpu::Operations {
726                load,
727                store: wgpu::StoreOp::Store,
728            },
729        })],
730        depth_stencil_attachment: None,
731        timestamp_writes: None,
732        occlusion_query_set: None,
733        multiview_mask: None,
734    });
735    pass.set_pipeline(pipeline);
736    pass.set_bind_group(0, bind_group, &[]);
737    if let Some(scissor) = scissor {
738        pass.set_scissor_rect(scissor.x, scissor.y, scissor.width, scissor.height);
739    }
740    pass.draw(0..3, 0..1);
741}
742
743/// Copies one region between two textures of the same size and format.
744fn copy_region(
745    encoder: &mut wgpu::CommandEncoder,
746    source: &wgpu::Texture,
747    destination: &wgpu::Texture,
748    region: PixelRect,
749) {
750    let at = |texture| wgpu::TexelCopyTextureInfo {
751        texture,
752        mip_level: 0,
753        origin: wgpu::Origin3d {
754            x: region.x,
755            y: region.y,
756            z: 0,
757        },
758        aspect: wgpu::TextureAspect::All,
759    };
760    encoder.copy_texture_to_texture(
761        at(source),
762        at(destination),
763        wgpu::Extent3d {
764            width: region.width,
765            height: region.height,
766            depth_or_array_layers: 1,
767        },
768    );
769}
770
771/// A rectangle of target pixels.
772#[derive(Copy, Clone, Debug, Eq, PartialEq)]
773pub(crate) struct PixelRect {
774    pub(crate) x: u32,
775    pub(crate) y: u32,
776    pub(crate) width: u32,
777    pub(crate) height: u32,
778}
779
780impl PixelRect {
781    /// No pixels: the bounds of a layer that misses the target.
782    const EMPTY: Self = Self {
783        x: 0,
784        y: 0,
785        width: 0,
786        height: 0,
787    };
788
789    const fn whole(size: RenderSize) -> Self {
790        Self {
791            x: 0,
792            y: 0,
793            width: size.width,
794            height: size.height,
795        }
796    }
797
798    pub(crate) fn area(self) -> u64 {
799        u64::from(self.width) * u64::from(self.height)
800    }
801
802    /// The smallest rectangle holding both.
803    fn union(self, other: Self) -> Self {
804        let x = self.x.min(other.x);
805        let y = self.y.min(other.y);
806        let right = (self.x + self.width).max(other.x + other.width);
807        let bottom = (self.y + self.height).max(other.y + other.height);
808        Self {
809            x,
810            y,
811            width: right - x,
812            height: bottom - y,
813        }
814    }
815}
816
817/// Pixels beyond a layer's transformed edges that its coverage can still
818/// reach: the antialiasing footprint is at most about one and a half pixels,
819/// so this is generous.
820const COVERAGE_MARGIN: f64 = 4.0;
821
822/// The target pixels a layer can change: its transformed source rectangle,
823/// widened by [`COVERAGE_MARGIN`] and clamped to the target, or `None` when
824/// that misses the target. Everywhere else the layer's coverage is zero and
825/// compositing returns the backdrop unchanged.
826///
827/// Mirrors the composite shader's mapping: a source corner `(u, v)` lands at
828/// `position * size + R^T ((u, v) - anchor) * scale * size`, with `R` the
829/// shader's rotation.
830pub(crate) fn layer_bounds(visual: LayerTransform, size: RenderSize) -> Option<PixelRect> {
831    let (width, height) = (f64::from(size.width), f64::from(size.height));
832    let (turn_sin, turn_cos) = f64::from(visual.rotation_degrees).to_radians().sin_cos();
833    let corner = |u: f64, v: f64| {
834        let x = (u - f64::from(visual.anchor[0])) * f64::from(visual.scale[0]) * width;
835        let y = (v - f64::from(visual.anchor[1])) * f64::from(visual.scale[1]) * height;
836        (
837            f64::from(visual.position[0]) * width + turn_cos * x - turn_sin * y,
838            f64::from(visual.position[1]) * height + turn_sin * x + turn_cos * y,
839        )
840    };
841    let corners = [
842        corner(0.0, 0.0),
843        corner(1.0, 0.0),
844        corner(0.0, 1.0),
845        corner(1.0, 1.0),
846    ];
847    let fold = |pick: fn(&(f64, f64)) -> f64, extreme: fn(f64, f64) -> f64, start: f64| {
848        corners.iter().map(pick).fold(start, extreme)
849    };
850    let left = (fold(|c| c.0, f64::min, f64::INFINITY) - COVERAGE_MARGIN)
851        .floor()
852        .max(0.0);
853    let top = (fold(|c| c.1, f64::min, f64::INFINITY) - COVERAGE_MARGIN)
854        .floor()
855        .max(0.0);
856    let right = (fold(|c| c.0, f64::max, f64::NEG_INFINITY) + COVERAGE_MARGIN)
857        .ceil()
858        .min(width);
859    let bottom = (fold(|c| c.1, f64::max, f64::NEG_INFINITY) + COVERAGE_MARGIN)
860        .ceil()
861        .min(height);
862    if !(right > left && bottom > top) {
863        return None;
864    }
865    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
866    Some(PixelRect {
867        x: left as u32,
868        y: top as u32,
869        width: (right - left) as u32,
870        height: (bottom - top) as u32,
871    })
872}
873
874/// The canvas background as the intermediate texture stores it: 8-bit or
875/// half-float precision, rounded to nearest, transparent when absent.
876///
877/// Both ways a canvas gets its background take this value: the clear of a
878/// canvas with nothing drawn on it, and the first layer's pass, which writes
879/// it wherever the layer does not reach. A device converting the original
880/// value itself may round a tie, or a product `f32` makes a tie, the other
881/// way (0.7 is 178.5 in `f32` and 178.49999 exactly; a half-float clear of
882/// 0.3 read back as 76 and the layer pass's 77 on every Vulkan driver
883/// tested), so an empty frame and the frame beside it would differ by a
884/// code.
885fn stored_background(background: Option<Color>, format: wgpu::TextureFormat) -> [f32; 4] {
886    let Some(color) = background else {
887        return [0.0; 4];
888    };
889    // A clear value reaches the device as `f32` and is converted from there.
890    #[allow(clippy::cast_possible_truncation)]
891    let channel = |value: f64| {
892        let value = value as f32;
893        match format {
894            wgpu::TextureFormat::Rgba8Unorm => (value.clamp(0.0, 1.0) * 255.0).round() / 255.0,
895            wgpu::TextureFormat::Rgba16Float => half::f16::from_f32(value).to_f32(),
896            _ => value,
897        }
898    };
899    [
900        channel(color.red),
901        channel(color.green),
902        channel(color.blue),
903        channel(color.alpha),
904    ]
905}
906
907const SAMPLER_BINDING: u32 = 8;
908const BACKDROP_BINDING: u32 = 9;
909const UNIFORM_BINDING: u32 = 10;
910
911/// The bind group layout of a composite pass: a texture per layer slot, the
912/// sampler, the backdrop, and the batch uniform.
913fn batch_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
914    let mut entries = (0..COMPOSITE_BATCH_LAYERS)
915        .map(|slot| texture_entry(u32::try_from(slot).expect("few layer slots"), true))
916        .collect::<Vec<_>>();
917    entries.push(wgpu::BindGroupLayoutEntry {
918        binding: SAMPLER_BINDING,
919        visibility: wgpu::ShaderStages::FRAGMENT,
920        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
921        count: None,
922    });
923    entries.push(texture_entry(BACKDROP_BINDING, false));
924    entries.push(wgpu::BindGroupLayoutEntry {
925        binding: UNIFORM_BINDING,
926        visibility: wgpu::ShaderStages::FRAGMENT,
927        ty: batch_uniform_binding(),
928        count: None,
929    });
930    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
931        label: Some("alpha2 layer batch bind group layout"),
932        entries: &entries,
933    })
934}
935
936/// The batch uniform binding, at the size the graph reserves and each batch
937/// writes: a `BatchParams` that grew past it fails pipeline creation instead
938/// of overrunning the uniform arena on a device with a small alignment.
939fn batch_uniform_binding() -> wgpu::BindingType {
940    wgpu::BindingType::Buffer {
941        ty: wgpu::BufferBindingType::Uniform,
942        has_dynamic_offset: false,
943        min_binding_size: std::num::NonZeroU64::new(float_uniform_bytes(BATCH_UNIFORM_FLOATS)),
944    }
945}
946
947fn texture_entry(binding: u32, filterable: bool) -> wgpu::BindGroupLayoutEntry {
948    wgpu::BindGroupLayoutEntry {
949        binding,
950        visibility: wgpu::ShaderStages::FRAGMENT,
951        ty: wgpu::BindingType::Texture {
952            sample_type: wgpu::TextureSampleType::Float { filterable },
953            view_dimension: wgpu::TextureViewDimension::D2,
954            multisampled: false,
955        },
956        count: None,
957    }
958}
959
960fn composite_pipeline(
961    device: &wgpu::Device,
962    layout: &wgpu::BindGroupLayout,
963    shader: &wgpu::ShaderModule,
964    format: wgpu::TextureFormat,
965    fragment_entry: &str,
966    label: &str,
967) -> wgpu::RenderPipeline {
968    let label = Some(label);
969    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
970        label,
971        bind_group_layouts: &[Some(layout)],
972        immediate_size: 0,
973    });
974    let constants = [("STORED_FORMAT", StoredValue::of(format).shader_value())];
975    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
976        label,
977        layout: Some(&pipeline_layout),
978        vertex: wgpu::VertexState {
979            module: shader,
980            entry_point: Some("vs"),
981            buffers: &[],
982            compilation_options: wgpu::PipelineCompilationOptions::default(),
983        },
984        fragment: Some(wgpu::FragmentState {
985            module: shader,
986            entry_point: Some(fragment_entry),
987            targets: &[Some(format.into())],
988            compilation_options: wgpu::PipelineCompilationOptions {
989                constants: &constants,
990                ..wgpu::PipelineCompilationOptions::default()
991            },
992        }),
993        primitive: wgpu::PrimitiveState::default(),
994        depth_stencil: None,
995        multisample: wgpu::MultisampleState::default(),
996        multiview_mask: None,
997        cache: None,
998    })
999}
1000
1001fn validate_visual(visual: LayerTransform) -> Result<(), RenderError> {
1002    let values = [
1003        visual.position[0],
1004        visual.position[1],
1005        visual.anchor[0],
1006        visual.anchor[1],
1007        visual.scale[0],
1008        visual.scale[1],
1009        visual.rotation_degrees,
1010        visual.opacity,
1011    ];
1012    if values.into_iter().any(|value| !value.is_finite()) || !(0.0..=1.0).contains(&visual.opacity)
1013    {
1014        return Err(RenderError::InvalidVisual);
1015    }
1016    Ok(())
1017}
1018
1019/// Whether a layer collapses to a line or a point and covers no pixel.
1020///
1021/// A scale that passes through zero (an animation from 1 to -1, or an eased
1022/// curve that overshoots) is a legitimate instant, not an error: the layer is
1023/// not visible, and its transform has no inverse to sample with.
1024fn is_degenerate(visual: LayerTransform) -> bool {
1025    visual.scale[0].abs() <= f32::EPSILON || visual.scale[1].abs() <= f32::EPSILON
1026}
1027
1028/// Whether a layer has no opacity, so it adds nothing to any pixel.
1029///
1030/// Every blend mode weighs its result by the source's alpha, which opacity
1031/// scales, so the layer leaves the backdrop as it is. Drawing it anyway would
1032/// cost a pass of the layer's size and change the backdrop only by rounding
1033/// it through the blend space. An inactive matte hides its clip this way, and
1034/// a fade or hold reaches it.
1035fn is_invisible(visual: LayerTransform) -> bool {
1036    visual.opacity <= 0.0
1037}