Skip to main content

renderling/stage/
cpu.rs

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