Skip to main content

viewport_lib/renderer/picking/
gpu.rs

1//! GPU object-ID picking: render the scene to an offscreen R32Uint texture
2//! and read back the single pixel under the cursor.
3
4use super::*;
5
6impl ViewportRenderer {
7    // -----------------------------------------------------------------------
8    // GPU object-ID picking
9    // -----------------------------------------------------------------------
10
11    /// GPU object-ID pick: renders the scene to an offscreen `R32Uint` texture
12    /// and reads back the single pixel under `cursor`.
13    ///
14    /// This is O(1) in mesh complexity : every object is rendered with a flat
15    /// `u32` ID, and only one pixel is read back. For triangle-level queries
16    /// (barycentric scalar probe, exact world position), use the CPU
17    /// [`crate::interaction::query::picking::pick_scene_cpu`] path instead.
18    ///
19    /// The pipeline is lazily initialized on first call : zero overhead when
20    /// this method is never invoked.
21    ///
22    /// # Arguments
23    /// * `device` : wgpu device
24    /// * `queue` : wgpu queue
25    /// * `cursor` : cursor position in viewport-local pixels (top-left origin)
26    /// * `frame` : current grouped frame data (camera, scene surfaces, viewport size)
27    ///
28    /// # Returns
29    /// `Some(GpuPickHit)` if an object is under the cursor, `None` if empty space.
30    pub fn pick_scene_gpu(
31        &mut self,
32        device: &wgpu::Device,
33        queue: &wgpu::Queue,
34        cursor: glam::Vec2,
35        frame: &FrameData,
36    ) -> Option<crate::interaction::query::picking::GpuPickHit> {
37        // In Playback mode, throttle picking to every 4th frame to reduce overhead
38        // during animation. Interactive, Paused, and Capture modes always pick.
39        if self.runtime_mode == crate::renderer::stats::RuntimeMode::Playback
40            && self.frame_counter % 4 != 0
41        {
42            return None;
43        }
44
45        // Read scene items from the surface submission.
46        let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
47            SurfaceSubmission::Flat(items) => items.as_ref(),
48        };
49
50        let ppp = frame.camera.pixels_per_point;
51        let vp_w = (frame.camera.viewport_size[0] * ppp).round() as u32;
52        let vp_h = (frame.camera.viewport_size[1] * ppp).round() as u32;
53
54        // --- bounds check (logical coordinates match the logical cursor) ---
55        if cursor.x < 0.0
56            || cursor.y < 0.0
57            || cursor.x >= frame.camera.viewport_size[0]
58            || cursor.y >= frame.camera.viewport_size[1]
59            || vp_w == 0
60            || vp_h == 0
61        {
62            return None;
63        }
64
65        // --- lazy pipeline init ---
66        self.resources.ensure_pick_pipeline(device);
67
68        // --- build PickInstance data ---
69        // Only surfaces with a nonzero pick_id participate in picking.
70        // Clear value 0 means "no hit" (or non-pickable surface).
71        let pickable_items: Vec<&SceneRenderItem> = scene_items
72            .iter()
73            .filter(|item| !item.settings.hidden && item.settings.pick_id != PickId::NONE)
74            .collect();
75
76        let pick_instances: Vec<PickInstance> = pickable_items
77            .iter()
78            .map(|item| {
79                let m = item.model;
80                PickInstance {
81                    model_c0: m[0],
82                    model_c1: m[1],
83                    model_c2: m[2],
84                    model_c3: m[3],
85                    object_id: item.settings.pick_id.0 as u32,
86                    _pad: [0; 3],
87                }
88            })
89            .collect();
90
91        if pick_instances.is_empty() {
92            return None;
93        }
94
95        // --- pick instance storage buffer + bind group ---
96        let pick_instance_bytes = bytemuck::cast_slice(&pick_instances);
97        let pick_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
98            label: Some("pick_instance_buf"),
99            size: pick_instance_bytes.len().max(80) as u64,
100            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
101            mapped_at_creation: false,
102        });
103        queue.write_buffer(&pick_instance_buf, 0, pick_instance_bytes);
104
105        let pick_instance_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
106            label: Some("pick_instance_bg"),
107            layout: self
108                .resources
109                .pick
110                .bind_group_layout_1
111                .as_ref()
112                .expect("ensure_pick_pipeline must be called first"),
113            entries: &[wgpu::BindGroupEntry {
114                binding: 0,
115                resource: pick_instance_buf.as_entire_binding(),
116            }],
117        });
118
119        // --- pick camera uniform buffer + bind group ---
120        let camera_uniform = frame.camera.render_camera.camera_uniform();
121        let camera_bytes = bytemuck::bytes_of(&camera_uniform);
122        let pick_camera_buf = device.create_buffer(&wgpu::BufferDescriptor {
123            label: Some("pick_camera_buf"),
124            size: std::mem::size_of::<CameraUniform>() as u64,
125            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
126            mapped_at_creation: false,
127        });
128        queue.write_buffer(&pick_camera_buf, 0, camera_bytes);
129
130        let pick_camera_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
131            label: Some("pick_camera_bg"),
132            layout: self
133                .resources
134                .pick
135                .camera_bgl
136                .as_ref()
137                .expect("ensure_pick_pipeline must be called first"),
138            entries: &[
139                wgpu::BindGroupEntry {
140                    binding: 0,
141                    resource: pick_camera_buf.as_entire_binding(),
142                },
143                wgpu::BindGroupEntry {
144                    binding: 6,
145                    resource: self.resources.clip_volume_uniform_buf.as_entire_binding(),
146                },
147            ],
148        });
149
150        // --- offscreen pick textures (R32Uint + R32Float) + depth ---
151        let pick_id_texture = device.create_texture(&wgpu::TextureDescriptor {
152            label: Some("pick_id_texture"),
153            size: wgpu::Extent3d {
154                width: vp_w,
155                height: vp_h,
156                depth_or_array_layers: 1,
157            },
158            mip_level_count: 1,
159            sample_count: 1,
160            dimension: wgpu::TextureDimension::D2,
161            format: wgpu::TextureFormat::R32Uint,
162            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
163            view_formats: &[],
164        });
165        let pick_id_view = pick_id_texture.create_view(&wgpu::TextureViewDescriptor::default());
166
167        let pick_depth_texture = device.create_texture(&wgpu::TextureDescriptor {
168            label: Some("pick_depth_colour_texture"),
169            size: wgpu::Extent3d {
170                width: vp_w,
171                height: vp_h,
172                depth_or_array_layers: 1,
173            },
174            mip_level_count: 1,
175            sample_count: 1,
176            dimension: wgpu::TextureDimension::D2,
177            format: wgpu::TextureFormat::R32Float,
178            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
179            view_formats: &[],
180        });
181        let pick_depth_view =
182            pick_depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
183
184        let depth_stencil_texture = device.create_texture(&wgpu::TextureDescriptor {
185            label: Some("pick_ds_texture"),
186            size: wgpu::Extent3d {
187                width: vp_w,
188                height: vp_h,
189                depth_or_array_layers: 1,
190            },
191            mip_level_count: 1,
192            sample_count: 1,
193            dimension: wgpu::TextureDimension::D2,
194            format: wgpu::TextureFormat::Depth24PlusStencil8,
195            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
196            view_formats: &[],
197        });
198        let depth_stencil_view =
199            depth_stencil_texture.create_view(&wgpu::TextureViewDescriptor::default());
200
201        // --- render pass ---
202        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
203            label: Some("pick_pass_encoder"),
204        });
205        {
206            let mut pick_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
207                label: Some("pick_pass"),
208                color_attachments: &[
209                    Some(wgpu::RenderPassColorAttachment {
210                        view: &pick_id_view,
211                        resolve_target: None,
212                        depth_slice: None,
213                        ops: wgpu::Operations {
214                            load: wgpu::LoadOp::Clear(wgpu::Color {
215                                r: 0.0,
216                                g: 0.0,
217                                b: 0.0,
218                                a: 0.0,
219                            }),
220                            store: wgpu::StoreOp::Store,
221                        },
222                    }),
223                    Some(wgpu::RenderPassColorAttachment {
224                        view: &pick_depth_view,
225                        resolve_target: None,
226                        depth_slice: None,
227                        ops: wgpu::Operations {
228                            load: wgpu::LoadOp::Clear(wgpu::Color {
229                                r: 1.0,
230                                g: 0.0,
231                                b: 0.0,
232                                a: 0.0,
233                            }),
234                            store: wgpu::StoreOp::Store,
235                        },
236                    }),
237                ],
238                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
239                    view: &depth_stencil_view,
240                    depth_ops: Some(wgpu::Operations {
241                        load: wgpu::LoadOp::Clear(1.0),
242                        store: wgpu::StoreOp::Store,
243                    }),
244                    stencil_ops: None,
245                }),
246                timestamp_writes: None,
247                occlusion_query_set: None,
248            });
249
250            pick_pass.set_pipeline(
251                self.resources
252                    .pick
253                    .pipeline
254                    .as_ref()
255                    .expect("ensure_pick_pipeline must be called first"),
256            );
257            pick_pass.set_bind_group(0, &pick_camera_bg, &[]);
258            pick_pass.set_bind_group(1, &pick_instance_bg, &[]);
259
260            // Draw each pickable item with its instance slot.
261            // Instance index in the storage buffer = position in pick_instances vec.
262            for (instance_slot, item) in pickable_items.iter().enumerate() {
263                let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
264                    continue;
265                };
266                pick_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
267                pick_pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
268                let slot = instance_slot as u32;
269                pick_pass.draw_indexed(0..mesh.index_count, 0, slot..slot + 1);
270            }
271        }
272
273        // --- copy 1x1 pixels to staging buffers ---
274        // R32Uint: 4 bytes per pixel, min bytes_per_row = 256 (wgpu alignment)
275        let bytes_per_row_aligned = 256u32; // wgpu requires multiples of 256
276
277        let id_staging = device.create_buffer(&wgpu::BufferDescriptor {
278            label: Some("pick_id_staging"),
279            size: bytes_per_row_aligned as u64,
280            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
281            mapped_at_creation: false,
282        });
283        let depth_staging = device.create_buffer(&wgpu::BufferDescriptor {
284            label: Some("pick_depth_staging"),
285            size: bytes_per_row_aligned as u64,
286            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
287            mapped_at_creation: false,
288        });
289
290        // Convert logical cursor to physical pixel coordinates for the pick texture readback.
291        let px = (cursor.x * ppp).round() as u32;
292        let py = (cursor.y * ppp).round() as u32;
293
294        encoder.copy_texture_to_buffer(
295            wgpu::TexelCopyTextureInfo {
296                texture: &pick_id_texture,
297                mip_level: 0,
298                origin: wgpu::Origin3d { x: px, y: py, z: 0 },
299                aspect: wgpu::TextureAspect::All,
300            },
301            wgpu::TexelCopyBufferInfo {
302                buffer: &id_staging,
303                layout: wgpu::TexelCopyBufferLayout {
304                    offset: 0,
305                    bytes_per_row: Some(bytes_per_row_aligned),
306                    rows_per_image: Some(1),
307                },
308            },
309            wgpu::Extent3d {
310                width: 1,
311                height: 1,
312                depth_or_array_layers: 1,
313            },
314        );
315        encoder.copy_texture_to_buffer(
316            wgpu::TexelCopyTextureInfo {
317                texture: &pick_depth_texture,
318                mip_level: 0,
319                origin: wgpu::Origin3d { x: px, y: py, z: 0 },
320                aspect: wgpu::TextureAspect::All,
321            },
322            wgpu::TexelCopyBufferInfo {
323                buffer: &depth_staging,
324                layout: wgpu::TexelCopyBufferLayout {
325                    offset: 0,
326                    bytes_per_row: Some(bytes_per_row_aligned),
327                    rows_per_image: Some(1),
328                },
329            },
330            wgpu::Extent3d {
331                width: 1,
332                height: 1,
333                depth_or_array_layers: 1,
334            },
335        );
336
337        queue.submit(std::iter::once(encoder.finish()));
338
339        // --- map and read ---
340        let (tx_id, rx_id) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
341        let (tx_dep, rx_dep) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
342        id_staging
343            .slice(..)
344            .map_async(wgpu::MapMode::Read, move |r| {
345                let _ = tx_id.send(r);
346            });
347        depth_staging
348            .slice(..)
349            .map_async(wgpu::MapMode::Read, move |r| {
350                let _ = tx_dep.send(r);
351            });
352        device
353            .poll(wgpu::PollType::Wait {
354                submission_index: None,
355                timeout: Some(std::time::Duration::from_secs(5)),
356            })
357            .unwrap();
358        let _ = rx_id.recv().unwrap_or(Err(wgpu::BufferAsyncError));
359        let _ = rx_dep.recv().unwrap_or(Err(wgpu::BufferAsyncError));
360
361        let object_id = {
362            let data = id_staging.slice(..).get_mapped_range();
363            u32::from_le_bytes([data[0], data[1], data[2], data[3]])
364        };
365        id_staging.unmap();
366
367        let depth = {
368            let data = depth_staging.slice(..).get_mapped_range();
369            f32::from_le_bytes([data[0], data[1], data[2], data[3]])
370        };
371        depth_staging.unmap();
372
373        // 0 = miss (clear colour or non-pickable surface).
374        if object_id == 0 {
375            return None;
376        }
377
378        Some(crate::interaction::query::picking::GpuPickHit {
379            object_id: PickId(object_id as u64),
380            depth,
381        })
382    }
383}