mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-12 00:45:16 -06:00
A wipeout was drawn only as its outline, so objects behind it stayed visible on the canvas and in PNG, SVG and PDF output. The display list gets a new primitive kind, Kind::Mask: the wipeout polygon as triangles that every backend fills with its background colour (paper white for SVG/PDF and plots, the raster background for PNG, the canvas colour or the white sheet on screen), in draw order, so it hides what came before it (DRAWORDER decides). The frame follows WIPEOUTFRAME: 0 none, 1 shown and plotted (default), 2 shown but not plotted. The GPU canvas drew all fills before all lines; it now splits the mesh into draw-order groups at each mask (MeshData::breaks) and draws each group's fills, lines and quads in turn. Infinite lines stay on top as before. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
647 lines
27 KiB
Rust
647 lines
27 KiB
Rust
//! GPU canvas: draws the drawing's display list with an `egui_wgpu` paint callback.
|
||
//!
|
||
//! The display list is converted once per (document, revision, tessellation band, background,
|
||
//! lineweight display, vertex origin) into vertex data ([`MeshData`]) and uploaded to wgpu
|
||
//! buffers; each frame only a small view uniform changes. Positions are stored as f32 relative to
|
||
//! a per-build origin so large world coordinates keep their precision; at deep zoom the origin
|
||
//! follows the view (see [`choose_origin`]).
|
||
//!
|
||
//! Batches: filled triangles (TriangleList), 1-px hairlines (LineList), and instanced screen-space
|
||
//! quads for wide lineweights and point markers. Fills are drawn first, then lines on top. A
|
||
//! wipeout starts a new draw-order group (see [`MeshData::breaks`]): its background-coloured mask
|
||
//! is drawn after everything before it and under everything after it. The shader only uses
|
||
//! vertex buffers and one uniform buffer, so it runs on WebGPU and WebGL2.
|
||
//!
|
||
//! Infinite lines (xline/ray) and all overlays stay egui shapes (see `canvas.rs`), and the
|
||
//! canvas falls back to the CPU path when no wgpu render state was handed to the app.
|
||
|
||
use std::sync::{Arc, Mutex};
|
||
|
||
use cadcraft_color::Rgb;
|
||
use cadcraft_geom::{Bounds2, Vec2};
|
||
use cadcraft_render::{DisplayList, Kind};
|
||
use egui_wgpu::wgpu;
|
||
use egui_wgpu::wgpu::util::DeviceExt;
|
||
|
||
/// Line/triangle vertex: position (2 × f32, relative to the origin) + sRGB colour (4 × u8).
|
||
const VERTEX_SIZE: usize = 12;
|
||
/// Wide-line / point instance: a, b (2 × f32 each) + colour (4 × u8) + width in points (f32).
|
||
const INSTANCE_SIZE: usize = 24;
|
||
/// Vertices (or instances) per GPU buffer; divisible by 2 and 3 so no primitive straddles two.
|
||
const CHUNK: usize = 4_194_300;
|
||
/// Point markers are 1.5 × 1.5 points (as on the CPU path).
|
||
const POINT_SIZE: f32 = 1.5;
|
||
|
||
/// How the native window presents frames (`NativeOptions::wgpu_options.surface`).
|
||
///
|
||
/// The canvas hides the OS pointer and draws its own crosshair, so every frame the swapchain holds
|
||
/// back is visible as cursor lag. With FIFO (vsync) wgpu asks for one queued frame, but Vulkan
|
||
/// clamps that to the driver's minimum swapchain size, which is three images on X11 (two frames
|
||
/// queued behind the one on screen; Windows DX12 honours one). `low_latency` (the
|
||
/// app passes it on Linux/BSD) prefers a mode that never queues: `AutoNoVsync` picks Immediate,
|
||
/// then Mailbox, then falls back to FIFO, so it is valid on every surface; under a compositor the
|
||
/// compositor still syncs to the display. `vsync` is the `CADCRAFT_VSYNC` override: `1`/`on`
|
||
/// forces FIFO (tear-free without a compositor), `0`/`off` forces the low-latency mode.
|
||
pub fn surface_config(vsync: Option<&str>, low_latency: bool) -> egui_wgpu::SurfaceConfig {
|
||
let no_vsync = match vsync.map(|v| v.trim().to_ascii_lowercase()).as_deref() {
|
||
Some("1" | "on" | "true" | "yes") => false,
|
||
Some("0" | "off" | "false" | "no") => true,
|
||
_ => low_latency,
|
||
};
|
||
egui_wgpu::SurfaceConfig {
|
||
present_mode: if no_vsync { wgpu::PresentMode::AutoNoVsync } else { wgpu::PresentMode::AutoVsync },
|
||
desired_maximum_frame_latency: Some(1),
|
||
}
|
||
}
|
||
|
||
/// Frame-rate cap for presenting without vsync. Nothing else paces frames then (on X11; Wayland
|
||
/// still throttles to the compositor), so a 1000 Hz mouse made the app render ~900 frames per
|
||
/// second and keep a CPU core busy. Waiting out the rest of a short minimum interval adds at most
|
||
/// that interval of input age, far less than a vsync queue.
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct FrameCap {
|
||
min: std::time::Duration,
|
||
next: Option<std::time::Instant>,
|
||
}
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
impl FrameCap {
|
||
/// At most `max_fps` frames per second.
|
||
pub fn new(max_fps: u32) -> Self {
|
||
FrameCap { min: std::time::Duration::from_secs(1) / max_fps.max(1), next: None }
|
||
}
|
||
|
||
/// How long a frame starting at `now` waits; records when the following one may start.
|
||
pub fn wait_at(&mut self, now: std::time::Instant) -> std::time::Duration {
|
||
let wait = self.next.map_or(std::time::Duration::ZERO, |n| n.saturating_duration_since(now));
|
||
self.next = now.checked_add(wait.saturating_add(self.min));
|
||
wait
|
||
}
|
||
|
||
/// Block until this frame may start (call once per frame, at its start).
|
||
pub fn wait(&mut self) {
|
||
let wait = self.wait_at(std::time::Instant::now());
|
||
if !wait.is_zero() {
|
||
std::thread::sleep(wait);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The wgpu target the app renders into, recorded by [`install`].
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct GpuTarget {
|
||
pub format: wgpu::TextureFormat,
|
||
}
|
||
|
||
/// CPU-side vertex data for one display list, waiting to be uploaded.
|
||
#[derive(Default)]
|
||
pub struct MeshData {
|
||
pub lines: Vec<u8>,
|
||
pub tris: Vec<u8>,
|
||
pub quads: Vec<u8>,
|
||
/// Draw-order breaks, one before each wipeout mask: the fill vertices, line vertices and
|
||
/// quad instances drawn before it. Each group is drawn fills, lines, quads in turn.
|
||
pub breaks: Vec<[usize; 3]>,
|
||
}
|
||
|
||
impl MeshData {
|
||
pub fn line_vertices(&self) -> usize {
|
||
self.lines.len() / VERTEX_SIZE
|
||
}
|
||
pub fn tri_vertices(&self) -> usize {
|
||
self.tris.len() / VERTEX_SIZE
|
||
}
|
||
pub fn quad_instances(&self) -> usize {
|
||
self.quads.len() / INSTANCE_SIZE
|
||
}
|
||
}
|
||
|
||
fn rel(p: Vec2, origin: Vec2) -> [f32; 2] {
|
||
[(p.x - origin.x) as f32, (p.y - origin.y) as f32]
|
||
}
|
||
|
||
fn push_vertex(out: &mut Vec<u8>, p: [f32; 2], c: [u8; 4]) {
|
||
out.extend_from_slice(&p[0].to_le_bytes());
|
||
out.extend_from_slice(&p[1].to_le_bytes());
|
||
out.extend_from_slice(&c);
|
||
}
|
||
|
||
fn push_instance(out: &mut Vec<u8>, a: [f32; 2], b: [f32; 2], c: [u8; 4], width: f32) {
|
||
for v in [a[0], a[1], b[0], b[1]] {
|
||
out.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
out.extend_from_slice(&c);
|
||
out.extend_from_slice(&width.to_le_bytes());
|
||
}
|
||
|
||
/// Lineweight in screen points for a primitive (as on the CPU path), or `None` for a hairline.
|
||
pub fn wide_width(lw: f32, lwdisplay: bool) -> Option<f32> {
|
||
if !lwdisplay || lw <= 0.0 {
|
||
return None;
|
||
}
|
||
let w = (lw * 3.78).clamp(1.0, 12.0);
|
||
(w > 1.0).then_some(w)
|
||
}
|
||
|
||
/// Convert a display list into vertex data relative to `origin`, colours resolved against `bg`.
|
||
pub fn build_mesh(list: &DisplayList, origin: Vec2, bg: Rgb, lwdisplay: bool) -> MeshData {
|
||
let mut m = MeshData {
|
||
lines: Vec::with_capacity(list.segment_count().saturating_mul(2 * VERTEX_SIZE)),
|
||
tris: Vec::with_capacity(list.tris.len().saturating_mul(VERTEX_SIZE)),
|
||
quads: Vec::new(),
|
||
breaks: Vec::new(),
|
||
};
|
||
for prim in &list.prims {
|
||
let c = prim.display_rgb(bg);
|
||
let col = [c.0, c.1, c.2, 255];
|
||
let pts = list.points(prim);
|
||
match prim.kind {
|
||
Kind::Polyline => match wide_width(prim.lw, lwdisplay) {
|
||
Some(w) => {
|
||
for s in pts.windows(2) {
|
||
if let [a, b] = s {
|
||
push_instance(&mut m.quads, rel(*a, origin), rel(*b, origin), col, w);
|
||
}
|
||
}
|
||
}
|
||
None => {
|
||
for s in pts.windows(2) {
|
||
if let [a, b] = s {
|
||
push_vertex(&mut m.lines, rel(*a, origin), col);
|
||
push_vertex(&mut m.lines, rel(*b, origin), col);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
Kind::Tris => {
|
||
for t in pts.as_chunks::<3>().0 {
|
||
for q in t {
|
||
push_vertex(&mut m.tris, rel(*q, origin), col);
|
||
}
|
||
}
|
||
}
|
||
Kind::Mask => {
|
||
m.breaks.push([m.tri_vertices(), m.line_vertices(), m.quad_instances()]);
|
||
for t in pts.as_chunks::<3>().0 {
|
||
for q in t {
|
||
push_vertex(&mut m.tris, rel(*q, origin), [bg.0, bg.1, bg.2, 255]);
|
||
}
|
||
}
|
||
}
|
||
Kind::Point => {
|
||
if let Some(q) = pts.first() {
|
||
let p = rel(*q, origin);
|
||
push_instance(&mut m.quads, p, p, col, POINT_SIZE);
|
||
}
|
||
}
|
||
// Drawn on the CPU each frame (clipped to the view).
|
||
Kind::Infinite { .. } => {}
|
||
}
|
||
}
|
||
m
|
||
}
|
||
|
||
/// The vertex origin for a display list seen at `view_center` / `view_height`: the list's centre
|
||
/// normally; at deep zoom (the view much smaller than the drawing) a point near the view, snapped
|
||
/// to a coarse grid so panning re-uploads only every few dozen screens.
|
||
pub fn choose_origin(bounds: &Bounds2, view_center: Vec2, view_height: f64) -> Vec2 {
|
||
let size = bounds.width().max(bounds.height());
|
||
if bounds.is_empty()
|
||
|| !view_center.is_finite()
|
||
|| view_height.partial_cmp(&0.0) != Some(std::cmp::Ordering::Greater)
|
||
|| view_height * 2000.0 >= size
|
||
{
|
||
return if bounds.is_empty() { Vec2::ZERO } else { bounds.center() };
|
||
}
|
||
let step = view_height * 64.0;
|
||
Vec2::new((view_center.x / step).round() * step, (view_center.y / step).round() * step)
|
||
}
|
||
|
||
/// Pending upload shared between the canvas (producer) and the paint callback (consumer).
|
||
pub type MeshSlot = Arc<Mutex<Option<MeshData>>>;
|
||
|
||
/// Per-frame parameters for the paint callback.
|
||
pub struct CanvasCallback {
|
||
/// Identifies the mesh in `slot`; the GPU buffers are rebuilt when it changes.
|
||
pub key: u64,
|
||
pub slot: MeshSlot,
|
||
/// (origin − view centre) in world units.
|
||
pub offset: [f32; 2],
|
||
/// Points per world unit.
|
||
pub scale: f32,
|
||
/// Canvas rect centre in points.
|
||
pub center: [f32; 2],
|
||
}
|
||
|
||
struct Batch {
|
||
buffer: wgpu::Buffer,
|
||
count: u32,
|
||
}
|
||
|
||
struct Resources {
|
||
line_pipeline: wgpu::RenderPipeline,
|
||
tri_pipeline: wgpu::RenderPipeline,
|
||
quad_pipeline: wgpu::RenderPipeline,
|
||
uniform: wgpu::Buffer,
|
||
bind_group: wgpu::BindGroup,
|
||
linear_out: bool,
|
||
key: Option<u64>,
|
||
lines: Vec<Batch>,
|
||
tris: Vec<Batch>,
|
||
quads: Vec<Batch>,
|
||
breaks: Vec<[usize; 3]>,
|
||
}
|
||
|
||
const UNIFORM_SIZE: u64 = 48;
|
||
|
||
const SHADER: &str = r#"
|
||
struct View {
|
||
a: vec4<f32>, // offset x, offset y (world), scale (device px per unit), 1 = linear output
|
||
b: vec4<f32>, // canvas centre x, y (device px), screen w, h (device px)
|
||
c: vec4<f32>, // pixels per point
|
||
};
|
||
@group(0) @binding(0) var<uniform> view: View;
|
||
|
||
struct VOut {
|
||
@builtin(position) pos: vec4<f32>,
|
||
@location(0) color: vec4<f32>,
|
||
};
|
||
|
||
fn to_px(p: vec2<f32>) -> vec2<f32> {
|
||
let w = (p + view.a.xy) * view.a.z;
|
||
return vec2<f32>(view.b.x + w.x, view.b.y - w.y);
|
||
}
|
||
|
||
fn px_to_clip(px: vec2<f32>) -> vec4<f32> {
|
||
return vec4<f32>(px.x / view.b.z * 2.0 - 1.0, 1.0 - px.y / view.b.w * 2.0, 0.0, 1.0);
|
||
}
|
||
|
||
fn out_color(c: vec4<f32>) -> vec4<f32> {
|
||
if (view.a.w > 0.5) {
|
||
let lo = c.rgb / 12.92;
|
||
let hi = pow((c.rgb + vec3<f32>(0.055)) / 1.055, vec3<f32>(2.4));
|
||
return vec4<f32>(select(hi, lo, c.rgb <= vec3<f32>(0.04045)), c.a);
|
||
}
|
||
return c;
|
||
}
|
||
|
||
@vertex
|
||
fn vs_line(@location(0) pos: vec2<f32>, @location(1) color: vec4<f32>) -> VOut {
|
||
var o: VOut;
|
||
o.pos = px_to_clip(to_px(pos));
|
||
o.color = out_color(color);
|
||
return o;
|
||
}
|
||
|
||
@vertex
|
||
fn vs_quad(
|
||
@builtin(vertex_index) vi: u32,
|
||
@location(0) a: vec2<f32>,
|
||
@location(1) b: vec2<f32>,
|
||
@location(2) color: vec4<f32>,
|
||
@location(3) width: f32,
|
||
) -> VOut {
|
||
let pa = to_px(a);
|
||
let pb = to_px(b);
|
||
let d = pb - pa;
|
||
let len = length(d);
|
||
var dir = vec2<f32>(1.0, 0.0);
|
||
if (len > 0.0001) {
|
||
dir = d / len;
|
||
}
|
||
let n = vec2<f32>(-dir.y, dir.x);
|
||
// Width arrives in points.
|
||
let hw = 0.5 * width * view.c.x;
|
||
let t = select(0.0, 1.0, vi == 1u || vi == 4u || vi == 5u);
|
||
let s = select(-1.0, 1.0, vi == 2u || vi == 3u || vi == 5u);
|
||
let along = mix(pa - dir * hw, pb + dir * hw, t);
|
||
var o: VOut;
|
||
o.pos = px_to_clip(along + n * hw * s);
|
||
o.color = out_color(color);
|
||
return o;
|
||
}
|
||
|
||
@fragment
|
||
fn fs_main(v: VOut) -> @location(0) vec4<f32> {
|
||
return v.color;
|
||
}
|
||
"#;
|
||
|
||
fn vertex_layout() -> wgpu::VertexBufferLayout<'static> {
|
||
const ATTRS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![0 => Float32x2, 1 => Unorm8x4];
|
||
wgpu::VertexBufferLayout { array_stride: VERTEX_SIZE as u64, step_mode: wgpu::VertexStepMode::Vertex, attributes: &ATTRS }
|
||
}
|
||
|
||
fn instance_layout() -> wgpu::VertexBufferLayout<'static> {
|
||
const ATTRS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Unorm8x4, 3 => Float32];
|
||
wgpu::VertexBufferLayout { array_stride: INSTANCE_SIZE as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: &ATTRS }
|
||
}
|
||
|
||
impl Resources {
|
||
fn new(device: &wgpu::Device, target: GpuTarget) -> Self {
|
||
let module =
|
||
device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("cad_canvas"), source: wgpu::ShaderSource::Wgsl(SHADER.into()) });
|
||
let entry = |binding: u32| wgpu::BindGroupLayoutEntry {
|
||
binding,
|
||
visibility: wgpu::ShaderStages::VERTEX,
|
||
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
|
||
count: None,
|
||
};
|
||
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("cad_canvas"), entries: &[entry(0)] });
|
||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||
label: Some("cad_canvas"),
|
||
bind_group_layouts: &[Some(&bgl)],
|
||
immediate_size: 0,
|
||
});
|
||
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("cad_canvas_view"),
|
||
size: UNIFORM_SIZE,
|
||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("cad_canvas"),
|
||
layout: &bgl,
|
||
entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() }],
|
||
});
|
||
let pipeline = |label: &str, vs: &str, topology: wgpu::PrimitiveTopology, buffer: wgpu::VertexBufferLayout<'static>| {
|
||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||
label: Some(label),
|
||
layout: Some(&layout),
|
||
vertex: wgpu::VertexState {
|
||
module: &module,
|
||
entry_point: Some(vs),
|
||
buffers: &[Some(buffer)],
|
||
compilation_options: Default::default(),
|
||
},
|
||
primitive: wgpu::PrimitiveState { topology, ..Default::default() },
|
||
depth_stencil: None,
|
||
multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false },
|
||
fragment: Some(wgpu::FragmentState {
|
||
module: &module,
|
||
entry_point: Some("fs_main"),
|
||
targets: &[Some(wgpu::ColorTargetState {
|
||
format: target.format,
|
||
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
|
||
write_mask: wgpu::ColorWrites::ALL,
|
||
})],
|
||
compilation_options: Default::default(),
|
||
}),
|
||
multiview_mask: None,
|
||
cache: None,
|
||
})
|
||
};
|
||
Resources {
|
||
line_pipeline: pipeline("cad_lines", "vs_line", wgpu::PrimitiveTopology::LineList, vertex_layout()),
|
||
tri_pipeline: pipeline("cad_fills", "vs_line", wgpu::PrimitiveTopology::TriangleList, vertex_layout()),
|
||
quad_pipeline: pipeline("cad_quads", "vs_quad", wgpu::PrimitiveTopology::TriangleList, instance_layout()),
|
||
uniform,
|
||
bind_group,
|
||
linear_out: target.format.is_srgb(),
|
||
key: None,
|
||
lines: Vec::new(),
|
||
tris: Vec::new(),
|
||
quads: Vec::new(),
|
||
breaks: Vec::new(),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn upload(device: &wgpu::Device, label: &str, bytes: &[u8], stride: usize, usage: wgpu::BufferUsages) -> Vec<Batch> {
|
||
bytes
|
||
.chunks(CHUNK * stride)
|
||
.filter(|c| c.len() >= stride)
|
||
.map(|c| Batch {
|
||
buffer: device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(label), contents: c, usage }),
|
||
count: u32::try_from(c.len() / stride).unwrap_or(0),
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// Create the canvas pipelines in the renderer's callback resources. Call once with the app's
|
||
/// render state (eframe `CreationContext::wgpu_render_state`).
|
||
pub fn install(rs: &egui_wgpu::RenderState) -> GpuTarget {
|
||
let target = GpuTarget { format: rs.target_format };
|
||
let res = Resources::new(&rs.device, target);
|
||
rs.renderer.write().callback_resources.insert(res);
|
||
log::info!("gpu canvas: target {:?}", rs.target_format);
|
||
target
|
||
}
|
||
|
||
impl egui_wgpu::CallbackTrait for CanvasCallback {
|
||
fn prepare(
|
||
&self,
|
||
device: &wgpu::Device,
|
||
queue: &wgpu::Queue,
|
||
screen: &egui_wgpu::ScreenDescriptor,
|
||
_encoder: &mut wgpu::CommandEncoder,
|
||
resources: &mut egui_wgpu::CallbackResources,
|
||
) -> Vec<wgpu::CommandBuffer> {
|
||
let Some(res) = resources.get_mut::<Resources>() else { return Vec::new() };
|
||
if res.key != Some(self.key) {
|
||
let pending = self.slot.lock().ok().and_then(|mut s| s.take());
|
||
if let Some(m) = pending {
|
||
let usage = wgpu::BufferUsages::VERTEX;
|
||
res.lines = upload(device, "cad_lines", &m.lines, VERTEX_SIZE, usage);
|
||
res.tris = upload(device, "cad_fills", &m.tris, VERTEX_SIZE, usage);
|
||
res.quads = upload(device, "cad_quads", &m.quads, INSTANCE_SIZE, usage);
|
||
res.breaks = m.breaks;
|
||
res.key = Some(self.key);
|
||
}
|
||
}
|
||
let ppp = screen.pixels_per_point;
|
||
let [w, h] = screen.size_in_pixels;
|
||
let a = [self.offset[0], self.offset[1], self.scale * ppp, if res.linear_out { 1.0 } else { 0.0 }];
|
||
let b = [self.center[0] * ppp, self.center[1] * ppp, w.max(1) as f32, h.max(1) as f32];
|
||
let c = [ppp, 0.0, 0.0, 0.0];
|
||
let bytes: Vec<u8> = a.iter().chain(&b).chain(&c).flat_map(|v| v.to_le_bytes()).collect();
|
||
queue.write_buffer(&res.uniform, 0, &bytes);
|
||
Vec::new()
|
||
}
|
||
|
||
fn paint(&self, info: egui::PaintCallbackInfo, pass: &mut wgpu::RenderPass<'static>, resources: &egui_wgpu::CallbackResources) {
|
||
let Some(res) = resources.get::<Resources>() else { return };
|
||
if res.key != Some(self.key) {
|
||
return;
|
||
}
|
||
let [sw, sh] = info.screen_size_px;
|
||
if sw == 0 || sh == 0 {
|
||
return;
|
||
}
|
||
// Whole-screen device pixels; egui already set the scissor to the canvas clip rect.
|
||
pass.set_viewport(0.0, 0.0, sw as f32, sh as f32, 0.0, 1.0);
|
||
pass.set_bind_group(0, &res.bind_group, &[]);
|
||
// Draw-order groups split at each wipeout; without wipeouts there is one group.
|
||
let total = |bs: &[Batch]| bs.iter().map(|b| b.count as usize).sum::<usize>();
|
||
let all = [total(&res.tris), total(&res.lines), total(&res.quads)];
|
||
let mut from = [0usize; 3];
|
||
for to in res.breaks.iter().copied().chain(std::iter::once(all)) {
|
||
for (i, (pipeline, batches)) in
|
||
[(&res.tri_pipeline, &res.tris), (&res.line_pipeline, &res.lines), (&res.quad_pipeline, &res.quads)].into_iter().enumerate()
|
||
{
|
||
let parts = batch_ranges(batches.iter().map(|b| b.count), from[i]..to[i]);
|
||
if parts.is_empty() {
|
||
continue;
|
||
}
|
||
pass.set_pipeline(pipeline);
|
||
for (j, r) in parts {
|
||
let Some(b) = batches.get(j) else { continue };
|
||
pass.set_vertex_buffer(0, b.buffer.slice(..));
|
||
// Quads are instanced: six vertices per instance.
|
||
if i == 2 { pass.draw(0..6, r) } else { pass.draw(r, 0..1) }
|
||
}
|
||
}
|
||
from = to;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// For batches holding `counts` vertices (or instances) each: the batch indices and the ranges
|
||
/// within them that cover `range` of the batches laid end to end.
|
||
fn batch_ranges(counts: impl IntoIterator<Item = u32>, range: std::ops::Range<usize>) -> Vec<(usize, std::ops::Range<u32>)> {
|
||
let mut out = Vec::new();
|
||
let mut off = 0usize;
|
||
for (j, n) in counts.into_iter().enumerate() {
|
||
let n = n as usize;
|
||
let (lo, hi) = (range.start.max(off), range.end.min(off + n));
|
||
if lo < hi {
|
||
out.push((j, (lo - off) as u32..(hi - off) as u32));
|
||
}
|
||
off += n;
|
||
}
|
||
out
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use cadcraft_doc::Handle;
|
||
use cadcraft_render::DPrim;
|
||
|
||
#[test]
|
||
fn surface_config_prefers_low_latency_where_asked_and_honours_override() {
|
||
use wgpu::PresentMode::{AutoNoVsync, AutoVsync};
|
||
assert_eq!(surface_config(None, true).present_mode, AutoNoVsync);
|
||
assert_eq!(surface_config(None, false).present_mode, AutoVsync);
|
||
for on in ["1", "on", " TRUE ", "yes"] {
|
||
assert_eq!(surface_config(Some(on), true).present_mode, AutoVsync, "{on}");
|
||
}
|
||
for off in ["0", "Off", "false", "no"] {
|
||
assert_eq!(surface_config(Some(off), false).present_mode, AutoNoVsync, "{off}");
|
||
}
|
||
assert_eq!(surface_config(Some("maybe"), true).present_mode, AutoNoVsync);
|
||
assert_eq!(surface_config(Some(""), false).present_mode, AutoVsync);
|
||
assert_eq!(surface_config(None, true).desired_maximum_frame_latency, Some(1));
|
||
}
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
#[test]
|
||
fn frame_cap_waits_out_the_minimum_interval_only() {
|
||
use std::time::{Duration, Instant};
|
||
let mut cap = FrameCap::new(250); // 4 ms
|
||
let t0 = Instant::now();
|
||
assert_eq!(cap.wait_at(t0), Duration::ZERO, "first frame never waits");
|
||
assert_eq!(cap.wait_at(t0 + Duration::from_millis(1)), Duration::from_millis(3));
|
||
// That frame started at 4 ms; the next one 10 ms later is long past the interval.
|
||
assert_eq!(cap.wait_at(t0 + Duration::from_millis(14)), Duration::ZERO);
|
||
assert_eq!(cap.wait_at(t0 + Duration::from_millis(18)), Duration::ZERO);
|
||
assert_eq!(FrameCap::new(0).min, Duration::from_secs(1));
|
||
}
|
||
|
||
fn list() -> DisplayList {
|
||
let mut l = DisplayList {
|
||
verts: vec![Vec2::new(1e6, 1e6), Vec2::new(1e6 + 1.0, 1e6), Vec2::new(1e6 + 1.0, 1e6 + 1.0), Vec2::new(5.0, 5.0)],
|
||
tris: vec![Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), Vec2::new(0.0, 1.0)],
|
||
..Default::default()
|
||
};
|
||
let p = |kind, start, len, lw| DPrim { handle: Handle(1), color: Rgb(255, 255, 255), aci7: true, lw, kind, start, len };
|
||
l.prims =
|
||
vec![p(Kind::Polyline, 0, 3, 0.0), p(Kind::Tris, 0, 3, 0.0), p(Kind::Point, 3, 1, 0.0), p(Kind::Infinite { ray: false }, 0, 2, 0.0)];
|
||
for v in l.verts.clone() {
|
||
l.bounds.add(v);
|
||
}
|
||
l
|
||
}
|
||
|
||
#[test]
|
||
fn mesh_counts_and_precision() {
|
||
let l = list();
|
||
let origin = Vec2::new(1e6, 1e6);
|
||
let m = build_mesh(&l, origin, Rgb(255, 255, 255), false);
|
||
assert_eq!(m.line_vertices(), 4, "two segments");
|
||
assert_eq!(m.tri_vertices(), 3);
|
||
assert_eq!(m.quad_instances(), 1, "the point marker");
|
||
// First vertex is stored relative to the origin, so it's exactly zero.
|
||
assert_eq!(&m.lines[0..8], &[0u8; 8]);
|
||
// Colour 7 on a white background inverts to black (display_rgb).
|
||
assert_eq!(&m.lines[8..12], &[0, 0, 0, 255]);
|
||
let x = f32::from_le_bytes([m.lines[12], m.lines[13], m.lines[14], m.lines[15]]);
|
||
assert_eq!(x, 1.0);
|
||
}
|
||
|
||
#[test]
|
||
fn lineweights_become_quads() {
|
||
let mut l = list();
|
||
if let Some(p) = l.prims.first_mut() {
|
||
p.lw = 0.7;
|
||
}
|
||
let m = build_mesh(&l, Vec2::ZERO, Rgb(0, 0, 0), true);
|
||
assert_eq!(m.line_vertices(), 0);
|
||
assert_eq!(m.quad_instances(), 3);
|
||
assert_eq!(wide_width(0.25, true), None, "thin lineweights stay hairlines");
|
||
assert_eq!(wide_width(0.7, false), None);
|
||
}
|
||
|
||
#[test]
|
||
#[allow(clippy::unwrap_used)]
|
||
fn shader_is_valid_for_webgpu_and_webgl2() {
|
||
let module = naga::front::wgsl::parse_str(SHADER).unwrap();
|
||
let info = naga::valid::Validator::new(naga::valid::ValidationFlags::all(), naga::valid::Capabilities::empty()).validate(&module).unwrap();
|
||
for (stage, entry) in
|
||
[(naga::ShaderStage::Vertex, "vs_line"), (naga::ShaderStage::Vertex, "vs_quad"), (naga::ShaderStage::Fragment, "fs_main")]
|
||
{
|
||
let options = naga::back::glsl::Options { version: naga::back::glsl::Version::new_gles(300), ..Default::default() };
|
||
let pipeline = naga::back::glsl::PipelineOptions { shader_stage: stage, entry_point: entry.into(), multiview: None };
|
||
let mut out = String::new();
|
||
let mut w =
|
||
naga::back::glsl::Writer::new(&mut out, &module, &info, &options, &pipeline, naga::proc::BoundsCheckPolicies::default()).unwrap();
|
||
w.write().unwrap();
|
||
assert!(out.contains("#version 300 es"), "{entry}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn origin_follows_deep_zoom() {
|
||
let b = Bounds2::new(Vec2::new(0.0, 0.0), Vec2::new(1e6, 1e6));
|
||
assert_eq!(choose_origin(&b, Vec2::new(10.0, 10.0), 1e5), b.center());
|
||
let o = choose_origin(&b, Vec2::new(123_456.7, 654_321.2), 1.0);
|
||
assert!((o.x - 123_456.7).abs() <= 32.0 && (o.y - 654_321.2).abs() <= 32.0);
|
||
assert_eq!(choose_origin(&Bounds2::EMPTY, Vec2::new(1.0, 1.0), 1.0), Vec2::ZERO);
|
||
assert_eq!(choose_origin(&b, Vec2::new(f64::NAN, 0.0), 1.0), b.center());
|
||
}
|
||
|
||
#[test]
|
||
fn wipeout_masks_start_a_draw_order_group() {
|
||
let mut l = list();
|
||
if let Some(p) = l.prims.get_mut(1) {
|
||
p.kind = Kind::Mask;
|
||
}
|
||
let bg = Rgb(10, 20, 30);
|
||
let m = build_mesh(&l, Vec2::ZERO, bg, false);
|
||
// The polyline's two segments come before the mask; the point marker after it.
|
||
assert_eq!(m.breaks, vec![[0, 4, 0]]);
|
||
assert_eq!(m.tri_vertices(), 3);
|
||
assert_eq!(&m.tris[8..12], &[10, 20, 30, 255], "the mask takes the background colour");
|
||
assert_eq!(m.quad_instances(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn batch_ranges_cover_a_range_across_batches() {
|
||
assert_eq!(batch_ranges([4, 4, 4], 2..9), vec![(0, 2..4), (1, 0..4), (2, 0..1)]);
|
||
assert_eq!(batch_ranges([4, 4], 4..8), vec![(1, 0..4)]);
|
||
assert!(batch_ranges([4, 4], 3..3).is_empty());
|
||
assert!(batch_ranges([], 0..5).is_empty());
|
||
}
|
||
}
|