use crate::engine::pick::PickId;
use egui::{Pos2, Rect};
use wgpu::TextureFormat;
pub const PICK_FORMAT: TextureFormat = TextureFormat::R32Uint;
pub const PICK_DEPTH_FORMAT: TextureFormat = super::offscreen::DEPTH_FORMAT;
pub use crate::render::wgsl::PICK_WGSL;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PickVertex {
pub pos_px: [f32; 2],
pub depth: f32,
pub id: u32,
}
pub const PICK_VERTEX_STRIDE: u64 = std::mem::size_of::<PickVertex>() as u64;
#[derive(Clone, Debug, Default)]
pub struct PickBatch {
verts: Vec<PickVertex>,
}
impl PickBatch {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn clear(&mut self) {
self.verts.clear();
}
pub fn push_quad_at_depth(&mut self, rect: Rect, id: PickId, depth: f32) {
let (l, t, r, b) = (rect.left(), rect.top(), rect.right(), rect.bottom());
let raw = id.0;
let v = |x: f32, y: f32| PickVertex { pos_px: [x, y], depth, id: raw };
self.verts.extend_from_slice(&[
v(l, t),
v(r, t),
v(r, b),
v(l, t),
v(r, b),
v(l, b),
]);
}
pub fn push_quad(&mut self, rect: Rect, id: PickId) {
self.push_quad_at_depth(rect, id, 0.0);
}
pub fn push_feature(&mut self, rect: Rect, layer: u8, feature: u32) {
self.push_quad(rect, PickId::new(layer, feature));
}
pub fn push_quad_in_painter_order(&mut self, rect: Rect, id: PickId, paint_index: usize, paint_count: usize) {
self.push_quad_at_depth(rect, id, painter_depth(paint_index, paint_count));
}
pub fn push_tri(&mut self, a: Pos2, b: Pos2, c: Pos2, id: PickId, depth: f32) {
let raw = id.0;
for p in [a, b, c] {
self.verts.push(PickVertex { pos_px: [p.x, p.y], depth, id: raw });
}
}
#[must_use]
pub fn vertices(&self) -> &[PickVertex] {
&self.verts
}
#[must_use]
pub fn len(&self) -> usize {
self.verts.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.verts.is_empty()
}
}
pub struct PickTarget {
tex: Option<wgpu::Texture>,
view: Option<wgpu::TextureView>,
depth_view: Option<wgpu::TextureView>,
size: (u32, u32),
with_depth: bool,
}
impl PickTarget {
#[must_use]
pub fn new() -> Self {
Self { tex: None, view: None, depth_view: None, size: (0, 0), with_depth: false }
}
#[must_use]
pub fn with_depth() -> Self {
Self { tex: None, view: None, depth_view: None, size: (0, 0), with_depth: true }
}
#[must_use]
pub fn has_depth(&self) -> bool {
self.with_depth
}
pub fn ensure(&mut self, device: &wgpu::Device, w: u32, h: u32) {
let w = w.max(1);
let h = h.max(1);
if self.size == (w, h) && self.view.is_some() {
return;
}
let size = wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 };
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("l0_pick_id_target"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: PICK_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = tex.create_view(&Default::default());
self.depth_view = self.with_depth.then(|| {
device
.create_texture(&wgpu::TextureDescriptor {
label: Some("l0_pick_depth"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: PICK_DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
.create_view(&Default::default())
});
self.tex = Some(tex);
self.view = Some(view);
self.size = (w, h);
}
#[must_use]
pub fn ready(&self) -> bool {
self.view.is_some()
}
#[must_use]
pub fn size(&self) -> (u32, u32) {
self.size
}
#[must_use]
pub fn view(&self) -> Option<&wgpu::TextureView> {
self.view.as_ref()
}
#[must_use]
pub fn depth_view(&self) -> Option<&wgpu::TextureView> {
self.depth_view.as_ref()
}
pub fn begin_pass<'a>(&'a self, enc: &'a mut wgpu::CommandEncoder) -> Option<wgpu::RenderPass<'a>> {
let view = self.view.as_ref()?;
Some(enc.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("l0_pick_id_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: self.depth_view.as_ref().map(|dv| wgpu::RenderPassDepthStencilAttachment {
view: dv,
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Store }),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
}))
}
#[must_use]
pub fn read_id(&self, device: &wgpu::Device, queue: &wgpu::Queue, x: u32, y: u32) -> PickId {
self.read_region(device, queue, x, y, 1, 1).first().copied().unwrap_or(PickId::NOTHING)
}
#[must_use]
pub fn read_id_logical(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
local: Pos2,
pixels_per_point: f32,
) -> PickId {
match logical_to_texel(local, pixels_per_point) {
Some((x, y)) => self.read_id(device, queue, x, y),
None => PickId::NOTHING,
}
}
#[must_use]
pub fn read_region(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
x: u32,
y: u32,
w: u32,
h: u32,
) -> Vec<PickId> {
let Some(tex) = &self.tex else { return Vec::new() };
let (tw, th) = self.size;
if x >= tw || y >= th || w == 0 || h == 0 {
return Vec::new();
}
let w = w.min(tw - x);
let h = h.min(th - y);
let data = super::readback::read_texture_region(device, queue, tex, 4, x, y, w, h);
let mut ids = Vec::with_capacity((w * h) as usize);
for chunk in data.chunks_exact(4) {
ids.push(PickId::from_rgba([chunk[0], chunk[1], chunk[2], chunk[3]]));
}
ids
}
}
impl Default for PickTarget {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn painter_depth(index: usize, count: usize) -> f32 {
let n = count.max(1) as f32;
((index as f32 + 0.5) / n).clamp(0.0, 1.0)
}
#[must_use]
pub fn logical_to_texel(local: Pos2, pixels_per_point: f32) -> Option<(u32, u32)> {
let ppp = if pixels_per_point > 0.0 { pixels_per_point } else { 1.0 };
let x = local.x * ppp;
let y = local.y * ppp;
if !(x >= 0.0) || !(y >= 0.0) {
return None; }
Some((x as u32, y as u32))
}
pub struct PickPass {
pipeline: wgpu::RenderPipeline,
ubo: wgpu::Buffer,
bind: wgpu::BindGroup,
verts: Option<wgpu::Buffer>,
vert_cap: u64,
vert_count: u32,
}
impl PickPass {
pub fn new(device: &wgpu::Device, depth: Option<TextureFormat>) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("l0_pick"),
source: wgpu::ShaderSource::Wgsl(super::wgsl(PICK_WGSL).into()),
});
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("l0_pick_bgl"),
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 ubo = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("l0_pick_ubo"),
size: 16, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("l0_pick_bind"),
layout: &bgl,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: ubo.as_entire_binding() }],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("l0_pick_pipeline"),
layout: Some(&device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("l0_pick_pll"),
bind_group_layouts: &[Some(&bgl)],
immediate_size: 0,
})),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("pick_vs"),
compilation_options: Default::default(),
buffers: &[wgpu::VertexBufferLayout {
array_stride: PICK_VERTEX_STRIDE,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x2, offset: 0, shader_location: 0 },
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32, offset: 8, shader_location: 1 },
wgpu::VertexAttribute { format: wgpu::VertexFormat::Uint32, offset: 12, shader_location: 2 },
],
}],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: depth.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Less),
stencil: Default::default(),
bias: Default::default(),
}),
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("pick_fs"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: PICK_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
});
Self { pipeline, ubo, bind, verts: None, vert_cap: 0, vert_count: 0 }
}
pub fn set_viewport(&self, queue: &wgpu::Queue, w: u32, h: u32) {
let u: [f32; 4] = [w.max(1) as f32, h.max(1) as f32, 0.0, 0.0];
queue.write_buffer(&self.ubo, 0, bytemuck::cast_slice(&u));
}
pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, batch: &PickBatch) {
let verts = batch.vertices();
self.vert_count = verts.len() as u32;
if verts.is_empty() {
return;
}
let bytes: &[u8] = bytemuck::cast_slice(verts);
let need = bytes.len() as u64;
if self.verts.is_none() || self.vert_cap < need {
let cap = need.next_power_of_two().max(1024);
self.verts = Some(device.create_buffer(&wgpu::BufferDescriptor {
label: Some("l0_pick_verts"),
size: cap,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}));
self.vert_cap = cap;
}
queue.write_buffer(self.verts.as_ref().expect("just allocated"), 0, bytes);
}
pub fn record(&self, pass: &mut wgpu::RenderPass<'_>) -> u32 {
if self.vert_count == 0 {
return 0;
}
let Some(vb) = &self.verts else { return 0 };
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind, &[]);
pass.set_vertex_buffer(0, vb.slice(..));
pass.draw(0..self.vert_count, 0..1);
self.vert_count
}
#[must_use]
pub fn vertex_count(&self) -> u32 {
self.vert_count
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::pick::MAX_FEATURE;
#[test]
fn pick_format_cannot_filter_blend_or_msaa() {
let f = PICK_FORMAT.guaranteed_format_features(wgpu::Features::empty());
assert!(
f.allowed_usages.contains(wgpu::TextureUsages::RENDER_ATTACHMENT),
"the id target must be renderable"
);
assert!(f.allowed_usages.contains(wgpu::TextureUsages::COPY_SRC), "the id target must be readable back");
let bad = [
(wgpu::TextureFormatFeatureFlags::FILTERABLE, "filtering blends two ids into a third"),
(wgpu::TextureFormatFeatureFlags::BLENDABLE, "blending blends two ids into a third"),
(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4, "MSAA averages ids across every silhouette"),
(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X2, "MSAA averages ids across every silhouette"),
];
for (flag, why) in bad {
assert!(!f.flags.contains(flag), "{PICK_FORMAT:?} permits {flag:?} — {why}");
}
assert!(!PICK_FORMAT.is_srgb(), "an sRGB transfer function would rewrite the id");
assert_eq!(PICK_FORMAT.target_pixel_byte_cost(), Some(4), "a 32-bit id needs 4 bytes/texel");
}
#[test]
fn every_carry_boundary_round_trips_through_the_rgba_byte_view() {
let features = [
0,
1,
2,
0xFE,
0xFF,
0x100, 0x101,
0xFFFE,
0xFFFF,
0x1_0000, 0x1_0001,
0xFF_FFFE,
MAX_FEATURE, ];
for layer in [1u8, 2, 0x7F, 0x80, 0xFE, 0xFF] {
for feat in features {
let id = PickId::new(layer, feat);
assert!(!id.is_nothing(), "layer {layer} feature {feat:#x} must be a real id");
assert_eq!(id.layer(), layer, "layer lost for feature {feat:#x}");
assert_eq!(id.feature(), feat, "feature {feat:#x} lost for layer {layer}");
let bytes = id.to_rgba();
assert_eq!(bytes, id.0.to_le_bytes(), "the byte view must BE the wire bytes");
assert_eq!(PickId::from_rgba(bytes), id, "round trip failed at layer {layer} feature {feat:#x}");
}
}
let top = PickId::new(0xFF, MAX_FEATURE);
assert_eq!(top.0, u32::MAX, "the id space tops out at 0xFFFF_FFFF");
assert_eq!(PickId::from_rgba([0xFF; 4]), top);
}
#[test]
fn the_miss_sentinel_is_below_every_real_id() {
assert_eq!(PickId::NOTHING.0, 0);
let lowest = PickId::new(1, 0);
assert_eq!(lowest.0, 0x0100_0000, "layer 1 feature 0 is the floor of the real id space");
assert!(lowest.0 > PickId::NOTHING.0);
for layer in 1..=255u8 {
assert!(!PickId::new(layer, 0).is_nothing(), "layer {layer} feature 0 must not read as a miss");
}
let mut b = PickBatch::new();
b.push_feature(Rect::from_min_size(Pos2::ZERO, egui::vec2(4.0, 4.0)), 1, 0);
assert!(b.vertices().iter().all(|v| v.id == 0x0100_0000), "push_feature must encode, not pass through");
}
#[test]
fn vertex_layout_matches_the_shader() {
assert_eq!(PICK_VERTEX_STRIDE, 16);
assert_eq!(std::mem::offset_of!(PickVertex, pos_px), 0);
assert_eq!(std::mem::offset_of!(PickVertex, depth), 8);
assert_eq!(std::mem::offset_of!(PickVertex, id), 12);
assert!(PICK_WGSL.contains("@location(2) id: u32"), "shader must read the id at location 2");
assert!(PICK_WGSL.contains("@interpolate(flat)"), "an id must never be interpolated");
assert!(PICK_WGSL.contains("-> @location(0) u32"), "the fragment stage writes a raw u32 id");
assert!(!PICK_WGSL.contains("fn px_to_ndc"), "pick.wgsl must take px_to_ndc from the ONE prelude");
assert!(super::super::wgsl(PICK_WGSL).contains("fn px_to_ndc"), "the composer supplies it");
}
#[test]
fn a_quad_is_six_vertices_of_one_id_over_its_rect() {
let r = Rect::from_min_max(Pos2::new(10.0, 20.0), Pos2::new(30.0, 50.0));
let id = PickId::new(2, 7);
let mut b = PickBatch::new();
b.push_quad(r, id);
assert_eq!(b.len(), 6);
assert!(b.vertices().iter().all(|v| v.id == id.0 && v.depth == 0.0));
for c in [(10.0, 20.0), (30.0, 20.0), (30.0, 50.0), (10.0, 50.0)] {
assert!(
b.vertices().iter().any(|v| v.pos_px == [c.0, c.1]),
"corner {c:?} missing — the quad does not cover its rect"
);
}
let mut d = PickBatch::new();
d.push_quad_at_depth(r, id, 0.0);
assert_eq!(b.vertices(), d.vertices());
}
#[test]
fn logical_to_texel_scales_and_rejects_outside() {
assert_eq!(logical_to_texel(Pos2::new(10.0, 20.0), 1.0), Some((10, 20)));
assert_eq!(logical_to_texel(Pos2::new(10.0, 20.0), 2.0), Some((20, 40)));
assert_eq!(logical_to_texel(Pos2::new(10.4, 20.9), 1.5), Some((15, 31)));
assert_eq!(logical_to_texel(Pos2::new(-0.5, 20.0), 1.0), None, "left of the widget is a miss");
assert_eq!(logical_to_texel(Pos2::new(10.0, -3.0), 1.0), None, "above the widget is a miss");
assert_eq!(logical_to_texel(Pos2::new(f32::NAN, 0.0), 1.0), None, "NaN is a miss, not texel 0");
assert_eq!(logical_to_texel(Pos2::new(7.0, 9.0), 0.0), Some((7, 9)));
}
}