Files
OneCAD/crates/gpu/src/pick.rs
T

504 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<f32>,
eye: vec4<f32>, // eye position, w = 1 for perspective
back: vec4<f32>, // toward the eye (orthographic), w = how far edges are lifted toward it
};
@group(0) @binding(0) var<uniform> u: U;
@vertex
fn vs_face(@location(0) p: vec3<f32>) -> @builtin(position) vec4<f32> {
return u.vp * vec4<f32>(p, 1.0);
}
@fragment
fn fs_face() -> @location(0) u32 {
return 0u;
}
struct EOut {
@builtin(position) pos: vec4<f32>,
@location(0) @interpolate(flat) id: u32,
};
@vertex
fn vs_edge(@location(0) p: vec3<f32>, @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<f32>(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<u8>,
edges: Vec<u8>,
}
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<u32>,
}
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<u32> {
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<PickTile>,
}
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<Mutex<PickResults>>;
/// 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<AtomicU8>,
}
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<Uploaded>,
target: Option<Target>,
/// The view drawn into the textures.
drawn: Option<u64>,
pending: Vec<Pending>,
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<u8> {
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;