Skip to main content

viewport_lib/renderer/
picking.rs

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