Files
solidcraft-upstream/crates/gpu/src/view.rs
T

1728 lines
82 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 viewport's drawing: shaded triangles and screen-space wide lines with a depth buffer,
//! the section cap, surface analysis, reference images and supersampling, as render pipelines on
//! any wgpu device. The desktop and web viewport draws through it (an `egui_wgpu` paint callback
//! hands it the device and the pass); headless pictures draw through it offscreen
//! ([`crate::capture`]). Geometry is uploaded when a scene changes; each frame only the 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};
use solidcraft_geom::Vec3;
use wgpu::util::DeviceExt;
use crate::pbr;
/// Triangle vertex: position (3 × f32), normal (3 × f32), sRGB colour (4 × u8).
pub const TRI_SIZE: usize = 28;
pub const PBR_SIZE: usize = pbr::VERTEX_SIZE;
/// Line instance: a, b (3 × f32 each), colour (4 × u8), width in pixels (f32).
pub const LINE_SIZE: usize = 32;
/// The shader's `U`: the view-projection matrix, 9 `vec4`s, the 8 analysis colours, their
/// flags and the depth direction (`uniform_bytes`).
const UNIFORM_SIZE: u64 = 368;
/// 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_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)]
pub struct GpuScene {
/// Opaque triangles. A zero normal draws the colour flat (unlit).
pub tris: Vec<u8>,
/// Opaque product PBR vertices; linear base color and explicit optical parameters.
pub pbr: Vec<u8>,
/// Translucent triangles (alpha from the colour), drawn after the opaque ones without
/// writing depth.
pub trans: Vec<u8>,
/// See-through bodies and faces (appearance opacity below 1), closed and outward-wound:
/// their far sides are drawn first, then their near sides, so they read like glass.
pub glass: Vec<u8>,
/// Depth-tested lines.
pub lines: Vec<u8>,
/// Lines drawn only where the model hides them (hidden edges).
pub hidden: Vec<u8>,
/// Lines drawn over everything (active sketch, highlights).
pub overlay: Vec<u8>,
/// Translucent triangles pushed slightly back in depth, so they only show where nothing
/// coincides with them (removed material in a preview).
pub ghost: Vec<u8>,
/// Translucent triangles drawn through everything (a cut's tool body).
pub xray: Vec<u8>,
/// Lines drawn first, under everything (the ground grid: the model hides it, it never
/// hides the model).
pub under: Vec<u8>,
/// Origin and construction planes (translucent, like `trans`) and their axes (lines, like
/// `lines`): reference geometry, never cut away by a section (#1229).
pub guide_trans: Vec<u8>,
pub guide_lines: Vec<u8>,
/// Opaque bodies drawn from their plans on the GPU, like `tris`.
pub planned: Vec<PlannedBody>,
/// The model's bodies, a few at a time, drawn before the loose geometry above (`tris`,
/// `trans`, `lines` and `hidden` of each block, in order, then the scene's own). A block
/// that comes back unchanged keeps its buffers: a design changing one body at a time (an
/// agent adding bodies over MCP) uploads only the blocks that changed, not the whole model.
pub blocks: Vec<SceneBlock>,
}
impl GpuScene {
/// The blocks' geometry moved into the scene's own lists, in drawing order (for tests that
/// count what the scene holds).
pub fn merged(mut self) -> GpuScene {
let mut out = GpuScene { glass: std::mem::take(&mut self.glass), ..Default::default() };
for block in std::mem::take(&mut self.blocks) {
out.tris.extend_from_slice(&block.bytes.tris);
out.trans.extend_from_slice(&block.bytes.trans);
out.lines.extend_from_slice(&block.bytes.lines);
out.hidden.extend_from_slice(&block.bytes.hidden);
}
out.tris.append(&mut self.tris);
out.trans.append(&mut self.trans);
out.lines.append(&mut self.lines);
out.hidden.append(&mut self.hidden);
GpuScene { tris: out.tris, trans: out.trans, glass: out.glass, lines: out.lines, hidden: out.hidden, ..self }
}
}
/// A run of the model's bodies in the scene: their triangles and edges, and a key that names
/// exactly this content (the viewport keeps the bytes while the key comes back).
#[derive(Clone)]
pub struct SceneBlock {
pub key: u64,
pub bytes: Arc<BlockBytes>,
}
/// A block's geometry, laid out as in [`GpuScene`].
#[derive(Default)]
pub struct BlockBytes {
pub tris: Vec<u8>,
pub trans: Vec<u8>,
pub glass: Vec<u8>,
pub lines: Vec<u8>,
pub hidden: Vec<u8>,
}
impl GpuScene {
pub fn pbr_tri(&mut self, p: [f32; 3], n: [f32; 3], surface: solidcraft_render::surface::OpaqueSurface) {
let base = surface.base_color().to_f32();
let parameters = [surface.metallic() as f32, surface.roughness() as f32, surface.dielectric_ior() as f32];
for value in p.into_iter().chain(n).chain(base).chain(parameters) {
self.pbr.extend_from_slice(&value.to_le_bytes());
}
}
pub fn tri(&mut self, p: [f32; 3], n: [f32; 3], c: [u8; 4]) {
for v in p.iter().chain(&n) {
self.tris.extend_from_slice(&v.to_le_bytes());
}
self.tris.extend_from_slice(&c);
}
pub fn trans_tri(&mut self, p: [f32; 3], n: [f32; 3], c: [u8; 4]) {
for v in p.iter().chain(&n) {
self.trans.extend_from_slice(&v.to_le_bytes());
}
self.trans.extend_from_slice(&c);
}
/// A line shown only where the model hides it.
pub fn hidden_line(&mut self, a: [f32; 3], b: [f32; 3], c: [u8; 4], width: f32) {
for v in a.iter().chain(&b) {
self.hidden.extend_from_slice(&v.to_le_bytes());
}
self.hidden.extend_from_slice(&c);
self.hidden.extend_from_slice(&width.to_le_bytes());
}
pub fn glass_tri(&mut self, p: [f32; 3], n: [f32; 3], c: [u8; 4]) {
for v in p.iter().chain(&n) {
self.glass.extend_from_slice(&v.to_le_bytes());
}
self.glass.extend_from_slice(&c);
}
pub fn ghost_tri(&mut self, p: [f32; 3], n: [f32; 3], c: [u8; 4]) {
for v in p.iter().chain(&n) {
self.ghost.extend_from_slice(&v.to_le_bytes());
}
self.ghost.extend_from_slice(&c);
}
pub fn xray_tri(&mut self, p: [f32; 3], n: [f32; 3], c: [u8; 4]) {
for v in p.iter().chain(&n) {
self.xray.extend_from_slice(&v.to_le_bytes());
}
self.xray.extend_from_slice(&c);
}
/// A line under everything (see [`GpuScene::under`]).
pub fn under_line(&mut self, a: [f32; 3], b: [f32; 3], c: [u8; 4], width: f32) {
for v in a.iter().chain(&b) {
self.under.extend_from_slice(&v.to_le_bytes());
}
self.under.extend_from_slice(&c);
self.under.extend_from_slice(&width.to_le_bytes());
}
/// A translucent triangle of a plane a section does not cut (see [`GpuScene::guide_trans`]).
pub fn guide_tri(&mut self, p: [f32; 3], c: [u8; 4]) {
for v in p.iter().chain(&[0.0; 3]) {
self.guide_trans.extend_from_slice(&v.to_le_bytes());
}
self.guide_trans.extend_from_slice(&c);
}
/// A depth-tested line a section does not cut (see [`GpuScene::guide_lines`]).
pub fn guide_line(&mut self, a: [f32; 3], b: [f32; 3], c: [u8; 4], width: f32) {
for v in a.iter().chain(&b) {
self.guide_lines.extend_from_slice(&v.to_le_bytes());
}
self.guide_lines.extend_from_slice(&c);
self.guide_lines.extend_from_slice(&width.to_le_bytes());
}
pub fn line(&mut self, a: [f32; 3], b: [f32; 3], c: [u8; 4], width: f32, on_top: bool) {
let out = if on_top { &mut self.overlay } else { &mut self.lines };
for v in a.iter().chain(&b) {
out.extend_from_slice(&v.to_le_bytes());
}
out.extend_from_slice(&c);
out.extend_from_slice(&width.to_le_bytes());
}
}
pub type SceneSlot = Arc<Mutex<Option<GpuScene>>>;
/// A reference image (canvas or decal): a textured quad, depth-tested against the model.
#[derive(Clone)]
pub struct GpuImage {
pub id: u64,
/// Changes when the pixels change (the texture is re-uploaded).
pub version: u64,
pub size: [u32; 2],
/// Straight RGBA8, `size[0] * size[1] * 4` bytes.
pub rgba: Arc<Vec<u8>>,
/// Bottom-left, bottom-right, top-right, top-left of the image.
pub corners: [[f32; 3]; 4],
pub opacity: f32,
}
/// Image vertex: position (3 × f32), uv (2 × f32), opacity (f32).
const IMG_SIZE: usize = 24;
/// Per-frame parameters.
pub struct Frame {
/// Product preview uses its own batches, retaining the Design viewport's uploads.
pub product: bool,
pub key: u64,
pub slot: SceneSlot,
pub sketch_key: u64,
pub sketch_slot: SceneSlot,
/// The highlight scene and its key.
pub hl_key: u64,
pub hl_slot: SceneSlot,
/// The live preview scene and its key.
pub pv_key: u64,
pub pv_slot: SceneSlot,
/// Column-major view-projection.
pub view_proj: [[f32; 4]; 4],
/// Direction toward the eye (for lighting).
pub back: [f32; 3],
/// Viewport size in physical pixels.
pub size_px: [f32; 2],
/// Section cut: (normal, d); fragments with normal·p > d are hidden. A zero normal: none.
pub clip: [f32; 4],
/// Colour of the inside of cut bodies (straight sRGBA, 0..1).
pub cap: [f32; 4],
/// Canvases and decals.
pub images: Vec<GpuImage>,
/// Surface analysis: (mode: 0 none, 1 zebra, 2 draft, 3 curvature, 4 environment,
/// 5 accessibility; parameter; scale; size) and the pull / access direction.
pub analysis: [f32; 8],
/// The analysis colours and the stripe flag ([`analysis_colors`]).
pub analysis_colors: [f32; 36],
/// Accessibility: the depth map seen from the access direction.
pub access: Option<AccessMap>,
/// Draw silhouette outlines around the model (visual styles with edges).
pub outline: bool,
/// Eye position and 1 for a perspective camera (0: orthographic), for the section cap's depth
/// and the depth bias.
pub eye: [f32; 4],
/// Supersampling (#1288): draw the frame this many times the viewport's pixels in each
/// direction and filter it down (1: off). Under 1 the frame is drawn smaller and filtered up
/// (the product preview past its pixel budget, #1464).
pub ssaa: f32,
}
/// Heights of the model seen from a direction (an orthographic depth map): a point is reachable
/// from that direction when nothing lies above it.
#[derive(Clone)]
pub struct AccessMap {
pub version: u64,
pub size: u32,
/// Row-major heights along the direction (−∞ where nothing is).
pub depth: Arc<Vec<f32>>,
/// Map axes: pixel = (p·u + u.w, p·v + v.w) × scale.
pub u: [f32; 4],
pub v: [f32; 4],
pub scale: f32,
}
/// The accessibility depth map's resolution (pixels per side).
pub const ACCESS_SIZE: u32 = 1024;
impl AccessMap {
/// The depth map of `meshes` seen along `dir` (`version`: what the GPU uploads it by):
/// rasterise the triangles seen along `dir` (orthographic), keeping the highest point per pixel.
pub fn build(meshes: &[&solidcraft_geom::Mesh], dir: Vec3, version: u64) -> Option<AccessMap> {
let d = dir.normalized()?;
let u = d.cross(if d.x.abs() < 0.9 { Vec3::X } else { Vec3::Y }).normalized()?;
let v = d.cross(u);
let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
for m in meshes {
for p in &m.positions {
let (a, b) = (p.dot(u), p.dot(v));
lo = [lo[0].min(a), lo[1].min(b)];
hi = [hi[0].max(a), hi[1].max(b)];
}
}
let ext = (hi[0] - lo[0]).max(hi[1] - lo[1]);
if !(ext.is_finite() && ext > 1e-9) {
return None;
}
let n = ACCESS_SIZE as usize;
let scale = (ACCESS_SIZE - 2) as f64 / ext;
// One pixel of margin on each side.
let (ou, ov) = (-lo[0] + 1.0 / scale, -lo[1] + 1.0 / scale);
let mut depth = vec![f32::NEG_INFINITY; n * n];
for m in meshes {
for t in &m.triangles {
let q: Vec<(f64, f64, f64)> = t
.iter()
.filter_map(|i| m.positions.get(*i as usize))
.map(|p| ((p.dot(u) + ou) * scale, (p.dot(v) + ov) * scale, p.dot(d)))
.collect();
let [a, b, c] = q[..] else { continue };
let area = (b.0 - a.0) * (c.1 - a.1) - (b.1 - a.1) * (c.0 - a.0);
let (x0, x1) = (a.0.min(b.0).min(c.0).floor().max(0.0) as usize, (a.0.max(b.0).max(c.0).ceil() as usize).min(n - 1));
let (y0, y1) = (a.1.min(b.1).min(c.1).floor().max(0.0) as usize, (a.1.max(b.1).max(c.1).ceil() as usize).min(n - 1));
// Edge-on triangles: their vertices still count.
if area.abs() < 1e-12 {
for p in [a, b, c] {
let (x, y) = (p.0 as usize, p.1 as usize);
if let Some(z) = depth.get_mut(y * n + x) {
*z = z.max(p.2 as f32);
}
}
continue;
}
for y in y0..=y1 {
for x in x0..=x1 {
let (px, py) = (x as f64 + 0.5, y as f64 + 0.5);
let w0 = ((b.0 - px) * (c.1 - py) - (b.1 - py) * (c.0 - px)) / area;
let w1 = ((c.0 - px) * (a.1 - py) - (c.1 - py) * (a.0 - px)) / area;
let w2 = 1.0 - w0 - w1;
if w0 < -1e-9 || w1 < -1e-9 || w2 < -1e-9 {
continue;
}
let z = (w0 * a.2 + w1 * b.2 + w2 * c.2) as f32;
if let Some(cell) = depth.get_mut(y * n + x) {
*cell = cell.max(z);
}
}
}
}
}
Some(AccessMap {
version,
size: ACCESS_SIZE,
depth: Arc::new(depth),
u: [u.x as f32, u.y as f32, u.z as f32, ou as f32],
v: [v.x as f32, v.y as f32, v.z as f32, ov as f32],
scale: scale as f32,
})
}
}
/// The shader's surface analysis uniform (see [`Frame::analysis`]) for `mode`; Accessibility
/// reads its depth map's scale and size from `access`.
pub fn analysis_uniform(mode: Option<solidcraft_render::AnalysisMode>, access: Option<&AccessMap>) -> [f32; 8] {
use solidcraft_render::AnalysisMode as A;
match mode {
Some(A::Zebra { stripes }) => [1.0, stripes as f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
Some(A::Draft { pull, angle }) => [2.0, angle as f32, 0.0, 0.0, pull.x as f32, pull.y as f32, pull.z as f32, 0.0],
Some(A::Curvature { radius }) => [3.0, radius as f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
Some(A::Environment) => [4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
Some(A::Access { dir }) => {
let (scale, size, eps) = access.map(|m| (m.scale, m.size as f32, 1.5 / m.scale.max(1e-9))).unwrap_or((0.0, 0.0, 0.0));
[5.0, eps, scale, size, dir.x as f32, dir.y as f32, dir.z as f32, 0.0]
}
None => [0.0; 8],
}
}
/// Vertices (or line instances) on the GPU, in as many buffers as the device's buffer size
/// limit needs: a large import's triangles can exceed one buffer (#883).
struct Batch {
/// Each buffer and its vertex (instance) count.
parts: Vec<(wgpu::Buffer, u32)>,
/// Bytes per buffer (all but the last are full).
chunk: usize,
}
/// A chunk of planned bodies on the GPU: their evaluated mesh and a colour per vertex.
struct PlannedDraw {
mesh: Arc<crate::GpuMesh>,
colors: wgpu::Buffer,
}
/// A scene block on the GPU, with the bytes it was uploaded from.
struct BlockGpu {
bytes: Arc<BlockBytes>,
tris: Option<Batch>,
trans: Option<Batch>,
lines: Option<Batch>,
hidden: Option<Batch>,
}
/// The blocks' buffers, then the scene's own, as one batch (the blocks' buffers are shared,
/// not copied).
fn joined<'a>(blocks: impl Iterator<Item = &'a Option<Batch>>, own: Option<Batch>) -> Option<Batch> {
let mut parts: Vec<(wgpu::Buffer, u32)> = blocks.flatten().flat_map(|b| b.parts.iter().cloned()).collect();
let chunk = own.as_ref().map(|b| b.chunk).unwrap_or(0);
parts.extend(own.into_iter().flat_map(|b| b.parts));
(!parts.is_empty()).then_some(Batch { parts, chunk })
}
/// Planned bodies end a GPU chunk one time in this many (by their plan's address), and a chunk
/// holds at most [`MAX_CHUNK_BODIES`]: a scene of a thousand bodies takes a few dozen dispatches
/// once, and a changed body re-evaluates a few dozen bodies, not all of them.
const CHUNK_SPLIT: u64 = 32;
const MAX_CHUNK_BODIES: usize = 128;
#[derive(Default)]
struct Batches {
/// The scene blocks on the GPU, by key.
blocks: std::collections::HashMap<u64, BlockGpu>,
key: Option<u64>,
tris: Option<Batch>,
pbr: Option<Batch>,
trans: Option<Batch>,
glass: Option<Batch>,
/// The glass triangles as built, and the view direction they were last sorted for.
glass_cpu: Vec<u8>,
glass_sorted_for: Option<[f32; 3]>,
lines: Option<Batch>,
hidden: Option<Batch>,
overlays: Option<Batch>,
ghost: Option<Batch>,
xray: Option<Batch>,
under: Option<Batch>,
guide_trans: Option<Batch>,
guide_lines: Option<Batch>,
planned: Vec<PlannedDraw>,
/// Evaluated chunks by their plans' addresses, holding the plans so the addresses stay
/// theirs: a chunk whose plans are unchanged is not evaluated again when the scene is rebuilt.
resident: std::collections::HashMap<Vec<usize>, (Vec<Arc<solidcraft_geom::tess::TessPlan>>, Arc<crate::GpuMesh>)>,
}
impl Batches {
/// 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<(&crate::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(),
};
let mut kept = std::mem::take(&mut self.blocks);
let mut blocks: Vec<&BlockGpu> = Vec::with_capacity(sc.blocks.len());
for block in &sc.blocks {
let gpu = match kept.remove(&block.key).filter(|g| Arc::ptr_eq(&g.bytes, &block.bytes)) {
Some(g) => g,
None => BlockGpu {
bytes: block.bytes.clone(),
tris: upload(device, "sc_tris", &block.bytes.tris, TRI_SIZE),
trans: upload(device, "sc_trans", &block.bytes.trans, TRI_SIZE),
lines: upload(device, "sc_lines", &block.bytes.lines, LINE_SIZE),
hidden: upload(device, "sc_hidden", &block.bytes.hidden, LINE_SIZE),
},
};
self.blocks.insert(block.key, gpu);
}
for block in &sc.blocks {
if let Some(g) = self.blocks.get(&block.key) {
blocks.push(g);
}
}
self.tris = joined(blocks.iter().map(|b| &b.tris), upload(device, "sc_tris", &sc.tris, TRI_SIZE));
self.pbr = upload(device, "sc_pbr", &sc.pbr, PBR_SIZE);
self.trans = joined(blocks.iter().map(|b| &b.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);
self.glass_cpu = sc.glass;
self.glass_sorted_for = None;
self.lines = joined(blocks.iter().map(|b| &b.lines), upload(device, "sc_lines", &sc.lines, LINE_SIZE));
self.hidden = joined(blocks.iter().map(|b| &b.hidden), upload(device, "sc_hidden", &sc.hidden, LINE_SIZE));
self.overlays = upload(device, "sc_overlay", &sc.overlay, LINE_SIZE);
self.ghost = upload(device, "sc_ghost", &sc.ghost, TRI_SIZE);
self.xray = upload(device, "sc_xray", &sc.xray, TRI_SIZE);
self.under = upload(device, "sc_under", &sc.under, LINE_SIZE);
self.guide_trans = upload(device, "sc_guide_trans", &sc.guide_trans, TRI_SIZE);
self.guide_lines = upload(device, "sc_guide_lines", &sc.guide_lines, LINE_SIZE);
self.key = Some(key);
}
}
/// The planned bodies on the GPU: in chunks of at most `MAX_BATCH` vertices (one for any
/// ordinary scene), each uploaded and evaluated in one dispatch and drawn in one call. A
/// chunk whose plans are unchanged is not evaluated again; colours are rebuilt each time.
fn planned(
&mut self,
device: &wgpu::Device,
tess: &crate::TessGpu,
encoder: &mut wgpu::CommandEncoder,
bodies: &[PlannedBody],
) -> Vec<PlannedDraw> {
// Chunks end where a body's own plan says (or when full), so a body whose plan changes
// (an edit, or a refinement as the view zooms in, #1320) re-evaluates only its own
// chunk, not the whole scene.
let mut chunks: Vec<Vec<&PlannedBody>> = Vec::new();
let mut filled = 0usize;
let mut open = false;
for b in bodies {
let n = b.plan.vertex_count();
match chunks.last_mut() {
Some(c) if open && filled + n <= crate::MAX_BATCH => c.push(b),
_ => {
chunks.push(vec![b]);
filled = 0;
}
}
filled += n;
let mut h = std::collections::hash_map::DefaultHasher::new();
std::hash::Hash::hash(&(Arc::as_ptr(&b.plan) as usize), &mut h);
open = !std::hash::Hasher::finish(&h).is_multiple_of(CHUNK_SPLIT) && chunks.last().is_some_and(|c| c.len() < MAX_CHUNK_BODIES);
}
let key = |c: &[&PlannedBody]| c.iter().map(|b| Arc::as_ptr(&b.plan) as usize).collect::<Vec<usize>>();
self.resident.retain(|k, _| chunks.iter().any(|c| key(c) == *k));
let mut out = Vec::with_capacity(chunks.len());
for chunk in &chunks {
let mesh = match self.resident.get(&key(chunk)) {
Some((_, m)) => m.clone(),
None => {
let plans: Vec<&solidcraft_geom::tess::TessPlan> = chunk.iter().map(|b| &*b.plan).collect();
match tess.prepare_many(&plans) {
Ok(m) => {
tess.record(encoder, &m);
let m = Arc::new(m);
self.resident.insert(key(chunk), (chunk.iter().map(|b| b.plan.clone()).collect(), m.clone()));
m
}
Err(e) => {
log::warn!("bodies are not drawn on the GPU: {e}");
continue;
}
}
}
};
let mut bytes = Vec::with_capacity(mesh.vertex_face.len() * 4);
for (part, b) in mesh.parts.iter().zip(chunk) {
let last = b.face_colors.last().copied().unwrap_or([200, 200, 200, 255]);
let faces = mesh.vertex_face.get(part.vertices.start as usize..part.vertices.end as usize).unwrap_or_default();
bytes.extend(faces.iter().flat_map(|f| b.face_colors.get(*f as usize).copied().unwrap_or(last)));
}
if let Some((colors, _)) = upload(device, "sc_planned_colors", &bytes, 4).and_then(|c| c.parts.into_iter().next()) {
out.push(PlannedDraw { mesh, colors });
}
}
out
}
}
/// The viewport's pipelines and the scenes uploaded to one device, for one kind of target.
pub struct Renderer {
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<crate::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)>,
/// Highlight triangles over the model (depth test ≤, no depth write).
tri_hl: wgpu::RenderPipeline,
pbr: wgpu::RenderPipeline,
trans: wgpu::RenderPipeline,
/// Glass: far sides (front faces culled), then near sides (back faces culled).
glass_far: wgpu::RenderPipeline,
glass_near: wgpu::RenderPipeline,
ghost: wgpu::RenderPipeline,
xray: wgpu::RenderPipeline,
line: wgpu::RenderPipeline,
/// Lines behind the model (depth test >).
line_hidden: wgpu::RenderPipeline,
overlay: wgpu::RenderPipeline,
/// Reference planes and axes: as `trans` and `line`, but not cut by a section.
guide_trans: wgpu::RenderPipeline,
guide_line: wgpu::RenderPipeline,
uniform: wgpu::Buffer,
bind: wgpu::BindGroup,
linear_out: bool,
model: Batches,
product: Batches,
empty: Batches,
sketch: Batches,
highlight: Batches,
preview: Batches,
image: wgpu::RenderPipeline,
image_bgl: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
/// Textures by image id: (version, bind group).
textures: std::collections::HashMap<u64, (u64, wgpu::BindGroup)>,
/// This frame's quads: (image id, vertices).
quads: Vec<(u64, Batch)>,
access_bgl: wgpu::BindGroupLayout,
/// The accessibility depth map (a 1 × 1 stand-in when there is none) and its version.
access_bind: wgpu::BindGroup,
access_version: u64,
/// The target the pipelines draw into (format, depth, samples): a supersampled frame is
/// drawn into textures of the same kind.
target: GpuTarget,
/// The device's largest 2D texture side (#869).
max_texture: u32,
/// Supersampling (#1288): the filter that draws the supersampled frame into the viewport.
ss_blit: wgpu::RenderPipeline,
ss_bgl: wgpu::BindGroupLayout,
/// The supersampled frame's textures, kept while its size stays.
ss: Option<Supersampled>,
/// This frame was drawn supersampled (in `prepare`): `paint` only filters it down.
ss_ready: bool,
/// Reversed depth drawn into the window's pass: egui clears its depth to 1, which is the near
/// plane then, so the viewport first sets its rectangle to the far plane (0).
depth_reset: Option<wgpu::RenderPipeline>,
}
/// A triangle over the whole viewport at depth 0 (the far plane of reversed depth), writing no
/// colour: [`Renderer::depth_reset`].
const DEPTH_RESET_SHADER: &str = r#"
@vertex
fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
let x = f32((i << 1u) & 2u);
let y = f32(i & 2u);
return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
}
@fragment
fn fs() -> @location(0) vec4<f32> {
return vec4<f32>(0.0);
}
"#;
/// A supersampled frame: the scene drawn at a multiple of the viewport's pixels.
struct Supersampled {
size: [u32; 2],
/// Drawn into (multisampled with the viewport's MSAA).
color: wgpu::TextureView,
/// The resolved colour (when `color` is multisampled).
resolve: Option<wgpu::TextureView>,
depth: Option<wgpu::TextureView>,
/// The single-sampled colour, for the filter.
bind: wgpu::BindGroup,
}
/// Most memory (bytes) a supersampled frame's colour and depth may take.
const SS_BUDGET: u64 = 384 << 20;
/// The size (px) to draw a `size` viewport supersampled `factor` times in each direction:
/// within the device's largest texture side (#869) and the memory budget (at `samples` per pixel,
/// four bytes of colour and four of depth each). `None` when that leaves under 1.25×: the frame
/// is drawn as usual. A `factor` under 1 draws it that much smaller, to be filtered up.
pub fn supersampled_size(size: [f32; 2], factor: f32, max_side: u32, samples: u32) -> Option<[u32; 2]> {
let [w, h] = size;
if w >= 1.0 && h >= 1.0 && factor > 0.0 && factor < 1.0 {
return Some([((w * factor).floor() as u32).clamp(1, max_side.max(1)), ((h * factor).floor() as u32).clamp(1, max_side.max(1))]);
}
if !(w >= 1.0 && h >= 1.0 && factor.is_finite() && factor > 1.0) {
return None;
}
let side = max_side as f32;
let bytes_per_px = 8.0 * samples.max(1) as f32;
let by_memory = (SS_BUDGET as f32 / (w * h * bytes_per_px)).sqrt();
let f = factor.min(side / w).min(side / h).min(by_memory);
(f >= 1.25).then(|| [((w * f).floor() as u32).clamp(1, max_side), ((h * f).floor() as u32).clamp(1, max_side)])
}
/// The filter: each viewport pixel the mean of the supersampled pixels under it (four bilinear
/// taps a quarter of its footprint from its centre: exactly the 2 × 2 box at 2×).
const SS_SHADER: &str = r#"
@group(0) @binding(0) var src: texture_2d<f32>;
@group(0) @binding(1) var smp: sampler;
struct O { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32> };
@vertex
fn vs(@builtin(vertex_index) i: u32) -> O {
let x = f32((i << 1u) & 2u);
let y = f32(i & 2u);
var o: O;
o.pos = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
o.uv = vec2<f32>(x, y);
return o;
}
@fragment
fn fs(i: O) -> @location(0) vec4<f32> {
let d = vec2<f32>(dpdx(i.uv.x), dpdy(i.uv.y)) * 0.25;
let a = textureSampleLevel(src, smp, i.uv + vec2<f32>(-d.x, -d.y), 0.0);
let b = textureSampleLevel(src, smp, i.uv + vec2<f32>(d.x, -d.y), 0.0);
let c = textureSampleLevel(src, smp, i.uv + vec2<f32>(-d.x, d.y), 0.0);
let e = textureSampleLevel(src, smp, i.uv + vec2<f32>(d.x, d.y), 0.0);
return (a + b + c + e) * 0.25;
}
"#;
fn access_texture(device: &wgpu::Device, queue: Option<&wgpu::Queue>, bgl: &wgpu::BindGroupLayout, size: u32, depth: &[f32]) -> wgpu::BindGroup {
let extent = wgpu::Extent3d { width: size.max(1), height: size.max(1), depth_or_array_layers: 1 };
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("sc_access"),
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R32Float,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
if let Some(q) = queue
&& depth.len() == (size as usize) * (size as usize)
{
let bytes: Vec<u8> = depth.iter().flat_map(|x| x.to_le_bytes()).collect();
q.write_texture(
wgpu::TexelCopyTextureInfo { texture: &tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
&bytes,
wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(4 * size), rows_per_image: Some(size) },
extent,
);
}
let view = tex.create_view(&Default::default());
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("sc_access"),
layout: bgl,
entries: &[wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&view) }],
})
}
pub const SHADER: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/shaders/viewport.wgsl"));
fn tri_layout() -> wgpu::VertexBufferLayout<'static> {
const A: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Unorm8x4];
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 [`crate::NORMAL_OFFSET`] (each padded to four floats).
fn pbr_layout() -> wgpu::VertexBufferLayout<'static> {
const A: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32x3, 3 => Float32x3];
wgpu::VertexBufferLayout { array_stride: PBR_SIZE as u64, step_mode: wgpu::VertexStepMode::Vertex, attributes: &A }
}
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: crate::NORMAL_OFFSET, shader_location: 1 },
];
wgpu::VertexBufferLayout { array_stride: crate::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 }
}
/// The target configuration: colour format, depth format and MSAA sample count of the app.
#[derive(Clone, Copy, Debug)]
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 GpuTarget {
/// Depth runs from 1 at the near plane to 0 at the far one: see [`reversed_depth`].
pub fn reversed_depth(&self) -> bool {
reversed_depth(self.depth)
}
}
/// Is depth reversed (1 at the near plane, 0 at the far one) in a buffer of `format`? It is with a
/// 32-bit float buffer: a float's steps grow with its value, so with depth falling away from the
/// eye they follow the distance, about 2⁻²⁴ of it everywhere, instead of piling up at the near
/// plane (#1434: a face 0.3 mm in front of another speckled at 10 m). Other formats keep 0 near.
pub fn reversed_depth(format: Option<wgpu::TextureFormat>) -> bool {
matches!(format, Some(wgpu::TextureFormat::Depth32Float | wgpu::TextureFormat::Depth32FloatStencil8))
}
/// The depth test `compare`, written for depth growing away from the eye, for a buffer whose depth
/// may run the other way ([`reversed_depth`]). Every pass drawn with the viewport's depth takes
/// its tests from here, so it agrees with the viewport whichever way depth runs.
pub fn depth_compare(reversed: bool, compare: wgpu::CompareFunction) -> wgpu::CompareFunction {
use wgpu::CompareFunction::{Greater, GreaterEqual, Less, LessEqual};
match compare {
Less if reversed => Greater,
LessEqual if reversed => GreaterEqual,
Greater if reversed => Less,
GreaterEqual if reversed => LessEqual,
c => c,
}
}
/// A depth bias written for depth growing away from the eye (negative: toward it), turned round
/// for reversed depth.
pub fn depth_bias(reversed: bool, b: wgpu::DepthBiasState) -> wgpu::DepthBiasState {
if reversed { wgpu::DepthBiasState { constant: -b.constant, slope_scale: -b.slope_scale, clamp: -b.clamp } } else { b }
}
/// The depth of the far plane, which a depth buffer is cleared to.
pub fn far_depth(reversed: bool) -> f32 {
if reversed { 0.0 } else { 1.0 }
}
/// The view-projection the GPU draws with: `vp` (depth 0 near, 1 far, as the CPU uses it), with
/// depth turned round (z' = w - z) for reversed depth. Columns, as `Mat4::to_f32` gives them.
pub fn gpu_view_proj(vp: [[f32; 4]; 4], reversed: bool) -> [[f32; 4]; 4] {
if !reversed {
return vp;
}
vp.map(|c| [c[0], c[1], c[3] - c[2], c[3]])
}
impl Renderer {
/// The pipelines for target `t` on `device`; `tess` evaluates planned bodies on the GPU.
pub fn new(device: &wgpu::Device, t: GpuTarget, tess: Option<crate::TessGpu>) -> Self {
let source = format!("{SHADER}\n{}", pbr::SOURCE);
let module =
device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("sc_viewport"), source: wgpu::ShaderSource::Wgsl(source.into()) });
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("sc_viewport"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
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_viewport"),
bind_group_layouts: &[Some(&bgl)],
immediate_size: 0,
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("sc_viewport_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_viewport"),
layout: &bgl,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() }],
});
let reversed = t.reversed_depth();
let depth = |compare: wgpu::CompareFunction, write: bool| -> Option<wgpu::DepthStencilState> {
t.depth.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: Some(write),
depth_compare: Some(depth_compare(reversed, compare)),
stencil: Default::default(),
bias: Default::default(),
})
};
// Opaque faces pushed back by their own slope (FACE_BIAS); ghosts twice as far.
let pushed = |compare: wgpu::CompareFunction, write: bool, bias: wgpu::DepthBiasState| -> Option<wgpu::DepthStencilState> {
depth(compare, write).map(|d| wgpu::DepthStencilState { bias: depth_bias(reversed, bias), ..d })
};
let pipeline = |label: &str,
vs: &str,
fs: &str,
buf: wgpu::VertexBufferLayout<'static>,
ds: Option<wgpu::DepthStencilState>,
blend: Option<wgpu::BlendState>| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&layout),
vertex: wgpu::VertexState { module: &module, entry_point: Some(vs), buffers: &[Some(buf)], compilation_options: Default::default() },
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: ds,
multisample: wgpu::MultisampleState { count: t.samples.max(1), mask: !0, alpha_to_coverage_enabled: false },
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some(fs),
targets: &[Some(wgpu::ColorTargetState { format: t.format, blend, write_mask: wgpu::ColorWrites::ALL })],
compilation_options: Default::default(),
}),
multiview_mask: None,
cache: None,
})
};
let alpha = Some(wgpu::BlendState::ALPHA_BLENDING);
let glass = |label: &str, cull: wgpu::Face| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &module,
entry_point: Some("vs_tri"),
buffers: &[Some(tri_layout())],
compilation_options: Default::default(),
},
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, cull_mode: Some(cull), ..Default::default() },
depth_stencil: depth(wgpu::CompareFunction::LessEqual, false),
multisample: wgpu::MultisampleState { count: t.samples.max(1), mask: !0, alpha_to_coverage_enabled: false },
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some("fs_tri"),
targets: &[Some(wgpu::ColorTargetState { format: t.format, blend: alpha, write_mask: wgpu::ColorWrites::ALL })],
compilation_options: Default::default(),
}),
multiview_mask: None,
cache: None,
})
};
let image_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("sc_image"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let image_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("sc_image"),
bind_group_layouts: &[Some(&bgl), Some(&image_bgl)],
immediate_size: 0,
});
const IA: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2, 2 => Float32];
let image = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("sc_image"),
layout: Some(&image_layout),
vertex: wgpu::VertexState {
module: &module,
entry_point: Some("vs_img"),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: IMG_SIZE as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &IA,
})],
compilation_options: Default::default(),
},
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: depth(wgpu::CompareFunction::LessEqual, false),
multisample: wgpu::MultisampleState { count: t.samples.max(1), mask: !0, alpha_to_coverage_enabled: false },
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some("fs_img"),
targets: &[Some(wgpu::ColorTargetState { format: t.format, blend: alpha, write_mask: wgpu::ColorWrites::ALL })],
compilation_options: Default::default(),
}),
multiview_mask: None,
cache: None,
});
let access_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("sc_access"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}],
});
let solid_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("sc_solid"),
bind_group_layouts: &[Some(&bgl), Some(&access_bgl)],
immediate_size: 0,
});
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: if planned { &evaluated[..] } else { &built[..] },
compilation_options: Default::default(),
},
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, cull_mode: cull, ..Default::default() },
depth_stencil: ds,
multisample: wgpu::MultisampleState { count: t.samples.max(1), mask: !0, alpha_to_coverage_enabled: false },
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some(fs),
targets: &[Some(wgpu::ColorTargetState { format: t.format, blend: None, write_mask: wgpu::ColorWrites::ALL })],
compilation_options: Default::default(),
}),
multiview_mask: None,
cache: None,
})
};
// While a section is shown: the stencil marking and the cap (see `Renderer::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 };
let mark = wgpu::DepthStencilState {
format,
depth_write_enabled: Some(true),
depth_compare: Some(depth_compare(reversed, wgpu::CompareFunction::Less)),
stencil: wgpu::StencilState { front: face(Zero), back: face(Replace), read_mask: 0xff, write_mask: 0xff },
bias: depth_bias(reversed, FACE_BIAS),
};
// Then the cap's back faces again, only there, at the section plane's depth.
let only = wgpu::StencilFaceState { compare: wgpu::CompareFunction::Equal, ..face(Keep) };
let cap = wgpu::DepthStencilState {
format,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Always),
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), 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()
});
let ss_module =
device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("sc_ssaa"), source: wgpu::ShaderSource::Wgsl(SS_SHADER.into()) });
let ss_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("sc_ssaa"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let ss_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("sc_ssaa"),
bind_group_layouts: &[Some(&ss_bgl)],
immediate_size: 0,
});
// The frame is premultiplied (drawn over transparent black), so it goes over the
// viewport's background premultiplied; the viewport's depth is left as it is.
let ss_blit = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("sc_ssaa"),
layout: Some(&ss_layout),
vertex: wgpu::VertexState { module: &ss_module, entry_point: Some("vs"), buffers: &[], compilation_options: Default::default() },
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: depth(wgpu::CompareFunction::Always, false),
multisample: wgpu::MultisampleState { count: t.samples.max(1), mask: !0, alpha_to_coverage_enabled: false },
fragment: Some(wgpu::FragmentState {
module: &ss_module,
entry_point: Some("fs"),
targets: &[Some(wgpu::ColorTargetState {
format: t.format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
multiview_mask: None,
cache: None,
});
let depth_reset = t.depth.filter(|_| reversed).map(|format| {
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("sc_depth_reset"),
source: wgpu::ShaderSource::Wgsl(DEPTH_RESET_SHADER.into()),
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("sc_depth_reset"),
bind_group_layouts: &[],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("sc_depth_reset"),
layout: Some(&layout),
vertex: wgpu::VertexState { module: &module, entry_point: Some("vs"), buffers: &[], compilation_options: Default::default() },
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: Some(wgpu::DepthStencilState {
format,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: Default::default(),
bias: Default::default(),
}),
multisample: wgpu::MultisampleState { count: t.samples.max(1), mask: !0, alpha_to_coverage_enabled: false },
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some("fs"),
targets: &[Some(wgpu::ColorTargetState { format: t.format, blend: None, write_mask: wgpu::ColorWrites::empty() })],
compilation_options: Default::default(),
}),
multiview_mask: None,
cache: None,
})
});
Renderer {
depth_reset,
target: t,
max_texture: device.limits().max_texture_dimension_2d,
ss_blit,
ss_bgl,
ss: None,
ss_ready: false,
image,
image_bgl,
sampler,
textures: Default::default(),
quads: Vec::new(),
tri: solid("sc_tris", "fs_solid", None, pushed(wgpu::CompareFunction::Less, true, FACE_BIAS), false),
tri_cut: cut(false),
tri_planned: solid("sc_tris_planned", "fs_solid", None, pushed(wgpu::CompareFunction::Less, true, FACE_BIAS), true),
tri_cut_planned: cut(true),
tess,
access_bgl,
access_bind,
access_version: 0,
tri_hl: pipeline("sc_tris_hl", "vs_tri", "fs_tri", tri_layout(), highlight_depth(t.depth), alpha),
pbr: pipeline("sc_pbr", "vs_pbr", "fs_pbr", pbr_layout(), pushed(wgpu::CompareFunction::Less, true, FACE_BIAS), None),
trans: pipeline("sc_trans", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
glass_far: glass("sc_glass_far", wgpu::Face::Front),
glass_near: glass("sc_glass_near", wgpu::Face::Back),
ghost: pipeline("sc_ghost", "vs_tri", "fs_tri", tri_layout(), pushed(wgpu::CompareFunction::LessEqual, false, GHOST_BIAS), alpha),
xray: pipeline("sc_xray", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::Always, false), alpha),
line: pipeline("sc_lines", "vs_line", "fs_line", line_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
line_hidden: pipeline("sc_lines_hidden", "vs_line", "fs_line", line_layout(), depth(wgpu::CompareFunction::Greater, false), alpha),
overlay: pipeline("sc_overlay", "vs_line", "fs_line_all", line_layout(), depth(wgpu::CompareFunction::Always, false), alpha),
guide_trans: pipeline("sc_guide_trans", "vs_tri", "fs_tri_all", tri_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
guide_line: pipeline("sc_guide_lines", "vs_line", "fs_line_all", line_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
uniform,
bind,
linear_out: t.format.is_srgb(),
model: Batches::default(),
product: Batches::default(),
empty: Batches::default(),
sketch: Batches::default(),
highlight: Batches::default(),
preview: Batches::default(),
}
}
}
/// Depth state of faces lit over the model (hover, selection): tested, not written, and pulled a
/// little toward the eye. They lie on the model's own faces, but the GPU evaluates a body's plan
/// in f32 while the highlight's copy of the same triangles is evaluated on the CPU, so the two
/// depths differ by rounding and an unbiased equal test lights the face in speckles and wedges.
/// The bias is a few depth steps plus the face's own slope: far less than any real gap.
fn highlight_depth(format: Option<wgpu::TextureFormat>) -> Option<wgpu::DepthStencilState> {
let reversed = reversed_depth(format);
format.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: Some(false),
depth_compare: Some(depth_compare(reversed, wgpu::CompareFunction::LessEqual)),
stencil: Default::default(),
bias: depth_bias(reversed, HIGHLIGHT_BIAS),
})
}
/// See [`highlight_depth`]. Negative: toward the eye (depth grows away from it).
const HIGHLIGHT_BIAS: wgpu::DepthBiasState = wgpu::DepthBiasState { constant: -8, slope_scale: -1.0, clamp: 0.0 };
/// Opaque faces are drawn pushed back from the eye by a few depth steps and by their own depth
/// slope over [`FACE_SLOPE`] pixels, so whatever lies on a face (its edges, an origin plane, a
/// sketch, a decal) is drawn over it at any angle, and an edge behind it stays hidden however
/// large the scene (#1434, #1739). The CPU renderer has the same rule (`raster.rs`, #983).
/// Lines used to be pulled toward the eye by a share of the depth range instead, which on a large
/// design reached tens of millimetres. Positive: away from the eye.
pub const FACE_BIAS: wgpu::DepthBiasState = wgpu::DepthBiasState { constant: FACE_STEPS, slope_scale: FACE_SLOPE, clamp: 0.0 };
/// The depth steps of [`FACE_BIAS`]: f32 rounding between a face and what lies on it (the GPU
/// evaluates planned bodies in f32; edges and overlays come from the CPU).
pub const FACE_STEPS: i32 = 8;
/// The pixels of [`FACE_BIAS`]: half a 2 px line, so edges (1.2 px) draw whole on a steep face.
/// It is also how far behind a face an edge may show, in that face's own depth per pixel: at 10 m
/// a pixel is about 9 mm, and a face turned 25° gains 4 mm of depth across it, so a larger value
/// let a washer 12 mm behind a plate show through it (#1434). A thicker line (a selected edge,
/// 3.5 px) may lose its outer edge on one side where it lies on a steep face. The section cap,
/// which writes its own depth, adds the same in the shader.
pub const FACE_SLOPE: f32 = 1.0;
/// The removed material of a preview, drawn translucent: behind the faces it shares with the new
/// shape, so twice [`FACE_BIAS`].
const GHOST_BIAS: wgpu::DepthBiasState = wgpu::DepthBiasState { constant: 2 * FACE_STEPS, slope_scale: 2.0 * FACE_SLOPE, clamp: 0.0 };
fn upload(device: &wgpu::Device, label: &str, bytes: &[u8], stride: usize) -> Option<Batch> {
upload_with(device, label, bytes, stride, wgpu::BufferUsages::VERTEX)
}
fn upload_with(device: &wgpu::Device, label: &str, bytes: &[u8], stride: usize, usage: wgpu::BufferUsages) -> Option<Batch> {
if bytes.len() < stride || stride == 0 {
return None;
}
let chunk = buffer_chunk(stride, device.limits().max_buffer_size);
let parts = bytes
.chunks(chunk)
.filter(|part| part.len() >= stride)
.map(|part| {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(label), contents: part, usage });
(buffer, u32::try_from(part.len() / stride).unwrap_or(0))
})
.collect();
Some(Batch { parts, chunk })
}
/// Bytes per GPU buffer for vertices of `stride` bytes: within the device's limit and a
/// whole number of triangles and line pairs, so no primitive straddles two buffers.
fn buffer_chunk(stride: usize, max_buffer_size: u64) -> usize {
let unit = stride.max(1) * 6;
let max = usize::try_from(max_buffer_size).unwrap_or(usize::MAX).min(1 << 30);
(max / unit).max(1) * unit
}
/// Triangles (3 vertices of `TRI_SIZE` bytes each, position first) ordered far to near along
/// the view direction `back` (toward the eye), so blended glass draws back to front.
pub fn sort_back_to_front(bytes: &[u8], back: [f32; 3]) -> Vec<u8> {
let tri = TRI_SIZE * 3;
let depth = |t: &[u8]| -> f32 {
let f = |o: usize| t.get(o..o + 4).and_then(|b| b.try_into().ok()).map_or(0.0, f32::from_le_bytes);
(0..3).map(|v| (0..3).map(|k| f(v * TRI_SIZE + k * 4) * back[k]).sum::<f32>()).sum::<f32>()
};
let mut tris: Vec<(f32, &[u8])> = bytes.chunks_exact(tri).map(|t| (depth(t), t)).collect();
tris.sort_by(|a, b| a.0.total_cmp(&b.0));
tris.into_iter().flat_map(|(_, t)| t.iter().copied()).collect()
}
impl Batches {
/// Re-sort the glass for a new view direction (when it turned more than about 2 degrees).
fn sort_glass(&mut self, queue: &wgpu::Queue, back: [f32; 3]) {
let (Some(g), false) = (&self.glass, self.glass_cpu.is_empty()) else { return };
let turned = self.glass_sorted_for.is_none_or(|s| s[0] * back[0] + s[1] * back[1] + s[2] * back[2] < 0.9994);
if turned {
let sorted = sort_back_to_front(&self.glass_cpu, back);
for ((buffer, _), bytes) in g.parts.iter().zip(sorted.chunks(g.chunk.max(1))) {
queue.write_buffer(buffer, 0, bytes);
}
self.glass_sorted_for = Some(back);
}
}
}
/// The tessellation pipeline (`shader`: its source), where display plans are evaluated: on a
/// device that runs compute shaders with the storage buffers the evaluation binds (not WebGL2)
/// and builds the pipeline. Elsewhere `None`, and bodies are built on the CPU: a device whose
/// shader compiler rejects the shader (D3D12's FXC once did) only loses GPU evaluation.
pub fn tessellation(adapter: &wgpu::Adapter, device: &wgpu::Device, queue: &wgpu::Queue, shader: &str) -> Option<crate::TessGpu> {
let compute = adapter.get_downlevel_capabilities().flags.contains(wgpu::DownlevelFlags::COMPUTE_SHADERS)
&& device.limits().max_storage_buffers_per_shader_stage >= 4;
if !compute {
return None;
}
let info = adapter.get_info();
if hangs_on_gpu_tessellation(info.backend, &info.name) || std::env::var_os("SOLIDCRAFT_CPU_TESS").is_some() {
log::warn!("bodies are built on the CPU on {} ({:?})", info.name, info.backend);
return None;
}
match crate::TessGpu::with_shader(info.name.clone(), device.clone(), queue.clone(), shader) {
Ok(t) => Some(t),
Err(e) => {
log::warn!("bodies are built on the CPU on {} ({:?}): {e}", info.name, info.backend);
None
}
}
}
/// A device whose driver hangs on the evaluation shader, so bodies are built on the CPU there:
/// the AMD FirePro D300/D500/D700 (Mac Pro 2013, Tahiti) under Metal, where macOS resets the GPU
/// and the whole desktop freezes. `SOLIDCRAFT_CPU_TESS=1` forces CPU evaluation on any device.
pub fn hangs_on_gpu_tessellation(backend: wgpu::Backend, name: &str) -> bool {
backend == wgpu::Backend::Metal && name.contains("FirePro")
}
impl Renderer {
/// Before the frame's render pass: upload the scenes that changed (planned bodies not yet on
/// the GPU are evaluated by dispatches recorded into `encoder`), the uniform, the images and
/// the accessibility map, and draw the frame supersampled when it asks for that.
pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, frame: &Frame) {
let res = self;
let cb = frame;
if cb.product {
res.product.take(device, cb.key, &cb.slot, None);
res.product.sort_glass(queue, cb.back);
queue.write_buffer(&res.uniform, 0, &uniform_bytes(cb, cb.size_px[0], cb.size_px[1], res.linear_out, res.target.reversed_depth()));
return;
}
res.model.take(device, cb.key, &cb.slot, res.tess.as_ref().map(|t| (t, &mut *encoder)));
res.sketch.take(device, cb.sketch_key, &cb.sketch_slot, None);
res.highlight.take(device, cb.hl_key, &cb.hl_slot, None);
res.preview.take(device, cb.pv_key, &cb.pv_slot, None);
res.model.sort_glass(queue, cb.back);
queue.write_buffer(&res.uniform, 0, &uniform_bytes(cb, cb.size_px[0], cb.size_px[1], res.linear_out, res.target.reversed_depth()));
if let Some(a) = &cb.access
&& a.version != res.access_version
{
res.access_bind = access_texture(device, Some(queue), &res.access_bgl, a.size, &a.depth);
res.access_version = a.version;
}
// Images: upload new or changed textures, drop removed ones, rebuild the quads.
res.textures.retain(|id, _| cb.images.iter().any(|i| i.id == *id));
res.quads.clear();
for img in &cb.images {
let [w, h] = img.size;
if w == 0 || h == 0 || img.rgba.len() != (w as usize) * (h as usize) * 4 {
continue;
}
if res.textures.get(&img.id).is_none_or(|t| t.0 != img.version) {
let size = wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 };
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("sc_image"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo { texture: &tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
&img.rgba,
wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(4 * w), rows_per_image: Some(h) },
size,
);
let view = tex.create_view(&Default::default());
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("sc_image"),
layout: &res.image_bgl,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&res.sampler) },
],
});
res.textures.insert(img.id, (img.version, bind));
}
let c = img.corners;
let uv = [[0.0_f32, 1.0], [1.0, 1.0], [1.0, 0.0], [0.0, 0.0]];
let mut bytes = Vec::with_capacity(6 * IMG_SIZE);
for k in [0, 1, 2, 0, 2, 3] {
for v in c[k].iter().chain(&uv[k]).chain(std::iter::once(&img.opacity)) {
bytes.extend_from_slice(&v.to_le_bytes());
}
}
if let Some(b) = upload(device, "sc_image_quad", &bytes, IMG_SIZE) {
res.quads.push((img.id, b));
}
}
res.ss_ready = cb.ssaa != 1.0 && res.supersample(device, encoder, cb);
}
/// Draw the frame into `pass` within `viewport` (x, y, width, height in physical pixels):
/// filtered down from the supersampled frame [`Renderer::prepare`] drew, or drawn directly.
pub fn paint(&self, frame: &Frame, pass: &mut wgpu::RenderPass<'static>, viewport: [f32; 4]) {
let [x, y, w, h] = viewport;
if !(w > 0.0 && h > 0.0) {
return;
}
pass.set_viewport(x, y, w, h, 0.0, 1.0);
if self.ss_ready
&& let Some(ss) = &self.ss
{
pass.set_pipeline(&self.ss_blit);
pass.set_bind_group(0, &ss.bind, &[]);
pass.draw(0..3, 0..1);
return;
}
if let Some(reset) = &self.depth_reset {
pass.set_pipeline(reset);
pass.draw(0..3, 0..1);
}
self.draw(frame, pass);
}
/// The target the pipelines draw into.
pub fn target(&self) -> GpuTarget {
self.target
}
}
impl Renderer {
/// Draw the frame's scenes into `pass` (its viewport already set).
fn draw(&self, cb: &Frame, pass: &mut wgpu::RenderPass<'static>) {
let res = self;
pass.set_bind_group(0, &res.bind, &[]);
let tris = |pass: &mut wgpu::RenderPass<'static>, p: &wgpu::RenderPipeline, b: &Option<Batch>| {
if let Some(b) = b {
pass.set_pipeline(p);
for (buffer, count) in &b.parts {
pass.set_vertex_buffer(0, buffer.slice(..));
pass.draw(0..*count, 0..1);
}
}
};
let lines = |pass: &mut wgpu::RenderPass<'static>, p: &wgpu::RenderPipeline, b: &Option<Batch>| {
if let Some(b) = b {
pass.set_pipeline(p);
for (buffer, count) in &b.parts {
pass.set_vertex_buffer(0, buffer.slice(..));
pass.draw(0..6, 0..*count);
}
}
};
let (m, sk, h, pv) =
if cb.product { (&res.product, &res.empty, &res.empty, &res.empty) } else { (&res.model, &res.sketch, &res.highlight, &res.preview) };
lines(pass, &res.overlay, &m.under);
lines(pass, &res.overlay, &sk.under);
pass.set_bind_group(1, &res.access_bind, &[]);
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 = cb.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);
}
_ => {
tris(pass, &res.tri, &m.tris);
tris(pass, &res.tri, &pv.tris);
planned(pass, &res.tri_planned);
}
}
tris(pass, &res.pbr, &m.pbr);
tris(pass, &res.tri_hl, &h.tris);
lines(pass, &res.line, &m.lines);
lines(pass, &res.line, &sk.lines);
lines(pass, &res.line_hidden, &m.hidden);
lines(pass, &res.line_hidden, &h.hidden);
lines(pass, &res.line, &pv.lines);
lines(pass, &res.line, &h.lines);
lines(pass, &res.guide_line, &h.guide_lines);
for (id, q) in res.quads.iter().filter(|_| !cb.product) {
if let Some((_, bind)) = res.textures.get(id) {
pass.set_pipeline(&res.image);
pass.set_bind_group(1, bind, &[]);
for (buffer, count) in &q.parts {
pass.set_vertex_buffer(0, buffer.slice(..));
pass.draw(0..*count, 0..1);
}
}
}
tris(pass, &res.ghost, &pv.ghost);
tris(pass, &res.glass_far, &m.glass);
tris(pass, &res.glass_near, &m.glass);
tris(pass, &res.trans, &m.trans);
tris(pass, &res.trans, &sk.trans);
tris(pass, &res.trans, &h.trans);
tris(pass, &res.guide_trans, &h.guide_trans);
tris(pass, &res.xray, &pv.xray);
tris(pass, &res.xray, &h.xray);
lines(pass, &res.overlay, &m.overlays);
lines(pass, &res.overlay, &sk.overlays);
lines(pass, &res.overlay, &h.overlays);
}
}
impl Renderer {
/// Draw the frame supersampled (`cb.ssaa` times the viewport's pixels each way, within the
/// device's texture limit and the memory budget) into textures of the viewport's kind.
/// `false` when it cannot be (too small a gain, a zero-sized viewport): drawn as usual.
fn supersample(&mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, cb: &Frame) -> bool {
let Some(size) = supersampled_size(cb.size_px, cb.ssaa, self.max_texture, self.target.samples) else {
self.ss = None;
return false;
};
if self.ss.as_ref().is_none_or(|ss| ss.size != size) {
self.ss = Some(self.supersampled(device, size));
}
let Some(ss) = &self.ss else { return false };
let depth_format = self.target.depth;
let mut pass = encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("sc_ssaa"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &ss.color,
depth_slice: None,
resolve_target: ss.resolve.as_ref(),
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store },
})],
depth_stencil_attachment: ss.depth.as_ref().map(|view| wgpu::RenderPassDepthStencilAttachment {
view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(far_depth(self.target.reversed_depth())),
store: wgpu::StoreOp::Discard,
}),
stencil_ops: depth_format
.filter(|f| f.has_stencil_aspect())
.map(|_| wgpu::Operations { load: wgpu::LoadOp::Clear(0), store: wgpu::StoreOp::Discard }),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
})
.forget_lifetime();
pass.set_viewport(0.0, 0.0, size[0] as f32, size[1] as f32, 0.0, 1.0);
self.draw(cb, &mut pass);
true
}
/// Textures for a supersampled frame of `size` pixels.
fn supersampled(&self, device: &wgpu::Device, size: [u32; 2]) -> Supersampled {
let t = self.target;
let texture = |label: &str, format: wgpu::TextureFormat, samples: u32, usage: wgpu::TextureUsages| {
device
.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d { width: size[0], height: size[1], depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: samples,
dimension: wgpu::TextureDimension::D2,
format,
usage,
view_formats: &[],
})
.create_view(&Default::default())
};
let samples = t.samples.max(1);
let sampled = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING;
let (color, resolve) = if samples > 1 {
(texture("sc_ssaa_ms", t.format, samples, wgpu::TextureUsages::RENDER_ATTACHMENT), Some(texture("sc_ssaa", t.format, 1, sampled)))
} else {
(texture("sc_ssaa", t.format, 1, sampled), None)
};
let depth = t.depth.map(|f| texture("sc_ssaa_depth", f, samples, wgpu::TextureUsages::RENDER_ATTACHMENT));
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("sc_ssaa"),
layout: &self.ss_bgl,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(resolve.as_ref().unwrap_or(&color)) },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
],
});
Supersampled { size, color, resolve, depth, bind }
}
}
/// Uniform bytes for a frame.
pub fn uniform_bytes(cb: &Frame, w: f32, h: f32, linear: bool, reversed: bool) -> Vec<u8> {
let mut v: Vec<f32> = gpu_view_proj(cb.view_proj, reversed).iter().flatten().copied().collect();
v.extend([cb.back[0], cb.back[1], cb.back[2], if linear { 1.0 } else { 0.0 }]);
v.extend([w.max(1.0), h.max(1.0), 0.0, if cb.outline { 1.0 } else { 0.0 }]);
v.extend(cb.clip);
v.extend(cb.cap);
v.extend(cb.analysis);
match &cb.access {
Some(a) => {
v.extend(a.u);
v.extend(a.v);
}
None => v.extend([0.0; 8]),
}
v.extend(cb.eye);
v.extend(cb.analysis_colors);
v.extend([if reversed { 1.0 } else { 0.0 }, FACE_STEPS as f32, FACE_SLOPE, 0.0]);
v.iter().flat_map(|x| x.to_le_bytes()).collect()
}
/// The shader's analysis colours (`ana_c`: draft positive, between, negative; reachable,
/// blocked; curvature flat, middle, tight; alpha 1) and flags (`ana_f.x`: 1 to stripe the bad
/// faces).
pub fn analysis_colors(c: &solidcraft_render::AnalysisColors) -> [f32; 36] {
let mut v = [0.0; 36];
let colors = c.draft.iter().chain(&c.access).chain(&c.curvature).flat_map(|rgb| [rgb[0], rgb[1], rgb[2], 1.0]);
for (slot, x) in v.iter_mut().zip(colors) {
*slot = x;
}
v[32] = if c.hatch { 1.0 } else { 0.0 };
v
}
#[cfg(test)]
mod tests {
/// Only the FirePro under Metal skips GPU evaluation; the same card under another backend
/// and other Metal GPUs keep it.
#[test]
fn firepro_under_metal_builds_bodies_on_the_cpu() {
assert!(super::hangs_on_gpu_tessellation(wgpu::Backend::Metal, "AMD FirePro D500"));
assert!(super::hangs_on_gpu_tessellation(wgpu::Backend::Metal, "AMD FirePro D700"));
assert!(!super::hangs_on_gpu_tessellation(wgpu::Backend::Metal, "Apple M2"));
assert!(!super::hangs_on_gpu_tessellation(wgpu::Backend::Metal, "AMD Radeon Pro 560X"));
assert!(!super::hangs_on_gpu_tessellation(wgpu::Backend::Vulkan, "AMD FirePro W5100"));
}
/// The viewport shader parses and validates (a bad shader would only fail at run time).
#[test]
fn shader_is_valid() {
let module = naga::front::wgsl::parse_str(super::SHADER).unwrap_or_else(|e| panic!("{}", e.emit_to_string(super::SHADER)));
naga::valid::Validator::new(naga::valid::ValidationFlags::all(), naga::valid::Capabilities::empty()).validate(&module).unwrap();
}
/// Derivatives and implicit-lod sampling run only in uniform control flow: browsers' WGSL
/// compilers (Tint) reject them after a branch, and the whole viewport shader then fails,
/// a black screen on the web (naga accepts it). Every fragment entry point makes such calls
/// (directly or through a helper) before its first branch or return.
#[test]
fn derivatives_only_in_uniform_control_flow() {
// Without comments (their words are not code).
let src: String = super::SHADER.lines().map(|l| l.split("//").next().unwrap_or("")).collect::<Vec<_>>().join("\n");
let src = src.as_str();
let derivs = ["dpdx(", "dpdy(", "fwidth(", "textureSample("];
// Functions: name -> body.
let mut fns: Vec<(String, String, bool)> = Vec::new();
let mut rest = src;
let mut fragment = false;
while let Some(at) = rest.find("fn ") {
let before = &rest[..at];
if before.contains("@fragment") {
fragment = true;
}
let Some(open) = rest[at..].find('{') else { break };
let name = rest[at + 3..].split('(').next().unwrap_or("").trim().to_string();
let start = at + open;
let mut depth = 0;
let mut end = start;
for (k, ch) in rest[start..].char_indices() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
end = start + k;
break;
}
}
_ => {}
}
}
fns.push((name, rest[start..=end].to_string(), fragment));
fragment = false;
rest = &rest[end + 1..];
}
// Helpers that take derivatives (directly or through another helper).
let mut uses: Vec<String> = Vec::new();
for _ in 0..8 {
for (n, body, _) in &fns {
if !uses.contains(n) && (derivs.iter().any(|d| body.contains(d)) || uses.iter().any(|u| body.contains(&format!("{u}(")))) {
uses.push(n.clone());
}
}
}
for (n, body, frag) in &fns {
let calls: Vec<usize> = derivs
.iter()
.map(|d| d.to_string())
.chain(uses.iter().map(|u| format!("{u}(")))
.flat_map(|c| body.match_indices(&c).map(|(i, _)| i).collect::<Vec<_>>())
.collect();
if calls.is_empty() {
continue;
}
if !*frag {
// A helper: its own derivatives must come before any branch or return in it.
let first = ["if (", "return", "discard"].iter().filter_map(|k| body.find(k)).min().unwrap_or(usize::MAX);
for d in derivs {
if let Some(i) = body.find(d) {
assert!(i < first, "{n}: {d} after a branch");
}
}
continue;
}
let first = ["if (", "return", "discard"].iter().filter_map(|k| body.find(k)).min().unwrap_or(usize::MAX);
for c in calls {
assert!(c < first, "fragment {n}: a derivative after a branch or return: {}", &body[c..(c + 40).min(body.len())]);
}
}
}
}
#[cfg(test)]
mod glass_tests {
use super::*;
/// A large import's triangles once went into one buffer past the device's limit (524 MB
/// against 256 MB) and the app crashed (#883): they are split into whole triangles.
#[test]
fn large_scenes_are_split_into_buffers_within_the_device_limit() {
let limit = 256u64 << 20;
for stride in [TRI_SIZE, LINE_SIZE, 4] {
let chunk = buffer_chunk(stride, limit);
assert!(chunk as u64 <= limit && chunk > limit as usize / 2);
assert_eq!(chunk % (stride * 3), 0, "whole triangles");
assert_eq!(chunk % (stride * 2), 0, "whole line pairs");
}
let scene = 524_434_680usize;
assert_eq!(scene.div_ceil(buffer_chunk(TRI_SIZE, limit)), 2);
assert!(buffer_chunk(TRI_SIZE, 1) >= TRI_SIZE * 6, "a tiny limit still holds one unit");
}
#[test]
fn glass_is_drawn_far_to_near() {
// Two triangles at z = 5 and z = -5, given near first; looking down from +Z.
let mut sc = GpuScene::default();
for z in [5.0f32, -5.0] {
for p in [[0.0, 0.0, z], [1.0, 0.0, z], [0.0, 1.0, z]] {
sc.glass_tri(p, [0.0, 0.0, 1.0], [255, 0, 0, 100]);
}
}
let sorted = sort_back_to_front(&sc.glass, [0.0, 0.0, 1.0]);
let z0 = f32::from_le_bytes(sorted[8..12].try_into().unwrap());
assert_eq!(z0, -5.0, "the far triangle first");
assert_eq!(sorted.len(), sc.glass.len());
// Seen from below, the order flips.
let sorted = sort_back_to_front(&sc.glass, [0.0, 0.0, -1.0]);
assert_eq!(f32::from_le_bytes(sorted[8..12].try_into().unwrap()), 5.0);
}
}