Skip to main content

unlit_wgpu/
scene.rs

1//! The declarative description of a frame.
2//!
3//! A [`Scene`] is pure data: it names the pipelines to run, the bind groups
4//! and vertex buffers each draw binds, and the ranges to draw. Recording it
5//! ([`Scene::record`]) is a separate step, so a caller can build the
6//! description once and reuse, inspect or modify it — and custom draw
7//! commands are expressed with the same shapes as the built-in ones.
8//!
9//! The recording loop is flat: each draw names its own pipeline and every
10//! resource it binds.
11//!
12//! ```text
13//! for draw in scene.draws {
14//!     set the pipeline, then the draw's bind groups
15//!     set the draw's vertex and index buffers
16//!     draw (indexed or not)
17//! }
18//! ```
19//!
20//! Consecutive draws that bind the same resource skip the redundant
21//! `set_*` call.
22//!
23//! A draw owns the handles it names — `wgpu` handles are reference-counted, so
24//! holding one keeps the resource alive and cloning one is cheap. A scene
25//! therefore borrows nothing, which is what lets several scenes be built from
26//! one resource graph and then recorded in turn.
27//!
28//! Scissor rectangles are the exception to "a draw is independent": wgpu has
29//! no reset call, so a rectangle set by one draw stays set for every draw
30//! after it in the pass. A scene therefore orders its clipped draws last —
31//! or sets [`DrawEntry::scissor`] on every draw. In a composite,
32//! [`DrawOrder::After`] is that last stretch: nothing outside the trailing
33//! tier follows the clipped draws, so their rectangle cannot leak into one.
34//! The stencil reference is the
35//! same kind of pass state, but it is set on every draw (from
36//! [`DrawEntry::stencil_reference`]), so it never leaks from one draw to the
37//! next. Both are per-[`Scene`] state: [`Scene::record`] starts from the pass's
38//! own defaults, so nothing a scene leaves behind is seen by the next one.
39//!
40//! The viewport is pass state of the same kind — wgpu has no call that resets
41//! it either — but it covers a whole scene rather than one draw, so a scene
42//! states it once in [`Scene::viewport`]. A scene that states none draws into
43//! whatever region the pass already holds, which is the whole target only if
44//! the caller put it there; the frame driver in `unlit3d` does that for every
45//! scene that states none, so one scene's letterbox never crops the next one's.
46//!
47//! [`Scene::record`] lists a single scene's draws in the order they are
48//! given. Recording several scenes as one composite —
49//! [`Scene::record_composited`] — orders them by [`DrawOrder`] instead:
50//! the sequenced draws of every scene first, then the depth-sorted ones of
51//! every scene pooled together back-to-front, then the trailing ones. A
52//! scene that states no order keeps its place: [`DrawOrder::Before`] is
53//! the default, and only draws that join the sorted pool are reordered.
54
55use arrayvec::ArrayVec;
56use core::ops::Range;
57
58/// One bind-group slot: the group index and the group to bind there.
59pub type BindGroupBinding = (u32, wgpu::BindGroup);
60
61/// One vertex-buffer slot: the slot index, the buffer to bind and the byte
62/// range of it the draw reads.
63pub type VertexBufferBinding = (u32, wgpu::Buffer, Range<u64>);
64
65/// A scissor rectangle, in pixels.
66///
67/// Fragments outside it are discarded, which is how a clipped draw — a
68/// tessellated user-interface primitive, for example — keeps to its clip
69/// rectangle without extra geometry.
70#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
71pub struct ScissorRect {
72    /// Left edge, in pixels.
73    pub x: u32,
74    /// Top edge, in pixels.
75    pub y: u32,
76    /// Width in pixels.
77    pub width: u32,
78    /// Height in pixels.
79    pub height: u32,
80}
81
82impl ScissorRect {
83    /// A rectangle covering `width` x `height` pixels from the origin.
84    pub fn new(width: u32, height: u32) -> Self {
85        Self {
86            x: 0,
87            y: 0,
88            width,
89            height,
90        }
91    }
92
93    /// Same rectangle, with its origin moved to (`x`, `y`).
94    pub fn at(self, x: u32, y: u32) -> Self {
95        Self { x, y, ..self }
96    }
97}
98
99/// The region of the render target a scene draws into, in pixels.
100///
101/// Fragments outside it are discarded and the clip space a pipeline writes is
102/// stretched across it, which is what letterboxes a scene whose own aspect
103/// differs from its target's: the projection keeps the scene's aspect, and the
104/// viewport places the result inside the target without distortion.
105///
106/// Unlike [`ScissorRect`], which clips in whole pixels, a viewport is
107/// fractional — a centred letterbox rarely lands on a pixel boundary — so its
108/// fields are floats. It is pass state with no reset call, so see
109/// [`Scene::viewport`].
110#[derive(Clone, Copy, Debug, PartialEq)]
111pub struct ViewportRect {
112    /// Left edge, in pixels.
113    pub x: f32,
114    /// Top edge, in pixels.
115    pub y: f32,
116    /// Width in pixels.
117    pub width: f32,
118    /// Height in pixels.
119    pub height: f32,
120    /// The depth the near plane of the viewport's clip space maps to.
121    pub min_depth: f32,
122    /// The depth the far plane of the viewport's clip space maps to.
123    pub max_depth: f32,
124}
125
126impl ViewportRect {
127    /// A rectangle covering `width` x `height` pixels from the origin, over the
128    /// whole `0.0..=1.0` depth range.
129    pub fn new(width: f32, height: f32) -> Self {
130        Self {
131            x: 0.0,
132            y: 0.0,
133            width,
134            height,
135            min_depth: 0.0,
136            max_depth: 1.0,
137        }
138    }
139
140    /// Same rectangle, with its origin moved to (`x`, `y`).
141    pub fn at(self, x: f32, y: f32) -> Self {
142        Self { x, y, ..self }
143    }
144
145    /// Same rectangle, mapped over `min_depth..=max_depth` instead of the whole
146    /// depth range.
147    pub fn with_depth(self, min_depth: f32, max_depth: f32) -> Self {
148        Self {
149            min_depth,
150            max_depth,
151            ..self
152        }
153    }
154
155    /// The largest rectangle of `aspect` that fits inside a
156    /// `target_width` x `target_height` target, centred.
157    ///
158    /// This is the letterbox: a target wider than `aspect` gets a rectangle
159    /// spanning the full height with bars at the sides, a narrower one spans
160    /// the full width with bars above and below, and the rectangle's own aspect
161    /// is `aspect` either way — so content drawn through it is scaled
162    /// uniformly, showing the same picture on any target shape rather than
163    /// being stretched to the target's.
164    ///
165    /// A degenerate target, or an `aspect` that is not a positive finite
166    /// number, has no rectangle to fit, so the whole target comes back rather
167    /// than a zero-sized region.
168    pub fn fit_aspect(aspect: f32, target_width: u32, target_height: u32) -> Self {
169        let (width, height) = (target_width as f32, target_height as f32);
170        // `is_finite` is what rejects a NaN aspect, which no comparison would.
171        if !aspect.is_finite() || aspect <= 0.0 || width <= 0.0 || height <= 0.0 {
172            return Self::new(width.max(0.0), height.max(0.0));
173        }
174        if width / height > aspect {
175            let fitted = height * aspect;
176            Self::new(fitted, height).at((width - fitted) * 0.5, 0.0)
177        } else {
178            let fitted = width / aspect;
179            Self::new(width, fitted).at(0.0, (height - fitted) * 0.5)
180        }
181    }
182}
183
184/// What to draw for one mesh.
185#[derive(Clone, Debug)]
186pub enum DrawRange {
187    /// Draw `vertices` without an index buffer, instanced over `instances`.
188    Vertices {
189        /// Vertex range to draw.
190        vertices: Range<u32>,
191        /// Instance range to draw.
192        instances: Range<u32>,
193    },
194    /// Draw indexed, instanced over `instances`.
195    Indexed {
196        /// Index range to draw.
197        indices: Range<u32>,
198        /// Value added to every index before fetching a vertex.
199        base_vertex: i32,
200        /// Instance range to draw.
201        instances: Range<u32>,
202    },
203}
204
205impl DrawRange {
206    /// A single non-indexed draw of `vertices`.
207    pub fn vertices(vertices: Range<u32>) -> Self {
208        Self::Vertices {
209            vertices,
210            instances: 0..1,
211        }
212    }
213
214    /// A single indexed draw of `indices`.
215    pub fn indexed(indices: Range<u32>) -> Self {
216        Self::Indexed {
217            indices,
218            base_vertex: 0,
219            instances: 0..1,
220        }
221    }
222
223    /// Draw `count` instances instead of one.
224    pub fn with_instances(self, instances: Range<u32>) -> Self {
225        match self {
226            Self::Vertices { vertices, .. } => Self::Vertices {
227                vertices,
228                instances,
229            },
230            Self::Indexed {
231                indices,
232                base_vertex,
233                ..
234            } => Self::Indexed {
235                indices,
236                base_vertex,
237                instances,
238            },
239        }
240    }
241
242    /// Offset every fetched vertex index by `base_vertex` (indexed draws
243    /// only; a no-op for non-indexed draws).
244    pub fn with_base_vertex(self, base_vertex: i32) -> Self {
245        match self {
246            Self::Indexed {
247                indices, instances, ..
248            } => Self::Indexed {
249                indices,
250                base_vertex,
251                instances,
252            },
253            other => other,
254        }
255    }
256}
257
258/// Where a draw sits in the composite of a pass's scenes.
259///
260/// [`Scene::record`] runs a scene's draws in the order they are listed and
261/// reads none of this. [`Scene::record_composited`] groups the draws of all
262/// of its scenes: the [sequenced](DrawOrder::Before) ones first in scene
263/// order, then the [sorted](DrawOrder::Sorted) ones of every scene together,
264/// ordered back-to-front by their depth, then the
265/// [trailing](DrawOrder::After) ones in scene order.
266#[derive(Clone, Copy, Debug, PartialEq)]
267pub enum DrawOrder {
268    /// Draw in sequence, ahead of the sorted pool. The default: opaque
269    /// geometry and anything whose listed order is its own business stays
270    /// exactly where its scene put it.
271    Before,
272    /// Join the pass's depth-sorted pool at `depth`, measured along the
273    /// camera's view axis — `Camera::view_depth` in `unlit3d` reports the
274    /// same quantity: positive into the screen, so the farthest draw sorts
275    /// first. The pool holds the sorted draws of every scene, which is what
276    /// lets blended geometry composite by depth instead of by which scene
277    /// recorded first. A `NaN` depth is ordered by [`f32::total_cmp`],
278    /// which places it relative to the finite depths by the sign bit its
279    /// payload carries; the order is total, so it cannot poison the sort.
280    Sorted(f32),
281    /// Draw in sequence, behind the sorted pool: UI and overlays that sit
282    /// above the sorted draws whatever their depth. Clipped draws belong
283    /// here too, where nothing unclipped follows them — see
284    /// [`DrawEntry::scissor`].
285    After,
286}
287
288/// One draw: the pipeline and every resource it binds, followed by what to
289/// draw.
290///
291/// A draw carries the union of the bind groups the pipeline, its material and
292/// the mesh need, at the slots they name. The renderer's built-in pipeline
293/// expects three or four vertex buffers — position, optional joints and
294/// weights, UV and vertex color, and the per-instance model matrix and base
295/// color — but the shape is generic, so custom pipelines can bind whatever
296/// they declare.
297///
298/// All slots are inline: a draw never allocates, so a whole scene can be
299/// rebuilt each frame without touching the allocator.
300#[derive(Clone, Debug)]
301pub struct DrawEntry {
302    /// The render pipeline to bind.
303    pub pipeline: wgpu::RenderPipeline,
304    /// Bind groups, bound at the slots they name. The built-in pipeline uses
305    /// index 0 for the camera, frame globals and mesh metadata, index 1 for
306    /// the material, and any remaining slots for mesh-level groups.
307    pub bind_groups: ArrayVec<BindGroupBinding, MAX_BIND_GROUPS>,
308    /// Vertex buffers, bound at the slots they name.
309    pub vertex_buffers: ArrayVec<VertexBufferBinding, MAX_VERTEX_BUFFERS>,
310    /// Index buffer, the byte range read from it and its format, when the draw
311    /// is indexed.
312    pub index_buffer: Option<(wgpu::Buffer, Range<u64>, wgpu::IndexFormat)>,
313    /// Pixels outside this rectangle are discarded.
314    ///
315    /// A draw that sets it clips in the rasterizer instead of in the geometry.
316    /// `None` means "leave whatever the pass already has", **not** "cover the
317    /// whole target": wgpu's scissor is pass state with no reset call, so
318    /// once a draw sets one it stays set for every later draw in the pass —
319    /// ordering a clipped draw last is the caller's job, which in a composite
320    /// is what [`DrawOrder::After`] is for.
321    pub scissor: Option<ScissorRect>,
322    /// The stencil reference value the draw tests against.
323    ///
324    /// Set on every draw (defaulting to `0`, the pass's own default), so a
325    /// draw that does not name one resets a value an earlier draw left behind.
326    pub stencil_reference: u32,
327    /// Where the draw sits in the composite of a pass's scenes.
328    ///
329    /// [`Scene::record`] ignores it. [`Scene::record_composited`] groups
330    /// its scenes' draws by it; the default, [`DrawOrder::Before`], keeps
331    /// the draw where its scene listed it.
332    pub order: DrawOrder,
333    /// What to draw.
334    pub range: DrawRange,
335}
336
337impl DrawEntry {
338    /// A draw of `range` with the given pipeline, no bind groups, no vertex
339    /// buffers, no index buffer, no scissor and a zero stencil reference; fill
340    /// in the fields the pipeline needs.
341    pub fn new(pipeline: &wgpu::RenderPipeline, range: DrawRange) -> Self {
342        Self {
343            pipeline: pipeline.clone(),
344            bind_groups: ArrayVec::new(),
345            vertex_buffers: ArrayVec::new(),
346            index_buffer: None,
347            scissor: None,
348            stencil_reference: 0,
349            order: DrawOrder::Before,
350            range,
351        }
352    }
353
354    /// Bind `bind_group` at `index`.
355    pub fn with_bind_group(mut self, index: u32, bind_group: &wgpu::BindGroup) -> Self {
356        self.bind_groups.push((index, bind_group.clone()));
357        self
358    }
359
360    /// Bind `buffer` whole at vertex-buffer `slot`.
361    pub fn with_vertex_buffer(mut self, slot: u32, buffer: &wgpu::Buffer) -> Self {
362        self.vertex_buffers
363            .push((slot, buffer.clone(), 0..buffer.size()));
364        self
365    }
366
367    /// Bind `range` of `buffer` at vertex-buffer `slot`.
368    pub fn with_vertex_buffer_range(
369        mut self,
370        slot: u32,
371        buffer: &wgpu::Buffer,
372        range: Range<u64>,
373    ) -> Self {
374        self.vertex_buffers.push((slot, buffer.clone(), range));
375        self
376    }
377
378    /// Discard fragments outside `scissor`. It stays set for every draw after
379    /// this one in the pass, so see [`DrawEntry::scissor`].
380    pub fn with_scissor(mut self, scissor: ScissorRect) -> Self {
381        self.scissor = Some(scissor);
382        self
383    }
384
385    /// Bind `buffer` as the index buffer, reading all of it.
386    pub fn with_index_buffer(mut self, buffer: &wgpu::Buffer, format: wgpu::IndexFormat) -> Self {
387        self.index_buffer = Some((buffer.clone(), 0..buffer.size(), format));
388        self
389    }
390
391    /// Place the draw in the composite of a pass's scenes; see [`DrawOrder`].
392    pub fn with_order(mut self, order: DrawOrder) -> Self {
393        self.order = order;
394        self
395    }
396}
397
398/// A frame's worth of draws.
399///
400/// The order of [`DrawEntry`]s is the order they are recorded in. Recording
401/// compares every `set_*` against the state the previous draw left behind, so
402/// consecutive draws that share a pipeline or bind group skip the redundant
403/// call: ordering draws by pipeline, and then so that neighbours share their
404/// bind groups, minimizes state changes without changing what is drawn. A
405/// caller is also free to order the draws for opaque-then-transparent
406/// compositing, which is a correctness requirement rather than a performance
407/// one; across scenes, [`DrawOrder`] and [`Scene::record_composited`]
408/// take that job over.
409#[derive(Clone, Debug, Default)]
410pub struct Scene {
411    /// The draws to run, in order.
412    pub draws: Vec<DrawEntry>,
413    /// The region of the target every draw is recorded into, or `None` to
414    /// leave the pass's own viewport alone.
415    ///
416    /// Whole-scene state rather than per-draw: wgpu's viewport has no reset
417    /// call, so a scene that letterboxes itself states one here and a scene
418    /// that draws full-target states none.
419    pub viewport: Option<ViewportRect>,
420}
421
422impl Scene {
423    /// An empty scene.
424    pub fn new() -> Self {
425        Self::default()
426    }
427
428    /// The region the scene records into when `fallback` may stand in for the
429    /// viewport the scene itself states.
430    ///
431    /// Split out from [`Scene::record`] so the choice is testable without a
432    /// render pass: the recording needs a real one, and which region a scene
433    /// ends up with is the rule that matters.
434    fn resolved_viewport(&self, fallback: Option<ViewportRect>) -> Option<ViewportRect> {
435        self.viewport.or(fallback)
436    }
437
438    /// Record the scene into `pass`, drawing into `fallback` when the scene
439    /// states no viewport of its own.
440    ///
441    /// wgpu has no call that resets a viewport, so a caller recording several
442    /// scenes into one pass has to pass the whole target as `fallback`: a scene
443    /// that states none would otherwise inherit the region the previous scene
444    /// set. `None` leaves the pass's region exactly as it is, for a caller that
445    /// set one itself.
446    pub fn record(&self, pass: &mut wgpu::RenderPass<'_>, fallback: Option<ViewportRect>) {
447        let mut state = PassState::default();
448        if let Some(viewport) = self.resolved_viewport(fallback) {
449            state.set_viewport(pass, viewport);
450        }
451        for draw in &self.draws {
452            record_draw(pass, &mut state, draw);
453        }
454    }
455
456    /// Record the draws of several scenes into `pass` as one composite,
457    /// drawing into `fallback` where a scene states no viewport of its own.
458    ///
459    /// The scenes' draws are grouped by their [`DrawOrder`] rather than kept
460    /// in scene order: the sequenced ones of every scene first, then the
461    /// depth-sorted ones of every scene pooled together back-to-front, then
462    /// the trailing ones. The pool spanning scenes is what lets blended
463    /// geometry from one source composite by depth against another's, instead
464    /// of by which scene recorded first. A scene whose draws state no order
465    /// keeps its place, so a caller that never tags a draw sees the frame
466    /// [`Scene::record`] would have given.
467    ///
468    /// Every draw draws into the region its own scene resolves to — the
469    /// scene's [`Scene::viewport`] or `fallback` — so the pool interleaving
470    /// scenes never moves a draw out of its scene's letterbox; the
471    /// deduplicated `set_*` recording keeps the repeated regions from
472    /// repeating the calls.
473    pub fn record_composited(
474        pass: &mut wgpu::RenderPass<'_>,
475        scenes: &[&Scene],
476        fallback: Option<ViewportRect>,
477    ) {
478        let mut state = PassState::default();
479        record_composited_into(pass, &mut state, scenes, fallback);
480    }
481}
482
483/// Records one draw: the state it changes, then the draw call itself.
484///
485/// The body of [`Scene::record`]'s loop, split out so the composite recorder
486/// runs the same state-reuse rules over an interleaving of several scenes'
487/// draws.
488fn record_draw<'a, P: RenderPassInterface<'a>>(
489    pass: &mut P,
490    state: &mut PassState<'a>,
491    draw: &'a DrawEntry,
492) {
493    if state.pipeline != Some(&draw.pipeline) {
494        pass.set_pipeline(&draw.pipeline);
495        state.pipeline = Some(&draw.pipeline);
496    }
497    for (index, bind_group) in &draw.bind_groups {
498        state.set_bind_group(pass, *index, bind_group);
499    }
500    for (slot, buffer, range) in &draw.vertex_buffers {
501        state.set_vertex_buffer(pass, *slot, buffer, range.clone());
502    }
503    if let Some((buffer, range, format)) = &draw.index_buffer {
504        state.set_index_buffer(pass, buffer, range.clone(), *format);
505    }
506    if let Some(scissor) = &draw.scissor {
507        state.set_scissor(pass, *scissor);
508    }
509    state.set_stencil_reference(pass, draw.stencil_reference);
510
511    match &draw.range {
512        DrawRange::Vertices {
513            vertices,
514            instances,
515        } => pass.draw(vertices.clone(), instances.clone()),
516        DrawRange::Indexed {
517            indices,
518            base_vertex,
519            instances,
520        } => pass.draw_indexed(indices.clone(), *base_vertex, instances.clone()),
521    }
522}
523
524/// A depth-sorted draw waiting for the pool to be complete.
525///
526/// The pool interleaves scenes, so the draw carries the region its own scene
527/// resolves to rather than relying on the pass holding it.
528struct SortedDraw<'a> {
529    draw: &'a DrawEntry,
530    depth: f32,
531    viewport: Option<ViewportRect>,
532}
533
534/// The composite recorder behind [`Scene::record_composited`], generic over
535/// the pass so tests can drive it with a mock.
536///
537/// Three linear passes over the scenes, and a sort over only the pool: the
538/// sequenced and trailing draws are emitted in scene order without being
539/// compared, so an untagged scene's batching survives the composite, and a
540/// frame whose draws all state no order never sorts at all.
541fn record_composited_into<'a, P: RenderPassInterface<'a>>(
542    pass: &mut P,
543    state: &mut PassState<'a>,
544    scenes: &[&'a Scene],
545    fallback: Option<ViewportRect>,
546) {
547    // The sequenced draws first, in scene order: opaque geometry keeps the
548    // order its scene listed, which is the order its batching is arranged
549    // for.
550    for scene in scenes {
551        let viewport = scene.resolved_viewport(fallback);
552        for draw in &scene.draws {
553            match draw.order {
554                DrawOrder::Before => {
555                    if let Some(viewport) = viewport {
556                        state.set_viewport(pass, viewport);
557                    }
558                    record_draw(pass, state, draw);
559                }
560                DrawOrder::Sorted(_) | DrawOrder::After => {}
561            }
562        }
563    }
564
565    // The depth-sorted pool: the sorted draws of every scene, ordered
566    // back-to-front. Farthest first is the order translucent colours layer
567    // in; the sort is stable, so draws that tie on depth keep the order
568    // their scenes listed them in.
569    let mut pool: Vec<SortedDraw<'_>> = Vec::new();
570    for scene in scenes {
571        for draw in &scene.draws {
572            if let DrawOrder::Sorted(depth) = draw.order {
573                pool.push(SortedDraw {
574                    draw,
575                    depth,
576                    viewport: scene.resolved_viewport(fallback),
577                });
578            }
579        }
580    }
581    pool.sort_by(|a, b| b.depth.total_cmp(&a.depth));
582    for sorted in &pool {
583        if let Some(viewport) = sorted.viewport {
584            state.set_viewport(pass, viewport);
585        }
586        record_draw(pass, state, sorted.draw);
587    }
588
589    // The trailing draws last, in scene order: UI and overlays sit above the
590    // sorted pool whatever their depth, and a clipped draw recorded here is
591    // followed by nothing its rectangle could leak into.
592    for scene in scenes {
593        let viewport = scene.resolved_viewport(fallback);
594        for draw in &scene.draws {
595            match draw.order {
596                DrawOrder::After => {
597                    if let Some(viewport) = viewport {
598                        state.set_viewport(pass, viewport);
599                    }
600                    record_draw(pass, state, draw);
601                }
602                DrawOrder::Before | DrawOrder::Sorted(_) => {}
603            }
604        }
605    }
606}
607
608/// Maximum number of bind-group slots a pass is tracked for.
609///
610/// The largest `max_bind_groups` a WebGPU device can advertise for this
611/// renderer; exceeding it is a programming error caught while recording.
612pub const MAX_BIND_GROUPS: usize = 8;
613
614/// Maximum number of vertex-buffer slots a pass is tracked for.
615pub const MAX_VERTEX_BUFFERS: usize = 16;
616
617/// The render-pass operations [`PassState`] records through.
618///
619/// [`wgpu::RenderPass`] is the production implementation. Tests implement it
620/// with a mock that counts calls, so the state-reuse rules — a `set_*` whose
621/// target is already bound is skipped — can be checked without asking wgpu to
622/// expose its command stream.
623trait RenderPassInterface<'a> {
624    /// Bind `bind_group` at `index`.
625    fn set_bind_group(&mut self, index: u32, bind_group: &'a wgpu::BindGroup);
626    /// Bind `range` of `buffer` at vertex-buffer `slot`.
627    fn set_vertex_buffer(&mut self, slot: u32, buffer: &'a wgpu::Buffer, range: Range<u64>);
628    /// Bind `range` of `buffer` as the index buffer.
629    fn set_index_buffer(
630        &mut self,
631        buffer: &'a wgpu::Buffer,
632        range: Range<u64>,
633        format: wgpu::IndexFormat,
634    );
635    /// Set the scissor rectangle.
636    fn set_scissor_rect(&mut self, scissor: ScissorRect);
637    /// Set the viewport.
638    fn set_viewport_rect(&mut self, viewport: ViewportRect);
639    /// Set the stencil reference.
640    fn set_stencil_reference(&mut self, reference: u32);
641    /// Bind the pipeline the draws rasterize with.
642    fn set_pipeline(&mut self, pipeline: &'a wgpu::RenderPipeline);
643    /// Draw a non-indexed range of vertices.
644    fn draw(&mut self, vertices: Range<u32>, instances: Range<u32>);
645    /// Draw an indexed range of vertices.
646    fn draw_indexed(&mut self, indices: Range<u32>, base_vertex: i32, instances: Range<u32>);
647}
648
649impl<'a, 'p> RenderPassInterface<'a> for wgpu::RenderPass<'p> {
650    fn set_bind_group(&mut self, index: u32, bind_group: &'a wgpu::BindGroup) {
651        wgpu::RenderPass::set_bind_group(self, index, bind_group, &[]);
652    }
653
654    fn set_vertex_buffer(&mut self, slot: u32, buffer: &'a wgpu::Buffer, range: Range<u64>) {
655        wgpu::RenderPass::set_vertex_buffer(self, slot, buffer.slice(range));
656    }
657
658    fn set_index_buffer(
659        &mut self,
660        buffer: &'a wgpu::Buffer,
661        range: Range<u64>,
662        format: wgpu::IndexFormat,
663    ) {
664        wgpu::RenderPass::set_index_buffer(self, buffer.slice(range), format);
665    }
666
667    fn set_scissor_rect(&mut self, scissor: ScissorRect) {
668        wgpu::RenderPass::set_scissor_rect(
669            self,
670            scissor.x,
671            scissor.y,
672            scissor.width,
673            scissor.height,
674        );
675    }
676
677    fn set_viewport_rect(&mut self, viewport: ViewportRect) {
678        wgpu::RenderPass::set_viewport(
679            self,
680            viewport.x,
681            viewport.y,
682            viewport.width,
683            viewport.height,
684            viewport.min_depth,
685            viewport.max_depth,
686        );
687    }
688
689    fn set_stencil_reference(&mut self, reference: u32) {
690        wgpu::RenderPass::set_stencil_reference(self, reference);
691    }
692
693    fn set_pipeline(&mut self, pipeline: &'a wgpu::RenderPipeline) {
694        wgpu::RenderPass::set_pipeline(self, pipeline);
695    }
696
697    fn draw(&mut self, vertices: Range<u32>, instances: Range<u32>) {
698        wgpu::RenderPass::draw(self, vertices, instances);
699    }
700
701    fn draw_indexed(&mut self, indices: Range<u32>, base_vertex: i32, instances: Range<u32>) {
702        wgpu::RenderPass::draw_indexed(self, indices, base_vertex, instances);
703    }
704}
705
706/// Which resources the previous draw left bound.
707///
708/// The entries borrow from the draws being recorded, which outlive the
709/// recording, so tracking them costs no clones. `wgpu` resources compare by
710/// identity, so a plain `==` is the fast "is this the same resource as last
711/// time" test the recording loop needs. The slots are fixed-capacity
712/// ([`MAX_BIND_GROUPS`] / [`MAX_VERTEX_BUFFERS`]) so recording a frame never
713/// allocates.
714#[derive(Default)]
715struct PassState<'a> {
716    pipeline: Option<&'a wgpu::RenderPipeline>,
717    bind_groups: ArrayVec<(u32, &'a wgpu::BindGroup), MAX_BIND_GROUPS>,
718    vertex_buffers: ArrayVec<(u32, &'a wgpu::Buffer, Range<u64>), MAX_VERTEX_BUFFERS>,
719    index_buffer: Option<(&'a wgpu::Buffer, Range<u64>, wgpu::IndexFormat)>,
720    scissor: Option<ScissorRect>,
721    viewport: Option<ViewportRect>,
722    stencil_reference: u32,
723}
724
725impl<'a> PassState<'a> {
726    fn set_bind_group(
727        &mut self,
728        pass: &mut impl RenderPassInterface<'a>,
729        index: u32,
730        bind_group: &'a wgpu::BindGroup,
731    ) {
732        if let Some(slot) = self
733            .bind_groups
734            .iter_mut()
735            .find(|(bound, _)| *bound == index)
736        {
737            if slot.1 == bind_group {
738                return;
739            }
740            slot.1 = bind_group;
741        } else {
742            assert!(
743                index < MAX_BIND_GROUPS as u32,
744                "bind group index {index} exceeds MAX_BIND_GROUPS ({MAX_BIND_GROUPS})"
745            );
746            self.bind_groups.push((index, bind_group));
747        }
748        pass.set_bind_group(index, bind_group);
749    }
750
751    /// Vertex-buffer slots are keyed by slot, and their value is the buffer
752    /// and the byte range bound to it.
753    fn set_vertex_buffer(
754        &mut self,
755        pass: &mut impl RenderPassInterface<'a>,
756        slot: u32,
757        buffer: &'a wgpu::Buffer,
758        range: Range<u64>,
759    ) {
760        if let Some(bound) = self
761            .vertex_buffers
762            .iter_mut()
763            .find(|(bound, _, _)| *bound == slot)
764        {
765            if core::ptr::eq(bound.1, buffer) && bound.2 == range {
766                return;
767            }
768            *bound = (slot, buffer, range.clone());
769        } else {
770            assert!(
771                slot < MAX_VERTEX_BUFFERS as u32,
772                "vertex buffer slot {slot} exceeds MAX_VERTEX_BUFFERS ({MAX_VERTEX_BUFFERS})"
773            );
774            self.vertex_buffers.push((slot, buffer, range.clone()));
775        }
776        pass.set_vertex_buffer(slot, buffer, range);
777    }
778
779    fn set_index_buffer(
780        &mut self,
781        pass: &mut impl RenderPassInterface<'a>,
782        buffer: &'a wgpu::Buffer,
783        range: Range<u64>,
784        format: wgpu::IndexFormat,
785    ) {
786        if let Some((bound, bound_range, bound_format)) = &self.index_buffer
787            && core::ptr::eq(*bound, buffer)
788            && *bound_range == range
789            && *bound_format == format
790        {
791            return;
792        }
793        pass.set_index_buffer(buffer, range.clone(), format);
794        self.index_buffer = Some((buffer, range, format));
795    }
796
797    /// Narrow rasterization to `scissor`, skipping the call when the pass
798    /// already has that rectangle — a clip rectangle is usually shared by
799    /// many consecutive draws.
800    fn set_scissor(&mut self, pass: &mut impl RenderPassInterface<'a>, scissor: ScissorRect) {
801        if self.scissor == Some(scissor) {
802            return;
803        }
804        self.scissor = Some(scissor);
805        pass.set_scissor_rect(scissor);
806    }
807
808    /// Set the viewport, skipping the call when the pass already holds that
809    /// rectangle.
810    fn set_viewport(&mut self, pass: &mut impl RenderPassInterface<'a>, viewport: ViewportRect) {
811        if self.viewport == Some(viewport) {
812            return;
813        }
814        self.viewport = Some(viewport);
815        pass.set_viewport_rect(viewport);
816    }
817
818    /// Set the stencil reference, skipping the call when the pass already
819    /// holds it.
820    fn set_stencil_reference(&mut self, pass: &mut impl RenderPassInterface<'a>, reference: u32) {
821        if self.stencil_reference == reference {
822            return;
823        }
824        self.stencil_reference = reference;
825        pass.set_stencil_reference(reference);
826    }
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832
833    /// The smallest shader that compiles: a vertex stage returning the origin
834    /// and a fragment stage returning opaque white.
835    const MINIMAL_WGSL: &str = r#"
836@vertex
837fn vs_main() -> @builtin(position) vec4<f32> {
838    return vec4<f32>(0.0, 0.0, 0.0, 1.0);
839}
840
841@fragment
842fn fs_main() -> @location(0) vec4<f32> {
843    return vec4<f32>(1.0);
844}
845"#;
846
847    /// A render pipeline on the noop device, for builder tests that only need
848    /// a handle to name.
849    fn noop_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {
850        let module = crate::util::shader_module(device, MINIMAL_WGSL, "test::shader");
851        let targets = [Some(wgpu::ColorTargetState {
852            format: wgpu::TextureFormat::Rgba8UnormSrgb,
853            blend: None,
854            write_mask: wgpu::ColorWrites::ALL,
855        })];
856        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
857            label: Some("test::pipeline"),
858            layout: None,
859            vertex: wgpu::VertexState {
860                module: &module,
861                entry_point: Some("vs_main"),
862                compilation_options: Default::default(),
863                buffers: &[],
864            },
865            primitive: Default::default(),
866            depth_stencil: None,
867            multisample: Default::default(),
868            fragment: Some(wgpu::FragmentState {
869                module: &module,
870                entry_point: Some("fs_main"),
871                compilation_options: Default::default(),
872                targets: &targets,
873            }),
874            multiview_mask: None,
875            cache: None,
876        })
877    }
878
879    #[test]
880    fn draw_range_builders_compose() {
881        let range = DrawRange::vertices(0..36).with_instances(2..5);
882        match range {
883            DrawRange::Vertices {
884                vertices,
885                instances,
886            } => {
887                assert_eq!(vertices, 0..36);
888                assert_eq!(instances, 2..5);
889            }
890            other => panic!("expected a non-indexed draw, got {other:?}"),
891        }
892
893        let range = DrawRange::indexed(0..12)
894            .with_base_vertex(7)
895            .with_instances(0..3);
896        match range {
897            DrawRange::Indexed {
898                indices,
899                base_vertex,
900                instances,
901            } => {
902                assert_eq!(indices, 0..12);
903                assert_eq!(base_vertex, 7);
904                assert_eq!(instances, 0..3);
905            }
906            other => panic!("expected an indexed draw, got {other:?}"),
907        }
908    }
909
910    #[test]
911    fn base_vertex_is_ignored_for_non_indexed_draws() {
912        let range = DrawRange::vertices(0..3).with_base_vertex(9);
913        assert!(matches!(range, DrawRange::Vertices { .. }));
914    }
915
916    #[test]
917    fn scissor_rect_builders_compose() {
918        let rect = ScissorRect::new(64, 32).at(8, 16);
919        assert_eq!(
920            rect,
921            ScissorRect {
922                x: 8,
923                y: 16,
924                width: 64,
925                height: 32
926            }
927        );
928        assert_eq!(ScissorRect::new(64, 32), rect.at(0, 0));
929    }
930
931    #[test]
932    fn viewport_rect_builders_compose() {
933        let rect = ViewportRect::new(64.0, 32.0).at(8.5, 16.25);
934        assert_eq!(
935            rect,
936            ViewportRect {
937                x: 8.5,
938                y: 16.25,
939                width: 64.0,
940                height: 32.0,
941                min_depth: 0.0,
942                max_depth: 1.0,
943            }
944        );
945        assert_eq!(
946            rect.with_depth(0.5, 1.0).min_depth,
947            0.5,
948            "the depth range is the caller's to narrow"
949        );
950    }
951
952    /// A letterbox keeps the requested aspect and stays inside the target,
953    /// centred, whichever way the target and the aspect differ.
954    #[test]
955    fn fitting_an_aspect_letterboxes_within_the_target() {
956        // A 4:3 target is the aspect itself: no bars, no offset.
957        assert_eq!(
958            ViewportRect::fit_aspect(4.0 / 3.0, 800, 600),
959            ViewportRect::new(800.0, 600.0)
960        );
961
962        // A wider target gets bars at the sides: the height is the constraint,
963        // and the rectangle is centred horizontally.
964        let wide = ViewportRect::fit_aspect(4.0 / 3.0, 1600, 600);
965        assert_eq!(wide, ViewportRect::new(800.0, 600.0).at(400.0, 0.0));
966        assert!((wide.width / wide.height - 4.0 / 3.0).abs() < 1e-5);
967        assert_eq!(wide.x + wide.width + wide.x, 1600.0, "centred horizontally");
968        assert_eq!(wide.y, 0.0);
969
970        // A narrower target gets bars above and below, centred vertically.
971        let tall = ViewportRect::fit_aspect(4.0 / 3.0, 600, 900);
972        assert_eq!(tall, ViewportRect::new(600.0, 450.0).at(0.0, 225.0));
973        assert!((tall.width / tall.height - 4.0 / 3.0).abs() < 1e-5);
974        assert_eq!(tall.y + tall.height + tall.y, 900.0, "centred vertically");
975        assert_eq!(tall.x, 0.0);
976
977        // The depth range is left at the whole of clip space.
978        assert_eq!((tall.min_depth, tall.max_depth), (0.0, 1.0));
979    }
980
981    /// A target that cannot hold a letterbox — a degenerate one, or an aspect
982    /// that is not a positive number — comes back whole rather than empty.
983    #[test]
984    fn an_impossible_letterbox_is_the_whole_target() {
985        for aspect in [0.0, -1.0, f32::NAN, f32::INFINITY] {
986            assert_eq!(
987                ViewportRect::fit_aspect(aspect, 320, 240),
988                ViewportRect::new(320.0, 240.0),
989                "aspect {aspect} has no rectangle to fit"
990            );
991        }
992        for target in [(0, 0), (0, 240), (320, 0)] {
993            let fitted = ViewportRect::fit_aspect(4.0 / 3.0, target.0, target.1);
994            assert!(fitted.width >= 0.0 && fitted.height >= 0.0);
995            assert!(fitted.width.is_finite() && fitted.height.is_finite());
996        }
997    }
998
999    /// A scene's own viewport is recorded, and a scene that states none leaves
1000    /// the pass's region to whoever set it.
1001    #[test]
1002    fn a_scene_records_only_the_viewport_it_states() {
1003        let own = ViewportRect::new(320.0, 180.0).at(0.0, 20.0);
1004        let scene = Scene {
1005            viewport: Some(own),
1006            ..Scene::default()
1007        };
1008        assert_eq!(scene.viewport, Some(own));
1009
1010        // A scene that never states one issues no viewport call of its own: the
1011        // pass keeps whatever region it holds.
1012        let plain = Scene::new();
1013        assert_eq!(plain.viewport, None);
1014        assert_eq!(plain.resolved_viewport(None), None);
1015    }
1016
1017    /// A fallback region stands in only for a scene that states no viewport of
1018    /// its own, which is what keeps one scene's letterbox from cropping the
1019    /// next one's.
1020    #[test]
1021    fn a_fallback_viewport_fills_in_for_a_scene_that_states_none() {
1022        let whole = ViewportRect::new(800.0, 600.0);
1023        let own = ViewportRect::new(320.0, 180.0).at(0.0, 20.0);
1024
1025        let plain = Scene::new();
1026        assert_eq!(plain.resolved_viewport(Some(whole)), Some(whole));
1027
1028        // The scene's own region wins over the fallback rather than the two
1029        // being combined.
1030        let letterboxed = Scene {
1031            viewport: Some(own),
1032            ..Scene::default()
1033        };
1034        assert_eq!(letterboxed.resolved_viewport(Some(whole)), Some(own));
1035
1036        // With no fallback either, there is nothing to set.
1037        assert_eq!(letterboxed.resolved_viewport(None), Some(own));
1038    }
1039
1040    /// Setting the same viewport twice records one call, and a later scene
1041    /// that states a different one records again rather than inheriting it.
1042    #[test]
1043    fn viewport_calls_are_deduplicated() {
1044        let mut pass = MockPass::default();
1045        let mut state = PassState::default();
1046        let first = ViewportRect::new(320.0, 180.0);
1047        let second = ViewportRect::new(160.0, 90.0);
1048
1049        state.set_viewport(&mut pass, first);
1050        // The same rectangle again is a no-op: the pass already has it.
1051        state.set_viewport(&mut pass, first);
1052        // A fractional difference is a real one: a centred letterbox rarely
1053        // lands on a pixel boundary.
1054        let nudged = ViewportRect::new(320.0, 180.0).at(0.5, 0.5);
1055        state.set_viewport(&mut pass, nudged);
1056        state.set_viewport(&mut pass, second);
1057
1058        assert_eq!(pass.viewports, vec![first, nudged, second]);
1059        assert_eq!(state.viewport, Some(second));
1060    }
1061
1062    /// The builders thread through a draw unchanged, so a draw keeps every
1063    /// bound resource while adding the scissor.
1064    #[test]
1065    fn draw_builders_keep_the_scissor() {
1066        let (device, _queue) = crate::util::test::noop_device();
1067        let pipeline = noop_pipeline(&device);
1068        let draw = DrawEntry::new(&pipeline, DrawRange::indexed(0..6))
1069            .with_scissor(ScissorRect::new(8, 8));
1070        assert_eq!(draw.scissor, Some(ScissorRect::new(8, 8)));
1071        assert!(matches!(
1072            draw.range,
1073            DrawRange::Indexed { ref indices, .. } if *indices == (0..6)
1074        ));
1075
1076        // A draw that never sets one leaves the pass's rectangle alone.
1077        let plain = DrawEntry::new(&pipeline, DrawRange::vertices(0..3));
1078        assert_eq!(plain.scissor, None);
1079    }
1080
1081    /// The stencil reference defaults to zero and the builder sets it without
1082    /// disturbing the rest of the draw.
1083    #[test]
1084    fn stencil_reference_builder_composes() {
1085        let (device, _queue) = crate::util::test::noop_device();
1086        let pipeline = noop_pipeline(&device);
1087        let mut draw = DrawEntry::new(&pipeline, DrawRange::indexed(0..6))
1088            .with_scissor(ScissorRect::new(8, 8));
1089        draw.stencil_reference = 3;
1090        assert_eq!(draw.stencil_reference, 3);
1091        assert_eq!(draw.scissor, Some(ScissorRect::new(8, 8)));
1092
1093        let plain = DrawEntry::new(&pipeline, DrawRange::vertices(0..3));
1094        assert_eq!(plain.stencil_reference, 0);
1095    }
1096
1097    /// A mock [`RenderPassInterface`] that records the calls it receives, so a
1098    /// test can assert exactly which `set_*` calls `PassState` skipped.
1099    /// Recording needs no device and no render pass of its own.
1100    #[derive(Default)]
1101    struct MockPass {
1102        bind_groups: Vec<u32>,
1103        vertex_buffers: Vec<u32>,
1104        index_buffers: Vec<wgpu::IndexFormat>,
1105        scissors: Vec<ScissorRect>,
1106        viewports: Vec<ViewportRect>,
1107        stencil_references: Vec<u32>,
1108        draws: Vec<(Range<u32>, Range<u32>)>,
1109        indexed_draws: Vec<(Range<u32>, i32, Range<u32>)>,
1110    }
1111
1112    impl<'a> RenderPassInterface<'a> for MockPass {
1113        fn set_bind_group(&mut self, index: u32, _bind_group: &'a wgpu::BindGroup) {
1114            self.bind_groups.push(index);
1115        }
1116
1117        fn set_vertex_buffer(&mut self, slot: u32, _buffer: &'a wgpu::Buffer, _range: Range<u64>) {
1118            self.vertex_buffers.push(slot);
1119        }
1120
1121        fn set_index_buffer(
1122            &mut self,
1123            _buffer: &'a wgpu::Buffer,
1124            _range: Range<u64>,
1125            format: wgpu::IndexFormat,
1126        ) {
1127            self.index_buffers.push(format);
1128        }
1129
1130        fn set_scissor_rect(&mut self, scissor: ScissorRect) {
1131            self.scissors.push(scissor);
1132        }
1133
1134        fn set_viewport_rect(&mut self, viewport: ViewportRect) {
1135            self.viewports.push(viewport);
1136        }
1137
1138        fn set_stencil_reference(&mut self, reference: u32) {
1139            self.stencil_references.push(reference);
1140        }
1141
1142        fn set_pipeline(&mut self, _pipeline: &'a wgpu::RenderPipeline) {}
1143
1144        fn draw(&mut self, vertices: Range<u32>, instances: Range<u32>) {
1145            self.draws.push((vertices, instances));
1146        }
1147
1148        fn draw_indexed(&mut self, indices: Range<u32>, base_vertex: i32, instances: Range<u32>) {
1149            self.indexed_draws.push((indices, base_vertex, instances));
1150        }
1151    }
1152
1153    /// A repeated scissor rectangle is recorded once; a different one after it
1154    /// again — so a draw never silently inherits a stale clip.
1155    #[test]
1156    fn scissor_calls_are_deduplicated() {
1157        let mut pass = MockPass::default();
1158        let mut state = PassState::default();
1159        let first = ScissorRect::new(16, 8).at(4, 2);
1160        let second = ScissorRect::new(16, 8).at(0, 0);
1161
1162        state.set_scissor(&mut pass, first);
1163        // The same rectangle again is a no-op: the pass already has it.
1164        state.set_scissor(&mut pass, first);
1165        state.set_scissor(&mut pass, second);
1166
1167        assert_eq!(pass.scissors, vec![first, second]);
1168        assert_eq!(state.scissor, Some(second));
1169    }
1170
1171    /// The stencil reference is recorded once per distinct value, so a value
1172    /// an earlier draw left behind never leaks into one that names none.
1173    #[test]
1174    fn stencil_reference_calls_are_deduplicated() {
1175        let mut pass = MockPass::default();
1176        let mut state = PassState::default();
1177
1178        state.set_stencil_reference(&mut pass, 3);
1179        // The same value again is a no-op: the pass already holds it.
1180        state.set_stencil_reference(&mut pass, 3);
1181        // A later draw that names none resets it to zero.
1182        state.set_stencil_reference(&mut pass, 0);
1183
1184        assert_eq!(pass.stencil_references, vec![3, 0]);
1185        assert_eq!(state.stencil_reference, 0);
1186    }
1187
1188    /// Bind groups, vertex buffers and index buffers are recorded once per
1189    /// distinct target too. The handles are real — identity is what the
1190    /// dedup compares — but the pass is the mock, so the calls are visible.
1191    #[test]
1192    fn bound_resources_are_deduplicated() {
1193        let (device, _queue) = crate::util::test::noop_device();
1194        let buffer = device.create_buffer(&wgpu::BufferDescriptor {
1195            label: Some("test::buffer"),
1196            size: 64,
1197            usage: wgpu::BufferUsages::VERTEX
1198                | wgpu::BufferUsages::INDEX
1199                | wgpu::BufferUsages::UNIFORM,
1200            mapped_at_creation: false,
1201        });
1202        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1203            label: Some("test::layout"),
1204            entries: &[wgpu::BindGroupLayoutEntry {
1205                binding: 0,
1206                visibility: wgpu::ShaderStages::VERTEX,
1207                ty: wgpu::BindingType::Buffer {
1208                    ty: wgpu::BufferBindingType::Uniform,
1209                    has_dynamic_offset: false,
1210                    min_binding_size: wgpu::BufferSize::new(buffer.size()),
1211                },
1212                count: None,
1213            }],
1214        });
1215        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1216            label: Some("test::bind_group"),
1217            layout: &layout,
1218            entries: &[wgpu::BindGroupEntry {
1219                binding: 0,
1220                resource: buffer.as_entire_binding(),
1221            }],
1222        });
1223
1224        let mut pass = MockPass::default();
1225        let mut state = PassState::default();
1226        let whole = 0..buffer.size();
1227        let head = 0..32;
1228        let tail = 32..64;
1229
1230        state.set_bind_group(&mut pass, 0, &bind_group);
1231        state.set_bind_group(&mut pass, 0, &bind_group);
1232        state.set_bind_group(&mut pass, 3, &bind_group);
1233
1234        state.set_vertex_buffer(&mut pass, 0, &buffer, whole.clone());
1235        state.set_vertex_buffer(&mut pass, 0, &buffer, whole.clone());
1236        // The same buffer under a different range is a different binding, so
1237        // the call is not skipped.
1238        state.set_vertex_buffer(&mut pass, 0, &buffer, head.clone());
1239        state.set_vertex_buffer(&mut pass, 2, &buffer, whole.clone());
1240
1241        state.set_index_buffer(&mut pass, &buffer, head.clone(), wgpu::IndexFormat::Uint16);
1242        state.set_index_buffer(&mut pass, &buffer, head.clone(), wgpu::IndexFormat::Uint16);
1243        // A different range and a different format each re-bind.
1244        state.set_index_buffer(&mut pass, &buffer, tail.clone(), wgpu::IndexFormat::Uint16);
1245        state.set_index_buffer(&mut pass, &buffer, tail.clone(), wgpu::IndexFormat::Uint32);
1246
1247        assert_eq!(pass.bind_groups, vec![0, 3]);
1248        // Slot 0 is bound whole, skipped as a repeat, re-bound for the
1249        // narrower range, then slot 2 is bound for the first time.
1250        assert_eq!(pass.vertex_buffers, vec![0, 0, 2]);
1251        assert_eq!(
1252            pass.index_buffers,
1253            vec![
1254                wgpu::IndexFormat::Uint16,
1255                wgpu::IndexFormat::Uint16,
1256                wgpu::IndexFormat::Uint32
1257            ]
1258        );
1259    }
1260
1261    #[test]
1262    fn pass_state_capacity_covers_the_wgpu_defaults() {
1263        // The fixed arrays must have room for everything a default device
1264        // accepts, otherwise the recording asserts would fire on valid scenes.
1265        assert!(MAX_BIND_GROUPS >= wgpu::Limits::default().max_bind_groups as usize);
1266        assert!(MAX_VERTEX_BUFFERS >= wgpu::Limits::default().max_vertex_buffers as usize);
1267    }
1268
1269    /// The composite draws the sequenced draws of every scene first, then the
1270    /// depth-sorted ones of every scene pooled together back-to-front, then
1271    /// the trailing ones — which is what lets a blended stroke from one scene
1272    /// sort against a blended pane from another instead of always painting
1273    /// over it.
1274    #[test]
1275    fn a_composite_orders_before_sorted_and_after() {
1276        let (device, _queue) = crate::util::test::noop_device();
1277        let pipeline = noop_pipeline(&device);
1278        let draw = |range: DrawRange| DrawEntry::new(&pipeline, range);
1279
1280        // Meshes: the opaque run, then a z-sorted pane far away and one near.
1281        let meshes = Scene {
1282            draws: vec![
1283                draw(DrawRange::vertices(0..4).with_instances(0..1)),
1284                draw(DrawRange::vertices(0..4).with_instances(1..2))
1285                    .with_order(DrawOrder::Sorted(7.0)),
1286                draw(DrawRange::vertices(0..4).with_instances(2..3))
1287                    .with_order(DrawOrder::Sorted(2.0)),
1288            ],
1289            ..Scene::default()
1290        };
1291        // Lines: an opaque stroke, then a blended one between the panes.
1292        let lines = Scene {
1293            draws: vec![
1294                draw(DrawRange::vertices(0..4).with_instances(3..4)),
1295                draw(DrawRange::vertices(0..4).with_instances(4..5))
1296                    .with_order(DrawOrder::Sorted(4.0)),
1297            ],
1298            ..Scene::default()
1299        };
1300        // UI: a trailing widget.
1301        let ui = Scene {
1302            draws: vec![
1303                draw(DrawRange::vertices(0..4).with_instances(5..6)).with_order(DrawOrder::After),
1304            ],
1305            ..Scene::default()
1306        };
1307
1308        let mut pass = MockPass::default();
1309        record_composited_into(
1310            &mut pass,
1311            &mut PassState::default(),
1312            &[&meshes, &lines, &ui],
1313            None,
1314        );
1315
1316        // Opaque meshes, opaque line, then panes and the stroke farthest
1317        // first, then the widget.
1318        assert_eq!(
1319            pass.draws,
1320            vec![
1321                (0..4, 0..1),
1322                (0..4, 3..4),
1323                (0..4, 1..2),
1324                (0..4, 4..5),
1325                (0..4, 2..3),
1326                (0..4, 5..6),
1327            ]
1328        );
1329    }
1330
1331    /// The sort is stable: sorted draws that tie on depth keep the order
1332    /// their scenes were listed in, and the sequenced and trailing tiers keep
1333    /// scene order around the pool.
1334    #[test]
1335    fn a_composite_keeps_the_order_of_equal_draws() {
1336        let (device, _queue) = crate::util::test::noop_device();
1337        let pipeline = noop_pipeline(&device);
1338        let draw = |range: DrawRange| DrawEntry::new(&pipeline, range);
1339
1340        let meshes = Scene {
1341            draws: vec![
1342                draw(DrawRange::vertices(0..4).with_instances(0..1))
1343                    .with_order(DrawOrder::Sorted(3.0)),
1344                draw(DrawRange::vertices(0..4).with_instances(1..2)),
1345            ],
1346            ..Scene::default()
1347        };
1348        let lines = Scene {
1349            draws: vec![
1350                draw(DrawRange::vertices(0..4).with_instances(2..3))
1351                    .with_order(DrawOrder::Sorted(3.0)),
1352                draw(DrawRange::vertices(0..4).with_instances(3..4)).with_order(DrawOrder::After),
1353            ],
1354            ..Scene::default()
1355        };
1356
1357        let mut pass = MockPass::default();
1358        record_composited_into(
1359            &mut pass,
1360            &mut PassState::default(),
1361            &[&meshes, &lines],
1362            None,
1363        );
1364
1365        assert_eq!(
1366            pass.draws,
1367            vec![(0..4, 1..2), (0..4, 0..1), (0..4, 2..3), (0..4, 3..4)]
1368        );
1369    }
1370
1371    /// A scene that states no order keeps its place: with every draw
1372    /// sequenced, the composite is the draws in scene order, and no depth is
1373    /// ever compared.
1374    #[test]
1375    fn a_composite_without_sorted_draws_keeps_scene_order() {
1376        let (device, _queue) = crate::util::test::noop_device();
1377        let pipeline = noop_pipeline(&device);
1378        let draw = |range: DrawRange| DrawEntry::new(&pipeline, range);
1379
1380        // Listed back to front on purpose: nothing here joins the pool, so
1381        // nothing may be reordered.
1382        let lines = Scene {
1383            draws: vec![draw(DrawRange::vertices(0..4).with_instances(0..1))],
1384            ..Scene::default()
1385        };
1386        let meshes = Scene {
1387            draws: vec![draw(DrawRange::vertices(0..4).with_instances(1..2))],
1388            ..Scene::default()
1389        };
1390
1391        let mut pass = MockPass::default();
1392        record_composited_into(
1393            &mut pass,
1394            &mut PassState::default(),
1395            &[&lines, &meshes],
1396            None,
1397        );
1398
1399        assert_eq!(pass.draws, vec![(0..4, 0..1), (0..4, 1..2)]);
1400    }
1401
1402    /// Every draw draws into the region its own scene resolves to, so the
1403    /// pool interleaving scenes never moves a draw out of its scene's
1404    /// letterbox; identical regions collapse through the usual dedup.
1405    #[test]
1406    fn a_composite_draws_into_each_scenes_own_region() {
1407        let (device, _queue) = crate::util::test::noop_device();
1408        let pipeline = noop_pipeline(&device);
1409        let draw = |range: DrawRange| DrawEntry::new(&pipeline, range);
1410
1411        let letter = ViewportRect::new(320.0, 180.0).at(0.0, 20.0);
1412        let whole = ViewportRect::new(800.0, 600.0);
1413        let meshes = Scene {
1414            viewport: Some(letter),
1415            draws: vec![draw(DrawRange::vertices(0..4).with_instances(0..1))],
1416        };
1417        let lines = Scene {
1418            draws: vec![
1419                draw(DrawRange::vertices(0..4).with_instances(1..2))
1420                    .with_order(DrawOrder::Sorted(3.0)),
1421            ],
1422            ..Scene::default()
1423        };
1424        let ui = Scene {
1425            draws: vec![
1426                draw(DrawRange::vertices(0..4).with_instances(2..3)).with_order(DrawOrder::After),
1427            ],
1428            ..Scene::default()
1429        };
1430
1431        let mut pass = MockPass::default();
1432        record_composited_into(
1433            &mut pass,
1434            &mut PassState::default(),
1435            &[&meshes, &lines, &ui],
1436            Some(whole),
1437        );
1438
1439        assert_eq!(pass.draws.len(), 3);
1440        // The mesh scene states its own region; the line and UI scenes state
1441        // none, so they draw into the fallback — stated once, the second
1442        // being a repeat the pass already holds.
1443        assert_eq!(pass.viewports, vec![letter, whole]);
1444    }
1445}