//! The pick buffer: which model edges are visible at each pixel of the viewport, for picking on a //! large model without testing every edge. Surfaces are drawn into the depth buffer only; then //! every edge segment is drawn as a line carrying its id, depth tested against them (lifted //! toward the eye by the picker's slack, the depth by which an edge may lie behind a face and //! still count as visible) and written into an `R32Uint` id texture (0: no edge). //! //! The pass is drawn once per view (camera, viewport size, model), and only small tiles around //! the cursor are read back, so a pointer moving over a still view reads a few kilobytes and //! draws nothing. Read-backs arrive a frame or two later ([`PickResults`]); until a tile is in, //! the picker answers on the CPU. The ids are hints: the picker still runs its exact tests on //! the edges a tile names. use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::{Arc, Mutex}; use wgpu::util::DeviceExt; /// The side of a read-back tile, in pixels (a multiple of 64, so rows need no padding). pub const TILE: u32 = 128; /// Most tiles kept from one view. const KEEP: usize = 6; /// Most read-backs in flight. const IN_FLIGHT: usize = 3; /// Bytes per surface vertex (position) and per edge vertex (position, id). const FACE_VERTEX: u64 = 12; const EDGE_VERTEX: u64 = 16; const UNIFORM_SIZE: u64 = 96; const SHADER: &str = r#" struct U { vp: mat4x4, eye: vec4, // eye position, w = 1 for perspective back: vec4, // toward the eye (orthographic), w = how far edges are lifted toward it }; @group(0) @binding(0) var u: U; @vertex fn vs_face(@location(0) p: vec3) -> @builtin(position) vec4 { return u.vp * vec4(p, 1.0); } @fragment fn fs_face() -> @location(0) u32 { return 0u; } struct EOut { @builtin(position) pos: vec4, @location(0) @interpolate(flat) id: u32, }; @vertex fn vs_edge(@location(0) p: vec3, @location(1) id: u32) -> EOut { var toward = u.back.xyz; if (u.eye.w > 0.5) { let v = u.eye.xyz - p; let l = length(v); if (l > 0.0) { toward = v / l; } } var o: EOut; o.pos = u.vp * vec4(p + toward * u.back.w, 1.0); o.id = id; return o; } @fragment fn fs_edge(i: EOut) -> @location(0) u32 { return i.id; } "#; /// What the pass draws: surfaces (for depth) and edge segments with their ids (from 1). #[derive(Default, Clone)] pub struct PickScene { faces: Vec, edges: Vec, } impl PickScene { pub fn triangle(&mut self, t: [[f32; 3]; 3]) { for p in t { for c in p { self.faces.extend_from_slice(&c.to_le_bytes()); } } } /// Segment `a`–`b` of the edge `id` (0 is no edge: not drawn). pub fn segment(&mut self, a: [f32; 3], b: [f32; 3], id: u32) { if id == 0 { return; } for p in [a, b] { for c in p { self.edges.extend_from_slice(&c.to_le_bytes()); } self.edges.extend_from_slice(&id.to_le_bytes()); } } pub fn is_empty(&self) -> bool { self.edges.is_empty() } } /// The view the pass is drawn for. #[derive(Clone, Copy, Debug)] pub struct PickView { /// Changes whenever anything below or the scene changes. pub key: u64, /// Column-major view-projection (as [`crate::view::Frame::view_proj`]). pub view_proj: [[f32; 4]; 4], /// Eye position, and 1 for a perspective camera (0: orthographic). pub eye: [f32; 4], /// Direction toward the eye (orthographic). pub back: [f32; 3], /// How far edges are lifted toward the eye (model units). pub lift: f32, /// The viewport in physical pixels. pub size: [u32; 2], } /// Edge ids of a block of pixels of one view. #[derive(Clone, Debug, PartialEq, Eq)] pub struct PickTile { pub key: u64, pub origin: [u32; 2], pub size: [u32; 2], /// Row-major, `size[0] * size[1]`. pub ids: Vec, } impl PickTile { /// Whether the tile is of view `key` and holds every pixel of `[x0, y0, x1, y1]` (inclusive) /// that lies inside a viewport of `view` pixels. pub fn covers(&self, key: u64, view: [u32; 2], [x0, y0, x1, y1]: [i64; 4]) -> bool { let clamp = |v: i64, n: u32| v.clamp(0, i64::from(n.max(1)) - 1); let (x0, x1, y0, y1) = (clamp(x0, view[0]), clamp(x1, view[0]), clamp(y0, view[1]), clamp(y1, view[1])); let [ox, oy] = self.origin.map(i64::from); let [w, h] = self.size.map(i64::from); self.key == key && x0 >= ox && y0 >= oy && x1 < ox + w && y1 < oy + h } /// The ids within `[x0, y0, x1, y1]` (inclusive; outside the tile: none), each once. pub fn ids_in(&self, [x0, y0, x1, y1]: [i64; 4]) -> Vec { let [ox, oy] = self.origin.map(i64::from); let [w, h] = self.size.map(i64::from); let mut out = Vec::new(); for y in y0.max(oy)..=y1.min(oy + h - 1) { for x in x0.max(ox)..=x1.min(ox + w - 1) { if let Some(&id) = self.ids.get(((y - oy) * w + (x - ox)) as usize) && id != 0 { out.push(id); } } } out.sort_unstable(); out.dedup(); out } } /// The tiles read back so far (of the latest view only), newest last. #[derive(Default, Debug)] pub struct PickResults { pub tiles: Vec, } impl PickResults { /// The newest tile of view `key` covering `window`. pub fn tile(&self, key: u64, view: [u32; 2], window: [i64; 4]) -> Option<&PickTile> { self.tiles.iter().rev().find(|t| t.covers(key, view, window)) } } pub type PickOut = Arc>; /// The tile (origin and size) around pixel `centre` of a viewport of `view` pixels. pub fn tile_around(centre: [u32; 2], view: [u32; 2]) -> ([u32; 2], [u32; 2]) { let size = [TILE.min(view[0]), TILE.min(view[1])]; let origin = [0, 1].map(|k| centre[k].saturating_sub(size[k] / 2).min(view[k].saturating_sub(size[k]))); (origin, size) } const NEW: u8 = 0; const MAPPING: u8 = 1; const READY: u8 = 2; const FAILED: u8 = 3; /// A tile being copied out of the id texture. struct Pending { key: u64, origin: [u32; 2], size: [u32; 2], bytes_per_row: u32, buffer: wgpu::Buffer, /// The frame whose commands copy it (it is mapped once they are submitted). frame: u64, state: Arc, } struct Target { size: [u32; 2], ids: wgpu::Texture, ids_view: wgpu::TextureView, depth_view: wgpu::TextureView, } struct Uploaded { key: u64, faces: Option<(wgpu::Buffer, u32)>, edges: Option<(wgpu::Buffer, u32)>, } /// The pass's pipelines, its textures and the read-backs in flight, on one device. pub struct PickBuffer { face: wgpu::RenderPipeline, edge: wgpu::RenderPipeline, uniform: wgpu::Buffer, bind: wgpu::BindGroup, scene: Option, target: Option, /// The view drawn into the textures. drawn: Option, pending: Vec, frame: u64, } const DEPTH: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; const IDS: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint; impl PickBuffer { pub fn new(device: &wgpu::Device) -> PickBuffer { let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("sc_pick"), source: wgpu::ShaderSource::Wgsl(SHADER.into()) }); let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("sc_pick"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None }, count: None, }], }); let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("sc_pick"), bind_group_layouts: &[Some(&bgl)], immediate_size: 0, }); let uniform = device.create_buffer(&wgpu::BufferDescriptor { label: Some("sc_pick_u"), size: UNIFORM_SIZE, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let bind = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("sc_pick"), layout: &bgl, entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() }], }); const FA: [wgpu::VertexAttribute; 1] = wgpu::vertex_attr_array![0 => Float32x3]; const EA: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![0 => Float32x3, 1 => Uint32]; let pipeline = |label: &str, vs: &str, fs: &str, stride: u64, attributes: &'static [wgpu::VertexAttribute], topology: wgpu::PrimitiveTopology, write: bool, mask: wgpu::ColorWrites| { device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some(label), layout: Some(&layout), vertex: wgpu::VertexState { module: &module, entry_point: Some(vs), buffers: &[Some(wgpu::VertexBufferLayout { array_stride: stride, step_mode: wgpu::VertexStepMode::Vertex, attributes })], compilation_options: Default::default(), }, primitive: wgpu::PrimitiveState { topology, ..Default::default() }, depth_stencil: Some(wgpu::DepthStencilState { format: DEPTH, depth_write_enabled: Some(write), depth_compare: Some(wgpu::CompareFunction::LessEqual), stencil: Default::default(), bias: Default::default(), }), multisample: wgpu::MultisampleState::default(), fragment: Some(wgpu::FragmentState { module: &module, entry_point: Some(fs), targets: &[Some(wgpu::ColorTargetState { format: IDS, blend: None, write_mask: mask })], compilation_options: Default::default(), }), multiview_mask: None, cache: None, }) }; let face = pipeline("sc_pick_face", "vs_face", "fs_face", FACE_VERTEX, &FA, wgpu::PrimitiveTopology::TriangleList, true, wgpu::ColorWrites::empty()); let edge = pipeline("sc_pick_edge", "vs_edge", "fs_edge", EDGE_VERTEX, &EA, wgpu::PrimitiveTopology::LineList, false, wgpu::ColorWrites::ALL); PickBuffer { face, edge, uniform, bind, scene: None, target: None, drawn: None, pending: Vec::new(), frame: 0 } } /// Before the frame is submitted: collect the tiles read back, then draw `view` of `scene` /// (`scene_key` names it; it is uploaded when that changes) unless it is drawn already, and /// start reading back the tile around pixel `centre` unless it is in or on its way. The tiles /// land in `out` a frame or two later; keep frames coming until then. #[allow(clippy::too_many_arguments)] pub fn prepare( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, scene_key: u64, scene: &PickScene, view: &PickView, centre: Option<[u32; 2]>, out: &PickOut, ) { self.frame += 1; self.collect(device, out); let [w, h] = view.size; let max = device.limits().max_texture_dimension_2d; if w == 0 || h == 0 || w > max || h > max { return; } if self.scene.as_ref().is_none_or(|s| s.key != scene_key) { let limit = device.limits().max_buffer_size; let upload = |label: &str, bytes: &[u8], stride: u64| { let n = bytes.len() as u64 / stride; (n > 0 && (bytes.len() as u64) <= limit && n <= u64::from(u32::MAX)).then(|| { ( device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(label), contents: bytes, usage: wgpu::BufferUsages::VERTEX, }), n as u32, ) }) }; self.scene = Some(Uploaded { key: scene_key, faces: upload("sc_pick_faces", &scene.faces, FACE_VERTEX), edges: upload("sc_pick_edges", &scene.edges, EDGE_VERTEX), }); self.drawn = None; } if self.target.as_ref().is_none_or(|t| t.size != view.size) { let texture = |label: &str, format, usage| { device.create_texture(&wgpu::TextureDescriptor { label: Some(label), size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format, usage, view_formats: &[], }) }; let ids = texture("sc_pick_ids", IDS, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC); let depth = texture("sc_pick_depth", DEPTH, wgpu::TextureUsages::RENDER_ATTACHMENT); let (ids_view, depth_view) = (ids.create_view(&Default::default()), depth.create_view(&Default::default())); self.target = Some(Target { size: view.size, ids, ids_view, depth_view }); self.drawn = None; } let (Some(target), Some(scene)) = (&self.target, &self.scene) else { return }; if self.drawn != Some(view.key) { queue.write_buffer(&self.uniform, 0, &uniform_bytes(view)); let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("sc_pick"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &target.ids_view, depth_slice: None, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store }, })], depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { view: &target.depth_view, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Discard }), stencil_ops: None, }), timestamp_writes: None, occlusion_query_set: None, multiview_mask: None, }); pass.set_bind_group(0, &self.bind, &[]); for (pipeline, part) in [(&self.face, &scene.faces), (&self.edge, &scene.edges)] { if let Some((buffer, n)) = part { pass.set_pipeline(pipeline); pass.set_vertex_buffer(0, buffer.slice(..)); pass.draw(0..*n, 0..1); } } drop(pass); self.drawn = Some(view.key); // Tiles of other views no longer answer. self.pending.retain(|p| p.key == view.key); if let Ok(mut o) = out.lock() { o.tiles.retain(|t| t.key == view.key); } } let Some(centre) = centre else { return }; let (origin, size) = tile_around(centre, view.size); let known = out.lock().is_ok_and(|o| o.tiles.iter().any(|t| t.key == view.key && t.origin == origin && t.size == size)) || self.pending.iter().any(|p| p.key == view.key && p.origin == origin && p.size == size); if known || self.pending.len() >= IN_FLIGHT { return; } let bytes_per_row = (size[0] * 4).div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("sc_pick_tile"), size: u64::from(bytes_per_row) * u64::from(size[1]), usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); encoder.copy_texture_to_buffer( wgpu::TexelCopyTextureInfo { texture: &target.ids, mip_level: 0, origin: wgpu::Origin3d { x: origin[0], y: origin[1], z: 0 }, aspect: wgpu::TextureAspect::All, }, wgpu::TexelCopyBufferInfo { buffer: &buffer, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(bytes_per_row), rows_per_image: Some(size[1]) }, }, wgpu::Extent3d { width: size[0], height: size[1], depth_or_array_layers: 1 }, ); self.pending.push(Pending { key: view.key, origin, size, bytes_per_row, buffer, frame: self.frame, state: Arc::new(AtomicU8::new(NEW)) }); } /// Whether read-backs are still on their way (keep frames coming). pub fn waiting(&self) -> bool { !self.pending.is_empty() } /// Maps the copies earlier frames submitted and moves the mapped ones into `out`. fn collect(&mut self, device: &wgpu::Device, out: &PickOut) { let frame = self.frame; for p in &self.pending { if p.frame < frame && p.state.load(Ordering::Acquire) == NEW { p.state.store(MAPPING, Ordering::Release); let state = p.state.clone(); p.buffer.slice(..).map_async(wgpu::MapMode::Read, move |r| state.store(if r.is_ok() { READY } else { FAILED }, Ordering::Release)); } } if self.pending.iter().any(|p| p.state.load(Ordering::Acquire) == MAPPING) { let _ = device.poll(wgpu::PollType::Poll); } let mut done = Vec::new(); self.pending.retain(|p| match p.state.load(Ordering::Acquire) { READY => { if let Ok(bytes) = p.buffer.slice(..).get_mapped_range() { let [w, h] = p.size; let mut ids = Vec::with_capacity(w as usize * h as usize); for row in bytes.chunks(p.bytes_per_row as usize).take(h as usize) { ids.extend(row.as_chunks::<4>().0.iter().take(w as usize).map(|c| u32::from_le_bytes(*c))); } done.push(PickTile { key: p.key, origin: p.origin, size: p.size, ids }); } p.buffer.unmap(); false } FAILED => false, _ => true, }); if !done.is_empty() && let Ok(mut o) = out.lock() { for t in done { if self.drawn == Some(t.key) && t.ids.len() == t.size[0] as usize * t.size[1] as usize { o.tiles.retain(|x| x.key == t.key); o.tiles.push(t); } } let excess = o.tiles.len().saturating_sub(KEEP); o.tiles.drain(..excess); } } } fn uniform_bytes(v: &PickView) -> Vec { let mut b = Vec::with_capacity(UNIFORM_SIZE as usize); for col in v.view_proj { for x in col { b.extend_from_slice(&x.to_le_bytes()); } } for x in v.eye.into_iter().chain(v.back).chain([v.lift]) { b.extend_from_slice(&x.to_le_bytes()); } b } #[cfg(test)] #[path = "pick_tests.rs"] mod tests;