Display tessellation plans (3/4): display meshes from plans, GPU evaluation in the viewport (#805)
Display meshes are built from tessellation plans, with GPU evaluation in the viewport.
This commit is contained in:
1 parent
534393ed5a
commit
ff7a347013
19 files changed
+1474
-77
No files matched your search
+251
-33
@@ -1,6 +1,8 @@
|
||||
//! GPU viewport: shaded triangles and screen-space wide lines with a depth buffer, drawn in an
|
||||
//! `egui_wgpu` paint callback. Geometry is uploaded when the scene changes; each frame only the
|
||||
//! camera uniform changes.
|
||||
//! camera uniform changes. Where the device runs compute shaders, opaque bodies are drawn
|
||||
//! straight from their display plans: the plan is evaluated on the GPU into the vertex buffer
|
||||
//! the pass draws (see [`PlannedBody`]), with nothing built or read back on the CPU.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -13,6 +15,14 @@ pub const TRI_SIZE: usize = 28;
|
||||
pub const LINE_SIZE: usize = 32;
|
||||
const UNIFORM_SIZE: u64 = 208;
|
||||
|
||||
/// A body drawn from its display plan, evaluated on the GPU: the plan, and a colour per face
|
||||
/// (the plan's face numbering; faces past the end take the last colour).
|
||||
#[derive(Clone)]
|
||||
pub struct PlannedBody {
|
||||
pub plan: Arc<solidcraft_engine::geom::tess::TessPlan>,
|
||||
pub face_colors: Vec<[u8; 4]>,
|
||||
}
|
||||
|
||||
/// CPU-side geometry waiting to be uploaded. The model scene changes with the design; the
|
||||
/// highlight scene (hover, selection, the origin widget) changes often and is small.
|
||||
#[derive(Default, Clone)]
|
||||
@@ -39,6 +49,8 @@ pub struct GpuScene {
|
||||
/// Lines drawn first, under everything (the ground grid: the model hides it, it never
|
||||
/// hides the model).
|
||||
pub under: Vec<u8>,
|
||||
/// Opaque bodies drawn from their plans on the GPU, like `tris`.
|
||||
pub planned: Vec<PlannedBody>,
|
||||
}
|
||||
|
||||
impl GpuScene {
|
||||
@@ -172,6 +184,12 @@ struct Batch {
|
||||
count: u32,
|
||||
}
|
||||
|
||||
/// A planned body on the GPU: its evaluated mesh and a colour per vertex.
|
||||
struct PlannedDraw {
|
||||
mesh: Arc<solidcraft_gpu::GpuMesh>,
|
||||
colors: wgpu::Buffer,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Batches {
|
||||
key: Option<u64>,
|
||||
@@ -187,14 +205,24 @@ struct Batches {
|
||||
ghost: Option<Batch>,
|
||||
xray: Option<Batch>,
|
||||
under: Option<Batch>,
|
||||
planned: Vec<PlannedDraw>,
|
||||
/// Evaluated plans by the plan's address, holding the plan so the address stays its own:
|
||||
/// a body whose plan is unchanged is not evaluated again when the scene is rebuilt.
|
||||
resident: std::collections::HashMap<usize, (Arc<solidcraft_engine::geom::tess::TessPlan>, Arc<solidcraft_gpu::GpuMesh>)>,
|
||||
}
|
||||
|
||||
impl Batches {
|
||||
fn take(&mut self, device: &wgpu::Device, key: u64, slot: &SceneSlot) {
|
||||
/// Upload a new scene. Planned bodies not yet on the GPU are evaluated there: their
|
||||
/// dispatches go into `encoder`, ahead of this frame's render pass.
|
||||
fn take(&mut self, device: &wgpu::Device, key: u64, slot: &SceneSlot, tess: Option<(&solidcraft_gpu::TessGpu, &mut wgpu::CommandEncoder)>) {
|
||||
if self.key == Some(key) {
|
||||
return;
|
||||
}
|
||||
if let Some(sc) = slot.lock().ok().and_then(|mut s| s.take()) {
|
||||
self.planned = match tess {
|
||||
Some((tess, encoder)) => self.planned(device, tess, encoder, &sc.planned),
|
||||
None => Vec::new(),
|
||||
};
|
||||
self.tris = upload(device, "sc_tris", &sc.tris, TRI_SIZE);
|
||||
self.trans = upload(device, "sc_trans", &sc.trans, TRI_SIZE);
|
||||
self.glass = upload_with(device, "sc_glass", &sc.glass, TRI_SIZE, wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST);
|
||||
@@ -209,10 +237,51 @@ impl Batches {
|
||||
self.key = Some(key);
|
||||
}
|
||||
}
|
||||
|
||||
fn planned(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
tess: &solidcraft_gpu::TessGpu,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
bodies: &[PlannedBody],
|
||||
) -> Vec<PlannedDraw> {
|
||||
let ptr = |b: &PlannedBody| Arc::as_ptr(&b.plan) as usize;
|
||||
self.resident.retain(|k, _| bodies.iter().any(|b| ptr(b) == *k));
|
||||
let mut out = Vec::with_capacity(bodies.len());
|
||||
for b in bodies {
|
||||
let mesh = match self.resident.get(&ptr(b)) {
|
||||
Some((_, m)) => m.clone(),
|
||||
None => match tess.prepare(&b.plan) {
|
||||
Ok(m) => {
|
||||
tess.record(encoder, &m);
|
||||
let m = Arc::new(m);
|
||||
self.resident.insert(ptr(b), (b.plan.clone(), m.clone()));
|
||||
m
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("a body is not drawn on the GPU: {e}");
|
||||
continue;
|
||||
}
|
||||
},
|
||||
};
|
||||
let last = b.face_colors.last().copied().unwrap_or([200, 200, 200, 255]);
|
||||
let bytes: Vec<u8> = mesh.vertex_face.iter().flat_map(|f| b.face_colors.get(*f as usize).copied().unwrap_or(last)).collect();
|
||||
if let Some(colors) = upload(device, "sc_planned_colors", &bytes, 4) {
|
||||
out.push(PlannedDraw { mesh, colors: colors.buffer });
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
struct Resources {
|
||||
tri: wgpu::RenderPipeline,
|
||||
/// `tri` and `tri_cut` for planned bodies (positions and normals from the GPU evaluation,
|
||||
/// colours per vertex, indexed).
|
||||
tri_planned: wgpu::RenderPipeline,
|
||||
tri_cut_planned: Option<(wgpu::RenderPipeline, wgpu::RenderPipeline)>,
|
||||
/// The tessellation compute pipeline, where the device runs compute shaders.
|
||||
tess: Option<solidcraft_gpu::TessGpu>,
|
||||
/// While a section is shown (when the depth buffer has a stencil): `tri` marking where the
|
||||
/// cap shows, then the cap again at the section plane's depth, so it hides the inside.
|
||||
tri_cut: Option<(wgpu::RenderPipeline, wgpu::RenderPipeline)>,
|
||||
@@ -602,6 +671,22 @@ fn tri_layout() -> wgpu::VertexBufferLayout<'static> {
|
||||
wgpu::VertexBufferLayout { array_stride: TRI_SIZE as u64, step_mode: wgpu::VertexStepMode::Vertex, attributes: &A }
|
||||
}
|
||||
|
||||
/// A planned body's vertices as the GPU evaluation writes them: the position, then the normal
|
||||
/// at [`solidcraft_gpu::NORMAL_OFFSET`] (each padded to four floats).
|
||||
fn planned_layout() -> wgpu::VertexBufferLayout<'static> {
|
||||
const A: [wgpu::VertexAttribute; 2] = [
|
||||
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x3, offset: 0, shader_location: 0 },
|
||||
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x3, offset: solidcraft_gpu::NORMAL_OFFSET, shader_location: 1 },
|
||||
];
|
||||
wgpu::VertexBufferLayout { array_stride: solidcraft_gpu::VERTEX_STRIDE, step_mode: wgpu::VertexStepMode::Vertex, attributes: &A }
|
||||
}
|
||||
|
||||
/// A planned body's colours, one per vertex.
|
||||
fn color_layout() -> wgpu::VertexBufferLayout<'static> {
|
||||
const A: [wgpu::VertexAttribute; 1] = wgpu::vertex_attr_array![2 => Unorm8x4];
|
||||
wgpu::VertexBufferLayout { array_stride: 4, step_mode: wgpu::VertexStepMode::Vertex, attributes: &A }
|
||||
}
|
||||
|
||||
fn line_layout() -> wgpu::VertexBufferLayout<'static> {
|
||||
const A: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Unorm8x4, 3 => Float32];
|
||||
wgpu::VertexBufferLayout { array_stride: LINE_SIZE as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: &A }
|
||||
@@ -613,10 +698,12 @@ pub struct GpuTarget {
|
||||
pub format: wgpu::TextureFormat,
|
||||
pub depth: Option<wgpu::TextureFormat>,
|
||||
pub samples: u32,
|
||||
/// The device evaluates display plans (compute shaders): bodies may be drawn from them.
|
||||
pub tess: bool,
|
||||
}
|
||||
|
||||
impl Resources {
|
||||
fn new(device: &wgpu::Device, t: GpuTarget) -> Self {
|
||||
fn new(device: &wgpu::Device, t: GpuTarget, tess: Option<solidcraft_gpu::TessGpu>) -> Self {
|
||||
let module =
|
||||
device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("sc_viewport"), source: wgpu::ShaderSource::Wgsl(SHADER.into()) });
|
||||
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
@@ -770,14 +857,16 @@ impl Resources {
|
||||
bind_group_layouts: &[Some(&bgl), Some(&access_bgl)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let solid = |label: &str, fs: &str, cull: Option<wgpu::Face>, ds: Option<wgpu::DepthStencilState>| {
|
||||
let built = [Some(tri_layout())];
|
||||
let evaluated = [Some(planned_layout()), Some(color_layout())];
|
||||
let solid = |label: &str, fs: &str, cull: Option<wgpu::Face>, ds: Option<wgpu::DepthStencilState>, planned: bool| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(&solid_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &module,
|
||||
entry_point: Some("vs_tri"),
|
||||
buffers: &[Some(tri_layout())],
|
||||
buffers: if planned { &evaluated[..] } else { &built[..] },
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, cull_mode: cull, ..Default::default() },
|
||||
@@ -793,21 +882,9 @@ impl Resources {
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let access_bind = access_texture(device, None, &access_bgl, 1, &[]);
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("sc_image"),
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
..Default::default()
|
||||
});
|
||||
Resources {
|
||||
image,
|
||||
image_bgl,
|
||||
sampler,
|
||||
textures: Default::default(),
|
||||
quads: Vec::new(),
|
||||
tri: solid("sc_tris", "fs_solid", None, depth(wgpu::CompareFunction::Less, true)),
|
||||
tri_cut: t.depth.filter(|f| f.has_stencil_aspect()).map(|format| {
|
||||
// While a section is shown: the stencil marking and the cap (see `Resources::tri_cut`).
|
||||
let cut = |planned: bool| {
|
||||
t.depth.filter(|f| f.has_stencil_aspect()).map(|format| {
|
||||
// The visible surface marks the stencil: 1 where it is a back face (the cap).
|
||||
use wgpu::StencilOperation::{Keep, Replace, Zero};
|
||||
let face = |pass_op| wgpu::StencilFaceState { compare: wgpu::CompareFunction::Always, fail_op: Keep, depth_fail_op: Keep, pass_op };
|
||||
@@ -827,8 +904,30 @@ impl Resources {
|
||||
stencil: wgpu::StencilState { front: only, back: only, read_mask: 0xff, write_mask: 0 },
|
||||
bias: Default::default(),
|
||||
};
|
||||
(solid("sc_tris_mark", "fs_solid", None, Some(mark)), solid("sc_tris_cap", "fs_solid_cut", Some(wgpu::Face::Front), Some(cap)))
|
||||
}),
|
||||
(
|
||||
solid("sc_tris_mark", "fs_solid", None, Some(mark), planned),
|
||||
solid("sc_tris_cap", "fs_solid_cut", Some(wgpu::Face::Front), Some(cap), planned),
|
||||
)
|
||||
})
|
||||
};
|
||||
let access_bind = access_texture(device, None, &access_bgl, 1, &[]);
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("sc_image"),
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
..Default::default()
|
||||
});
|
||||
Resources {
|
||||
image,
|
||||
image_bgl,
|
||||
sampler,
|
||||
textures: Default::default(),
|
||||
quads: Vec::new(),
|
||||
tri: solid("sc_tris", "fs_solid", None, depth(wgpu::CompareFunction::Less, true), false),
|
||||
tri_cut: cut(false),
|
||||
tri_planned: solid("sc_tris_planned", "fs_solid", None, depth(wgpu::CompareFunction::Less, true), true),
|
||||
tri_cut_planned: cut(true),
|
||||
tess,
|
||||
access_bgl,
|
||||
access_bind,
|
||||
access_version: 0,
|
||||
@@ -894,8 +993,13 @@ impl Batches {
|
||||
/// Create the pipelines. Call once with the app's render state and its depth/stencil/MSAA
|
||||
/// settings. Without a stencil the section cap does not hide the inside of cut bodies.
|
||||
pub fn install(rs: &egui_wgpu::RenderState, depth_bits: u8, stencil_bits: u8, samples: u32) -> GpuTarget {
|
||||
let t = GpuTarget { format: rs.target_format, depth: egui_wgpu::depth_format_from_bits(depth_bits, stencil_bits), samples };
|
||||
let res = Resources::new(&rs.device, t);
|
||||
// Display plans are evaluated where the device runs compute shaders with the storage
|
||||
// buffers the evaluation binds (not WebGL2): elsewhere bodies are built on the CPU.
|
||||
let compute = rs.adapter.get_downlevel_capabilities().flags.contains(wgpu::DownlevelFlags::COMPUTE_SHADERS)
|
||||
&& rs.device.limits().max_storage_buffers_per_shader_stage >= 4;
|
||||
let tess = compute.then(|| solidcraft_gpu::TessGpu::new(rs.adapter.get_info().name, rs.device.clone(), rs.queue.clone()));
|
||||
let t = GpuTarget { format: rs.target_format, depth: egui_wgpu::depth_format_from_bits(depth_bits, stencil_bits), samples, tess: tess.is_some() };
|
||||
let res = Resources::new(&rs.device, t, tess);
|
||||
rs.renderer.write().callback_resources.insert(res);
|
||||
t
|
||||
}
|
||||
@@ -906,14 +1010,14 @@ impl egui_wgpu::CallbackTrait for ViewportCallback {
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
_screen: &egui_wgpu::ScreenDescriptor,
|
||||
_encoder: &mut wgpu::CommandEncoder,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
resources: &mut egui_wgpu::CallbackResources,
|
||||
) -> Vec<wgpu::CommandBuffer> {
|
||||
let Some(res) = resources.get_mut::<Resources>() else { return Vec::new() };
|
||||
res.model.take(device, self.key, &self.slot);
|
||||
res.sketch.take(device, self.sketch_key, &self.sketch_slot);
|
||||
res.highlight.take(device, self.hl_key, &self.hl_slot);
|
||||
res.preview.take(device, self.pv_key, &self.pv_slot);
|
||||
res.model.take(device, self.key, &self.slot, res.tess.as_ref().map(|t| (t, &mut *encoder)));
|
||||
res.sketch.take(device, self.sketch_key, &self.sketch_slot, None);
|
||||
res.highlight.take(device, self.hl_key, &self.hl_slot, None);
|
||||
res.preview.take(device, self.pv_key, &self.pv_slot, None);
|
||||
res.model.sort_glass(queue, self.back);
|
||||
queue.write_buffer(&res.uniform, 0, &uniform_bytes(self, self.size_px[0], self.size_px[1], res.linear_out));
|
||||
if let Some(a) = &self.access
|
||||
@@ -1000,18 +1104,30 @@ impl egui_wgpu::CallbackTrait for ViewportCallback {
|
||||
lines(pass, &res.overlay, &m.under);
|
||||
lines(pass, &res.overlay, &sk.under);
|
||||
pass.set_bind_group(1, &res.access_bind, &[]);
|
||||
let cut = res.tri_cut.as_ref().filter(|_| self.clip[..3].iter().map(|c| c * c).sum::<f32>() > 0.25);
|
||||
match cut {
|
||||
Some((mark, cap)) => {
|
||||
let planned = |pass: &mut wgpu::RenderPass<'static>, p: &wgpu::RenderPipeline| {
|
||||
for d in &m.planned {
|
||||
pass.set_pipeline(p);
|
||||
pass.set_vertex_buffer(0, d.mesh.vertices.slice(..));
|
||||
pass.set_vertex_buffer(1, d.colors.slice(..));
|
||||
pass.set_index_buffer(d.mesh.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.draw_indexed(0..d.mesh.index_count, 0, 0..1);
|
||||
}
|
||||
};
|
||||
let sectioned = self.clip[..3].iter().map(|c| c * c).sum::<f32>() > 0.25;
|
||||
match (res.tri_cut.as_ref().filter(|_| sectioned), res.tri_cut_planned.as_ref()) {
|
||||
(Some((mark, cap)), Some((mark_planned, cap_planned))) => {
|
||||
pass.set_stencil_reference(1);
|
||||
tris(pass, mark, &m.tris);
|
||||
tris(pass, mark, &pv.tris);
|
||||
planned(pass, mark_planned);
|
||||
tris(pass, cap, &m.tris);
|
||||
tris(pass, cap, &pv.tris);
|
||||
planned(pass, cap_planned);
|
||||
}
|
||||
None => {
|
||||
_ => {
|
||||
tris(pass, &res.tri, &m.tris);
|
||||
tris(pass, &res.tri, &pv.tris);
|
||||
planned(pass, &res.tri_planned);
|
||||
}
|
||||
}
|
||||
tris(pass, &res.tri_hl, &h.tris);
|
||||
@@ -1042,6 +1158,108 @@ impl egui_wgpu::CallbackTrait for ViewportCallback {
|
||||
}
|
||||
}
|
||||
|
||||
/// Render `scene` offscreen on a device of its own, through the viewport's real resources and
|
||||
/// frame callback (`prepare`, then `paint`), and read the pixels back: straight RGBA, `size` ×
|
||||
/// `size`. `None` without a GPU adapter. With `tess`, planned bodies are evaluated on the GPU.
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
pub(crate) fn render_offscreen(scene: GpuScene, view_proj: [[f32; 4]; 4], back: [f32; 3], eye: [f32; 4], size: u32, tess: bool) -> Option<Vec<u8>> {
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits()),
|
||||
..Default::default()
|
||||
}))
|
||||
.ok()?;
|
||||
let (format, depth) = (wgpu::TextureFormat::Rgba8Unorm, wgpu::TextureFormat::Depth24PlusStencil8);
|
||||
let target = GpuTarget { format, depth: Some(depth), samples: 1, tess };
|
||||
let compute = tess.then(|| solidcraft_gpu::TessGpu::new(adapter.get_info().name, device.clone(), queue.clone()));
|
||||
let mut resources = egui_wgpu::CallbackResources::default();
|
||||
resources.insert(Resources::new(&device, target, compute));
|
||||
let none = || Arc::new(Mutex::new(None));
|
||||
let cb = ViewportCallback {
|
||||
key: 1,
|
||||
slot: Arc::new(Mutex::new(Some(scene))),
|
||||
sketch_key: 0,
|
||||
sketch_slot: none(),
|
||||
hl_key: 0,
|
||||
hl_slot: none(),
|
||||
pv_key: 0,
|
||||
pv_slot: none(),
|
||||
view_proj,
|
||||
back,
|
||||
size_px: [size as f32; 2],
|
||||
clip: [0.0; 4],
|
||||
cap: [0.0; 4],
|
||||
images: Vec::new(),
|
||||
analysis: [0.0; 8],
|
||||
access: None,
|
||||
outline: false,
|
||||
eye,
|
||||
};
|
||||
let texture = |format, usage| {
|
||||
device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("offscreen"),
|
||||
size: wgpu::Extent3d { width: size, height: size, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage,
|
||||
view_formats: &[],
|
||||
})
|
||||
};
|
||||
let color = texture(format, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC);
|
||||
let depth_tex = texture(depth, wgpu::TextureUsages::RENDER_ATTACHMENT);
|
||||
let (color_view, depth_view) = (color.create_view(&Default::default()), depth_tex.create_view(&Default::default()));
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
let screen = egui_wgpu::ScreenDescriptor { size_in_pixels: [size, size], pixels_per_point: 1.0 };
|
||||
let extra = egui_wgpu::CallbackTrait::prepare(&cb, &device, &queue, &screen, &mut encoder, &mut resources);
|
||||
{
|
||||
let mut pass = encoder
|
||||
.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("offscreen"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &color_view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), store: wgpu::StoreOp::Store },
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &depth_view,
|
||||
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Store }),
|
||||
stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(0), store: wgpu::StoreOp::Store }),
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
})
|
||||
.forget_lifetime();
|
||||
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(size as f32, size as f32));
|
||||
let info = egui::PaintCallbackInfo { viewport: rect, clip_rect: rect, pixels_per_point: 1.0, screen_size_px: [size, size] };
|
||||
egui_wgpu::CallbackTrait::paint(&cb, info, &mut pass, &resources);
|
||||
}
|
||||
let read = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("offscreen read"),
|
||||
size: u64::from(size * size * 4),
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
encoder.copy_texture_to_buffer(
|
||||
color.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &read,
|
||||
layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(size * 4), rows_per_image: Some(size) },
|
||||
},
|
||||
wgpu::Extent3d { width: size, height: size, depth_or_array_layers: 1 },
|
||||
);
|
||||
queue.submit(extra.into_iter().chain([encoder.finish()]));
|
||||
let slice = read.slice(..);
|
||||
slice.map_async(wgpu::MapMode::Read, |_| {});
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).ok()?;
|
||||
let bytes = slice.get_mapped_range().ok()?.to_vec();
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
/// Uniform bytes for a frame.
|
||||
pub fn uniform_bytes(cb: &ViewportCallback, w: f32, h: f32, linear: bool) -> Vec<u8> {
|
||||
let mut v: Vec<f32> = cb.view_proj.iter().flatten().copied().collect();
|
||||
|
||||
@@ -435,14 +435,14 @@ fn cylinder_with_cluster(sketch: Value, at: [f64; 2], view: &str) -> Cluster {
|
||||
|
||||
#[test]
|
||||
fn select_other_over_a_dense_cluster_looks_the_face_up_once() {
|
||||
// The points on the cylinder's top face, a disc of a tenth of the body's triangles. Select
|
||||
// The points on the cylinder's top face, a disc of a small part of the body's triangles. Select
|
||||
// Other looks the face under the cursor up once for all of them, not once per point (#718
|
||||
// review): one planarity lookup, which tessellates the body, and one pass over the body's
|
||||
// triangles. Each point is then tested against the face's triangles near the points only,
|
||||
// not the face's or the body's; every point on the face is still counted.
|
||||
let mut c = cylinder_with_cluster(json!({"plane": {"face": [0, 0, 20]}}), [5.0, 4.0], "top");
|
||||
let (many, body, face, near) = (c.many, c.body_tris, c.face_tris, c.near_tris);
|
||||
assert!(body >= 8 * face && 4 * near <= face, "{body} triangles in the body, {face} in the face, {near} of them near the points");
|
||||
assert!(body >= 4 * face && 4 * near <= face, "{body} triangles in the body, {face} in the face, {near} of them near the points");
|
||||
let (list, (lookups, tested)) = c.select_other();
|
||||
assert_eq!((rows_named(&list, "Point"), rows_named(&list, "Face")), (crate::select_other::MAX_PER_KIND, 1), "{list}");
|
||||
assert_eq!(list["total"], json!(many + 1), "every point on the face is counted: {list}");
|
||||
|
||||
@@ -338,7 +338,24 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
let opacity = s.doc.model_body_opacity(b);
|
||||
let face_looks: std::collections::HashMap<usize, solidcraft_engine::doc::appearance::Look> =
|
||||
if shaded { s.doc.face_colors(b).into_iter().collect() } else { Default::default() };
|
||||
for (ti, t) in m.triangles.iter().enumerate().filter(|_| shaded) {
|
||||
// Opaque bodies are drawn from their display plans on the GPU where the device evaluates
|
||||
// them; faded and see-through ones (sorted and blended per triangle) are built here.
|
||||
let opaque = !faded && opacity >= 0.999 && face_looks.values().all(|l| l.opacity >= 0.999);
|
||||
let planned = (shaded && opaque && app.viewport.gpu.is_some_and(|g| g.tess))
|
||||
.then(|| b.display_plan())
|
||||
.flatten()
|
||||
.filter(|plan| plan.vertex_count() <= solidcraft_gpu::MAX_BATCH);
|
||||
let on_gpu = planned.is_some();
|
||||
if let Some(plan) = planned {
|
||||
let face_colors = (0..b.body.face_count())
|
||||
.map(|f| match face_looks.get(&f) {
|
||||
Some(l) => c4(Color32::from_rgb(l.color[0], l.color[1], l.color[2])),
|
||||
None => col,
|
||||
})
|
||||
.collect();
|
||||
sc.planned.push(crate::gpu::PlannedBody { plan, face_colors });
|
||||
}
|
||||
for (ti, t) in m.triangles.iter().enumerate().filter(|_| shaded && !on_gpu) {
|
||||
let (mut col, a) = match m.tri_face.get(ti).and_then(|f| face_looks.get(&(*f as usize))) {
|
||||
Some(l) => {
|
||||
let c = Color32::from_rgb(l.color[0], l.color[1], l.color[2]);
|
||||
@@ -3189,6 +3206,44 @@ fn view_menu(app: &mut SolveApp, ui: &mut egui::Ui, button: Rect) -> bool {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Bodies drawn from their display plans, evaluated on the GPU, look like the same bodies
|
||||
/// built into triangles on the CPU: the sample design rendered both ways through the
|
||||
/// viewport's real pipelines, offscreen, on this machine's GPU (skipped without one).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn bodies_evaluated_on_the_gpu_look_like_bodies_built_on_the_cpu() {
|
||||
let mut s = solidcraft_engine::Session::default();
|
||||
s.run_script(&solidcraft_engine::sample::script()).unwrap();
|
||||
let mut app = SolveApp::new(s, crate::Services::default());
|
||||
app.viewport.no_pixels = true;
|
||||
app.fit_view();
|
||||
const SIZE: u32 = 256;
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(SIZE as f32, SIZE as f32));
|
||||
let mut render = |tess: bool| {
|
||||
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess });
|
||||
let scene = build_scene(&app);
|
||||
let proj = projection(&app, rect);
|
||||
let e = proj.cam.eye().to_f32();
|
||||
let eye = [e[0], e[1], e[2], if proj.cam.fov > 0.0 { 1.0 } else { 0.0 }];
|
||||
let planned = scene.planned.len();
|
||||
(planned, scene.tris.len(), crate::gpu::render_offscreen(scene, proj.vp.to_f32(), proj.cam.back().to_f32(), eye, SIZE, tess))
|
||||
};
|
||||
let (planned, gpu_tris, gpu) = render(true);
|
||||
let (none, cpu_tris, cpu) = render(false);
|
||||
let (Some(gpu), Some(cpu)) = (gpu, cpu) else {
|
||||
eprintln!("skipping: no GPU adapter");
|
||||
return;
|
||||
};
|
||||
assert!(planned > 0 && none == 0, "{planned} bodies planned");
|
||||
assert!(gpu_tris < cpu_tris, "the planned bodies' triangles are not built on the CPU ({gpu_tris} vs {cpu_tris} bytes)");
|
||||
let px = |img: &[u8], i: usize| [img[4 * i], img[4 * i + 1], img[4 * i + 2]];
|
||||
let n = (SIZE * SIZE) as usize;
|
||||
let drawn = (0..n).filter(|&i| px(&cpu, i) != [255, 255, 255]).count();
|
||||
assert!(drawn > n / 20, "the model covers the view: {drawn} pixels");
|
||||
let differ = (0..n).filter(|&i| px(&gpu, i).iter().zip(px(&cpu, i)).any(|(a, b)| a.abs_diff(b) > 24)).count();
|
||||
assert!(differ * 100 < drawn, "{differ} of {drawn} model pixels differ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_drawing_and_hover_overlays_preserve_visible_gaps() {
|
||||
fn length(shape: &Shape) -> f32 {
|
||||
@@ -3383,7 +3438,7 @@ mod tests {
|
||||
app.viewport.no_pixels = true;
|
||||
if gpu {
|
||||
// The callback is queued by egui; no device is needed to inspect scene updates.
|
||||
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1 });
|
||||
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess: false });
|
||||
}
|
||||
app
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user