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::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::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_bind_group_layout_1
110                .as_ref()
111                .expect("ensure_pick_pipeline must be called first"),
112            entries: &[wgpu::BindGroupEntry {
113                binding: 0,
114                resource: pick_instance_buf.as_entire_binding(),
115            }],
116        });
117
118        // --- pick camera uniform buffer + bind group ---
119        let camera_uniform = frame.camera.render_camera.camera_uniform();
120        let camera_bytes = bytemuck::bytes_of(&camera_uniform);
121        let pick_camera_buf = device.create_buffer(&wgpu::BufferDescriptor {
122            label: Some("pick_camera_buf"),
123            size: std::mem::size_of::<CameraUniform>() as u64,
124            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
125            mapped_at_creation: false,
126        });
127        queue.write_buffer(&pick_camera_buf, 0, camera_bytes);
128
129        let pick_camera_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
130            label: Some("pick_camera_bg"),
131            layout: self
132                .resources
133                .pick_camera_bgl
134                .as_ref()
135                .expect("ensure_pick_pipeline must be called first"),
136            entries: &[
137                wgpu::BindGroupEntry {
138                    binding: 0,
139                    resource: pick_camera_buf.as_entire_binding(),
140                },
141                wgpu::BindGroupEntry {
142                    binding: 6,
143                    resource: self.resources.clip_volume_uniform_buf.as_entire_binding(),
144                },
145            ],
146        });
147
148        // --- offscreen pick textures (R32Uint + R32Float) + depth ---
149        let pick_id_texture = device.create_texture(&wgpu::TextureDescriptor {
150            label: Some("pick_id_texture"),
151            size: wgpu::Extent3d {
152                width: vp_w,
153                height: vp_h,
154                depth_or_array_layers: 1,
155            },
156            mip_level_count: 1,
157            sample_count: 1,
158            dimension: wgpu::TextureDimension::D2,
159            format: wgpu::TextureFormat::R32Uint,
160            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
161            view_formats: &[],
162        });
163        let pick_id_view = pick_id_texture.create_view(&wgpu::TextureViewDescriptor::default());
164
165        let pick_depth_texture = device.create_texture(&wgpu::TextureDescriptor {
166            label: Some("pick_depth_colour_texture"),
167            size: wgpu::Extent3d {
168                width: vp_w,
169                height: vp_h,
170                depth_or_array_layers: 1,
171            },
172            mip_level_count: 1,
173            sample_count: 1,
174            dimension: wgpu::TextureDimension::D2,
175            format: wgpu::TextureFormat::R32Float,
176            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
177            view_formats: &[],
178        });
179        let pick_depth_view =
180            pick_depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
181
182        let depth_stencil_texture = device.create_texture(&wgpu::TextureDescriptor {
183            label: Some("pick_ds_texture"),
184            size: wgpu::Extent3d {
185                width: vp_w,
186                height: vp_h,
187                depth_or_array_layers: 1,
188            },
189            mip_level_count: 1,
190            sample_count: 1,
191            dimension: wgpu::TextureDimension::D2,
192            format: wgpu::TextureFormat::Depth24PlusStencil8,
193            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
194            view_formats: &[],
195        });
196        let depth_stencil_view =
197            depth_stencil_texture.create_view(&wgpu::TextureViewDescriptor::default());
198
199        // --- render pass ---
200        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
201            label: Some("pick_pass_encoder"),
202        });
203        {
204            let mut pick_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
205                label: Some("pick_pass"),
206                color_attachments: &[
207                    Some(wgpu::RenderPassColorAttachment {
208                        view: &pick_id_view,
209                        resolve_target: None,
210                        depth_slice: None,
211                        ops: wgpu::Operations {
212                            load: wgpu::LoadOp::Clear(wgpu::Color {
213                                r: 0.0,
214                                g: 0.0,
215                                b: 0.0,
216                                a: 0.0,
217                            }),
218                            store: wgpu::StoreOp::Store,
219                        },
220                    }),
221                    Some(wgpu::RenderPassColorAttachment {
222                        view: &pick_depth_view,
223                        resolve_target: None,
224                        depth_slice: None,
225                        ops: wgpu::Operations {
226                            load: wgpu::LoadOp::Clear(wgpu::Color {
227                                r: 1.0,
228                                g: 0.0,
229                                b: 0.0,
230                                a: 0.0,
231                            }),
232                            store: wgpu::StoreOp::Store,
233                        },
234                    }),
235                ],
236                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
237                    view: &depth_stencil_view,
238                    depth_ops: Some(wgpu::Operations {
239                        load: wgpu::LoadOp::Clear(1.0),
240                        store: wgpu::StoreOp::Store,
241                    }),
242                    stencil_ops: None,
243                }),
244                timestamp_writes: None,
245                occlusion_query_set: None,
246            });
247
248            pick_pass.set_pipeline(
249                self.resources
250                    .pick_pipeline
251                    .as_ref()
252                    .expect("ensure_pick_pipeline must be called first"),
253            );
254            pick_pass.set_bind_group(0, &pick_camera_bg, &[]);
255            pick_pass.set_bind_group(1, &pick_instance_bg, &[]);
256
257            // Draw each pickable item with its instance slot.
258            // Instance index in the storage buffer = position in pick_instances vec.
259            for (instance_slot, item) in pickable_items.iter().enumerate() {
260                let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
261                    continue;
262                };
263                pick_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
264                pick_pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
265                let slot = instance_slot as u32;
266                pick_pass.draw_indexed(0..mesh.index_count, 0, slot..slot + 1);
267            }
268        }
269
270        // --- copy 1x1 pixels to staging buffers ---
271        // R32Uint: 4 bytes per pixel, min bytes_per_row = 256 (wgpu alignment)
272        let bytes_per_row_aligned = 256u32; // wgpu requires multiples of 256
273
274        let id_staging = device.create_buffer(&wgpu::BufferDescriptor {
275            label: Some("pick_id_staging"),
276            size: bytes_per_row_aligned as u64,
277            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
278            mapped_at_creation: false,
279        });
280        let depth_staging = device.create_buffer(&wgpu::BufferDescriptor {
281            label: Some("pick_depth_staging"),
282            size: bytes_per_row_aligned as u64,
283            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
284            mapped_at_creation: false,
285        });
286
287        // Convert logical cursor to physical pixel coordinates for the pick texture readback.
288        let px = (cursor.x * ppp).round() as u32;
289        let py = (cursor.y * ppp).round() as u32;
290
291        encoder.copy_texture_to_buffer(
292            wgpu::TexelCopyTextureInfo {
293                texture: &pick_id_texture,
294                mip_level: 0,
295                origin: wgpu::Origin3d { x: px, y: py, z: 0 },
296                aspect: wgpu::TextureAspect::All,
297            },
298            wgpu::TexelCopyBufferInfo {
299                buffer: &id_staging,
300                layout: wgpu::TexelCopyBufferLayout {
301                    offset: 0,
302                    bytes_per_row: Some(bytes_per_row_aligned),
303                    rows_per_image: Some(1),
304                },
305            },
306            wgpu::Extent3d {
307                width: 1,
308                height: 1,
309                depth_or_array_layers: 1,
310            },
311        );
312        encoder.copy_texture_to_buffer(
313            wgpu::TexelCopyTextureInfo {
314                texture: &pick_depth_texture,
315                mip_level: 0,
316                origin: wgpu::Origin3d { x: px, y: py, z: 0 },
317                aspect: wgpu::TextureAspect::All,
318            },
319            wgpu::TexelCopyBufferInfo {
320                buffer: &depth_staging,
321                layout: wgpu::TexelCopyBufferLayout {
322                    offset: 0,
323                    bytes_per_row: Some(bytes_per_row_aligned),
324                    rows_per_image: Some(1),
325                },
326            },
327            wgpu::Extent3d {
328                width: 1,
329                height: 1,
330                depth_or_array_layers: 1,
331            },
332        );
333
334        queue.submit(std::iter::once(encoder.finish()));
335
336        // --- map and read ---
337        let (tx_id, rx_id) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
338        let (tx_dep, rx_dep) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
339        id_staging
340            .slice(..)
341            .map_async(wgpu::MapMode::Read, move |r| {
342                let _ = tx_id.send(r);
343            });
344        depth_staging
345            .slice(..)
346            .map_async(wgpu::MapMode::Read, move |r| {
347                let _ = tx_dep.send(r);
348            });
349        device
350            .poll(wgpu::PollType::Wait {
351                submission_index: None,
352                timeout: Some(std::time::Duration::from_secs(5)),
353            })
354            .unwrap();
355        let _ = rx_id.recv().unwrap_or(Err(wgpu::BufferAsyncError));
356        let _ = rx_dep.recv().unwrap_or(Err(wgpu::BufferAsyncError));
357
358        let object_id = {
359            let data = id_staging.slice(..).get_mapped_range();
360            u32::from_le_bytes([data[0], data[1], data[2], data[3]])
361        };
362        id_staging.unmap();
363
364        let depth = {
365            let data = depth_staging.slice(..).get_mapped_range();
366            f32::from_le_bytes([data[0], data[1], data[2], data[3]])
367        };
368        depth_staging.unmap();
369
370        // 0 = miss (clear colour or non-pickable surface).
371        if object_id == 0 {
372            return None;
373        }
374
375        Some(crate::interaction::picking::GpuPickHit {
376            object_id: PickId(object_id as u64),
377            depth,
378        })
379    }
380}