//! Capability-selected hatch renderer. //! //! `Pipeline` sees one [`HatchGpu`]. This module owns the renderer selection, //! render pipeline, resident/preview uploads, visibility updates, and draw //! dispatch. The storage and texture transports remain private implementation //! details because their bind groups and upload layouts are fundamentally //! different. mod storage; mod texture; use super::device_capabilities::DeviceCapabilities; use crate::scene::model::hatch_model::HatchModel; use iced::wgpu; use storage::StorageHatchBatch; use texture::TextureHatch; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum HatchBackendKind { Storage, Texture, } impl HatchBackendKind { fn select(capabilities: DeviceCapabilities, force_compatibility: bool) -> Self { if !force_compatibility && capabilities.supports_batched_hatch() { Self::Storage } else { Self::Texture } } } enum HatchBackend { Storage { resident: Option, preview: Option, }, Texture { resident: Vec, preview: Vec, }, } /// One hatch renderer whose transport is selected from the active device. pub struct HatchGpu { pipeline: wgpu::RenderPipeline, bind_group_layout: wgpu::BindGroupLayout, backend: HatchBackend, } impl HatchGpu { pub fn new( device: &wgpu::Device, format: wgpu::TextureFormat, frame_bind_group_layout: &wgpu::BindGroupLayout, content_stencil: &wgpu::StencilState, capabilities: DeviceCapabilities, force_compatibility: bool, ) -> Self { let backend_kind = HatchBackendKind::select(capabilities, force_compatibility); let uses_storage = backend_kind == HatchBackendKind::Storage; let bind_group_layout = if uses_storage { StorageHatchBatch::bind_group_layout(device) } else { TextureHatch::bind_group_layout(device) }; let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("hatch.pipeline_layout"), bind_group_layouts: &[frame_bind_group_layout, &bind_group_layout].map(Some), immediate_size: 0, }); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some(if uses_storage { "hatch.storage.shader" } else { "hatch.texture.shader" }), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(if uses_storage { include_str!("../../../shaders/hatch.wgsl") } else { include_str!("../../../shaders/hatch_texture.wgsl") })), }); let vertex_layout = if uses_storage { storage::HatchVertex::layout() } else { texture::vertex_layout() }; let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some(if uses_storage { "hatch.storage.pipeline" } else { "hatch.texture.pipeline" }), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_main"), buffers: &[vertex_layout], compilation_options: wgpu::PipelineCompilationOptions::default(), }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, cull_mode: None, ..Default::default() }, depth_stencil: Some(wgpu::DepthStencilState { format: wgpu::TextureFormat::Depth24PlusStencil8, depth_write_enabled: Some(true), depth_compare: Some(wgpu::CompareFunction::LessEqual), stencil: content_stencil.clone(), bias: wgpu::DepthBiasState { constant: 1, slope_scale: 1.0, clamp: 0.0, }, }), multisample: wgpu::MultisampleState { count: super::MSAA_SAMPLES, mask: !0, alpha_to_coverage_enabled: false, }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState::ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: wgpu::PipelineCompilationOptions::default(), }), multiview_mask: None, cache: None, }); let backend = match backend_kind { HatchBackendKind::Storage => HatchBackend::Storage { resident: None, preview: None, }, HatchBackendKind::Texture => HatchBackend::Texture { resident: Vec::new(), preview: Vec::new(), }, }; Self { pipeline, bind_group_layout, backend, } } pub fn backend_name(&self) -> &'static str { match self.backend { HatchBackend::Storage { .. } => "storage", HatchBackend::Texture { .. } => "texture", } } pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, hatches: &[HatchModel]) { match &mut self.backend { HatchBackend::Storage { resident, .. } => { let renderable: Vec = hatches .iter() .filter(|hatch| hatch.boundary.len() >= 3) .cloned() .collect(); *resident = StorageHatchBatch::build(device, &self.bind_group_layout, &renderable); } HatchBackend::Texture { resident, .. } => { *resident = hatches .iter() .filter(|hatch| hatch.boundary.len() >= 3) .map(|hatch| TextureHatch::new(device, queue, hatch, &self.bind_group_layout)) .collect(); } } } pub fn upload_preview( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, hatches: &[HatchModel], ) { match &mut self.backend { HatchBackend::Storage { preview, .. } => { let renderable: Vec = hatches .iter() .filter(|hatch| hatch.boundary.len() >= 3) .cloned() .collect(); *preview = StorageHatchBatch::build(device, &self.bind_group_layout, &renderable); } HatchBackend::Texture { preview, .. } => { *preview = hatches .iter() .filter(|hatch| hatch.boundary.len() >= 3) .map(|hatch| TextureHatch::new(device, queue, hatch, &self.bind_group_layout)) .collect(); } } } /// Refresh resident hatch visibility. Texture hatches retain their existing /// per-draw behavior and return zero because they have no visibility buffer. pub fn update_visibility( &mut self, queue: &wgpu::Queue, mut is_visible: impl FnMut([f32; 4]) -> bool, ) -> usize { let HatchBackend::Storage { resident: Some(batch), .. } = &mut self.backend else { return 0; }; for (index, aabb) in batch.instance_aabbs.iter().copied().enumerate() { batch.visibility[index] = u32::from(is_visible(aabb)); } batch.upload_visibility(queue); batch.instance_aabbs.len() } pub fn draw<'pass>( &'pass self, pass: &mut wgpu::RenderPass<'pass>, frame_bind_group: &'pass wgpu::BindGroup, stencil_reference: u32, ) { pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, frame_bind_group, &[]); pass.set_stencil_reference(stencil_reference); match &self.backend { HatchBackend::Storage { resident, preview } => { for batch in [resident.as_ref(), preview.as_ref()].into_iter().flatten() { pass.set_bind_group(1, &batch.bind_group, &[]); pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..)); pass.draw(0..batch.vertex_count, 0..1); } } HatchBackend::Texture { resident, preview } => { for hatch in resident.iter().chain(preview) { pass.set_bind_group(1, &hatch.bind_group, &[]); pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..)); pass.draw(0..6, 0..1); } } } } } #[cfg(test)] mod tests { use super::{DeviceCapabilities, HatchBackendKind}; fn capabilities(storage_bindings: u32) -> DeviceCapabilities { DeviceCapabilities { max_storage_buffers_per_shader_stage: storage_bindings, max_inter_stage_shader_components: 31, max_vertex_attributes: 16, } } #[test] fn selects_backend_from_device_limits() { assert_eq!( HatchBackendKind::select(capabilities(5), false), HatchBackendKind::Storage ); assert_eq!( HatchBackendKind::select(capabilities(4), false), HatchBackendKind::Texture ); } #[test] fn compatibility_override_selects_texture_backend() { assert_eq!( HatchBackendKind::select(capabilities(8), true), HatchBackendKind::Texture ); } }