Skip to main content

threecrate_gpu/
device.rs

1//! GPU device management
2
3use std::sync::OnceLock;
4use threecrate_core::{Error, Result};
5use wgpu::util::DeviceExt;
6
7/// Compute pipelines that are built once per context and reused across calls.
8/// Building a pipeline compiles its shader, which costs far more than running
9/// it on small inputs.
10#[derive(Default)]
11pub(crate) struct PipelineCache {
12    pub(crate) knn: OnceLock<wgpu::ComputePipeline>,
13    pub(crate) normals: OnceLock<wgpu::ComputePipeline>,
14    pub(crate) icp_match: OnceLock<wgpu::ComputePipeline>,
15    pub(crate) icp_match_plane: OnceLock<wgpu::ComputePipeline>,
16    pub(crate) radius_outlier: OnceLock<wgpu::ComputePipeline>,
17}
18
19/// GPU context for managing compute and rendering operations
20pub struct GpuContext {
21    pub instance: wgpu::Instance,
22    pub device: wgpu::Device,
23    pub queue: wgpu::Queue,
24    pub adapter: wgpu::Adapter,
25    pub(crate) pipelines: PipelineCache,
26}
27
28impl GpuContext {
29    /// Create a new GPU context
30    pub async fn new() -> Result<Self> {
31        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
32            backends: wgpu::Backends::all(),
33            flags: wgpu::InstanceFlags::default(),
34            ..wgpu::InstanceDescriptor::new_without_display_handle_from_env()
35        });
36
37        let adapter = instance
38            .request_adapter(&wgpu::RequestAdapterOptions {
39                power_preference: wgpu::PowerPreference::HighPerformance,
40                compatible_surface: None,
41                force_fallback_adapter: false,
42            })
43            .await
44            .map_err(|e| {
45                threecrate_core::Error::Gpu(format!("Failed to find suitable adapter: {:?}", e))
46            })?;
47
48        let (device, queue) = adapter
49            .request_device(&wgpu::DeviceDescriptor {
50                label: Some("ThreeCrate GPU Device"),
51                required_features: wgpu::Features::empty(),
52                required_limits: wgpu::Limits::default(),
53                ..Default::default()
54            })
55            .await
56            .map_err(|e| threecrate_core::Error::Gpu(format!("Failed to create device: {}", e)))?;
57
58        Ok(Self::from_parts(instance, adapter, device, queue))
59    }
60
61    /// Wrap a device the caller already created (for example to share it with
62    /// a renderer). Pipelines are compiled on first use, as with [`Self::new`].
63    pub fn from_parts(
64        instance: wgpu::Instance,
65        adapter: wgpu::Adapter,
66        device: wgpu::Device,
67        queue: wgpu::Queue,
68    ) -> Self {
69        Self {
70            instance,
71            device,
72            queue,
73            adapter,
74            pipelines: PipelineCache::default(),
75        }
76    }
77
78    /// Return the pipeline in `slot`, compiling `source` on first use. The bind
79    /// group layout is derived from the shader (`layout: None`).
80    pub(crate) fn cached_pipeline<'a>(
81        &self,
82        slot: &'a OnceLock<wgpu::ComputePipeline>,
83        label: &str,
84        source: impl FnOnce() -> String,
85        entry_point: &str,
86    ) -> &'a wgpu::ComputePipeline {
87        slot.get_or_init(|| {
88            let shader = self.create_shader_module(label, &source());
89            self.create_compute_pipeline(label, &shader, entry_point)
90        })
91    }
92
93    /// Copy `buffer` (which needs `COPY_SRC`) to the CPU and return its contents
94    /// as `T`s. Blocks until the GPU has finished all submitted work. For
95    /// repeated reads of the same size, keep a staging buffer and use
96    /// [`Self::read_staging`] instead.
97    pub(crate) fn read_buffer<T: bytemuck::Pod>(&self, buffer: &wgpu::Buffer) -> Result<Vec<T>> {
98        let staging = self.create_staging_buffer("Readback", buffer.size());
99        let mut encoder = self
100            .device
101            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
102                label: Some("Readback"),
103            });
104        encoder.copy_buffer_to_buffer(buffer, 0, &staging, 0, buffer.size());
105        self.queue.submit(std::iter::once(encoder.finish()));
106        self.read_staging(&staging)
107    }
108
109    /// A `MAP_READ` buffer that GPU results can be copied into.
110    pub(crate) fn create_staging_buffer(&self, label: &str, size: u64) -> wgpu::Buffer {
111        self.create_buffer(
112            label,
113            size,
114            wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
115        )
116    }
117
118    /// Read a staging buffer whose copy has already been submitted, and unmap
119    /// it again so it can be reused. Blocks until the GPU has finished.
120    pub(crate) fn read_staging<T: bytemuck::Pod>(&self, staging: &wgpu::Buffer) -> Result<Vec<T>> {
121        let slice = staging.slice(..);
122        let (sender, receiver) = std::sync::mpsc::channel();
123        slice.map_async(wgpu::MapMode::Read, move |result| {
124            let _ = sender.send(result);
125        });
126        self.device
127            .poll(wgpu::PollType::Wait {
128                submission_index: None,
129                timeout: None,
130            })
131            .map_err(|e| Error::Gpu(format!("GPU poll failed: {e}")))?;
132        receiver
133            .recv()
134            .map_err(|_| Error::Gpu("GPU readback was dropped".to_string()))?
135            .map_err(|e| Error::Gpu(format!("Failed to map GPU buffer: {e}")))?;
136
137        // `pod_collect_to_vec` copies into a correctly aligned Vec; the mapped
138        // range itself is only guaranteed 8-byte alignment.
139        let data = bytemuck::pod_collect_to_vec(&slice.get_mapped_range());
140        staging.unmap();
141        Ok(data)
142    }
143
144    /// Create a buffer from data
145    pub fn create_buffer_init<T: bytemuck::Pod>(
146        &self,
147        label: &str,
148        data: &[T],
149        usage: wgpu::BufferUsages,
150    ) -> wgpu::Buffer {
151        self.device
152            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
153                label: Some(label),
154                contents: bytemuck::cast_slice(data),
155                usage,
156            })
157    }
158
159    /// Create an empty buffer
160    pub fn create_buffer(&self, label: &str, size: u64, usage: wgpu::BufferUsages) -> wgpu::Buffer {
161        self.device.create_buffer(&wgpu::BufferDescriptor {
162            label: Some(label),
163            size,
164            usage,
165            mapped_at_creation: false,
166        })
167    }
168
169    /// Create a compute pipeline
170    pub fn create_compute_pipeline(
171        &self,
172        label: &str,
173        shader: &wgpu::ShaderModule,
174        entry_point: &str,
175    ) -> wgpu::ComputePipeline {
176        self.device
177            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
178                label: Some(label),
179                layout: None,
180                module: shader,
181                entry_point: Some(entry_point),
182                compilation_options: wgpu::PipelineCompilationOptions::default(),
183                cache: None,
184            })
185    }
186
187    /// Create a shader module from WGSL source
188    pub fn create_shader_module(&self, label: &str, source: &str) -> wgpu::ShaderModule {
189        self.device
190            .create_shader_module(wgpu::ShaderModuleDescriptor {
191                label: Some(label),
192                source: wgpu::ShaderSource::Wgsl(source.into()),
193            })
194    }
195
196    /// Create a bind group layout
197    pub fn create_bind_group_layout(
198        &self,
199        label: &str,
200        entries: &[wgpu::BindGroupLayoutEntry],
201    ) -> wgpu::BindGroupLayout {
202        self.device
203            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
204                label: Some(label),
205                entries,
206            })
207    }
208
209    /// Create a bind group
210    pub fn create_bind_group(
211        &self,
212        label: &str,
213        layout: &wgpu::BindGroupLayout,
214        entries: &[wgpu::BindGroupEntry],
215    ) -> wgpu::BindGroup {
216        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
217            label: Some(label),
218            layout,
219            entries,
220        })
221    }
222}