renderling/stage/
cpu.rs

1//! GPU staging area.
2use core::ops::Deref;
3use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
4use craballoc::prelude::*;
5use crabslab::Id;
6use glam::{Mat4, UVec2, Vec4};
7use snafu::{ResultExt, Snafu};
8use std::sync::{atomic::AtomicBool, Arc, Mutex, RwLock};
9
10use crate::atlas::AtlasTexture;
11use crate::camera::Camera;
12use crate::geometry::{shader::GeometryDescriptor, MorphTarget, Vertex};
13#[cfg(gltf)]
14use crate::gltf::GltfDocument;
15use crate::light::{DirectionalLight, IsLight, Light, PointLight, SpotLight};
16use crate::material::Material;
17use crate::pbr::brdf::BrdfLut;
18use crate::pbr::ibl::Ibl;
19use crate::primitive::Primitive;
20use crate::{
21    atlas::{AtlasError, AtlasImage, AtlasImageError},
22    bindgroup::ManagedBindGroup,
23    bloom::Bloom,
24    camera::shader::CameraDescriptor,
25    debug::DebugOverlay,
26    draw::DrawCalls,
27    geometry::{Geometry, Indices, MorphTargetWeights, MorphTargets, Skin, SkinJoint, Vertices},
28    light::{
29        AnalyticalLight, LightTiling, LightTilingConfig, Lighting, LightingBindGroupLayoutEntries,
30        LightingError, ShadowMap,
31    },
32    material::Materials,
33    pbr::debug::DebugChannel,
34    skybox::{Skybox, SkyboxRenderPipeline},
35    texture::{DepthTexture, Texture},
36    tonemapping::Tonemapping,
37    transform::{NestedTransform, Transform},
38};
39
40/// Enumeration of errors that may be the result of [`Stage`] functions.
41#[derive(Debug, Snafu)]
42pub enum StageError {
43    #[snafu(display("{source}"))]
44    Atlas { source: AtlasError },
45
46    #[snafu(display("{source}"))]
47    Lighting { source: LightingError },
48
49    #[cfg(gltf)]
50    #[snafu(display("{source}"))]
51    Gltf { source: crate::gltf::StageGltfError },
52}
53
54impl From<AtlasError> for StageError {
55    fn from(source: AtlasError) -> Self {
56        Self::Atlas { source }
57    }
58}
59
60impl From<LightingError> for StageError {
61    fn from(source: LightingError) -> Self {
62        Self::Lighting { source }
63    }
64}
65
66#[cfg(gltf)]
67impl From<crate::gltf::StageGltfError> for StageError {
68    fn from(source: crate::gltf::StageGltfError) -> Self {
69        Self::Gltf { source }
70    }
71}
72
73fn create_msaa_textureview(
74    device: &wgpu::Device,
75    width: u32,
76    height: u32,
77    format: wgpu::TextureFormat,
78    sample_count: u32,
79) -> wgpu::TextureView {
80    device
81        .create_texture(&wgpu::TextureDescriptor {
82            label: Some("stage msaa render target"),
83            size: wgpu::Extent3d {
84                width,
85                height,
86                depth_or_array_layers: 1,
87            },
88            mip_level_count: 1,
89            sample_count,
90            dimension: wgpu::TextureDimension::D2,
91            format,
92            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
93            view_formats: &[],
94        })
95        .create_view(&wgpu::TextureViewDescriptor::default())
96}
97
98/// Result of calling [`Stage::commit`].
99pub struct StageCommitResult {
100    pub(crate) geometry_buffer: SlabBuffer<wgpu::Buffer>,
101    pub(crate) lighting_buffer: SlabBuffer<wgpu::Buffer>,
102    pub(crate) materials_buffer: SlabBuffer<wgpu::Buffer>,
103}
104
105impl StageCommitResult {
106    /// Timestamp of the most recently created buffer used by the stage.
107    pub(crate) fn latest_creation_time(&self) -> usize {
108        [
109            &self.geometry_buffer,
110            &self.materials_buffer,
111            &self.lighting_buffer,
112        ]
113        .iter()
114        .map(|buffer| buffer.creation_time())
115        .max()
116        .unwrap_or_default()
117    }
118
119    /// Whether or not the stage's bindgroups need to be invalidated as a result
120    /// of the call to [`Stage::commit`] that produced this `StageCommitResult`.
121    pub(crate) fn should_invalidate(&self, previous_creation_time: usize) -> bool {
122        let mut should = false;
123        if self.geometry_buffer.is_new_this_commit() {
124            log::trace!("geometry buffer is new this frame");
125            should = true;
126        }
127        if self.materials_buffer.is_new_this_commit() {
128            log::trace!("materials buffer is new this frame");
129            should = true;
130        }
131        if self.lighting_buffer.is_new_this_commit() {
132            log::trace!("lighting buffer is new this frame");
133            should = true;
134        }
135        let current = self.latest_creation_time();
136        if current > previous_creation_time {
137            log::trace!(
138                "current latest buffer creation time {current} > previous {previous_creation_time}"
139            );
140            should = true;
141        }
142        should
143    }
144}
145
146/// Bindgroup used to render primitives.
147///
148/// This is the bindgroup that occupies `descriptor_set = 0` in
149/// [crate::primitive::shader::primitive_vertex] and
150/// [crate::primitive::shader::primitive_fragment].
151struct PrimitiveBindGroup<'a> {
152    device: &'a wgpu::Device,
153    layout: &'a wgpu::BindGroupLayout,
154    geometry_buffer: &'a wgpu::Buffer,
155    material_buffer: &'a wgpu::Buffer,
156    light_buffer: &'a wgpu::Buffer,
157    atlas_texture_view: &'a wgpu::TextureView,
158    atlas_texture_sampler: &'a wgpu::Sampler,
159    irradiance_texture_view: &'a wgpu::TextureView,
160    irradiance_texture_sampler: &'a wgpu::Sampler,
161    prefiltered_texture_view: &'a wgpu::TextureView,
162    prefiltered_texture_sampler: &'a wgpu::Sampler,
163    brdf_texture_view: &'a wgpu::TextureView,
164    brdf_texture_sampler: &'a wgpu::Sampler,
165    shadow_map_texture_view: &'a wgpu::TextureView,
166    shadow_map_texture_sampler: &'a wgpu::Sampler,
167}
168
169impl PrimitiveBindGroup<'_> {
170    pub fn create(self) -> wgpu::BindGroup {
171        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
172            label: Some("primitive"),
173            layout: self.layout,
174            entries: &[
175                wgpu::BindGroupEntry {
176                    binding: 0,
177                    resource: self.geometry_buffer.as_entire_binding(),
178                },
179                wgpu::BindGroupEntry {
180                    binding: 1,
181                    resource: self.material_buffer.as_entire_binding(),
182                },
183                wgpu::BindGroupEntry {
184                    binding: 2,
185                    resource: wgpu::BindingResource::TextureView(self.atlas_texture_view),
186                },
187                wgpu::BindGroupEntry {
188                    binding: 3,
189                    resource: wgpu::BindingResource::Sampler(self.atlas_texture_sampler),
190                },
191                wgpu::BindGroupEntry {
192                    binding: 4,
193                    resource: wgpu::BindingResource::TextureView(self.irradiance_texture_view),
194                },
195                wgpu::BindGroupEntry {
196                    binding: 5,
197                    resource: wgpu::BindingResource::Sampler(self.irradiance_texture_sampler),
198                },
199                wgpu::BindGroupEntry {
200                    binding: 6,
201                    resource: wgpu::BindingResource::TextureView(self.prefiltered_texture_view),
202                },
203                wgpu::BindGroupEntry {
204                    binding: 7,
205                    resource: wgpu::BindingResource::Sampler(self.prefiltered_texture_sampler),
206                },
207                wgpu::BindGroupEntry {
208                    binding: 8,
209                    resource: wgpu::BindingResource::TextureView(self.brdf_texture_view),
210                },
211                wgpu::BindGroupEntry {
212                    binding: 9,
213                    resource: wgpu::BindingResource::Sampler(self.brdf_texture_sampler),
214                },
215                wgpu::BindGroupEntry {
216                    binding: 10,
217                    resource: self.light_buffer.as_entire_binding(),
218                },
219                wgpu::BindGroupEntry {
220                    binding: 11,
221                    resource: wgpu::BindingResource::TextureView(self.shadow_map_texture_view),
222                },
223                wgpu::BindGroupEntry {
224                    binding: 12,
225                    resource: wgpu::BindingResource::Sampler(self.shadow_map_texture_sampler),
226                },
227            ],
228        })
229    }
230}
231
232/// Performs a rendering of an entire scene, given the resources at hand.
233pub(crate) struct StageRendering<'a> {
234    // TODO: include the rest of the needed paramaters from `stage`, and then remove `stage`
235    pub stage: &'a Stage,
236    pub pipeline: &'a wgpu::RenderPipeline,
237    pub color_attachment: wgpu::RenderPassColorAttachment<'a>,
238    pub depth_stencil_attachment: wgpu::RenderPassDepthStencilAttachment<'a>,
239}
240
241impl StageRendering<'_> {
242    /// Run the stage rendering.
243    ///
244    /// Returns the queue submission index and the indirect draw buffer, if available.
245    pub fn run(self) -> (wgpu::SubmissionIndex, Option<SlabBuffer<wgpu::Buffer>>) {
246        let commit_result = self.stage.commit();
247        let current_primitive_bind_group_creation_time = commit_result.latest_creation_time();
248        log::trace!("current_primitive_bind_group_creation_time: {current_primitive_bind_group_creation_time}");
249        let previous_primitive_bind_group_creation_time =
250            self.stage.primitive_bind_group_created.swap(
251                current_primitive_bind_group_creation_time,
252                std::sync::atomic::Ordering::Relaxed,
253            );
254        let should_invalidate_primitive_bind_group =
255            commit_result.should_invalidate(previous_primitive_bind_group_creation_time);
256        log::trace!(
257            "should_invalidate_primitive_bind_group: {should_invalidate_primitive_bind_group}"
258        );
259        let primitive_bind_group =
260            self.stage
261                .primitive_bind_group
262                .get(should_invalidate_primitive_bind_group, || {
263                    log::trace!("recreating primitive bind group");
264                    let atlas_texture = self.stage.materials.atlas().get_texture();
265                    let ibl = self.stage.ibl.read().unwrap();
266                    let shadow_map = self.stage.lighting.shadow_map_atlas.get_texture();
267                    PrimitiveBindGroup {
268                        device: self.stage.device(),
269                        layout: &Stage::primitive_pipeline_bindgroup_layout(self.stage.device()),
270                        geometry_buffer: &commit_result.geometry_buffer,
271                        material_buffer: &commit_result.materials_buffer,
272                        light_buffer: &commit_result.lighting_buffer,
273                        atlas_texture_view: &atlas_texture.view,
274                        atlas_texture_sampler: &atlas_texture.sampler,
275                        irradiance_texture_view: &ibl.irradiance_cubemap.view,
276                        irradiance_texture_sampler: &ibl.irradiance_cubemap.sampler,
277                        prefiltered_texture_view: &ibl.prefiltered_environment_cubemap.view,
278                        prefiltered_texture_sampler: &ibl.prefiltered_environment_cubemap.sampler,
279                        brdf_texture_view: &self.stage.brdf_lut.inner.view,
280                        brdf_texture_sampler: &self.stage.brdf_lut.inner.sampler,
281                        shadow_map_texture_view: &shadow_map.view,
282                        shadow_map_texture_sampler: &shadow_map.sampler,
283                    }
284                    .create()
285                });
286
287        let mut draw_calls = self.stage.draw_calls.write().unwrap();
288        let depth_texture = self.stage.depth_texture.read().unwrap();
289        // UNWRAP: safe because we know the depth texture format will always match
290        let maybe_indirect_buffer = draw_calls.pre_draw(&depth_texture).unwrap();
291
292        log::trace!("rendering");
293        let label = Some("stage render");
294
295        let mut encoder = self
296            .stage
297            .device()
298            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
299        {
300            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
301                label,
302                color_attachments: &[Some(self.color_attachment)],
303                depth_stencil_attachment: Some(self.depth_stencil_attachment),
304                ..Default::default()
305            });
306
307            render_pass.set_pipeline(self.pipeline);
308            render_pass.set_bind_group(0, Some(primitive_bind_group.as_ref()), &[]);
309            draw_calls.draw(&mut render_pass);
310
311            let has_skybox = self.stage.has_skybox.load(Ordering::Relaxed);
312            if has_skybox {
313                let (pipeline, bindgroup) = self
314                    .stage
315                    .get_skybox_pipeline_and_bindgroup(&commit_result.geometry_buffer);
316                render_pass.set_pipeline(&pipeline.pipeline);
317                render_pass.set_bind_group(0, Some(bindgroup.as_ref()), &[]);
318                let camera_id = self.stage.geometry.descriptor().get().camera_id.inner();
319                render_pass.draw(0..36, camera_id..camera_id + 1);
320            }
321        }
322        let sindex = self.stage.queue().submit(std::iter::once(encoder.finish()));
323        (sindex, maybe_indirect_buffer)
324    }
325}
326
327/// Entrypoint for staging data on the GPU and interacting with lighting.
328///
329/// # Design
330///
331/// The `Stage` struct serves as the central hub for managing and staging data on the GPU.
332/// It provides a consistent API for creating resources, applying effects, and customizing parameters.
333///
334/// The `Stage` uses a combination of `new_*`, `with_*`, `set_*`, and getter functions to facilitate
335/// resource management and customization.
336///
337/// Resources are managed internally, requiring no additional lifecycle work from the user.
338/// This design simplifies the process of resource management, allowing developers to focus on creating and rendering
339/// their scenes without worrying about the underlying GPU resource management.
340///
341/// # Resources
342///
343/// The `Stage` is responsible for creating various resources and staging them on the GPU.
344/// It handles the setup and management of the following resources:
345///
346/// * [`Camera`]: Manages the view and projection matrices for rendering scenes.
347///   - [`Stage::new_camera`] creates a new [`Camera`].
348///   - [`Stage::use_camera`] tells the `Stage` to use a camera.
349/// * [`Transform`]: Represents the position, rotation, and scale of objects.
350///   - [`Stage::new_transform`] creates a new [`Transform`].
351/// * [`NestedTransform`]: Allows for hierarchical transformations, useful for complex object hierarchies.
352///   - [`Stage::new_nested_transform`] creates a new [`NestedTransform`]
353/// * [`Vertices`]: Manages vertex data for rendering meshes.
354///   - [`Stage::new_vertices`]
355/// * [`Indices`]: Manages index data for rendering meshes with indexed drawing.
356///   - [`Stage::new_indices`]
357/// * [`Primitive`]: Represents a drawable object in the scene.
358///   - [`Stage::new_primitive`]
359/// * [`GltfDocument`]: Handles loading and managing GLTF assets.
360///   - [`Stage::load_gltf_document_from_path`] loads a new GLTF document from the local filesystem.
361///   - [`Stage::load_gltf_document_from_bytes`] parses a new GLTF document from pre-loaded bytes.
362/// * [`Skin`]: Animation and rigging information.
363///   - [`Stage::new_skin`]
364///
365/// # Lighting effects
366///
367/// The `Stage` also manages various lighting effects, which enhance the visual quality of the scene:
368///
369/// * [`AnalyticalLight`]: Simulates a single light source, with three flavors:
370///   - [`DirectionalLight`]: Represents sunlight or other distant light sources.
371///   - [`PointLight`]: Represents a light source that emits light in all directions from a single point.
372///   - [`SpotLight`]: Represents a light source that emits light in a cone shape.
373/// * [`Skybox`]: Provides image-based lighting (IBL) for realistic environmental reflections and ambient lighting.
374/// * [`Bloom`]: Adds a glow effect to bright areas of the scene, enhancing visual appeal.
375/// * [`ShadowMap`]: Manages shadow mapping for realistic shadow rendering.
376/// * [`LightTiling`]: Optimizes lighting calculations by dividing the scene into tiles for efficient processing.
377///
378/// # Note
379///
380/// Clones of [`Stage`] all point to the same underlying data.
381#[derive(Clone)]
382pub struct Stage {
383    pub(crate) geometry: Geometry,
384    pub(crate) materials: Materials,
385    pub(crate) lighting: Lighting,
386
387    pub(crate) primitive_pipeline: Arc<RwLock<wgpu::RenderPipeline>>,
388    pub(crate) primitive_bind_group: ManagedBindGroup,
389    pub(crate) primitive_bind_group_created: Arc<AtomicUsize>,
390
391    pub(crate) skybox_pipeline: Arc<RwLock<Option<Arc<SkyboxRenderPipeline>>>>,
392
393    pub(crate) hdr_texture: Arc<RwLock<Texture>>,
394    pub(crate) depth_texture: Arc<RwLock<Texture>>,
395    pub(crate) msaa_render_target: Arc<RwLock<Option<wgpu::TextureView>>>,
396    pub(crate) msaa_sample_count: Arc<AtomicU32>,
397    pub(crate) clear_color_attachments: Arc<AtomicBool>,
398    pub(crate) clear_depth_attachments: Arc<AtomicBool>,
399
400    pub(crate) bloom: Bloom,
401
402    pub(crate) tonemapping: Tonemapping,
403    pub(crate) debug_overlay: DebugOverlay,
404    pub(crate) background_color: Arc<RwLock<wgpu::Color>>,
405
406    pub(crate) brdf_lut: BrdfLut,
407
408    pub(crate) ibl: Arc<RwLock<Ibl>>,
409
410    pub(crate) skybox: Arc<RwLock<Skybox>>,
411    pub(crate) skybox_bindgroup: Arc<Mutex<Option<Arc<wgpu::BindGroup>>>>,
412    // TODO: remove Stage.has_skybox, replace with Skybox::is_empty
413    pub(crate) has_skybox: Arc<AtomicBool>,
414
415    pub(crate) has_bloom: Arc<AtomicBool>,
416    pub(crate) has_debug_overlay: Arc<AtomicBool>,
417
418    pub(crate) stage_slab_buffer: Arc<RwLock<SlabBuffer<wgpu::Buffer>>>,
419
420    pub(crate) textures_bindgroup: Arc<Mutex<Option<Arc<wgpu::BindGroup>>>>,
421
422    pub(crate) draw_calls: Arc<RwLock<DrawCalls>>,
423}
424
425impl AsRef<WgpuRuntime> for Stage {
426    fn as_ref(&self) -> &WgpuRuntime {
427        self.geometry.as_ref()
428    }
429}
430
431impl AsRef<Geometry> for Stage {
432    fn as_ref(&self) -> &Geometry {
433        &self.geometry
434    }
435}
436
437impl AsRef<Materials> for Stage {
438    fn as_ref(&self) -> &Materials {
439        &self.materials
440    }
441}
442
443impl AsRef<Lighting> for Stage {
444    fn as_ref(&self) -> &Lighting {
445        &self.lighting
446    }
447}
448
449#[cfg(gltf)]
450/// GLTF functions
451impl Stage {
452    pub fn load_gltf_document_from_path(
453        &self,
454        path: impl AsRef<std::path::Path>,
455    ) -> Result<GltfDocument, StageError> {
456        use snafu::ResultExt;
457
458        let (document, buffers, images) =
459            gltf::import(&path).with_context(|_| crate::gltf::GltfSnafu {
460                path: Some(path.as_ref().to_path_buf()),
461            })?;
462        GltfDocument::from_gltf(self, &document, buffers, images)
463    }
464
465    pub fn load_gltf_document_from_bytes(
466        &self,
467        bytes: impl AsRef<[u8]>,
468    ) -> Result<GltfDocument, StageError> {
469        let (document, buffers, images) =
470            gltf::import_slice(bytes).context(crate::gltf::GltfSnafu { path: None })?;
471        GltfDocument::from_gltf(self, &document, buffers, images)
472    }
473}
474
475/// Geometry functions
476impl Stage {
477    /// Returns the vertices of a white unit cube.
478    ///
479    /// This is the mesh of every [`Primitive`] that has not had its vertices set.
480    pub fn default_vertices(&self) -> &Vertices {
481        self.geometry.default_vertices()
482    }
483
484    /// Stage a new [`Camera`] on the GPU.
485    ///
486    /// If no camera is currently in use on the [`Stage`] through
487    /// [`Stage::use_camera`], this new camera will be used automatically.
488    pub fn new_camera(&self) -> Camera {
489        self.geometry.new_camera()
490    }
491
492    /// Use the given camera when rendering.
493    pub fn use_camera(&self, camera: impl AsRef<Camera>) {
494        self.geometry.use_camera(camera.as_ref());
495    }
496
497    /// Return the `Id` of the camera currently in use.
498    pub fn used_camera_id(&self) -> Id<CameraDescriptor> {
499        self.geometry.descriptor().get().camera_id
500    }
501
502    /// Set the default camera `Id`.
503    pub fn use_camera_id(&self, camera_id: Id<CameraDescriptor>) {
504        self.geometry
505            .descriptor()
506            .modify(|desc| desc.camera_id = camera_id);
507    }
508
509    /// Stage a [`Transform`] on the GPU.
510    pub fn new_transform(&self) -> Transform {
511        self.geometry.new_transform()
512    }
513
514    /// Stage some vertex geometry data.
515    pub fn new_vertices(&self, data: impl IntoIterator<Item = Vertex>) -> Vertices {
516        self.geometry.new_vertices(data)
517    }
518
519    /// Stage some vertex index data.
520    pub fn new_indices(&self, data: impl IntoIterator<Item = u32>) -> Indices {
521        self.geometry.new_indices(data)
522    }
523
524    /// Stage new morph targets.
525    pub fn new_morph_targets(
526        &self,
527        data: impl IntoIterator<Item = Vec<MorphTarget>>,
528    ) -> MorphTargets {
529        self.geometry.new_morph_targets(data)
530    }
531
532    /// Stage new morph target weights.
533    pub fn new_morph_target_weights(
534        &self,
535        data: impl IntoIterator<Item = f32>,
536    ) -> MorphTargetWeights {
537        self.geometry.new_morph_target_weights(data)
538    }
539
540    /// Stage a new skin.
541    pub fn new_skin(
542        &self,
543        joints: impl IntoIterator<Item = impl Into<SkinJoint>>,
544        inverse_bind_matrices: impl IntoIterator<Item = impl Into<Mat4>>,
545    ) -> Skin {
546        self.geometry.new_skin(joints, inverse_bind_matrices)
547    }
548
549    /// Stage a new [`Primitive`] on the GPU.
550    ///
551    /// The returned [`Primitive`] will automatically be added to this [`Stage`].
552    ///
553    /// The returned [`Primitive`] will have the stage's default [`Vertices`], which is an all-white
554    /// unit cube.
555    ///
556    /// The returned [`Primitive`] uses the stage's default [`Material`], which is white and
557    /// **does not** participate in lighting. To change this, first create a [`Material`] with
558    /// [`Stage::new_material`] and then call [`Primitive::set_material`] with the new material.
559    pub fn new_primitive(&self) -> Primitive {
560        Primitive::new(self)
561    }
562
563    /// Returns a reference to the descriptor stored at the root of the
564    /// geometry slab.
565    pub fn geometry_descriptor(&self) -> &Hybrid<GeometryDescriptor> {
566        self.geometry.descriptor()
567    }
568}
569
570/// Materials methods.
571impl Stage {
572    /// Returns the default [`Material`].
573    ///
574    /// The default is an all-white matte material.
575    pub fn default_material(&self) -> &Material {
576        self.materials.default_material()
577    }
578
579    /// Stage a new [`Material`] on the GPU.
580    ///
581    /// The returned [`Material`] can be customized using the builder pattern.
582    pub fn new_material(&self) -> Material {
583        self.materials.new_material()
584    }
585
586    /// Set the size of the atlas.
587    ///
588    /// This will cause a repacking.
589    pub fn set_atlas_size(&self, size: wgpu::Extent3d) -> Result<(), StageError> {
590        log::info!("resizing atlas to {size:?}");
591        self.materials.atlas().resize(self.runtime(), size)?;
592        Ok(())
593    }
594
595    /// Add images to the set of atlas images.
596    ///
597    /// This returns a vector of [`Hybrid<AtlasTexture>`], which
598    /// is a descriptor of each image on the GPU. Dropping these entries
599    /// will invalidate those images and cause the atlas to be repacked, and any raw
600    /// GPU references to the underlying [`AtlasTexture`] will also be invalidated.
601    ///     
602    /// Adding an image can be quite expensive, as it requires creating a new texture
603    /// array for the atlas and repacking all previous images. For that reason it is
604    /// good to batch images to reduce the number of calls.
605    pub fn add_images(
606        &self,
607        images: impl IntoIterator<Item = impl Into<AtlasImage>>,
608    ) -> Result<Vec<AtlasTexture>, StageError> {
609        let images = images.into_iter().map(|i| i.into()).collect::<Vec<_>>();
610        let frames = self.materials.atlas().add_images(&images)?;
611
612        // The textures bindgroup will have to be remade
613        let _ = self.textures_bindgroup.lock().unwrap().take();
614
615        Ok(frames)
616    }
617
618    /// Clear all images from the atlas.
619    ///
620    /// ## WARNING
621    /// This invalidates any previously staged [`AtlasTexture`]s.
622    pub fn clear_images(&self) -> Result<(), StageError> {
623        let none = Option::<AtlasImage>::None;
624        let _ = self.set_images(none)?;
625        Ok(())
626    }
627
628    /// Set the images to use for the atlas.
629    ///
630    /// Resets the atlas, packing it with the given images and returning a
631    /// vector of the frames already staged.
632    ///
633    /// ## WARNING
634    /// This invalidates any previously staged [`AtlasTexture`]s.
635    pub fn set_images(
636        &self,
637        images: impl IntoIterator<Item = impl Into<AtlasImage>>,
638    ) -> Result<Vec<AtlasTexture>, StageError> {
639        let images = images.into_iter().map(|i| i.into()).collect::<Vec<_>>();
640        let frames = self.materials.atlas().set_images(&images)?;
641
642        // The textures bindgroup will have to be remade
643        let _ = self.textures_bindgroup.lock().unwrap().take();
644
645        Ok(frames)
646    }
647}
648
649/// Lighting methods.
650impl Stage {
651    /// Stage a new directional light.
652    pub fn new_directional_light(&self) -> AnalyticalLight<DirectionalLight> {
653        self.lighting.new_directional_light()
654    }
655
656    /// Stage a new point light.
657    pub fn new_point_light(&self) -> AnalyticalLight<PointLight> {
658        self.lighting.new_point_light()
659    }
660
661    /// Stage a new spot light.
662    pub fn new_spot_light(&self) -> AnalyticalLight<SpotLight> {
663        self.lighting.new_spot_light()
664    }
665
666    /// Add an [`AnalyticalLight`] to the internal list of lights.
667    ///
668    /// This is called implicitly by `Stage::new_*_light`.
669    ///
670    /// This can be used to add the light back to the scene after using
671    /// [`Stage::remove_light`].
672    pub fn add_light<T>(&self, bundle: &AnalyticalLight<T>)
673    where
674        T: IsLight,
675        Light: From<T>,
676    {
677        self.lighting.add_light(bundle)
678    }
679
680    /// Remove an [`AnalyticalLight`] from the internal list of lights.
681    ///
682    /// Use this to exclude a light from rendering, without dropping the light.
683    ///
684    /// After calling this function you can include the light again using [`Stage::add_light`].
685    pub fn remove_light<T: IsLight>(&self, bundle: &AnalyticalLight<T>) {
686        self.lighting.remove_light(bundle);
687    }
688
689    /// Enable shadow mapping for the given [`AnalyticalLight`], creating
690    /// a new [`ShadowMap`].
691    ///
692    /// ## Tips for making a good shadow map
693    ///
694    /// 1. Make sure the map is big enough.
695    ///    Using a big map can fix some peter panning issues, even before
696    ///    playing with bias in the returned [`ShadowMap`].
697    ///    The bigger the map, the cleaner the shadows will be. This can
698    ///    also solve PCF problems.
699    /// 2. Don't set PCF samples too high in the returned [`ShadowMap`], as
700    ///    this can _cause_ peter panning.
701    /// 3. Ensure the **znear** and **zfar** parameters make sense, as the
702    ///    shadow map uses these to determine how much of the scene to cover.
703    ///    If you find that shadows are cut off in a straight line, it's likely
704    ///    `znear` or `zfar` needs adjustment.
705    pub fn new_shadow_map<T>(
706        &self,
707        analytical_light: &AnalyticalLight<T>,
708        // Size of the shadow map
709        size: UVec2,
710        // Distance to the near plane of the shadow map's frustum.
711        //
712        // Only objects within the shadow map's frustum will cast shadows.
713        z_near: f32,
714        // Distance to the far plane of the shadow map's frustum
715        //
716        // Only objects within the shadow map's frustum will cast shadows.
717        z_far: f32,
718    ) -> Result<ShadowMap, StageError>
719    where
720        T: IsLight,
721        Light: From<T>,
722    {
723        Ok(self
724            .lighting
725            .new_shadow_map(analytical_light, size, z_near, z_far)?)
726    }
727
728    /// Enable light tiling, creating a new [`LightTiling`].
729    pub fn new_light_tiling(&self, config: LightTilingConfig) -> LightTiling {
730        let lighting = self.as_ref();
731        let multisampled = self.get_msaa_sample_count() > 1;
732        let depth_texture_size = self.get_depth_texture().size();
733        LightTiling::new(
734            lighting,
735            multisampled,
736            UVec2::new(depth_texture_size.width, depth_texture_size.height),
737            config,
738        )
739    }
740}
741
742/// Skybox methods
743impl Stage {
744    /// Return the cached skybox render pipeline, creating it if necessary.
745    fn get_skybox_pipeline_and_bindgroup(
746        &self,
747        geometry_slab_buffer: &wgpu::Buffer,
748    ) -> (Arc<SkyboxRenderPipeline>, Arc<wgpu::BindGroup>) {
749        let msaa_sample_count = self.msaa_sample_count.load(Ordering::Relaxed);
750        // UNWRAP: safe because we're only ever called from the render thread.
751        let mut pipeline_guard = self.skybox_pipeline.write().unwrap();
752        let pipeline = if let Some(pipeline) = pipeline_guard.as_mut() {
753            if pipeline.msaa_sample_count() != msaa_sample_count {
754                *pipeline = Arc::new(crate::skybox::create_skybox_render_pipeline(
755                    self.device(),
756                    Texture::HDR_TEXTURE_FORMAT,
757                    Some(msaa_sample_count),
758                ));
759            }
760            pipeline.clone()
761        } else {
762            let pipeline = Arc::new(crate::skybox::create_skybox_render_pipeline(
763                self.device(),
764                Texture::HDR_TEXTURE_FORMAT,
765                Some(msaa_sample_count),
766            ));
767            *pipeline_guard = Some(pipeline.clone());
768            pipeline
769        };
770        // UNWRAP: safe because we're only ever called from the render thread.
771        let mut bindgroup = self.skybox_bindgroup.lock().unwrap();
772        let bindgroup = if let Some(bindgroup) = bindgroup.as_ref() {
773            bindgroup.clone()
774        } else {
775            let bg = Arc::new(crate::skybox::create_skybox_bindgroup(
776                self.device(),
777                geometry_slab_buffer,
778                self.skybox.read().unwrap().environment_cubemap_texture(),
779            ));
780            *bindgroup = Some(bg.clone());
781            bg
782        };
783        (pipeline, bindgroup)
784    }
785
786    /// Used the given [`Skybox`].
787    ///
788    /// To remove the currently used [`Skybox`], call [`Skybox::remove_skybox`].
789    pub fn use_skybox(&self, skybox: &Skybox) -> &Self {
790        // UNWRAP: if we can't acquire the lock we want to panic.
791        let mut guard = self.skybox.write().unwrap();
792        *guard = skybox.clone();
793        self.has_skybox
794            .store(true, std::sync::atomic::Ordering::Relaxed);
795        *self.skybox_bindgroup.lock().unwrap() = None;
796        *self.textures_bindgroup.lock().unwrap() = None;
797        self
798    }
799
800    /// Removes the currently used [`Skybox`].
801    ///
802    /// Returns the currently used [`Skybox`], if any.
803    ///
804    /// After calling this the [`Stage`] will not render with any [`Skybox`], until
805    /// [`Skybox::use_skybox`] is called with another [`Skybox`].
806    pub fn remove_skybox(&self) -> Option<Skybox> {
807        let mut guard = self.skybox.write().unwrap();
808        if guard.is_empty() {
809            // Do nothing, the skybox is already empty
810            None
811        } else {
812            let skybox = guard.clone();
813            *guard = Skybox::empty(self.runtime());
814            self.skybox_bindgroup.lock().unwrap().take();
815            self.textures_bindgroup.lock().unwrap().take();
816            Some(skybox)
817        }
818    }
819
820    /// Returns a new [`Skybox`] using the HDR image at the given path, if possible.
821    ///
822    /// The returned [`Skybox`] must be **used** with [`Stage::use_skybox`].
823    pub fn new_skybox_from_path(
824        &self,
825        path: impl AsRef<std::path::Path>,
826    ) -> Result<Skybox, AtlasImageError> {
827        let hdr = AtlasImage::from_hdr_path(path)?;
828        Ok(Skybox::new(self.runtime(), hdr))
829    }
830
831    /// Returns a new [`Skybox`] using the bytes of an HDR image, if possible.
832    ///
833    /// The returned [`Skybox`] must be **used** with [`Stage::use_skybox`].
834    pub fn new_skybox_from_bytes(&self, bytes: &[u8]) -> Result<Skybox, AtlasImageError> {
835        let hdr = AtlasImage::from_hdr_bytes(bytes)?;
836        Ok(Skybox::new(self.runtime(), hdr))
837    }
838}
839
840/// Image based lighting methods
841impl Stage {
842    /// Crate a new [`Ibl`] from the given [`Skybox`].
843    pub fn new_ibl(&self, skybox: &Skybox) -> Ibl {
844        Ibl::new(self.runtime(), skybox)
845    }
846
847    /// Use the given image based lighting.
848    ///
849    /// Use [`Stage::new_ibl`] to create a new [`Ibl`].
850    pub fn use_ibl(&self, ibl: &Ibl) -> &Self {
851        let mut guard = self.ibl.write().unwrap();
852        *guard = ibl.clone();
853        self.primitive_bind_group.invalidate();
854        self
855    }
856
857    /// Remove the current image based lighting from the stage and return it, if any.
858    pub fn remove_ibl(&self) -> Option<Ibl> {
859        let mut guard = self.ibl.write().unwrap();
860        if guard.is_empty() {
861            // Do nothing, we're already not using IBL
862            None
863        } else {
864            let ibl = guard.clone();
865            *guard = Ibl::new(self.runtime(), &Skybox::empty(self.runtime()));
866            self.primitive_bind_group.invalidate();
867            Some(ibl)
868        }
869    }
870}
871
872impl Stage {
873    /// Returns the runtime.
874    pub fn runtime(&self) -> &WgpuRuntime {
875        self.as_ref()
876    }
877
878    pub fn device(&self) -> &wgpu::Device {
879        &self.runtime().device
880    }
881
882    pub fn queue(&self) -> &wgpu::Queue {
883        &self.runtime().queue
884    }
885
886    /// Returns a reference to the [`BrdfLut`].
887    ///
888    /// This is used for creating skyboxes used in image based lighting.
889    pub fn brdf_lut(&self) -> &BrdfLut {
890        &self.brdf_lut
891    }
892
893    /// Sum the byte size of all used GPU memory.
894    ///
895    /// Adds together the byte size of all underlying slab buffers.
896    ///
897    /// ## Note
898    /// This does not take into consideration staged data that has not yet
899    /// been committed with either [`Stage::commit`] or [`Stage::render`].
900    pub fn used_gpu_buffer_byte_size(&self) -> usize {
901        let num_u32s = self.geometry.slab_allocator().len()
902            + self.lighting.slab_allocator().len()
903            + self.materials.slab_allocator().len()
904            + self.bloom.slab_allocator().len()
905            + self.tonemapping.slab_allocator().len()
906            + self
907                .draw_calls
908                .read()
909                .unwrap()
910                .drawing_strategy()
911                .as_indirect()
912                .map(|draws| draws.slab_allocator().len())
913                .unwrap_or_default();
914        4 * num_u32s
915    }
916
917    pub fn hdr_texture(&self) -> impl Deref<Target = crate::texture::Texture> + '_ {
918        self.hdr_texture.read().unwrap()
919    }
920
921    /// Run all upkeep and commit all staged changes to the GPU.
922    ///
923    /// This is done implicitly in [`Stage::render`].
924    ///
925    /// This can be used after dropping resources to reclaim those resources on the GPU.
926    #[must_use]
927    pub fn commit(&self) -> StageCommitResult {
928        let (materials_atlas_texture_was_recreated, materials_buffer) = self.materials.commit();
929        if materials_atlas_texture_was_recreated {
930            self.primitive_bind_group.invalidate();
931        }
932        let geometry_buffer = self.geometry.commit();
933        let lighting_buffer = self.lighting.commit();
934        StageCommitResult {
935            geometry_buffer,
936            lighting_buffer,
937            materials_buffer,
938        }
939    }
940
941    fn primitive_pipeline_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
942        let geometry_slab = wgpu::BindGroupLayoutEntry {
943            binding: 0,
944            visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
945            ty: wgpu::BindingType::Buffer {
946                ty: wgpu::BufferBindingType::Storage { read_only: true },
947                has_dynamic_offset: false,
948                min_binding_size: None,
949            },
950            count: None,
951        };
952        let material_slab = wgpu::BindGroupLayoutEntry {
953            binding: 1,
954            visibility: wgpu::ShaderStages::FRAGMENT,
955            ty: wgpu::BindingType::Buffer {
956                ty: wgpu::BufferBindingType::Storage { read_only: true },
957                has_dynamic_offset: false,
958                min_binding_size: None,
959            },
960            count: None,
961        };
962
963        fn image2d_entry(binding: u32) -> (wgpu::BindGroupLayoutEntry, wgpu::BindGroupLayoutEntry) {
964            let img = wgpu::BindGroupLayoutEntry {
965                binding,
966                visibility: wgpu::ShaderStages::FRAGMENT,
967                ty: wgpu::BindingType::Texture {
968                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
969                    view_dimension: wgpu::TextureViewDimension::D2,
970                    multisampled: false,
971                },
972                count: None,
973            };
974            let sampler = wgpu::BindGroupLayoutEntry {
975                binding: binding + 1,
976                visibility: wgpu::ShaderStages::FRAGMENT,
977                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
978                count: None,
979            };
980            (img, sampler)
981        }
982
983        fn cubemap_entry(binding: u32) -> (wgpu::BindGroupLayoutEntry, wgpu::BindGroupLayoutEntry) {
984            let img = wgpu::BindGroupLayoutEntry {
985                binding,
986                visibility: wgpu::ShaderStages::FRAGMENT,
987                ty: wgpu::BindingType::Texture {
988                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
989                    view_dimension: wgpu::TextureViewDimension::Cube,
990                    multisampled: false,
991                },
992                count: None,
993            };
994            let sampler = wgpu::BindGroupLayoutEntry {
995                binding: binding + 1,
996                visibility: wgpu::ShaderStages::FRAGMENT,
997                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
998                count: None,
999            };
1000            (img, sampler)
1001        }
1002
1003        let atlas = wgpu::BindGroupLayoutEntry {
1004            binding: 2,
1005            visibility: wgpu::ShaderStages::FRAGMENT,
1006            ty: wgpu::BindingType::Texture {
1007                sample_type: wgpu::TextureSampleType::Float { filterable: true },
1008                view_dimension: wgpu::TextureViewDimension::D2Array,
1009                multisampled: false,
1010            },
1011            count: None,
1012        };
1013        let atlas_sampler = wgpu::BindGroupLayoutEntry {
1014            binding: 3,
1015            visibility: wgpu::ShaderStages::FRAGMENT,
1016            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1017            count: None,
1018        };
1019        let (irradiance, irradiance_sampler) = cubemap_entry(4);
1020        let (prefilter, prefilter_sampler) = cubemap_entry(6);
1021        let (brdf, brdf_sampler) = image2d_entry(8);
1022
1023        let LightingBindGroupLayoutEntries {
1024            light_slab,
1025            shadow_map_image,
1026            shadow_map_sampler,
1027        } = LightingBindGroupLayoutEntries::new(10);
1028
1029        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1030            label: Some("primitive"),
1031            entries: &[
1032                geometry_slab,
1033                material_slab,
1034                atlas,
1035                atlas_sampler,
1036                irradiance,
1037                irradiance_sampler,
1038                prefilter,
1039                prefilter_sampler,
1040                brdf,
1041                brdf_sampler,
1042                light_slab,
1043                shadow_map_image,
1044                shadow_map_sampler,
1045            ],
1046        })
1047    }
1048
1049    pub fn create_primitive_pipeline(
1050        device: &wgpu::Device,
1051        fragment_color_format: wgpu::TextureFormat,
1052        multisample_count: u32,
1053    ) -> wgpu::RenderPipeline {
1054        log::trace!("creating stage render pipeline");
1055        let label = Some("primitive");
1056        let vertex_linkage = crate::linkage::primitive_vertex::linkage(device);
1057        let fragment_linkage = crate::linkage::primitive_fragment::linkage(device);
1058
1059        let bind_group_layout = Self::primitive_pipeline_bindgroup_layout(device);
1060        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1061            label,
1062            bind_group_layouts: &[&bind_group_layout],
1063            push_constant_ranges: &[],
1064        });
1065
1066        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1067            label,
1068            layout: Some(&layout),
1069            vertex: wgpu::VertexState {
1070                module: &vertex_linkage.module,
1071                entry_point: Some(vertex_linkage.entry_point),
1072                buffers: &[],
1073                compilation_options: Default::default(),
1074            },
1075            primitive: wgpu::PrimitiveState {
1076                topology: wgpu::PrimitiveTopology::TriangleList,
1077                strip_index_format: None,
1078                front_face: wgpu::FrontFace::Ccw,
1079                cull_mode: None,
1080                unclipped_depth: false,
1081                polygon_mode: wgpu::PolygonMode::Fill,
1082                conservative: false,
1083            },
1084            depth_stencil: Some(wgpu::DepthStencilState {
1085                format: wgpu::TextureFormat::Depth32Float,
1086                depth_write_enabled: true,
1087                depth_compare: wgpu::CompareFunction::Less,
1088                stencil: wgpu::StencilState::default(),
1089                bias: wgpu::DepthBiasState::default(),
1090            }),
1091            multisample: wgpu::MultisampleState {
1092                mask: !0,
1093                alpha_to_coverage_enabled: false,
1094                count: multisample_count,
1095            },
1096            fragment: Some(wgpu::FragmentState {
1097                module: &fragment_linkage.module,
1098                entry_point: Some(fragment_linkage.entry_point),
1099                targets: &[Some(wgpu::ColorTargetState {
1100                    format: fragment_color_format,
1101                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1102                    write_mask: wgpu::ColorWrites::ALL,
1103                })],
1104                compilation_options: Default::default(),
1105            }),
1106            multiview: None,
1107            cache: None,
1108        })
1109    }
1110
1111    /// Create a new stage.
1112    pub fn new(ctx: &crate::context::Context) -> Self {
1113        let runtime = ctx.runtime();
1114        let device = &runtime.device;
1115        let resolution @ UVec2 { x: w, y: h } = ctx.get_size();
1116        let stage_config = *ctx.stage_config.read().unwrap();
1117        let geometry = Geometry::new(
1118            ctx,
1119            resolution,
1120            UVec2::new(
1121                stage_config.atlas_size.width,
1122                stage_config.atlas_size.height,
1123            ),
1124        );
1125        let materials = Materials::new(runtime, stage_config.atlas_size);
1126        let multisample_count = 1;
1127        let hdr_texture = Arc::new(RwLock::new(Texture::create_hdr_texture(
1128            device,
1129            w,
1130            h,
1131            multisample_count,
1132        )));
1133        let depth_texture =
1134            Texture::create_depth_texture(device, w, h, multisample_count, Some("stage-depth"));
1135        let msaa_render_target = Default::default();
1136        // UNWRAP: safe because no other references at this point (created above^)
1137        let bloom = Bloom::new(ctx, &hdr_texture.read().unwrap());
1138        let tonemapping = Tonemapping::new(
1139            runtime,
1140            ctx.get_render_target().format().add_srgb_suffix(),
1141            &bloom.get_mix_texture(),
1142        );
1143        let stage_pipeline = Self::create_primitive_pipeline(
1144            device,
1145            wgpu::TextureFormat::Rgba16Float,
1146            multisample_count,
1147        );
1148        let geometry_buffer = geometry.slab_allocator().commit();
1149        let lighting = Lighting::new(stage_config.shadow_map_atlas_size, &geometry);
1150
1151        let brdf_lut = BrdfLut::new(runtime);
1152        let skybox = Skybox::empty(runtime);
1153        let ibl = Ibl::new(runtime, &skybox);
1154
1155        Self {
1156            materials,
1157            draw_calls: Arc::new(RwLock::new(DrawCalls::new(
1158                ctx,
1159                ctx.get_use_direct_draw(),
1160                &geometry_buffer,
1161                &depth_texture,
1162            ))),
1163            lighting,
1164            depth_texture: Arc::new(RwLock::new(depth_texture)),
1165            stage_slab_buffer: Arc::new(RwLock::new(geometry_buffer)),
1166            geometry,
1167
1168            primitive_pipeline: Arc::new(RwLock::new(stage_pipeline)),
1169            primitive_bind_group: ManagedBindGroup::default(),
1170            primitive_bind_group_created: Arc::new(0.into()),
1171
1172            ibl: Arc::new(RwLock::new(ibl)),
1173            skybox: Arc::new(RwLock::new(skybox)),
1174            skybox_bindgroup: Default::default(),
1175            skybox_pipeline: Default::default(),
1176            has_skybox: Arc::new(AtomicBool::new(false)),
1177            brdf_lut,
1178            bloom,
1179            tonemapping,
1180            has_bloom: AtomicBool::from(true).into(),
1181            textures_bindgroup: Default::default(),
1182            debug_overlay: DebugOverlay::new(device, ctx.get_render_target().format()),
1183            has_debug_overlay: Arc::new(false.into()),
1184            hdr_texture,
1185            msaa_render_target,
1186            msaa_sample_count: Arc::new(multisample_count.into()),
1187            clear_color_attachments: Arc::new(true.into()),
1188            clear_depth_attachments: Arc::new(true.into()),
1189            background_color: Arc::new(RwLock::new(wgpu::Color::TRANSPARENT)),
1190        }
1191    }
1192
1193    pub fn set_background_color(&self, color: impl Into<Vec4>) {
1194        let color = color.into();
1195        *self.background_color.write().unwrap() = wgpu::Color {
1196            r: color.x as f64,
1197            g: color.y as f64,
1198            b: color.z as f64,
1199            a: color.w as f64,
1200        };
1201    }
1202
1203    pub fn with_background_color(self, color: impl Into<Vec4>) -> Self {
1204        self.set_background_color(color);
1205        self
1206    }
1207
1208    /// Return the multisample count.
1209    pub fn get_msaa_sample_count(&self) -> u32 {
1210        self.msaa_sample_count
1211            .load(std::sync::atomic::Ordering::Relaxed)
1212    }
1213
1214    /// Set the MSAA multisample count.
1215    ///
1216    /// Set to `1` to disable MSAA. Setting to `0` will be treated the same as
1217    /// setting to `1`.
1218    pub fn set_msaa_sample_count(&self, multisample_count: u32) {
1219        let multisample_count = multisample_count.max(1);
1220        let prev_multisample_count = self
1221            .msaa_sample_count
1222            .swap(multisample_count, Ordering::Relaxed);
1223        if prev_multisample_count == multisample_count {
1224            log::warn!("set_multisample_count: multisample count is unchanged, noop");
1225            return;
1226        }
1227
1228        log::debug!("setting multisample count to {multisample_count}");
1229        // UNWRAP: POP
1230        *self.primitive_pipeline.write().unwrap() = Self::create_primitive_pipeline(
1231            self.device(),
1232            wgpu::TextureFormat::Rgba16Float,
1233            multisample_count,
1234        );
1235        let size = self.get_size();
1236        // UNWRAP: POP
1237        *self.depth_texture.write().unwrap() = Texture::create_depth_texture(
1238            self.device(),
1239            size.x,
1240            size.y,
1241            multisample_count,
1242            Some("stage-depth"),
1243        );
1244        // UNWRAP: POP
1245        let format = self.hdr_texture.read().unwrap().texture.format();
1246        *self.msaa_render_target.write().unwrap() = if multisample_count == 1 {
1247            None
1248        } else {
1249            Some(create_msaa_textureview(
1250                self.device(),
1251                size.x,
1252                size.y,
1253                format,
1254                multisample_count,
1255            ))
1256        };
1257
1258        // Invalidate the textures bindgroup - it must be recreated
1259        let _ = self.textures_bindgroup.lock().unwrap().take();
1260    }
1261
1262    /// Set the MSAA multisample count.
1263    ///
1264    /// Set to `1` to disable MSAA. Setting to `0` will be treated the same as
1265    /// setting to `1`.
1266    pub fn with_msaa_sample_count(self, multisample_count: u32) -> Self {
1267        self.set_msaa_sample_count(multisample_count);
1268        self
1269    }
1270
1271    /// Set whether color attachments are cleared before rendering.
1272    pub fn set_clear_color_attachments(&self, should_clear: bool) {
1273        self.clear_color_attachments
1274            .store(should_clear, Ordering::Relaxed);
1275    }
1276
1277    /// Set whether color attachments are cleared before rendering.
1278    pub fn with_clear_color_attachments(self, should_clear: bool) -> Self {
1279        self.set_clear_color_attachments(should_clear);
1280        self
1281    }
1282
1283    /// Set whether color attachments are cleared before rendering.
1284    pub fn set_clear_depth_attachments(&self, should_clear: bool) {
1285        self.clear_depth_attachments
1286            .store(should_clear, Ordering::Relaxed);
1287    }
1288
1289    /// Set whether color attachments are cleared before rendering.
1290    pub fn with_clear_depth_attachments(self, should_clear: bool) -> Self {
1291        self.set_clear_depth_attachments(should_clear);
1292        self
1293    }
1294
1295    /// Set the debug mode.
1296    pub fn set_debug_mode(&self, debug_mode: DebugChannel) {
1297        self.geometry
1298            .descriptor()
1299            .modify(|cfg| cfg.debug_channel = debug_mode);
1300    }
1301
1302    /// Set the debug mode.
1303    pub fn with_debug_mode(self, debug_mode: DebugChannel) -> Self {
1304        self.set_debug_mode(debug_mode);
1305        self
1306    }
1307
1308    /// Set whether to render the debug overlay.
1309    pub fn set_use_debug_overlay(&self, use_debug_overlay: bool) {
1310        self.has_debug_overlay
1311            .store(use_debug_overlay, std::sync::atomic::Ordering::Relaxed);
1312    }
1313
1314    /// Set whether to render the debug overlay.
1315    pub fn with_debug_overlay(self, use_debug_overlay: bool) -> Self {
1316        self.set_use_debug_overlay(use_debug_overlay);
1317        self
1318    }
1319
1320    /// Set whether to use frustum culling on GPU before drawing.
1321    ///
1322    /// This defaults to `true`.
1323    pub fn set_use_frustum_culling(&self, use_frustum_culling: bool) {
1324        self.geometry
1325            .descriptor()
1326            .modify(|cfg| cfg.perform_frustum_culling = use_frustum_culling);
1327    }
1328
1329    /// Set whether to render the debug overlay.
1330    pub fn with_frustum_culling(self, use_frustum_culling: bool) -> Self {
1331        self.set_use_frustum_culling(use_frustum_culling);
1332        self
1333    }
1334
1335    /// Set whether to use occlusion culling on GPU before drawing.
1336    ///
1337    /// This defaults to `false`.
1338    ///
1339    /// ## Warning
1340    ///
1341    /// Occlusion culling is a feature in development. YMMV.
1342    pub fn set_use_occlusion_culling(&self, use_occlusion_culling: bool) {
1343        self.geometry
1344            .descriptor()
1345            .modify(|cfg| cfg.perform_occlusion_culling = use_occlusion_culling);
1346    }
1347
1348    /// Set whether to render the debug overlay.
1349    pub fn with_occlusion_culling(self, use_occlusion_culling: bool) -> Self {
1350        self.set_use_occlusion_culling(use_occlusion_culling);
1351        self
1352    }
1353
1354    /// Set whether the stage uses lighting.
1355    pub fn set_has_lighting(&self, use_lighting: bool) {
1356        self.geometry
1357            .descriptor()
1358            .modify(|cfg| cfg.has_lighting = use_lighting);
1359    }
1360
1361    /// Set whether the stage uses lighting.
1362    pub fn with_lighting(self, use_lighting: bool) -> Self {
1363        self.set_has_lighting(use_lighting);
1364        self
1365    }
1366
1367    /// Set whether to use vertex skinning.
1368    pub fn set_has_vertex_skinning(&self, use_skinning: bool) {
1369        self.geometry
1370            .descriptor()
1371            .modify(|cfg| cfg.has_skinning = use_skinning);
1372    }
1373
1374    /// Set whether to use vertex skinning.
1375    pub fn with_vertex_skinning(self, use_skinning: bool) -> Self {
1376        self.set_has_vertex_skinning(use_skinning);
1377        self
1378    }
1379
1380    pub fn get_size(&self) -> UVec2 {
1381        // UNWRAP: panic on purpose
1382        let hdr = self.hdr_texture.read().unwrap();
1383        let w = hdr.width();
1384        let h = hdr.height();
1385        UVec2::new(w, h)
1386    }
1387
1388    pub fn set_size(&self, size: UVec2) {
1389        if size == self.get_size() {
1390            return;
1391        }
1392
1393        self.geometry
1394            .descriptor()
1395            .modify(|cfg| cfg.resolution = size);
1396        let hdr_texture = Texture::create_hdr_texture(self.device(), size.x, size.y, 1);
1397        let sample_count = self.msaa_sample_count.load(Ordering::Relaxed);
1398        if let Some(msaa_view) = self.msaa_render_target.write().unwrap().as_mut() {
1399            *msaa_view = create_msaa_textureview(
1400                self.device(),
1401                size.x,
1402                size.y,
1403                hdr_texture.texture.format(),
1404                sample_count,
1405            );
1406        }
1407
1408        // UNWRAP: panic on purpose
1409        *self.depth_texture.write().unwrap() = Texture::create_depth_texture(
1410            self.device(),
1411            size.x,
1412            size.y,
1413            sample_count,
1414            Some("stage-depth"),
1415        );
1416        self.bloom.set_hdr_texture(self.runtime(), &hdr_texture);
1417        self.tonemapping
1418            .set_hdr_texture(self.device(), &hdr_texture);
1419        *self.hdr_texture.write().unwrap() = hdr_texture;
1420
1421        let _ = self.skybox_bindgroup.lock().unwrap().take();
1422        let _ = self.textures_bindgroup.lock().unwrap().take();
1423    }
1424
1425    pub fn with_size(self, size: UVec2) -> Self {
1426        self.set_size(size);
1427        self
1428    }
1429
1430    /// Turn the bloom effect on or off.
1431    pub fn set_has_bloom(&self, has_bloom: bool) {
1432        self.has_bloom
1433            .store(has_bloom, std::sync::atomic::Ordering::Relaxed);
1434    }
1435
1436    /// Turn the bloom effect on or off.
1437    pub fn with_bloom(self, has_bloom: bool) -> Self {
1438        self.set_has_bloom(has_bloom);
1439        self
1440    }
1441
1442    /// Set the amount of bloom that is mixed in with the input image.
1443    ///
1444    /// Defaults to `0.04`.
1445    pub fn set_bloom_mix_strength(&self, strength: f32) {
1446        self.bloom.set_mix_strength(strength);
1447    }
1448
1449    pub fn with_bloom_mix_strength(self, strength: f32) -> Self {
1450        self.set_bloom_mix_strength(strength);
1451        self
1452    }
1453
1454    /// Sets the bloom filter radius, in pixels.
1455    ///
1456    /// Default is `1.0`.
1457    pub fn set_bloom_filter_radius(&self, filter_radius: f32) {
1458        self.bloom.set_filter_radius(filter_radius);
1459    }
1460
1461    /// Sets the bloom filter radius, in pixels.
1462    ///
1463    /// Default is `1.0`.
1464    pub fn with_bloom_filter_radius(self, filter_radius: f32) -> Self {
1465        self.set_bloom_filter_radius(filter_radius);
1466        self
1467    }
1468
1469    /// Adds a primitive to the internal list of primitives to be drawn each
1470    /// frame.
1471    ///
1472    /// Returns the number of primitives added.
1473    ///
1474    /// If you drop the primitive and no other references are kept, it will be
1475    /// removed automatically from the internal list and will cease to be
1476    /// drawn each frame.
1477    pub fn add_primitive(&self, primitive: &Primitive) -> usize {
1478        // UNWRAP: if we can't acquire the lock we want to panic.
1479        let mut draws = self.draw_calls.write().unwrap();
1480        draws.add_primitive(primitive)
1481    }
1482
1483    /// Erase the given primitive from the internal list of primitives to be
1484    /// drawn each frame.
1485    ///
1486    /// Returns the number of primitives added.
1487    pub fn remove_primitive(&self, primitive: &Primitive) -> usize {
1488        let mut draws = self.draw_calls.write().unwrap();
1489        draws.remove_primitive(primitive)
1490    }
1491
1492    /// Sort the drawing order of primitives.
1493    ///
1494    /// This determines the order in which [`Primitive`]s are drawn each frame.
1495    pub fn sort_primitive(&self, f: impl Fn(&Primitive, &Primitive) -> std::cmp::Ordering) {
1496        // UNWRAP: panic on purpose
1497        let mut guard = self.draw_calls.write().unwrap();
1498        guard.sort_primitives(f);
1499    }
1500
1501    /// Returns a clone of the current depth texture.
1502    pub fn get_depth_texture(&self) -> DepthTexture {
1503        DepthTexture {
1504            runtime: self.runtime().clone(),
1505            texture: self.depth_texture.read().unwrap().texture.clone(),
1506        }
1507    }
1508
1509    /// Create a new [`NestedTransform`].
1510    pub fn new_nested_transform(&self) -> NestedTransform {
1511        NestedTransform::new(self.geometry.slab_allocator())
1512    }
1513
1514    /// Render the staged scene into the given view.
1515    pub fn render(&self, view: &wgpu::TextureView) {
1516        // UNWRAP: POP
1517        let background_color = *self.background_color.read().unwrap();
1518        // UNWRAP: POP
1519        let msaa_target = self.msaa_render_target.read().unwrap();
1520        let clear_colors = self.clear_color_attachments.load(Ordering::Relaxed);
1521        let hdr_texture = self.hdr_texture.read().unwrap();
1522
1523        let mk_ops = |store| wgpu::Operations {
1524            load: if clear_colors {
1525                wgpu::LoadOp::Clear(background_color)
1526            } else {
1527                wgpu::LoadOp::Load
1528            },
1529            store,
1530        };
1531        let render_pass_color_attachment = if let Some(msaa_view) = msaa_target.as_ref() {
1532            wgpu::RenderPassColorAttachment {
1533                ops: mk_ops(wgpu::StoreOp::Discard),
1534                view: msaa_view,
1535                resolve_target: Some(&hdr_texture.view),
1536                depth_slice: None,
1537            }
1538        } else {
1539            wgpu::RenderPassColorAttachment {
1540                ops: mk_ops(wgpu::StoreOp::Store),
1541                view: &hdr_texture.view,
1542                resolve_target: None,
1543                depth_slice: None,
1544            }
1545        };
1546
1547        let depth_texture = self.depth_texture.read().unwrap();
1548        let clear_depth = self.clear_depth_attachments.load(Ordering::Relaxed);
1549        let render_pass_depth_attachment = wgpu::RenderPassDepthStencilAttachment {
1550            view: &depth_texture.view,
1551            depth_ops: Some(wgpu::Operations {
1552                load: if clear_depth {
1553                    wgpu::LoadOp::Clear(1.0)
1554                } else {
1555                    wgpu::LoadOp::Load
1556                },
1557                store: wgpu::StoreOp::Store,
1558            }),
1559            stencil_ops: None,
1560        };
1561        let pipeline_guard = self.primitive_pipeline.read().unwrap();
1562        let (_submission_index, maybe_indirect_buffer) = StageRendering {
1563            pipeline: &pipeline_guard,
1564            stage: self,
1565            color_attachment: render_pass_color_attachment,
1566            depth_stencil_attachment: render_pass_depth_attachment,
1567        }
1568        .run();
1569
1570        // then render bloom
1571        if self.has_bloom.load(Ordering::Relaxed) {
1572            self.bloom.bloom(self.device(), self.queue());
1573        } else {
1574            // copy the input hdr texture to the bloom mix texture
1575            let mut encoder =
1576                self.device()
1577                    .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1578                        label: Some("no bloom copy"),
1579                    });
1580            let bloom_mix_texture = self.bloom.get_mix_texture();
1581            encoder.copy_texture_to_texture(
1582                wgpu::TexelCopyTextureInfo {
1583                    texture: &self.hdr_texture.read().unwrap().texture,
1584                    mip_level: 0,
1585                    origin: wgpu::Origin3d { x: 0, y: 0, z: 0 },
1586                    aspect: wgpu::TextureAspect::All,
1587                },
1588                wgpu::TexelCopyTextureInfo {
1589                    texture: &bloom_mix_texture.texture,
1590                    mip_level: 0,
1591                    origin: wgpu::Origin3d { x: 0, y: 0, z: 0 },
1592                    aspect: wgpu::TextureAspect::All,
1593                },
1594                wgpu::Extent3d {
1595                    width: bloom_mix_texture.width(),
1596                    height: bloom_mix_texture.height(),
1597                    depth_or_array_layers: 1,
1598                },
1599            );
1600            self.queue().submit(std::iter::once(encoder.finish()));
1601        }
1602
1603        // then render tonemapping
1604        self.tonemapping.render(self.device(), self.queue(), view);
1605
1606        // then render the debug overlay
1607        if self.has_debug_overlay.load(Ordering::Relaxed) {
1608            if let Some(indirect_draw_buffer) = maybe_indirect_buffer {
1609                self.debug_overlay.render(
1610                    self.device(),
1611                    self.queue(),
1612                    view,
1613                    &self.stage_slab_buffer.read().unwrap(),
1614                    &indirect_draw_buffer,
1615                );
1616            }
1617        }
1618    }
1619}
1620
1621#[cfg(test)]
1622mod test {
1623    use craballoc::{runtime::CpuRuntime, slab::SlabAllocator};
1624    use crabslab::{Array, Id, Slab};
1625    use glam::{Mat4, Vec2, Vec3, Vec4};
1626
1627    use crate::{
1628        context::Context,
1629        geometry::{shader::GeometryDescriptor, Geometry, Vertex},
1630        test::BlockOnFuture,
1631        transform::NestedTransform,
1632    };
1633
1634    #[test]
1635    fn vertex_slab_roundtrip() {
1636        let initial_vertices = {
1637            let tl = Vertex::default()
1638                .with_position(Vec3::ZERO)
1639                .with_uv0(Vec2::ZERO);
1640            let tr = Vertex::default()
1641                .with_position(Vec3::new(1.0, 0.0, 0.0))
1642                .with_uv0(Vec2::new(1.0, 0.0));
1643            let bl = Vertex::default()
1644                .with_position(Vec3::new(0.0, 1.0, 0.0))
1645                .with_uv0(Vec2::new(0.0, 1.0));
1646            let br = Vertex::default()
1647                .with_position(Vec3::new(1.0, 1.0, 0.0))
1648                .with_uv0(Vec2::splat(1.0));
1649            vec![tl, bl, br, tl, br, tr]
1650        };
1651        let mut slab = [0u32; 256];
1652        slab.write_indexed_slice(&initial_vertices, 0);
1653        let vertices = slab.read_vec(Array::<Vertex>::new(0, initial_vertices.len() as u32));
1654        pretty_assertions::assert_eq!(initial_vertices, vertices);
1655    }
1656
1657    #[test]
1658    fn matrix_subtraction_sanity() {
1659        let m = Mat4::IDENTITY - Mat4::IDENTITY;
1660        assert_eq!(Mat4::ZERO, m);
1661    }
1662
1663    #[test]
1664    fn can_global_transform_calculation() {
1665        #[expect(
1666            clippy::needless_borrows_for_generic_args,
1667            reason = "This is just riff-raff, as it doesn't compile without the borrow."
1668        )]
1669        let slab = SlabAllocator::<CpuRuntime>::new(&CpuRuntime, "transform", ());
1670        // Setup a hierarchy of transforms
1671        let root = NestedTransform::new(&slab);
1672        let child = NestedTransform::new(&slab).with_local_translation(Vec3::new(1.0, 0.0, 0.0));
1673        let grandchild =
1674            NestedTransform::new(&slab).with_local_translation(Vec3::new(1.0, 0.0, 0.0));
1675        log::info!("hierarchy");
1676        // Build the hierarchy
1677        root.add_child(&child);
1678        child.add_child(&grandchild);
1679
1680        log::info!("get_global_transform");
1681        // Calculate global transforms
1682        let grandchild_global_transform = grandchild.global_descriptor();
1683
1684        // Assert that the global transform is as expected
1685        assert_eq!(
1686            grandchild_global_transform.translation.x, 2.0,
1687            "Grandchild's global translation should   2.0 along the x-axis"
1688        );
1689    }
1690
1691    #[test]
1692    fn can_msaa() {
1693        let ctx = Context::headless(100, 100).block();
1694        let stage = ctx
1695            .new_stage()
1696            .with_background_color([1.0, 1.0, 1.0, 1.0])
1697            .with_lighting(false);
1698        let (projection, view) = crate::camera::default_ortho2d(100.0, 100.0);
1699        let _camera = stage
1700            .new_camera()
1701            .with_projection_and_view(projection, view);
1702        let _triangle_rez = stage.new_primitive().with_vertices(
1703            stage.new_vertices([
1704                Vertex::default()
1705                    .with_position([10.0, 10.0, 0.0])
1706                    .with_color([0.0, 1.0, 1.0, 1.0]),
1707                Vertex::default()
1708                    .with_position([10.0, 90.0, 0.0])
1709                    .with_color([1.0, 1.0, 0.0, 1.0]),
1710                Vertex::default()
1711                    .with_position([90.0, 10.0, 0.0])
1712                    .with_color([1.0, 0.0, 1.0, 1.0]),
1713            ]),
1714        );
1715
1716        log::debug!("rendering without msaa");
1717        let frame = ctx.get_next_frame().unwrap();
1718        stage.render(&frame.view());
1719        let img = frame.read_image().block().unwrap();
1720        img_diff::assert_img_eq_cfg(
1721            "msaa/without.png",
1722            img,
1723            img_diff::DiffCfg {
1724                pixel_threshold: img_diff::LOW_PIXEL_THRESHOLD,
1725                ..Default::default()
1726            },
1727        );
1728        frame.present();
1729        log::debug!("  all good!");
1730
1731        stage.set_msaa_sample_count(4);
1732        log::debug!("rendering with msaa");
1733        let frame = ctx.get_next_frame().unwrap();
1734        stage.render(&frame.view());
1735        let img = frame.read_image().block().unwrap();
1736        img_diff::assert_img_eq_cfg(
1737            "msaa/with.png",
1738            img,
1739            img_diff::DiffCfg {
1740                pixel_threshold: img_diff::LOW_PIXEL_THRESHOLD,
1741                ..Default::default()
1742            },
1743        );
1744        frame.present();
1745    }
1746
1747    #[test]
1748    /// Tests that the PBR descriptor is written to slot 0 of the geometry buffer,
1749    /// and that it contains what we think it contains.
1750    fn stage_geometry_desc_sanity() {
1751        let ctx = Context::headless(100, 100).block();
1752        let stage = ctx.new_stage();
1753        let _ = stage.commit();
1754
1755        let slab = futures_lite::future::block_on({
1756            let geometry: &Geometry = stage.as_ref();
1757            geometry.slab_allocator().read(..)
1758        })
1759        .unwrap();
1760        let pbr_desc = slab.read_unchecked(Id::<GeometryDescriptor>::new(0));
1761        pretty_assertions::assert_eq!(stage.geometry_descriptor().get(), pbr_desc);
1762    }
1763
1764    #[test]
1765    fn slabbed_vertices_native() {
1766        let ctx = Context::headless(100, 100).block();
1767        let runtime = ctx.as_ref();
1768
1769        // Create our geometry on the slab.
1770        let slab = SlabAllocator::new(
1771            runtime,
1772            "slabbed_isosceles_triangle",
1773            wgpu::BufferUsages::empty(),
1774        );
1775
1776        let geometry = vec![
1777            (Vec3::new(0.5, -0.5, 0.0), Vec4::new(1.0, 0.0, 0.0, 1.0)),
1778            (Vec3::new(0.0, 0.5, 0.0), Vec4::new(0.0, 1.0, 0.0, 1.0)),
1779            (Vec3::new(-0.5, -0.5, 0.0), Vec4::new(0.0, 0.0, 1.0, 1.0)),
1780            (Vec3::new(-1.0, 1.0, 0.0), Vec4::new(1.0, 0.0, 0.0, 1.0)),
1781            (Vec3::new(-1.0, 0.0, 0.0), Vec4::new(0.0, 1.0, 0.0, 1.0)),
1782            (Vec3::new(0.0, 1.0, 0.0), Vec4::new(0.0, 0.0, 1.0, 1.0)),
1783        ];
1784        let vertices = slab.new_array(geometry);
1785        let array = slab.new_value(vertices.array());
1786
1787        // Create a bindgroup for the slab so our shader can read out the types.
1788        let bindgroup_layout =
1789            runtime
1790                .device
1791                .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1792                    label: None,
1793                    entries: &[wgpu::BindGroupLayoutEntry {
1794                        binding: 0,
1795                        visibility: wgpu::ShaderStages::VERTEX,
1796                        ty: wgpu::BindingType::Buffer {
1797                            ty: wgpu::BufferBindingType::Storage { read_only: true },
1798                            has_dynamic_offset: false,
1799                            min_binding_size: None,
1800                        },
1801                        count: None,
1802                    }],
1803                });
1804        let pipeline_layout =
1805            runtime
1806                .device
1807                .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1808                    label: None,
1809                    bind_group_layouts: &[&bindgroup_layout],
1810                    push_constant_ranges: &[],
1811                });
1812
1813        let vertex = crate::linkage::slabbed_vertices::linkage(&runtime.device);
1814        let fragment = crate::linkage::passthru_fragment::linkage(&runtime.device);
1815        let pipeline = runtime
1816            .device
1817            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1818                label: None,
1819                cache: None,
1820                layout: Some(&pipeline_layout),
1821                vertex: wgpu::VertexState {
1822                    compilation_options: wgpu::PipelineCompilationOptions::default(),
1823                    module: &vertex.module,
1824                    entry_point: Some(vertex.entry_point),
1825                    buffers: &[],
1826                },
1827                primitive: wgpu::PrimitiveState {
1828                    topology: wgpu::PrimitiveTopology::TriangleList,
1829                    strip_index_format: None,
1830                    front_face: wgpu::FrontFace::Ccw,
1831                    cull_mode: None,
1832                    unclipped_depth: false,
1833                    polygon_mode: wgpu::PolygonMode::Fill,
1834                    conservative: false,
1835                },
1836                depth_stencil: None,
1837                multisample: wgpu::MultisampleState {
1838                    mask: !0,
1839                    alpha_to_coverage_enabled: false,
1840                    count: 1,
1841                },
1842                fragment: Some(wgpu::FragmentState {
1843                    compilation_options: Default::default(),
1844                    module: &fragment.module,
1845                    entry_point: Some(fragment.entry_point),
1846                    targets: &[Some(wgpu::ColorTargetState {
1847                        format: wgpu::TextureFormat::Rgba8UnormSrgb,
1848                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1849                        write_mask: wgpu::ColorWrites::ALL,
1850                    })],
1851                }),
1852                multiview: None,
1853            });
1854        let slab_buffer = slab.commit();
1855
1856        let bindgroup = runtime
1857            .device
1858            .create_bind_group(&wgpu::BindGroupDescriptor {
1859                label: None,
1860                layout: &bindgroup_layout,
1861                entries: &[wgpu::BindGroupEntry {
1862                    binding: 0,
1863                    resource: slab_buffer.as_entire_binding(),
1864                }],
1865            });
1866
1867        let frame = ctx.get_next_frame().unwrap();
1868        let mut encoder = runtime.device.create_command_encoder(&Default::default());
1869        {
1870            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1871                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1872                    view: &frame.view(),
1873                    resolve_target: None,
1874                    ops: wgpu::Operations {
1875                        load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
1876                        store: wgpu::StoreOp::Store,
1877                    },
1878                    depth_slice: None,
1879                })],
1880                ..Default::default()
1881            });
1882            render_pass.set_pipeline(&pipeline);
1883            render_pass.set_bind_group(0, &bindgroup, &[]);
1884            let id = array.id().inner();
1885            render_pass.draw(0..vertices.len() as u32, id..id + 1);
1886        }
1887        runtime.queue.submit(std::iter::once(encoder.finish()));
1888
1889        let img = frame
1890            .read_linear_image()
1891            .block()
1892            .expect("could not read frame");
1893        img_diff::assert_img_eq("tutorial/slabbed_isosceles_triangle.png", img);
1894    }
1895}