Files
OneCAD-Draft/src/scene/pipeline/mod.rs
T
Hakan Seven df1f9bd0e7 Merge pull request #1680 from ramox81/codex/block-wire-buffer-packing
Block wires: pack definitions into shared buffers

# Conflicts:
#	src/scene/pipeline/mod.rs
#	src/scene/pipeline/wire_gpu.rs
2026-10-11 23:50:38 +03:00

6239 lines
274 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.
// WGSL has no includes. Compose one shared draw-order function into each 2D
// shader at compile time, including the sources used by shader validation tests.
macro_rules! draw_order_shader {
($file:literal) => {
concat!(
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/shaders/draw_order.wgsl"
)),
"\n",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/shaders/", $file))
)
};
}
pub mod circle_gpu;
#[cfg(test)]
mod depth_tests;
mod device_capabilities;
pub mod ellipse_gpu;
pub mod face3d_gpu;
pub mod gpu_budget;
#[cfg(test)]
mod gpu_tests;
pub mod gpu_upload;
pub mod hatch_gpu;
pub mod image_gpu;
pub mod mesh_gpu;
pub mod point_cloud_gpu;
pub mod text_gpu;
pub mod uniforms;
pub mod viewcube;
pub mod wipeout_gpu;
/// Persistent per-entity wire instance arena. Its indexed-storage and packed
/// adapters share the same patch/cull lifecycle across native, WebGPU, and
/// WebGL2.
pub mod wire_arena;
pub mod wire_gpu;
use iced::wgpu;
use iced::{Rectangle, Size};
pub use circle_gpu::{CircleGpu, CircleInstance};
pub use ellipse_gpu::{EllipseGpu, EllipseInstance};
pub use face3d_gpu::Face3DGpu;
pub use image_gpu::ImageGpu;
pub use uniforms::Uniforms;
pub use viewcube::ViewCubePipeline;
pub use wipeout_gpu::WipeoutGpu;
pub use wire_gpu::{BlockWireGpu, WireGpu};
use crate::scene::model::hatch_model::HatchModel;
use crate::scene::model::image_model::ImageModel;
use crate::scene::model::mesh_model::MeshLodSet;
use crate::scene::model::wire_model::WireModel;
/// Worst-case raster depth bias (in 24-bit quanta, toward the camera) block
/// text is required to clear. The wipeout pipeline currently carries no bias
/// (a raster bias can jump a block's narrow depth band and erase its
/// foreground), so this is a safety budget: the block-text regression asserts
/// a larger margin over whatever bias the pipeline could reintroduce.
pub const WIPEOUT_DEPTH_BIAS_QUANTA: i32 = 2;
struct SilhouetteChunk {
vertex_buffer: wgpu::Buffer,
instance_buffer: wgpu::Buffer,
vertex_count: u32,
instance_count: u32,
}
struct SilhouetteSourceGroup {
color: [f32; 4],
sources: Vec<kernel::brep::mesh::SilhouetteSource>,
instance_buffers: Vec<(wgpu::Buffer, u32)>,
}
use device_capabilities::DeviceCapabilities;
/// MSAA sample count for the main drawing pipelines.
const MSAA_SAMPLES: u32 = 4;
const SHADOW_MAP_SIZE: u32 = 2048;
#[derive(Clone, Copy, PartialEq, Eq)]
enum MeshHighlightKind {
Selected,
Hover,
}
#[derive(Clone, Copy)]
struct MeshResidentRange {
chunk: usize,
dynamic: bool,
index_start: u32,
index_count: u32,
transparent: bool,
instance_start: u32,
instance_count: u32,
}
#[derive(Clone, Copy)]
struct MeshHighlightDraw {
range: MeshResidentRange,
kind: MeshHighlightKind,
}
pub struct Pipeline {
gpu_error_epoch: usize,
background_pipeline: wgpu::RenderPipeline,
shadow_pipeline: wgpu::RenderPipeline,
shadow_plain_pipeline: wgpu::RenderPipeline,
wire_pipeline: wgpu::RenderPipeline,
circle_pipeline: wgpu::RenderPipeline,
ellipse_pipeline: wgpu::RenderPipeline,
block_wire_pipeline: wgpu::RenderPipeline,
/// Stamps clip-boundary polygons into the stencil buffer (viewports + XCLIP).
clip_mask_pipeline: wgpu::RenderPipeline,
/// Black-fragment variant of `wire_pipeline` for 3D mesh outline edges in
/// filled render modes.
wire_black_pipeline: wgpu::RenderPipeline,
block_wire_black_pipeline: wgpu::RenderPipeline,
/// Same shader as wire_pipeline but depth_compare=Greater, depth_write_enabled=false.
/// Used to draw ghost copies of selected wires through occluding geometry.
wire_xray_pipeline: wgpu::RenderPipeline,
circle_xray_pipeline: wgpu::RenderPipeline,
ellipse_xray_pipeline: wgpu::RenderPipeline,
block_wire_xray_pipeline: wgpu::RenderPipeline,
/// Layout for the per-wire `WireConst` storage buffer (group 1 of the wire /
/// xray pipelines). `Some` on any storage-capable device; `None` in packed
/// compatibility mode. Passed to `WireGpu::from_run` / `from_batch`.
pub(crate) wire_const_bgl: Option<wgpu::BindGroupLayout>,
block_wire_const_bgl: wgpu::BindGroupLayout,
/// Which block-wire pipeline was built. Decides whether a definition's
/// segments go to a vertex buffer six times over or to a storage buffer
/// once.
block_wire_mode: wire_gpu::WirePipelineMode,
wipeout_pipeline: wgpu::RenderPipeline,
/// Capability-selected hatch renderer. Storage and texture transports are
/// private backends behind one upload/LOD/draw lifecycle.
hatch_gpu: hatch_gpu::HatchGpu,
image_pipeline: wgpu::RenderPipeline,
point_cloud_pipeline: wgpu::RenderPipeline,
/// SDF text-quad pipeline (Phase 2b): draws per-glyph quads sampling the
/// shared glyph atlas. Fed only when `OCS_TEXT_SDF` is set (else no verts).
text_pipeline: wgpu::RenderPipeline,
block_text_pipeline: wgpu::RenderPipeline,
/// Depth-independent variant used by selection / rollover highlighting.
text_highlight_pipeline: wgpu::RenderPipeline,
block_text_highlight_pipeline: wgpu::RenderPipeline,
mesh_pipeline: wgpu::RenderPipeline,
mesh_plain_pipeline: wgpu::RenderPipeline,
/// Depth-write-disabled variant of `mesh_pipeline` for non-opaque solids.
mesh_transparent_pipeline: wgpu::RenderPipeline,
mesh_plain_transparent_pipeline: wgpu::RenderPipeline,
mesh_selected_pipeline: wgpu::RenderPipeline,
mesh_plain_selected_pipeline: wgpu::RenderPipeline,
mesh_hover_pipeline: wgpu::RenderPipeline,
mesh_plain_hover_pipeline: wgpu::RenderPipeline,
/// Wireframe variant of the mesh pipeline (LineList topology, same
/// vertex layout / shader). Used when the active render mode is
/// Wireframe 2D or Wireframe 3D so 3D solids draw as their
/// triangle edges instead of filled faces.
mesh_wireframe_pipeline: wgpu::RenderPipeline,
/// Edge pipeline that forces black, for the edge overlay in filled modes.
mesh_edge_black_pipeline: wgpu::RenderPipeline,
silhouette_pipeline: wgpu::RenderPipeline,
silhouette_black_pipeline: wgpu::RenderPipeline,
/// Depth-only variant of the mesh pipeline (TriangleList, no color
/// writes, writes depth). Used in HiddenLine mode so 3D solids
/// occlude wires behind them without painting visible pixels.
mesh_depth_pipeline: wgpu::RenderPipeline,
mesh_plain_depth_pipeline: wgpu::RenderPipeline,
mesh_material_bgl: wgpu::BindGroupLayout,
mesh_default_material_bind_group: wgpu::BindGroup,
face3d_pipeline: wgpu::RenderPipeline,
block_face3d_pipeline: wgpu::RenderPipeline,
/// Depth-only variant of the face3d pipeline (no color writes,
/// writes depth). Paired with `mesh_depth_pipeline` for HiddenLine.
face3d_depth_pipeline: wgpu::RenderPipeline,
block_face3d_depth_pipeline: wgpu::RenderPipeline,
uniform_buffer: wgpu::Buffer,
frame_bgl: wgpu::BindGroupLayout,
uniform_bind_group: wgpu::BindGroup,
shadow_uniform_bind_group: wgpu::BindGroup,
background_texture: wgpu::Texture,
environment_texture: wgpu::Texture,
background_sampler: wgpu::Sampler,
background_source_id: usize,
environment_source_id: usize,
/// The shadow depth target, allocated only while this viewport actually
/// casts shadows. `SHADOW_MAP_SIZE` squared at `Depth32Float` is 16 MiB,
/// and every slot used to hold one whether or not its visual style enabled
/// shadows — 128 MiB across eight viewports, none of it ever sampled.
shadow_full: Option<(wgpu::Texture, wgpu::TextureView)>,
/// Bound at the frame group's shadow slot while `shadow_full` is `None`.
/// The binding must be filled for the layout to be satisfied, but nothing
/// samples it when shadows are off, so 1x1 is the whole requirement.
_shadow_fallback_texture: wgpu::Texture,
shadow_fallback_view: wgpu::TextureView,
shadow_sampler: wgpu::Sampler,
shadow_enabled: bool,
wipeout_bgl1: wgpu::BindGroupLayout,
image_bgl1: wgpu::BindGroupLayout,
/// Group-1 layout for the text pipeline (atlas texture + sampler).
text_atlas_bgl: wgpu::BindGroupLayout,
/// GPU glyph atlas (texture + sampler + bind group). Rebuilt when the shared
/// CPU atlas grows (new glyphs baked). `None` until the first text upload.
text_atlas_gpu: Option<text_gpu::TextAtlasGpu>,
/// Glyph-quad vertices for the frame, split at whole glyph boundaries.
text_gpu: Vec<text_gpu::TextGpu>,
block_text_gpu: Vec<text_gpu::BlockTextGpu>,
block_text_highlight_gpu: Vec<text_gpu::BlockTextGpu>,
/// Tinted glyph quads of just the selected / hovered text, drawn over the
/// base text pass so a selection / rollover recolours the glyphs (the text
/// analogue of the selected-wire xray overlay). Rebuilt on selection change.
text_highlight_gpu: Vec<text_gpu::TextGpu>,
/// Live grip-drag / command-preview SDF glyph quads. The base text buffer
/// only re-uploads on a geometry-epoch change, but a grip drag hides the
/// dragged entity from the base wire set and shows it as a per-frame
/// overlay — so its glyphs ride here, re-uploaded every frame like
/// `gpu_preview_wires`, or the dragged text vanishes until release re-tesses
/// it back into the base set (issue #316).
text_preview_gpu: Vec<text_gpu::TextGpu>,
/// Per-frame silhouette line lists from the kernel mesh and current view.
silhouette_chunks: Vec<SilhouetteChunk>,
silhouette_source_key: (usize, usize, u64),
silhouette_source_groups: Vec<SilhouetteSourceGroup>,
/// Last requested render size (the full viewport rect, in pixels). The
/// geometry passes render at this size; the blit UV is scaled by
/// `depth_texture_size / alloc_size` so it samples only the filled region.
depth_texture_size: Size<u32>,
pub(in crate::scene) viewport: Option<crate::scene::view::render::PhysicalViewport>,
/// Actual allocated size of the depth / MSAA / resolve textures. Rounded
/// up from the requested size to a coarse grid so a divider drag (which
/// changes the pane size a few pixels every frame) doesn't recreate these
/// textures every frame. Recreation is what hangs the ANGLE → D3D11 path
/// on Windows-Firefox (issue #191); grow-only bucketing makes it rare.
alloc_size: Size<u32>,
depth_view: wgpu::TextureView,
/// 4× MSAA color buffer for the main drawing passes.
msaa_view: wgpu::TextureView,
/// Single-sample texture that receives the MSAA resolve result.
resolve_view: wgpu::TextureView,
/// Pipeline + resources for blitting the resolve texture to the surface target.
blit_pipeline: wgpu::RenderPipeline,
blit_bind_group_layout: wgpu::BindGroupLayout,
blit_sampler: wgpu::Sampler,
blit_bind_group: wgpu::BindGroup,
/// UV transform (offset + scale) consumed by the blit shader so a
/// partially off-canvas viewport still composites the right portion of
/// its resolve texture to the visible portion of the surface.
blit_uniform_buffer: wgpu::Buffer,
/// Cached texture format (needed to recreate MSAA / depth textures on resize).
surface_format: wgpu::TextureFormat,
/// The resident wire batches, shared by `std::sync::Arc` with every other
/// pipeline slot rendering the *same* `wire_content_id` (see
/// `MultiPipeline::wire_buffer_cache`). A `wgpu::Buffer` is internally
/// ref-counted, so N paper viewports (or Model tiles) drawing one identical
/// resident set hold one copy of the GPU vertex buffers between them —
/// their camera uniforms + scissor stay per-slot, only the geometry is
/// deduplicated. Never mutated in place; an arena-backed slot may also
/// replace this thin draw-range list on camera changes without touching the
/// shared resident buffer.
pub(crate) gpu_wires: std::sync::Arc<Vec<WireGpu>>,
pub(crate) gpu_circles: std::sync::Arc<Vec<CircleGpu>>,
pub(crate) gpu_ellipses: std::sync::Arc<Vec<EllipseGpu>>,
pub(crate) gpu_block_wires: std::sync::Arc<Vec<BlockWireGpu>>,
/// Persistent per-entity wire instance arena (capability-selected format).
/// When active, `gpu_wires` is a thin wrapper over this arena's buffers and an
/// edit patches one entity's slab in place instead of rebuilding every wire.
pub(crate) wire_arena: Option<wire_arena::PersistentWireArena>,
/// Second arena for the mesh/solid EDGE wires (drawn with the mesh-edge skip /
/// black treatment); the resident set is split into this + `wire_arena` so
/// both patch incrementally. Shares `wire_arena_id`.
pub(crate) wire_arena_mesh: Option<wire_arena::PersistentWireArena>,
/// Chunked resident buffers for whichever arena partition exceeded one
/// GPU buffer. `Some(false)` = regular wires, `Some(true)` = mesh edges.
pub(crate) wire_arena_fallback: std::sync::Arc<Vec<WireGpu>>,
pub(crate) wire_arena_fallback_kind: Option<bool>,
pub(crate) wire_arena_fallback_handles: rustc_hash::FxHashSet<codec::Handle>,
/// The Model content id both arenas currently mirror (`u64::MAX` = none).
pub(crate) wire_arena_id: u64,
/// Handles that contributed to this slot's retained analytical uploads.
pub(crate) partition_contributors: rustc_hash::FxHashSet<codec::Handle>,
/// The draw-depth generation those uploads baked. A full depth rebuild
/// reassigns every label, so they stop being reusable when it moves.
pub(crate) partition_depth_generation: u64,
/// Last content/camera/viewport tuple used to derive visible instance
/// ranges from the resident arena.
pub(crate) wire_cull_key: (u64, u64, u32, u32),
/// View/source keys for CPU visibility passes.
pub(crate) hatch_lod_key: (usize, u64, u32, u32, bool),
pub(crate) wipeout_lod_key: (usize, u64, u32, u32, bool),
pub(crate) silhouette_key: (usize, u64, [u32; 3], bool),
/// This content viewport's non-rectangular clip boundary as a triangle-fan
/// vertex buffer in the render target's normalized device coords (`None` =
/// rectangular / unclipped, where the viewport's own render rectangle does
/// the clipping). Stamped into the stencil once per frame; every content
/// pass then draws with stencil reference 1 so only the interior survives.
clip_boundary: Option<(wgpu::Buffer, u32)>,
/// Ghost copies (25% alpha) of selected wires for the X-ray depth pass.
gpu_selected_wires: Vec<WireGpu>,
gpu_selected_circles: Vec<CircleGpu>,
gpu_selected_ellipses: Vec<EllipseGpu>,
gpu_selected_block_wires: Vec<BlockWireGpu>,
/// Command-preview / interim / grip-drag overlay wires. Re-uploaded every
/// frame they are present (small), drawn on top of the base wire pass — so
/// a live drag never re-uploads the resident base buffer.
gpu_preview_wires: Vec<WireGpu>,
gpu_preview_circles: Vec<CircleGpu>,
gpu_preview_ellipses: Vec<EllipseGpu>,
/// Wipeout masks — solid fills rendered after wires in a separate pass via
/// the legacy per-primitive `WipeoutGpu` renderer.
gpu_wipeouts: Vec<WipeoutGpu>,
/// Per-wipeout draw-time skip flag (Phase 2.3 frustum cull). `true`
/// when the wipeout's projected AABB sits entirely outside the
/// viewport rect. Recomputed by `compute_wipeout_lod`.
wipeout_skip_flags: Vec<bool>,
gpu_images: Vec<ImageGpu>,
gpu_point_clouds: point_cloud_gpu::PointCloudGpu,
/// Batched mesh geometry — every solid's LOD0 concatenated into a few large
/// buffers so the whole set draws in a handful of calls instead of one per
/// solid. Hover / selection never re-pack it.
gpu_mesh_batch: Vec<mesh_gpu::MeshBatchChunk>,
/// Small mutable working set rebuilt after entity-only edits. Static chunks
/// touched by an edit are disabled wholesale and their current handles are
/// repacked here, keeping the rest of a multi-million-triangle scene resident.
gpu_mesh_dynamic: Vec<mesh_gpu::MeshBatchChunk>,
mesh_disabled_chunks: rustc_hash::FxHashSet<usize>,
mesh_dynamic_handles: rustc_hash::FxHashSet<codec::Handle>,
/// Wire content generation whose geometry the static+dynamic mesh state
/// mirrors. It gates replay of the same per-entity journal handoff.
pub cached_mesh_content_id: u64,
/// Draw ranges for each entity inside the resident mesh chunks. Highlight
/// overlays reuse these buffers instead of uploading duplicate geometry.
mesh_ranges_by_handle: rustc_hash::FxHashMap<codec::Handle, Vec<MeshResidentRange>>,
mesh_highlight_draws: Vec<MeshHighlightDraw>,
/// `(geometry_epoch, selection_generation)` the highlight overlay was built for.
pub cached_highlight_key: (u64, u64),
/// Batched 3DFACE fill (all faces in one buffer) and edges (merged wire).
gpu_face3d_fill: Option<Face3DGpu>,
gpu_face3d_edges: Vec<WireGpu>,
pub viewcube: ViewCubePipeline,
/// Strong source guards for category-specific GPU uploads. Holding the old
/// Arc makes pointer identity ABA-safe: an unchanged category reuses the
/// same Arc even when an unrelated entity advances `geometry_epoch`.
pub cached_hatch_source: Option<std::sync::Arc<Vec<HatchModel>>>,
pub cached_preview_hatch_source: Option<std::sync::Arc<Vec<HatchModel>>>,
pub cached_wipeout_source: Option<std::sync::Arc<Vec<HatchModel>>>,
pub cached_image_source: Option<std::sync::Arc<Vec<ImageModel>>>,
pub cached_point_cloud_source:
Option<std::sync::Arc<crate::scene::model::point_cloud::PointCloudSet>>,
pub cached_text_source: Option<std::sync::Arc<Vec<text_gpu::TextVertex>>>,
pub cached_annotation_highlight_source: Option<std::sync::Arc<Vec<WireModel>>>,
pub cached_mesh_source: Option<std::sync::Arc<Vec<MeshLodSet>>>,
pub cached_face3d_source: Option<std::sync::Arc<Vec<WireModel>>>,
pub cached_face3d_depth_source: Option<std::sync::Weak<rustc_hash::FxHashMap<u64, [f32; 2]>>>,
pub cached_fill_mode: bool,
/// Last `(geometry_epoch, camera_generation)` value for which GPU buffers
/// were uploaded. We re-upload when either side changes — pan/zoom bumps
/// camera_generation, which triggers re-culling and a fresh upload.
/// `(geometry_epoch, camera_generation, selection_generation)` of the
/// last static-buffer upload. Selection is tracked so the selected-hatch
/// tint refreshes on select / deselect (issue #71).
pub cached_epoch: (u64, u64, u64),
/// Content id of the wire buffer currently resident on the GPU (Phase
/// 3.2). When the incoming `ViewportData.wire_content_id` matches, the
/// world-space wire vertices are unchanged (e.g. a pure pan reused the
/// Model-tile tessellation) and `upload_wires` is skipped. `u64::MAX` =
/// nothing uploaded yet.
pub cached_wire_id: u64,
/// `(wire_content_id, selection_generation)` the selection xray overlay
/// (`gpu_selected_wires`) was last built for. Rebuilt when either changes —
/// a pick bumps only `selection_generation`, refreshing the overlay without
/// touching the main wire buffers.
pub cached_selection: (u64, u64),
/// Face upload key; keeps the resident wire Arc uniquely owned by Scene.
pub cached_face3d_key: (u64, bool, bool, u64),
/// Cached planar-solid visibility for `(wire_content_id, view direction)`.
pub cached_solid_visibility: (u64, [u32; 3], u64),
/// Handle → indices into the resident wire set, built once per wire upload
/// (when `cached_wire_id` changes). Lets the selection/hover xray overlay
/// gather just the highlighted entity's wires (`O(highlighted)`) instead of
/// scanning and string-parsing every wire on each hover change. Shared by
/// `std::sync::Arc` alongside `gpu_wires` — built once per `wire_content_id`
/// and cloned into every slot that renders it.
pub(crate) wire_handle_index: std::sync::Arc<rustc_hash::FxHashMap<u64, Vec<u32>>>,
/// Signature of the last frame's rendered scene (camera uniforms, geometry
/// / selection generations, wire content id, render-mode flags, clip size,
/// live preview). When the next frame's signature matches, the image is
/// pixel-identical, so `render` skips every geometry pass + the MSAA
/// resolve and only re-blits the resolve texture (which still holds it).
/// This turns a pure cursor move over a large drawing from an O(N)
/// re-rasterization into a single fullscreen blit. `u64::MAX` = nothing
/// rendered yet (forces the first frame to render fully).
pub render_sig: u64,
/// Set by `prepare` each frame: `true` when this frame's signature matched
/// `render_sig`, so `render` may skip the geometry passes. Read (never
/// written) by `render`, which always runs after `prepare` for the frame.
pub skip_geometry: bool,
/// Interaction LOD: set by `prepare` when the view is actively navigating, so
/// `render` skips the (per-pixel, GPU-dominating) hatch pass this frame. The
/// scene-render cache holds the full-quality frame once the view settles.
pub skip_hatch_frame: bool,
/// Skip the canvas entirely — no clear colour, no background pass — so
/// whatever was drawn underneath this viewport shows through it.
pub skip_background: bool,
/// Stable identity of the viewport that last used this (index-addressed)
/// pipeline slot. The renderer's viewport list drops off-canvas viewports,
/// so a slot can be reused by a *different* viewport across frames; when the
/// occupant changes, all cache keys above belong to the previous one and are
/// reset so every buffer re-uploads. `u64::MAX` = never used.
pub slot_id: u64,
}
impl Pipeline {
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
// ── Shared frame uniform buffer (view_proj etc.) ───────────────────
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("viewer.uniform_buffer"),
size: std::mem::size_of::<Uniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Bind group layout 0 — shared by wire and hatch pipelines.
let frame_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("viewer.frame_bgl"),
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,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
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: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
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: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 6,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
count: None,
},
],
});
let shadow_frame_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("shadow.frame_bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let placeholder_texture = |label: &'static str, rgba: [u8; 4]| {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
texture.as_image_copy(),
&rgba,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4),
rows_per_image: Some(1),
},
wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
);
texture
};
let background_texture = placeholder_texture("background.placeholder", [0, 0, 0, 255]);
let environment_texture =
placeholder_texture("environment.placeholder", [128, 128, 128, 255]);
let background_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("background.sampler"),
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
});
let shadow_fallback_texture = create_shadow_texture(device, 1);
let shadow_fallback_view =
shadow_fallback_texture.create_view(&wgpu::TextureViewDescriptor::default());
let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("shadow.sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
compare: Some(wgpu::CompareFunction::LessEqual),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let background_view =
background_texture.create_view(&wgpu::TextureViewDescriptor::default());
let environment_view =
environment_texture.create_view(&wgpu::TextureViewDescriptor::default());
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("viewer.bind_group"),
layout: &frame_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&background_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&background_sampler),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(&environment_view),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Sampler(&background_sampler),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::TextureView(&shadow_fallback_view),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::Sampler(&shadow_sampler),
},
],
});
let shadow_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("shadow.frame_bind_group"),
layout: &shadow_frame_bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
// ── Wire pipeline ──────────────────────────────────────────────────
// Select the renderer tier from device limits.
let device_caps = DeviceCapabilities::detect(device);
#[cfg(not(target_arch = "wasm32"))]
let force_compat_renderer = crate::cli::gui_config().compat_renderer;
#[cfg(target_arch = "wasm32")]
let force_compat_renderer = false;
let wire_mode = wire_gpu::WirePipelineMode::select(device_caps, force_compat_renderer);
let wire_const_bgl = wire_mode
.uses_storage()
.then(|| wire_gpu::WireConst::bind_group_layout(device));
let mut wire_bgls: Vec<Option<&wgpu::BindGroupLayout>> = vec![Some(&frame_bgl)];
if let Some(bgl) = &wire_const_bgl {
wire_bgls.push(Some(bgl));
}
let wire_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("wire.pipeline_layout"),
bind_group_layouts: &wire_bgls,
immediate_size: 0,
});
let block_wire_const_bgl =
wire_gpu::block_const_bind_group_layout(device, wire_mode.uses_storage());
let block_wire_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("block_wire.pipeline_layout"),
bind_group_layouts: &[Some(&frame_bgl), Some(&block_wire_const_bgl)],
immediate_size: 0,
});
let depth_tex = create_depth_texture(device, Size::new(1, 1));
let depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
let wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("wire.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(match wire_mode {
wire_gpu::WirePipelineMode::IndexedStorage => {
draw_order_shader!("wire_indexed.wgsl")
}
wire_gpu::WirePipelineMode::Packed => draw_order_shader!("wire.wgsl"),
})),
});
let block_wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("block_wire.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
if wire_mode.uses_storage() {
draw_order_shader!("block_wire_storage.wgsl")
} else {
draw_order_shader!("block_wire.wgsl")
},
)),
});
// Stencil test shared by every paper content pipeline: draw only where
// the stencil equals the bound reference. Non-clipped content binds
// reference 0 (matching the frame's stencil, cleared to 0); a clip
// region's content binds reference 1 after its boundary has been stamped
// into the stencil by the clip-mask pipeline. In Model space nothing
// masks the stencil, so it stays 0 and reference 0 always passes.
let content_stencil_face = wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Equal,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Keep,
};
let content_stencil = wgpu::StencilState {
front: content_stencil_face,
back: content_stencil_face,
read_mask: 0xff,
write_mask: 0x00,
};
let background_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("background.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"../../shaders/background.wgsl"
))),
});
let background_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("background.pipeline_layout"),
bind_group_layouts: &[Some(&frame_bgl)],
immediate_size: 0,
});
let background_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("background.pipeline"),
layout: Some(&background_layout),
vertex: wgpu::VertexState {
module: &background_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &background_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
let wire_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("wire.pipeline"),
layout: Some(&wire_layout),
vertex: wgpu::VertexState {
module: &wire_shader,
entry_point: Some("vs_main"),
buffers: &[wire_mode.layout()],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &wire_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
// ── Clip-mask pipeline ─────────────────────────────────────────────
// Stamps a viewport / XCLIP boundary polygon into the stencil buffer:
// no colour, no depth, stencil `Invert` (even-odd fill → interior marked
// for any polygon, convex or not). The boundary is drawn as a triangle
// fan in paper coordinates and transformed exactly like wires.
let clip_mask_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("clip_mask.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"../../shaders/clip_mask.wgsl"
))),
});
let clip_mask_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("clip_mask.pipeline_layout"),
bind_group_layouts: &[],
immediate_size: 0,
});
let clip_mask_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("clip_mask.pipeline"),
layout: Some(&clip_mask_layout),
vertex: wgpu::VertexState {
module: &clip_mask_shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: 8,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x2,
}],
}],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: wgpu::StencilState {
front: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Invert,
},
back: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Invert,
},
read_mask: 0xff,
write_mask: 0xff,
},
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &clip_mask_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::empty(),
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
// Black variant of `wire_pipeline` — same geometry/depth, black fragment
// — for 3D mesh outline edges in filled modes (see the wire draw loop).
let wire_black_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("wire.black.pipeline"),
layout: Some(&wire_layout),
vertex: wgpu::VertexState {
module: &wire_shader,
entry_point: Some("vs_main"),
buffers: &[wire_mode.layout()],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &wire_shader,
entry_point: Some("fs_black"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
// Selection overlay variant: renders selected wires on top of everything
// (depth_compare=Always), without writing depth. Ensures selected entities
// are always fully visible regardless of occluding geometry.
let wire_xray_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("wire_xray.pipeline"),
layout: Some(&wire_layout),
vertex: wgpu::VertexState {
module: &wire_shader,
entry_point: Some("vs_main"),
buffers: &[wire_mode.layout()],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &wire_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
let block_wire_buffers: &[wgpu::VertexBufferLayout<'_>] = if wire_mode.uses_storage() {
&[wire_gpu::BlockWireInstance::layout()]
} else {
&[
wire_gpu::BlockWireVertex::layout(),
wire_gpu::BlockWireInstance::layout(),
]
};
let make_block_wire_pipeline =
|label: &'static str,
fragment: &'static str,
depth_write_enabled: bool,
depth_compare: wgpu::CompareFunction| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&block_wire_layout),
vertex: wgpu::VertexState {
module: &block_wire_shader,
entry_point: Some("vs_main"),
// Storage mode has no geometry vertex buffer: the
// instances become slot 0 and the segments arrive through
// the bind group.
buffers: block_wire_buffers,
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(depth_write_enabled),
depth_compare: Some(depth_compare),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &block_wire_shader,
entry_point: Some(fragment),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
})
};
let block_wire_pipeline = make_block_wire_pipeline(
"block_wire.pipeline",
"fs_main",
true,
wgpu::CompareFunction::LessEqual,
);
let block_wire_black_pipeline = make_block_wire_pipeline(
"block_wire.black.pipeline",
"fs_black",
true,
wgpu::CompareFunction::LessEqual,
);
let block_wire_xray_pipeline = make_block_wire_pipeline(
"block_wire.xray.pipeline",
"fs_main",
false,
wgpu::CompareFunction::Always,
);
// ── Hatch pipeline ─────────────────────────────────────────────────
// Binding 0 carries the fill plane; boundary and family data are
// fragment-only.
let hatch_entry = |binding: u32, vis: wgpu::ShaderStages| wgpu::BindGroupLayoutEntry {
binding,
visibility: vis,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
};
let frag = wgpu::ShaderStages::FRAGMENT;
let vert_frag = wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT;
let wipeout_bgl1 = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("wipeout.bgl1"),
entries: &[
hatch_entry(0, vert_frag),
hatch_entry(1, frag),
hatch_entry(2, frag),
],
});
let wipeout_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("wipeout.pipeline_layout"),
bind_group_layouts: &[&frame_bgl, &wipeout_bgl1].map(Some),
immediate_size: 0,
});
let wipeout_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("wipeout.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(draw_order_shader!(
"wipeout.wgsl"
))),
});
let wipeout_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("wipeout.pipeline"),
layout: Some(&wipeout_layout),
vertex: wgpu::VertexState {
module: &wipeout_shader,
entry_point: Some("vs_main"),
buffers: &[
wipeout_gpu::HatchVertex::layout(),
wipeout_gpu::WipeoutPlacement::layout(),
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
// The shader already applies draw order. A raster bias can
// jump across a block's narrow range and erase its foreground
// — block text clears its siblings' wipes by only 9-16 depth
// quanta, so the pipeline must stay bias-free. Coincident ties
// still resolve toward the mask, which draws later under
// LessEqual. WIPEOUT_DEPTH_BIAS_QUANTA is the budget the
// block-text regression asserts against if a bias ever
// returns.
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &wipeout_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
// ── Hatch renderer ─────────────────────────────────────────────────
// The façade owns capability selection plus both backend lifecycles.
let hatch_gpu = hatch_gpu::HatchGpu::new(
device,
format,
&frame_bgl,
&content_stencil,
device_caps,
force_compat_renderer,
);
#[cfg(not(target_arch = "wasm32"))]
if std::env::var_os("RUST_LOG").is_some() {
eprintln!(
"renderer pipelines: wire={} hatch={} mesh={} (storage buffers/stage: {})",
if wire_mode.uses_storage() {
"storage"
} else {
"packed"
},
hatch_gpu.backend_name(),
"vertex",
device.limits().max_storage_buffers_per_shader_stage
);
}
#[cfg(target_arch = "wasm32")]
log::info!(
"renderer pipelines: wire={} hatch={} mesh={} (storage buffers/stage: {})",
if wire_mode.uses_storage() {
"storage"
} else {
"packed"
},
hatch_gpu.backend_name(),
"vertex",
device.limits().max_storage_buffers_per_shader_stage
);
// ── Mesh pipeline ──────────────────────────────────────────────────
let mesh_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("mesh.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"../../shaders/mesh.wgsl"
))),
});
let mesh_material_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mesh.material.bgl"),
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::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
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: 3,
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: 4,
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: 5,
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: 6,
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: 7,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 8,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 9,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 10,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 11,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 12,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 13,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 14,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 16,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let mesh_default_material_bind_group =
mesh_gpu::create_material_bind_group(device, queue, &mesh_material_bgl, None);
let mesh_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("mesh.pipeline_layout"),
bind_group_layouts: &[&frame_bgl, &mesh_material_bgl].map(Some),
immediate_size: 0,
});
let shadow_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("shadow.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"../../shaders/shadow.wgsl"
))),
});
let shadow_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("shadow.pipeline_layout"),
bind_group_layouts: &[&shadow_frame_bgl, &mesh_material_bgl].map(Some),
immediate_size: 0,
});
let shadow_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("shadow.pipeline"),
layout: Some(&shadow_layout),
vertex: wgpu::VertexState {
module: &shadow_shader,
entry_point: Some("vs_main"),
buffers: &[
mesh_gpu::MeshVertex::layout(),
mesh_gpu::MeshInstanceGpu::layout(),
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState {
constant: 2,
slope_scale: 2.0,
clamp: 0.0,
},
}),
multisample: wgpu::MultisampleState::default(),
fragment: None,
multiview_mask: None,
cache: None,
});
let shadow_plain_pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("shadow.plain.pipeline"),
layout: Some(&shadow_layout),
vertex: wgpu::VertexState {
module: &shadow_shader,
entry_point: Some("vs_main"),
buffers: &[
mesh_gpu::MeshPlainVertex::layout(),
mesh_gpu::MeshInstanceGpu::layout(),
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState {
constant: 2,
slope_scale: 2.0,
clamp: 0.0,
},
}),
multisample: wgpu::MultisampleState::default(),
fragment: None,
multiview_mask: None,
cache: None,
});
let mesh_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("mesh.pipeline"),
layout: Some(&mesh_layout),
vertex: wgpu::VertexState {
module: &mesh_shader,
entry_point: Some("vs_main"),
buffers: &[
mesh_gpu::MeshVertex::layout(),
mesh_gpu::MeshInstanceGpu::layout(),
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None, // two-sided: ACIS import winding is unreliable
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState {
constant: 1,
slope_scale: 1.0,
clamp: 0.0,
},
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &mesh_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
let mesh_plain_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("mesh.plain.pipeline"),
layout: Some(&mesh_layout),
vertex: wgpu::VertexState {
module: &mesh_shader,
entry_point: Some("vs_main_plain"),
buffers: &[
mesh_gpu::MeshPlainVertex::layout(),
mesh_gpu::MeshInstanceGpu::layout(),
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState {
constant: 1,
slope_scale: 1.0,
clamp: 0.0,
},
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &mesh_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
// Transparent variant — identical to `mesh_pipeline` but with depth
// writes disabled. Non-opaque solids are drawn after the opaque fills
// with this pipeline so they blend over the geometry behind them (which
// has already written depth) instead of erasing it.
let mesh_transparent_pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("mesh.transparent.pipeline"),
layout: Some(&mesh_layout),
vertex: wgpu::VertexState {
module: &mesh_shader,
entry_point: Some("vs_main"),
buffers: &[
mesh_gpu::MeshVertex::layout(),
mesh_gpu::MeshInstanceGpu::layout(),
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState {
constant: 1,
slope_scale: 1.0,
clamp: 0.0,
},
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &mesh_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
let mesh_plain_transparent_pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("mesh.plain.transparent.pipeline"),
layout: Some(&mesh_layout),
vertex: wgpu::VertexState {
module: &mesh_shader,
entry_point: Some("vs_main_plain"),
buffers: &[
mesh_gpu::MeshPlainVertex::layout(),
mesh_gpu::MeshInstanceGpu::layout(),
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState {
constant: 1,
slope_scale: 1.0,
clamp: 0.0,
},
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &mesh_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
// Highlight variants reuse the resident mesh buffers and differ only in
// their fixed fragment tint. No selected mesh geometry is re-uploaded.
let make_mesh_highlight_pipeline =
|label: &'static str,
vertex_entry: &'static str,
vertex_layout: wgpu::VertexBufferLayout<'static>,
fragment_entry: &'static str| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&mesh_layout),
vertex: wgpu::VertexState {
module: &mesh_shader,
entry_point: Some(vertex_entry),
buffers: &[vertex_layout, mesh_gpu::MeshInstanceGpu::layout()],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &mesh_shader,
entry_point: Some(fragment_entry),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
})
};
let mesh_selected_pipeline = make_mesh_highlight_pipeline(
"mesh.highlight.selected.pipeline",
"vs_main",
mesh_gpu::MeshVertex::layout(),
"fs_highlight_selected",
);
let mesh_plain_selected_pipeline = make_mesh_highlight_pipeline(
"mesh.plain.highlight.selected.pipeline",
"vs_main_plain",
mesh_gpu::MeshPlainVertex::layout(),
"fs_highlight_selected",
);
let mesh_hover_pipeline = make_mesh_highlight_pipeline(
"mesh.highlight.hover.pipeline",
"vs_main",
mesh_gpu::MeshVertex::layout(),
"fs_highlight_hover",
);
let mesh_plain_hover_pipeline = make_mesh_highlight_pipeline(
"mesh.plain.highlight.hover.pipeline",
"vs_main_plain",
mesh_gpu::MeshPlainVertex::layout(),
"fs_highlight_hover",
);
// Wireframe variant — same shader / vertex layout / depth state,
// only the input topology changes (LineList) and back-face
// culling drops out (each triangle edge is shared between two
// faces, one of which would otherwise hide the edge).
// Edge/wireframe pipeline (LineList). `fs_edge` outputs the flat entity
// colour — no lighting — for the lines-only modes. A `fs_edge_black`
// twin (below) forces black for the edge overlay in filled modes.
let make_edge_pipeline =
|label: &'static str,
fs: &'static str,
vertex_layout: wgpu::VertexBufferLayout<'static>| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&mesh_layout),
vertex: wgpu::VertexState {
module: &mesh_shader,
entry_point: Some("vs_edge"),
buffers: &[vertex_layout, mesh_gpu::MeshInstanceGpu::layout()],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::LineList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &mesh_shader,
entry_point: Some(fs),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
})
};
let mesh_wireframe_pipeline = make_edge_pipeline(
"mesh.wireframe.pipeline",
"fs_edge",
mesh_gpu::MeshVertex::edge_layout(),
);
let mesh_edge_black_pipeline = make_edge_pipeline(
"mesh.edge_black.pipeline",
"fs_edge_black",
mesh_gpu::MeshVertex::edge_layout(),
);
let silhouette_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("mesh.silhouette.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"../../shaders/silhouette.wgsl"
))),
});
let silhouette_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("mesh.silhouette.layout"),
bind_group_layouts: &[Some(&frame_bgl)],
immediate_size: 0,
});
let make_silhouette_pipeline = |label: &'static str, fragment: &'static str| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&silhouette_layout),
vertex: wgpu::VertexState {
module: &silhouette_shader,
entry_point: Some("vs_main"),
buffers: &[
mesh_gpu::SilhouetteVertex::layout(),
mesh_gpu::SilhouetteInstance::layout(),
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::LineList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &silhouette_shader,
entry_point: Some(fragment),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
})
};
let silhouette_pipeline = make_silhouette_pipeline("mesh.silhouette.pipeline", "fs_main");
let silhouette_black_pipeline =
make_silhouette_pipeline("mesh.silhouette_black.pipeline", "fs_black");
// Depth-only variant — TriangleList, back-face culling stays on
// (we only want front-facing fragments to write depth so wires
// on the far side of the mesh stay hidden), `write_mask` zero
// so no fragment ever reaches the colour buffer.
let mesh_depth_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("mesh.depth.pipeline"),
layout: Some(&mesh_layout),
vertex: wgpu::VertexState {
module: &mesh_shader,
entry_point: Some("vs_main"),
buffers: &[
mesh_gpu::MeshVertex::layout(),
mesh_gpu::MeshInstanceGpu::layout(),
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None, // two-sided: ACIS import winding is unreliable
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState {
constant: 1,
slope_scale: 1.0,
clamp: 0.0,
},
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &mesh_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::empty(),
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
let mesh_plain_depth_pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("mesh.plain.depth.pipeline"),
layout: Some(&mesh_layout),
vertex: wgpu::VertexState {
module: &mesh_shader,
entry_point: Some("vs_main_plain"),
buffers: &[
mesh_gpu::MeshPlainVertex::layout(),
mesh_gpu::MeshInstanceGpu::layout(),
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState {
constant: 1,
slope_scale: 1.0,
clamp: 0.0,
},
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &mesh_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::empty(),
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
// ── Face3D pipeline ────────────────────────────────────────────────
let face3d_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("face3d.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(draw_order_shader!(
"face3d.wgsl"
))),
});
let block_face3d_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("block_face3d.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(draw_order_shader!(
"block_face3d.wgsl"
))),
});
let face3d_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("face3d.pipeline_layout"),
bind_group_layouts: &[&frame_bgl].map(Some),
immediate_size: 0,
});
let face3d_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("face3d.pipeline"),
layout: Some(&face3d_layout),
vertex: wgpu::VertexState {
module: &face3d_shader,
entry_point: Some("vs_main"),
buffers: &[face3d_gpu::Face3DVertex::layout()],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState {
constant: 1,
slope_scale: 1.0,
clamp: 0.0,
},
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &face3d_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
// Depth-only variant — write_mask zero, no blend. The face3d
// shader still runs but its colour output is discarded, so we
// get a pure depth prepass for HiddenLine.
let face3d_depth_pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("face3d.depth.pipeline"),
layout: Some(&face3d_layout),
vertex: wgpu::VertexState {
module: &face3d_shader,
entry_point: Some("vs_main"),
buffers: &[face3d_gpu::Face3DVertex::layout()],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState {
constant: 1,
slope_scale: 1.0,
clamp: 0.0,
},
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &face3d_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::empty(),
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
let make_block_face3d_pipeline = |label: &'static str, depth_only: bool| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&face3d_layout),
vertex: wgpu::VertexState {
module: &block_face3d_shader,
entry_point: Some("vs_main"),
buffers: &[
face3d_gpu::Face3DVertex::layout(),
face3d_gpu::Face3DInstance::layout(),
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState {
constant: 1,
slope_scale: 1.0,
clamp: 0.0,
},
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &block_face3d_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: (!depth_only).then_some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: if depth_only {
wgpu::ColorWrites::empty()
} else {
wgpu::ColorWrites::ALL
},
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
})
};
let block_face3d_pipeline = make_block_face3d_pipeline("block_face3d.pipeline", false);
let block_face3d_depth_pipeline =
make_block_face3d_pipeline("block_face3d.depth.pipeline", true);
// ── Image pipeline ─────────────────────────────────────────────────
let image_bgl1 = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("image.bgl1"),
entries: &[
// binding 0: texture
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,
},
// binding 1: sampler
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
// binding 2: ImageParams uniform (read in vertex for the
// draw-order z bias and in fragment for opacity).
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let image_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("image.pipeline_layout"),
bind_group_layouts: &[&frame_bgl, &image_bgl1].map(Some),
immediate_size: 0,
});
let image_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("image.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(draw_order_shader!(
"image.wgsl"
))),
});
let image_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("image.pipeline"),
layout: Some(&image_layout),
vertex: wgpu::VertexState {
module: &image_shader,
entry_point: Some("vs_main"),
buffers: &[
image_gpu::ImageVertex::layout(),
image_gpu::ImageInstance::layout(),
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &image_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
// ── Point cloud sprites ────────────────────────────────────────────
let point_cloud_bgl1 = point_cloud_gpu::params_layout(device);
let point_cloud_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("point_cloud.pipeline_layout"),
bind_group_layouts: &[&frame_bgl, &point_cloud_bgl1].map(Some),
immediate_size: 0,
});
let point_cloud_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("point_cloud.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/shaders/point_cloud.wgsl"
)))),
});
let point_cloud_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("point_cloud.pipeline"),
layout: Some(&point_cloud_layout),
vertex: wgpu::VertexState {
module: &point_cloud_shader,
entry_point: Some("vs_main"),
buffers: &[point_cloud_gpu::instance_layout()],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &point_cloud_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
let gpu_point_clouds = point_cloud_gpu::PointCloudGpu::new(device, &point_cloud_bgl1);
// ── Text (SDF glyph quads) ─────────────────────────────────────────
let text_atlas_bgl = text_gpu::TextAtlasGpu::bind_group_layout(device);
let (
text_pipeline,
text_highlight_pipeline,
block_text_pipeline,
block_text_highlight_pipeline,
) = text_gpu::create_pipelines(
device,
&frame_bgl,
&text_atlas_bgl,
format,
MSAA_SAMPLES,
&content_stencil,
);
let (circle_pipeline, circle_xray_pipeline) = circle_gpu::create_pipelines(
device,
&frame_bgl,
format,
MSAA_SAMPLES,
&content_stencil,
);
let (ellipse_pipeline, ellipse_xray_pipeline) = ellipse_gpu::create_pipelines(
device,
&frame_bgl,
format,
MSAA_SAMPLES,
&content_stencil,
);
let viewcube = ViewCubePipeline::new(device, queue, format);
let init_size = Size::new(1, 1);
let msaa_view = create_msaa_texture(device, init_size, format)
.create_view(&wgpu::TextureViewDescriptor::default());
let resolve_tex = create_resolve_texture(device, init_size, format);
let resolve_view = resolve_tex.create_view(&wgpu::TextureViewDescriptor::default());
// ── Blit pipeline (resolve texture → surface target) ──────────────
let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("blit.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"../../shaders/blit.wgsl"
))),
});
let blit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("blit.bgl"),
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,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
// UV crop uniform: [uv_offset_x, uv_offset_y, uv_scale_x, uv_scale_y]
// padded to 16 bytes (std140 vec2 alignment). Defaulted to the
// identity crop (offset 0, scale 1) for the common on-canvas case.
let blit_uniform_buffer = gpu_upload::upload_buffer(
device,
queue,
"blit.uniform_buffer",
&[0.0f32, 0.0, 1.0, 1.0],
wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
);
let blit_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("blit.pipeline_layout"),
bind_group_layouts: &[&blit_bgl].map(Some),
immediate_size: 0,
});
let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("blit.pipeline"),
layout: Some(&blit_layout),
vertex: wgpu::VertexState {
module: &blit_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &blit_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
// Premultiplied-alpha blend: the geometry passes already
// wrote into a transparent MSAA target with standard
// `SrcAlpha / 1-SrcAlpha` blending, so AA-edge fragments
// sit as `(rgb * a, a)` in the resolve texture. Treating
// that as straight alpha during the surface blit would
// multiply by alpha a second time and darken thin lines
// / curves. `PREMULTIPLIED_ALPHA_BLENDING` uses `One` as
// the source colour factor and leaves the dst weighted
// by `1-SrcAlpha`, which is the correct compositing
// operator for already-premultiplied content.
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
let blit_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("blit.sampler"),
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let blit_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("blit.bind_group"),
layout: &blit_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&resolve_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&blit_sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: blit_uniform_buffer.as_entire_binding(),
},
],
});
Self {
background_pipeline,
shadow_pipeline,
shadow_plain_pipeline,
wire_pipeline,
block_wire_pipeline,
clip_mask_pipeline,
wire_black_pipeline,
block_wire_black_pipeline,
wire_xray_pipeline,
circle_pipeline,
circle_xray_pipeline,
ellipse_pipeline,
ellipse_xray_pipeline,
block_wire_xray_pipeline,
wire_const_bgl,
block_wire_const_bgl,
block_wire_mode: wire_mode,
gpu_error_epoch: gpu_errors_seen(),
wipeout_pipeline,
hatch_gpu,
image_pipeline,
point_cloud_pipeline,
text_pipeline,
block_text_pipeline,
text_highlight_pipeline,
block_text_highlight_pipeline,
text_atlas_bgl,
text_atlas_gpu: None,
text_gpu: Vec::new(),
block_text_gpu: Vec::new(),
block_text_highlight_gpu: Vec::new(),
text_highlight_gpu: Vec::new(),
text_preview_gpu: Vec::new(),
silhouette_chunks: Vec::new(),
silhouette_source_key: (usize::MAX, usize::MAX, u64::MAX),
silhouette_source_groups: Vec::new(),
mesh_pipeline,
mesh_plain_pipeline,
mesh_transparent_pipeline,
mesh_plain_transparent_pipeline,
mesh_selected_pipeline,
mesh_plain_selected_pipeline,
mesh_hover_pipeline,
mesh_plain_hover_pipeline,
mesh_wireframe_pipeline,
mesh_edge_black_pipeline,
silhouette_pipeline,
silhouette_black_pipeline,
mesh_depth_pipeline,
mesh_plain_depth_pipeline,
mesh_material_bgl,
mesh_default_material_bind_group,
face3d_pipeline,
block_face3d_pipeline,
face3d_depth_pipeline,
block_face3d_depth_pipeline,
uniform_buffer,
frame_bgl,
uniform_bind_group,
shadow_uniform_bind_group,
background_texture,
environment_texture,
background_sampler,
background_source_id: 0,
environment_source_id: 0,
shadow_full: None,
_shadow_fallback_texture: shadow_fallback_texture,
shadow_fallback_view,
shadow_sampler,
shadow_enabled: false,
wipeout_bgl1,
image_bgl1,
depth_texture_size: Size::new(1, 1),
viewport: None,
// (0, 0) forces the first `ensure_depth_texture` to allocate at the
// real rounded size — the constructor textures above are placeholders.
alloc_size: Size::new(0, 0),
depth_view,
msaa_view,
resolve_view,
blit_pipeline,
blit_bind_group_layout: blit_bgl,
blit_sampler,
blit_bind_group,
blit_uniform_buffer,
surface_format: format,
gpu_wires: std::sync::Arc::new(vec![]),
gpu_circles: std::sync::Arc::new(vec![]),
gpu_ellipses: std::sync::Arc::new(vec![]),
gpu_block_wires: std::sync::Arc::new(vec![]),
wire_arena: None,
wire_arena_mesh: None,
wire_arena_fallback: std::sync::Arc::new(Vec::new()),
wire_arena_fallback_kind: None,
wire_arena_fallback_handles: rustc_hash::FxHashSet::default(),
wire_arena_id: u64::MAX,
partition_contributors: rustc_hash::FxHashSet::default(),
partition_depth_generation: u64::MAX,
wire_cull_key: (u64::MAX, u64::MAX, 0, 0),
hatch_lod_key: (usize::MAX, u64::MAX, 0, 0, false),
wipeout_lod_key: (usize::MAX, u64::MAX, 0, 0, false),
silhouette_key: (usize::MAX, u64::MAX, [u32::MAX; 3], false),
clip_boundary: None,
gpu_selected_wires: vec![],
gpu_selected_circles: vec![],
gpu_selected_ellipses: vec![],
gpu_selected_block_wires: vec![],
gpu_preview_wires: vec![],
gpu_preview_circles: vec![],
gpu_preview_ellipses: vec![],
gpu_wipeouts: vec![],
wipeout_skip_flags: vec![],
gpu_images: vec![],
gpu_point_clouds,
gpu_mesh_batch: vec![],
gpu_mesh_dynamic: vec![],
mesh_disabled_chunks: rustc_hash::FxHashSet::default(),
mesh_dynamic_handles: rustc_hash::FxHashSet::default(),
cached_mesh_content_id: u64::MAX,
mesh_ranges_by_handle: rustc_hash::FxHashMap::default(),
mesh_highlight_draws: vec![],
cached_highlight_key: (u64::MAX, u64::MAX),
gpu_face3d_fill: None,
gpu_face3d_edges: vec![],
viewcube,
cached_hatch_source: None,
cached_preview_hatch_source: None,
cached_wipeout_source: None,
cached_image_source: None,
cached_point_cloud_source: None,
cached_text_source: None,
cached_annotation_highlight_source: None,
cached_mesh_source: None,
cached_face3d_source: None,
cached_face3d_depth_source: None,
cached_fill_mode: false,
cached_epoch: (u64::MAX, u64::MAX, u64::MAX),
cached_wire_id: u64::MAX,
cached_selection: (u64::MAX, u64::MAX),
cached_face3d_key: (u64::MAX, false, false, u64::MAX),
cached_solid_visibility: (u64::MAX, [u32::MAX; 3], u64::MAX),
wire_handle_index: std::sync::Arc::new(rustc_hash::FxHashMap::default()),
render_sig: u64::MAX,
skip_geometry: false,
skip_hatch_frame: false,
skip_background: false,
slot_id: u64::MAX,
}
}
/// Upload block placements while reusing unchanged geometry.
pub fn upload_block_wires(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
wires: &[&WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
block_geometry: &mut wire_gpu::BlockGeometryCache,
) -> Vec<BlockWireGpu> {
BlockWireGpu::from_wires(
device,
queue,
wires,
depth_map,
None,
wire_gpu::BlockWireTarget {
const_bgl: &self.block_wire_const_bgl,
mode: self.block_wire_mode,
},
Some(block_geometry),
)
}
/// Upload GPU-instanced analytical circles from a wire slice.
pub fn upload_circles(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
wires: &[WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> Vec<CircleGpu> {
let mut instances: Vec<CircleInstance> = Vec::new();
for wire in wires {
if !wire.display_visible {
continue;
}
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
if let Some(insts) = circle_gpu::extract_circle_instances(wire, depth) {
instances.extend(insts);
}
}
self.upload_circles_from_instances(device, queue, &instances)
}
/// Upload GPU-instanced analytical circles from pre-extracted instances.
pub fn upload_circles_from_instances(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
instances: &[CircleInstance],
) -> Vec<CircleGpu> {
if instances.is_empty() {
vec![]
} else {
let mut sorted = instances.to_vec();
sorted.sort_by(|a, b| {
a.params[2]
.partial_cmp(&b.params[2])
.unwrap_or(std::cmp::Ordering::Equal)
});
vec![CircleGpu::from_instances(
device,
queue,
"viewer.circles",
&sorted,
)]
}
}
/// Upload GPU-instanced analytical ellipses from a wire slice.
pub fn upload_ellipses(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
wires: &[WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> Vec<EllipseGpu> {
let mut instances: Vec<EllipseInstance> = Vec::new();
for wire in wires {
if !wire.display_visible {
continue;
}
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
if let Some(insts) = ellipse_gpu::extract_ellipse_instances(wire, depth) {
instances.extend(insts);
}
}
self.upload_ellipses_from_instances(device, queue, &instances)
}
/// Upload GPU-instanced analytical ellipses from pre-extracted instances.
pub fn upload_ellipses_from_instances(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
instances: &[EllipseInstance],
) -> Vec<EllipseGpu> {
if instances.is_empty() {
vec![]
} else {
let mut sorted = instances.to_vec();
sorted.sort_by(|a, b| {
a.params[2]
.partial_cmp(&b.params[2])
.unwrap_or(std::cmp::Ordering::Equal)
});
vec![EllipseGpu::from_instances(
device,
queue,
"viewer.ellipses",
&sorted,
)]
}
}
/// Build resident batches and a handle index shared across viewport slots.
pub fn build_wire_buffers(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
wires: &[WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
block_geometry: &mut wire_gpu::BlockGeometryCache,
) -> (
std::sync::Arc<Vec<WireGpu>>,
std::sync::Arc<Vec<BlockWireGpu>>,
std::sync::Arc<rustc_hash::FxHashMap<u64, Vec<u32>>>,
std::sync::Arc<Vec<CircleGpu>>,
std::sync::Arc<Vec<EllipseGpu>>,
) {
// Batch the wire pass: instead of one GPU buffer + one draw call per
// WireModel (tens of thousands on a large drawing), merge maximal runs
// of *consecutive* wires that share scissor + mesh-edge state into one
// concatenated instance buffer each. The draw loop then issues one draw
// per run. Runs must be consecutive (not regrouped) so the original
// wire order — already sorted by draw order — is preserved; depth bias
// and alpha blending both depend on it. Scissor and mesh-edge stay
// grouping keys because the draw loop sets one scissor per batch and
let mut batches: Vec<WireGpu> = Vec::new();
let mut block_wires: Vec<&WireModel> = Vec::new();
let mut i = 0;
while i < wires.len() {
let kind = wire_arena::classify_wire(&wires[i]);
match kind {
wire_arena::WireKind::Invisible
| wire_arena::WireKind::Circle
| wire_arena::WireKind::Ellipse
| wire_arena::WireKind::Empty => {
i += 1;
continue;
}
wire_arena::WireKind::Block => {
block_wires.push(&wires[i]);
i += 1;
continue;
}
wire_arena::WireKind::Regular | wire_arena::WireKind::MeshEdge => {
let mesh_edge = kind == wire_arena::WireKind::MeshEdge;
let mut j = i + 1;
while j < wires.len() && wire_arena::classify_wire(&wires[j]) == kind {
j += 1;
}
let refs: Vec<&WireModel> = wires[i..j].iter().collect();
batches.extend(WireGpu::from_run_refs(
device,
queue,
&refs,
depth_map,
mesh_edge,
self.wire_const_bgl.as_ref(),
));
i = j;
}
}
}
let block_batches = BlockWireGpu::from_wires(
device,
queue,
&block_wires,
depth_map,
None,
wire_gpu::BlockWireTarget {
const_bgl: &self.block_wire_const_bgl,
mode: self.block_wire_mode,
},
Some(block_geometry),
);
// Parse handles in parallel, preserving wire order for hover slot lists.
let t_index = crate::perf::enabled().then(iced::time::Instant::now);
let parsed: Vec<(u64, u32)> = {
use crate::par::prelude::*;
wires
.par_iter()
.enumerate()
.filter_map(|(idx, w)| wire_gpu::fast_parse_u64(&w.name).map(|h| (h, idx as u32)))
.collect()
};
let mut index: rustc_hash::FxHashMap<u64, Vec<u32>> = rustc_hash::FxHashMap::default();
index.reserve(parsed.len());
for (handle, idx) in parsed {
index.entry(handle).or_default().push(idx);
}
if let Some(started) = t_index {
crate::perf_record!(
"[perf] wire-handle-index {:.1}ms wires={}",
started.elapsed().as_secs_f64() * 1000.0,
wires.len(),
);
}
let circle_batches = self.upload_circles(device, queue, wires, depth_map);
let ellipse_batches = self.upload_ellipses(device, queue, wires, depth_map);
(
std::sync::Arc::new(batches),
std::sync::Arc::new(block_batches),
std::sync::Arc::new(index),
std::sync::Arc::new(circle_batches),
std::sync::Arc::new(ellipse_batches),
)
}
/// Build the selection xray overlay: full-brightness copies of the wires
/// whose entity handle is in `highlight`, drawn on top so the selection is
/// always visible. Selection is no longer baked into the wire tessellation,
/// so this is driven by the live highlight set and rebuilt only when the
/// selection (or the underlying wire content) changes — picking an entity
/// refreshes just this overlay instead of re-tessellating the model. The
/// xray pass applies neither scissor nor mesh-edge skip, so everything
/// merges into one order-preserving run.
/// Split highlighted wires into analytic instances and the wires that need
/// real geometry, in one extraction pass per wire.
#[allow(clippy::type_complexity)]
fn classify_highlight_wires<'a>(
wires: &[&'a WireModel],
color: Option<[f32; 4]>,
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> (
Vec<CircleInstance>,
Vec<EllipseInstance>,
Vec<&'a WireModel>,
Vec<&'a WireModel>,
) {
use crate::par::prelude::*;
// Pure per-wire work, so it fans out; `collect` on an indexed
// parallel iterator keeps the order, and the order is what decides
// the instance layout.
let per: Vec<(
Option<Vec<CircleInstance>>,
Option<Vec<EllipseInstance>>,
bool,
)> = wires
.par_iter()
.map(|&wire| {
if wire.render_instance.is_some() {
return (None, None, true);
}
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
let mut circles = circle_gpu::extract_circle_instances(wire, depth);
let mut ellipses = if circles.is_none() {
ellipse_gpu::extract_ellipse_instances(wire, depth)
} else {
None
};
if let Some(color) = color {
if let Some(instances) = circles.as_mut() {
for instance in instances {
instance.color = color;
}
}
if let Some(instances) = ellipses.as_mut() {
for instance in instances {
instance.color = color;
}
}
}
(circles, ellipses, false)
})
.collect();
let mut circles: Vec<CircleInstance> = Vec::new();
let mut ellipses: Vec<EllipseInstance> = Vec::new();
let mut regular: Vec<&'a WireModel> = Vec::new();
let mut blocks: Vec<&'a WireModel> = Vec::new();
for (&wire, (wire_circles, wire_ellipses, is_block)) in wires.iter().zip(per) {
if is_block {
blocks.push(wire);
continue;
}
let shaped = wire_circles.is_some() || wire_ellipses.is_some();
if let Some(instances) = wire_circles {
circles.extend(instances);
}
if let Some(instances) = wire_ellipses {
ellipses.extend(instances);
}
if !shaped {
regular.push(wire);
}
}
(circles, ellipses, regular, blocks)
}
pub fn upload_selected_wires(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
wires: &[WireModel],
selected: &rustc_hash::FxHashSet<codec::Handle>,
hovered: &rustc_hash::FxHashSet<codec::Handle>,
annotation_context_wires: &[WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
selected_tint: Option<[f32; 4]>,
) {
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
if selected.is_empty() && hovered.is_empty() && annotation_context_wires.is_empty() {
self.gpu_selected_wires = vec![];
self.gpu_selected_block_wires = vec![];
self.gpu_selected_circles = vec![];
self.gpu_selected_ellipses = vec![];
return;
}
// Gather borrowed highlighted wires via the prebuilt index —
// O(highlighted), no per-wire string parse or deep geometry clone.
let mut selected_wires: Vec<&WireModel> = Vec::new();
let mut hover_wires: Vec<&WireModel> = Vec::new();
for h in selected {
if let Some(idxs) = self.wire_handle_index.get(&h.value()) {
debug_assert!(
idxs.windows(2).all(|pair| pair[0] <= pair[1]),
"wire_handle_index slots must stay ascending",
);
for &i in idxs {
if let Some(w) = wires.get(i as usize) {
// An unseen pick hull (a point cloud's extents box) shows while
// highlighted, as the reference does.
if !w.display_visible && (w.pick_tris.is_empty() || w.snap_only) {
continue;
}
selected_wires.push(w);
}
}
}
}
for h in hovered.iter().filter(|handle| !selected.contains(handle)) {
if let Some(idxs) = self.wire_handle_index.get(&h.value()) {
for &i in idxs {
if let Some(w) = wires.get(i as usize) {
// An unseen pick hull (a point cloud's extents box) shows while
// highlighted, as the reference does.
if !w.display_visible && (w.pick_tris.is_empty() || w.snap_only) {
continue;
}
hover_wires.push(w);
}
}
}
}
for wire in annotation_context_wires {
if !wire.display_visible {
continue;
}
if wire.selected {
selected_wires.push(wire);
} else {
hover_wires.push(wire);
}
}
let t_gather = perf_started.map(|t| t.elapsed().as_secs_f64() * 1000.0);
let (mut selected_circles, mut selected_ellipses, selected_regular, selected_blocks) =
Self::classify_highlight_wires(
&selected_wires,
Some(selected_tint.unwrap_or(WireModel::SELECTED)),
depth_map,
);
let (hover_circles, hover_ellipses, hover_regular, hover_blocks) =
Self::classify_highlight_wires(&hover_wires, Some(WireModel::HOVER), depth_map);
selected_circles.extend(hover_circles);
selected_ellipses.extend(hover_ellipses);
let t_shapes = perf_started.map(|t| t.elapsed().as_secs_f64() * 1000.0);
self.gpu_selected_circles = if selected_circles.is_empty() {
vec![]
} else {
vec![CircleGpu::from_instances(
device,
queue,
"viewer.selected_circles",
&selected_circles,
)]
};
self.gpu_selected_ellipses = if selected_ellipses.is_empty() {
vec![]
} else {
vec![EllipseGpu::from_instances(
device,
queue,
"viewer.selected_ellipses",
&selected_ellipses,
)]
};
let t_split = perf_started.map(|t| t.elapsed().as_secs_f64() * 1000.0);
let mut gpu = if let Some(tint) = selected_tint {
WireGpu::from_highlight_refs(
device,
queue,
&selected_regular,
tint,
depth_map,
self.wire_const_bgl.as_ref(),
)
} else {
Vec::new()
};
gpu.extend(WireGpu::from_highlight_refs(
device,
queue,
&hover_regular,
WireModel::HOVER,
depth_map,
self.wire_const_bgl.as_ref(),
));
let t_regular = perf_started.map(|t| t.elapsed().as_secs_f64() * 1000.0);
self.gpu_selected_wires = gpu;
let mut block_gpu = if let Some(tint) = selected_tint {
BlockWireGpu::from_wires(
device,
queue,
&selected_blocks,
depth_map,
Some(tint),
wire_gpu::BlockWireTarget {
const_bgl: &self.block_wire_const_bgl,
mode: self.block_wire_mode,
},
None,
)
} else {
Vec::new()
};
block_gpu.extend(BlockWireGpu::from_wires(
device,
queue,
&hover_blocks,
depth_map,
Some(WireModel::HOVER),
wire_gpu::BlockWireTarget {
const_bgl: &self.block_wire_const_bgl,
mode: self.block_wire_mode,
},
None,
));
self.gpu_selected_block_wires = block_gpu;
if let Some(started) = perf_started {
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
if elapsed_ms >= 1.0 {
let gather = t_gather.unwrap_or(0.0);
let classify = t_shapes.unwrap_or(0.0);
let analytic = t_split.unwrap_or(0.0);
let regular = t_regular.unwrap_or(0.0);
crate::perf_record!(
"[perf] wire-highlight-detail gather={gather:.1} classify={:.1} \
analytic={:.1} regular={:.1} blocks={:.1}",
classify - gather,
analytic - classify,
regular - analytic,
elapsed_ms - regular,
);
crate::perf_record!(
"[perf] wire-highlight {:>7.1}ms handles={} wires={}",
elapsed_ms,
selected.len()
+ hovered
.iter()
.filter(|handle| !selected.contains(handle))
.count(),
selected_wires.len() + hover_wires.len(),
);
}
}
}
/// Build the text-highlight overlay: the glyph quads of just the selected /
/// hovered text, recoloured (selection blue, hover orange) and drawn over
/// the base text pass. Uses the same handle→wire index as the selected-wire
/// overlay, so it is O(highlighted). Empty when nothing is highlighted.
pub fn upload_text_highlight(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
wires: &[WireModel],
selected: &rustc_hash::FxHashSet<codec::Handle>,
hovered: &rustc_hash::FxHashSet<codec::Handle>,
annotation_context_wires: &[WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
selected_tint: Option<[f32; 4]>,
) {
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
if selected.is_empty() && hovered.is_empty() && annotation_context_wires.is_empty() {
self.text_highlight_gpu.clear();
self.block_text_highlight_gpu.clear();
return;
}
let mut out: Vec<text_gpu::TextVertex> = Vec::new();
let mut selected_blocks: Vec<&WireModel> = Vec::new();
let mut hover_blocks: Vec<&WireModel> = Vec::new();
fn push<'a>(
index: &rustc_hash::FxHashMap<u64, Vec<u32>>,
handle_val: u64,
tint: [f32; 4],
wires: &'a [WireModel],
out: &mut Vec<text_gpu::TextVertex>,
blocks: &mut Vec<&'a WireModel>,
) {
if let Some(idxs) = index.get(&handle_val) {
for &i in idxs {
if let Some(w) = wires.get(i as usize) {
if w.render_instance.is_some() {
blocks.push(w);
continue;
}
for v in &w.text_verts {
out.push(text_gpu::TextVertex {
color: [tint[0], tint[1], tint[2], v.color[3]],
..*v
});
}
}
}
}
}
if let Some(tint) = selected_tint {
for h in selected {
push(
&self.wire_handle_index,
h.value(),
tint,
wires,
&mut out,
&mut selected_blocks,
);
}
}
for h in hovered.iter().filter(|handle| !selected.contains(handle)) {
push(
&self.wire_handle_index,
h.value(),
WireModel::HOVER,
wires,
&mut out,
&mut hover_blocks,
);
}
for wire in annotation_context_wires {
let tint = if wire.selected {
selected_tint.unwrap_or(WireModel::SELECTED)
} else {
WireModel::HOVER
};
if !wire.selected || selected_tint.is_some() {
for vertex in &wire.text_verts {
out.push(text_gpu::TextVertex {
color: [tint[0], tint[1], tint[2], vertex.color[3]],
..*vertex
});
}
}
}
self.text_highlight_gpu = text_gpu::upload_vertices(device, queue, &out);
let mut block_gpu = if let Some(tint) = selected_tint {
text_gpu::upload_block_vertex_refs(
device,
queue,
&selected_blocks,
depth_map,
Some(tint),
)
} else {
Vec::new()
};
block_gpu.extend(text_gpu::upload_block_vertex_refs(
device,
queue,
&hover_blocks,
depth_map,
Some(WireModel::HOVER),
));
self.block_text_highlight_gpu = block_gpu;
if let Some(started) = perf_started {
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
if elapsed_ms >= 1.0 {
crate::perf_record!(
"[perf] text-highlight {:>7.1}ms vertices={}",
elapsed_ms,
out.len(),
);
}
}
}
/// Upload the live overlay (command preview / interim / grip-drag) wires.
/// Small and refreshed each frame they're present; kept separate from the
/// base wire buffer so a drag never re-uploads the resident base set.
pub fn upload_preview_wires(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
wires: &[WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) {
if wires.is_empty() {
self.gpu_preview_wires.clear();
self.gpu_preview_circles.clear();
self.gpu_preview_ellipses.clear();
} else {
let partitioned = wire_arena::partition_wires(wires, depth_map);
let mut non_analytical = partitioned.regular;
non_analytical.extend(partitioned.mesh);
self.gpu_preview_wires = if non_analytical.is_empty() {
vec![]
} else {
WireGpu::from_run_refs(
device,
queue,
&non_analytical,
depth_map,
false,
self.wire_const_bgl.as_ref(),
)
};
self.gpu_preview_circles = if partitioned.circle_instances.is_empty() {
vec![]
} else {
vec![CircleGpu::from_instances(
device,
queue,
"preview.circles",
&partitioned.circle_instances,
)]
};
self.gpu_preview_ellipses = if partitioned.ellipse_instances.is_empty() {
vec![]
} else {
vec![EllipseGpu::from_instances(
device,
queue,
"preview.ellipses",
&partitioned.ellipse_instances,
)]
};
}
}
/// Upload the live grip-drag / command-preview SDF glyph quads. Re-uploaded
/// every frame they're present, kept separate from the epoch-cached base
/// text buffer so a drag never re-uploads the (potentially huge) resident
/// text set — and so the dragged entity's glyphs (hidden from the base set
/// while the drag is live) still reach the GPU each frame (issue #316).
pub fn upload_preview_text(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
verts: &[text_gpu::TextVertex],
) {
if verts.is_empty() {
self.text_preview_gpu.clear();
return;
}
// Ensure the glyph atlas exists / is current even when the base text
// pass didn't run this frame (e.g. the dragged text is the only text).
if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() {
if self.text_atlas_gpu.is_none() || atlas.is_dirty() {
self.text_atlas_gpu = Some(text_gpu::TextAtlasGpu::upload(
device,
queue,
&atlas,
&self.text_atlas_bgl,
));
atlas.clear_dirty();
}
}
self.text_preview_gpu = text_gpu::upload_vertices(device, queue, verts);
}
// The math lives outside the GPU method so it can be unit-tested; see the
// module test below.
/// Rebuild view-dependent silhouette lines through the kernel.
pub fn upload_silhouettes(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
sets: &[crate::scene::model::mesh_model::MeshLodSet],
content_id: u64,
view_dir: glam::Vec3,
) {
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
let view = glam::DVec3::new(view_dir.x as f64, view_dir.y as f64, view_dir.z as f64)
.normalize_or(glam::DVec3::NEG_Z);
use crate::scene::pipeline::mesh_gpu::{SilhouetteInstance, SilhouetteVertex};
let chunk_bytes = gpu_budget::buffer_budget(device);
let max_vertices = chunk_bytes / std::mem::size_of::<SilhouetteVertex>() / 2 * 2;
if max_vertices < 2 {
self.silhouette_chunks.clear();
return;
}
let source_key = (sets.as_ptr() as usize, sets.len(), content_id);
if self.silhouette_source_key != source_key {
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&crate::scene::model::mesh_model::MeshLodSet>> = Vec::new();
for (index, set) in sets.iter().enumerate() {
let color = set.display_color().unwrap_or([0.0, 0.0, 0.0, 1.0]);
let key = match (&set.instance_source, set.instance_transform) {
(Some(source), Some(transform)) => {
let matrix = &transform.matrix.m;
(
true,
source.handle.value(),
[
matrix[0][0].to_bits(),
matrix[0][1].to_bits(),
matrix[0][2].to_bits(),
matrix[1][0].to_bits(),
matrix[1][1].to_bits(),
matrix[1][2].to_bits(),
matrix[2][0].to_bits(),
matrix[2][1].to_bits(),
matrix[2][2].to_bits(),
],
color.map(f32::to_bits),
)
}
_ => (false, index as u64, [0; 9], color.map(f32::to_bits)),
};
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push(Vec::new());
slot
});
groups[slot].push(set);
}
let prepare_group = |group: Vec<&crate::scene::model::mesh_model::MeshLodSet>| {
let source = *group.first()?;
let translation = |set: &crate::scene::model::mesh_model::MeshLodSet| {
set.instance_transform.map_or([0.0; 3], |transform| {
let matrix = &transform.matrix.m;
[matrix[0][3], matrix[1][3], matrix[2][3]]
})
};
let base = translation(source);
let instances: Vec<SilhouetteInstance> = group
.iter()
.map(|set| {
let placement = translation(set);
let delta = [
placement[0] - base[0],
placement[1] - base[1],
placement[2] - base[2],
];
let high = delta.map(|value| value as f32);
SilhouetteInstance {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
(delta[2] - high[2] as f64) as f32,
],
}
})
.collect();
let max_instances = chunk_bytes / std::mem::size_of::<SilhouetteInstance>();
let instance_buffers = instances
.chunks(max_instances.max(1))
.map(|instances| {
(
gpu_upload::upload_buffer(
device,
queue,
"mesh.silhouette.instances",
instances,
wgpu::BufferUsages::VERTEX,
),
instances.len() as u32,
)
})
.collect();
let generators = source
.instance_source
.as_ref()
.map_or(source.curved_gens.as_slice(), |instance| {
instance.curved_gens.as_slice()
});
let mut sources = Vec::with_capacity(generators.len());
for generator in generators {
if let Some(transform) = source.instance_transform {
let origin = transform.apply(codec::types::Vector3::ZERO);
let vectors = [
transform.apply_rotation(codec::types::Vector3::UNIT_X),
transform.apply_rotation(codec::types::Vector3::UNIT_Y),
transform.apply_rotation(codec::types::Vector3::UNIT_Z),
];
if let Some(transformed) = kernel::brep::mesh::transform_silhouette_affine(
&generator.source,
vectors.map(|vector| [vector.x, vector.y, vector.z]),
[origin.x, origin.y, origin.z],
) {
sources.push(transformed);
}
} else {
sources.push(generator.source.clone());
}
}
Some(SilhouetteSourceGroup {
color: source.display_color().unwrap_or([0.0, 0.0, 0.0, 1.0]),
sources,
instance_buffers,
})
};
self.silhouette_source_groups = groups.into_iter().filter_map(prepare_group).collect();
self.silhouette_source_key = source_key;
}
let source_count = self.silhouette_source_groups.len();
let compute_group = |group: &SilhouetteSourceGroup| {
let mut points = Vec::new();
for source in &group.sources {
points.extend(kernel::brep::mesh::silhouette(
source,
[view.x, view.y, view.z],
));
}
points
};
#[cfg(not(target_arch = "wasm32"))]
let points: Vec<_> = {
use rayon::prelude::*;
self.silhouette_source_groups
.par_iter()
.map(compute_group)
.collect()
};
#[cfg(target_arch = "wasm32")]
let points: Vec<_> = self
.silhouette_source_groups
.iter()
.map(compute_group)
.collect();
let mut source_vertex_count = 0usize;
let mut chunks = Vec::new();
for (group, points) in self.silhouette_source_groups.iter().zip(points) {
let color = group.color;
let mk = |w: glam::DVec3| -> SilhouetteVertex {
let (hx, hy, hz) = (w.x as f32, w.y as f32, w.z as f32);
SilhouetteVertex {
position: [hx, hy, hz],
color,
position_low: [
(w.x - hx as f64) as f32,
(w.y - hy as f64) as f32,
(w.z - hz as f64) as f32,
],
}
};
let mut verts: Vec<SilhouetteVertex> = Vec::with_capacity(max_vertices);
let push_chunk = |verts: &[SilhouetteVertex], chunks: &mut Vec<SilhouetteChunk>| {
let vertex_count = verts.len() / 2 * 2;
if vertex_count == 0 {
return;
}
let vertex_buffer = gpu_upload::upload_buffer(
device,
queue,
"mesh.silhouette.vbuf",
&verts[..vertex_count],
wgpu::BufferUsages::VERTEX,
);
for (instance_buffer, instance_count) in &group.instance_buffers {
chunks.push(SilhouetteChunk {
vertex_buffer: vertex_buffer.clone(),
instance_buffer: instance_buffer.clone(),
vertex_count: vertex_count as u32,
instance_count: *instance_count,
});
}
};
source_vertex_count += points.len();
for point in points {
verts.push(mk(glam::DVec3::from_array(point)));
if verts.len() == max_vertices {
push_chunk(&verts, &mut chunks);
verts.clear();
}
}
push_chunk(&verts, &mut chunks);
}
self.silhouette_chunks = chunks;
if let Some(started) = perf_started {
crate::perf_record!(
"[perf] silhouettes {:>7.1}ms sets={} sources={} source_vertices={} chunks={}",
started.elapsed().as_secs_f64() * 1000.0,
sets.len(),
source_count,
source_vertex_count,
self.silhouette_chunks.len(),
);
}
}
/// Upload this content viewport's non-rectangular clip boundary as a
/// triangle-fan vertex buffer in render-target NDC. The boundary is already
/// projected (paper shape → the same visible-sub-rect crop as the content),
/// so the mask pipeline stamps it straight into the stencil with `Invert`
/// (even-odd fill → interior marked, any convexity). Empty input clears the
/// boundary so the viewport renders unclipped (its render rectangle clips).
pub fn upload_clip_boundary(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
boundary_ndc: &[[f32; 2]],
) {
if boundary_ndc.len() < 3 {
self.clip_boundary = None;
return;
}
// Triangle fan (p0, pi, pi+1).
let mut verts: Vec<f32> = Vec::with_capacity((boundary_ndc.len() - 2) * 6);
for i in 1..boundary_ndc.len() - 1 {
for &p in &[boundary_ndc[0], boundary_ndc[i], boundary_ndc[i + 1]] {
verts.extend_from_slice(&[p[0], p[1]]);
}
}
if verts.is_empty() {
self.clip_boundary = None;
return;
}
let vbuf = gpu_upload::upload_buffer(
device,
queue,
"clip_boundary.vbuf",
&verts,
wgpu::BufferUsages::VERTEX,
);
self.clip_boundary = Some((vbuf, (verts.len() / 2) as u32));
}
/// Per-frame visibility refresh for the batched hatch path.
/// Combines Phase 3.3 sub-pixel LOD skip with Phase 2.3 frustum
/// cull and pushes the resulting 0/1 mask through to the GPU
/// `visibility_buffer`. Vertex shader maps 0 → out-of-NDC clip,
/// so the rasterizer culls the primitive before any fragment
/// runs.
pub fn compute_hatch_lod(
&mut self,
queue: &wgpu::Queue,
view_rot: glam::Mat4,
eye: glam::DVec3,
clip_w: u32,
clip_h: u32,
) {
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
let instance_count = self.hatch_gpu.update_visibility(queue, |aabb| {
!aabb_below_pixel(aabb, view_rot, eye, clip_w, clip_h, 2.0)
&& !aabb_offscreen(aabb, view_rot, eye, clip_w, clip_h)
});
if instance_count == 0 {
return;
}
if let Some(started) = perf_started {
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
if elapsed_ms >= 1.0 {
crate::perf_record!(
"[perf] hatch-lod {:>7.1}ms instances={}",
elapsed_ms,
instance_count,
);
}
}
}
/// Per-frame wipeout frustum-skip flag (Phase 2.3). Mirrors
/// `compute_hatch_lod`'s frustum branch. No sub-pixel skip:
/// wipeouts mask, so dropping a sub-pixel one wouldn't be wrong
/// but also wouldn't pay off — they're usually few.
pub fn compute_wipeout_lod(
&mut self,
view_rot: glam::Mat4,
eye: glam::DVec3,
clip_w: u32,
clip_h: u32,
) {
self.wipeout_skip_flags = self
.gpu_wipeouts
.iter()
.map(|h| aabb_offscreen(h.world_aabb, view_rot, eye, clip_w, clip_h))
.collect();
}
/// Upload all 3DFACE entities as two batched GPU objects:
/// - `gpu_face3d_fill`: filled triangles (1 buffer, 1 draw call)
/// - `gpu_face3d_edges`: merged edge wires (1 buffer, 1 draw call)
pub fn upload_face3d(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
face3d_wires: &[WireModel],
all_wires: &[WireModel],
wireframe_only: bool,
show_2d_solid_fills: bool,
view_dir: glam::Vec3,
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) {
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
// Edge buffer is always built from `face3d_wires`, so 3DFACE
// outlines stay on the screen regardless of mode.
self.gpu_face3d_edges = WireGpu::from_batch(
device,
queue,
face3d_wires,
depth_map,
self.wire_const_bgl.as_ref(),
);
// Fill buffer split: 3D quads + PolyfaceMesh / PolygonMesh face
// tris go to `chunks_3d` (gated by `keep_3d_mesh_fills`);
// 2D fills (text-LOD greek, MultiLeader background, dimension arrows) go to
// `chunks_2d`. The 3-D wireframe additionally removes only legacy
// planar SOLID interiors; HATCH is handled by a separate pass.
let keep_3d_mesh_fills = !wireframe_only;
let solid_fill_hidden = |wire: &WireModel| !show_2d_solid_fills && wire.fill_is_2d_solid;
let has_any_2d_fill = all_wires
.iter()
.any(|w| !w.fill_tris.is_empty() && !w.fill_is_3d && !solid_fill_hidden(w));
let has_any_3d_fill = !face3d_wires.is_empty()
|| all_wires
.iter()
.any(|w| !w.fill_tris.is_empty() && w.fill_is_3d);
let has_fills = has_any_2d_fill || (keep_3d_mesh_fills && has_any_3d_fill);
if !has_fills {
self.gpu_face3d_fill = None;
} else {
self.gpu_face3d_fill = Some(Face3DGpu::from_wires(
device,
queue,
face3d_wires,
all_wires,
keep_3d_mesh_fills,
show_2d_solid_fills,
view_dir,
depth_map,
));
}
if let Some(started) = perf_started {
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
if elapsed_ms >= 1.0 {
crate::perf_record!(
"[perf] face3d-upload {:>7.1}ms face-wires={} all-wires={}",
elapsed_ms,
face3d_wires.len(),
all_wires.len(),
);
}
}
}
/// Build the batched mesh buffers (a few large buffers for the whole solid
/// set, drawn in a handful of calls). Selection/hover tint is intentionally
/// not applied here — the batch is geometry-only and stays resident across
/// camera moves and pick changes.
pub fn upload_mesh_batch(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
meshes: &[MeshLodSet],
) {
let started = iced::time::Instant::now();
let (mut chunks, triangles) = mesh_gpu::build_mesh_batch(device, queue, meshes);
let built_at = iced::time::Instant::now();
mesh_gpu::upload_chunk_material_bind_groups(
device,
queue,
&self.mesh_material_bgl,
&mut chunks,
);
let materials_at = iced::time::Instant::now();
self.mesh_ranges_by_handle.clear();
for (chunk_index, chunk) in chunks.iter().enumerate() {
for range in &chunk.highlight_ranges {
self.mesh_ranges_by_handle
.entry(range.handle)
.or_default()
.push(MeshResidentRange {
chunk: chunk_index,
dynamic: false,
index_start: range.index_start,
index_count: range.index_count,
transparent: range.transparent,
instance_start: range.instance_start,
instance_count: range.instance_count,
});
}
}
self.mesh_highlight_draws.clear();
self.cached_highlight_key = (u64::MAX, u64::MAX);
self.gpu_mesh_dynamic.clear();
self.mesh_disabled_chunks.clear();
self.mesh_dynamic_handles.clear();
self.gpu_mesh_batch = chunks;
let indexed_at = iced::time::Instant::now();
if crate::perf::enabled() {
let instances: u64 = self
.gpu_mesh_batch
.iter()
.map(|chunk| chunk.instance_count as u64)
.sum();
let compact_chunks = self
.gpu_mesh_batch
.iter()
.filter(|chunk| chunk.compact_vertices)
.count();
crate::perf_record!(
"[perf] mesh-batch {:>7.1}ms build={:.1} material={:.1} index={:.1} sets={} chunks={} compact={} instances={} triangles={}",
started.elapsed().as_secs_f64() * 1000.0,
built_at.duration_since(started).as_secs_f64() * 1000.0,
materials_at.duration_since(built_at).as_secs_f64() * 1000.0,
indexed_at.duration_since(materials_at).as_secs_f64() * 1000.0,
meshes.len(),
self.gpu_mesh_batch.len(),
compact_chunks,
instances,
triangles,
);
}
}
fn rebuild_mesh_range_map(&mut self) {
self.mesh_ranges_by_handle.clear();
for (chunk_index, chunk) in self.gpu_mesh_batch.iter().enumerate() {
if self.mesh_disabled_chunks.contains(&chunk_index) {
continue;
}
for range in &chunk.highlight_ranges {
self.mesh_ranges_by_handle
.entry(range.handle)
.or_default()
.push(MeshResidentRange {
chunk: chunk_index,
dynamic: false,
index_start: range.index_start,
index_count: range.index_count,
transparent: range.transparent,
instance_start: range.instance_start,
instance_count: range.instance_count,
});
}
}
for (chunk_index, chunk) in self.gpu_mesh_dynamic.iter().enumerate() {
for range in &chunk.highlight_ranges {
self.mesh_ranges_by_handle
.entry(range.handle)
.or_default()
.push(MeshResidentRange {
chunk: chunk_index,
dynamic: true,
index_start: range.index_start,
index_count: range.index_count,
transparent: range.transparent,
instance_start: range.instance_start,
instance_count: range.instance_count,
});
}
}
self.mesh_highlight_draws.clear();
self.cached_highlight_key = (u64::MAX, u64::MAX);
}
/// Patch an entity-only geometry change without rebuilding the resident
/// mesh set. Any 32 MiB static chunk containing a changed handle is retired;
/// the current versions of all its handles are rebuilt into a small dynamic
/// working set. A bounded threshold falls back to a full rebuild before the
/// working set can grow into a second copy of the drawing.
pub fn patch_mesh_batch(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
meshes: &[MeshLodSet],
changes: &[(codec::Handle, crate::scene::ChangeKind)],
) -> bool {
let started = iced::time::Instant::now();
if self.gpu_mesh_batch.is_empty() || changes.is_empty() {
return false;
}
let current_handles: rustc_hash::FxHashSet<_> = meshes
.iter()
.filter_map(MeshLodSet::entity_handle)
.collect();
let changed: rustc_hash::FxHashSet<_> = changes
.iter()
.map(|(handle, _)| *handle)
.filter(|handle| {
current_handles.contains(handle)
|| self.mesh_dynamic_handles.contains(handle)
|| self
.gpu_mesh_batch
.iter()
.any(|chunk| chunk.handles.contains(handle))
})
.collect();
if changed.is_empty() {
return true;
}
for (chunk_index, chunk) in self.gpu_mesh_batch.iter().enumerate() {
if chunk.handles.iter().any(|handle| changed.contains(handle)) {
self.mesh_disabled_chunks.insert(chunk_index);
self.mesh_dynamic_handles
.extend(chunk.handles.iter().copied());
}
}
self.mesh_dynamic_handles.extend(changed.iter().copied());
let disabled_limit = self.gpu_mesh_batch.len().div_ceil(2).max(8);
if self.mesh_disabled_chunks.len() > disabled_limit
|| self.mesh_dynamic_handles.len() > 4096
{
return false;
}
let (mut chunks, _) = mesh_gpu::build_mesh_batch_filtered(
device,
queue,
meshes,
Some(&self.mesh_dynamic_handles),
);
mesh_gpu::upload_chunk_material_bind_groups(
device,
queue,
&self.mesh_material_bgl,
&mut chunks,
);
self.gpu_mesh_dynamic = chunks;
self.rebuild_mesh_range_map();
if crate::perf::enabled() {
crate::perf_record!(
"[perf] mesh-patch {:>7.1}ms changed={} retired={} dynamic_handles={} dynamic_chunks={}",
started.elapsed().as_secs_f64() * 1000.0,
changed.len(),
self.mesh_disabled_chunks.len(),
self.mesh_dynamic_handles.len(),
self.gpu_mesh_dynamic.len(),
);
}
true
}
/// Refresh selection/hover draw ranges. The overlay references resident
/// chunk buffers; changing hover never allocates or uploads mesh geometry.
pub fn update_mesh_highlight(
&mut self,
selected: &rustc_hash::FxHashSet<codec::Handle>,
hovered: &rustc_hash::FxHashSet<codec::Handle>,
edge_wires: &[WireModel],
) {
let edge_handles: rustc_hash::FxHashSet<codec::Handle> = edge_wires
.iter()
.filter_map(|wire| wire.name.strip_prefix("mesh-edge:"))
.filter_map(|value| value.parse::<u64>().ok())
.map(codec::Handle::new)
.collect();
let mut out = Vec::new();
for handle in selected
.iter()
.filter(|handle| !edge_handles.contains(handle))
{
if let Some(ranges) = self.mesh_ranges_by_handle.get(handle) {
out.extend(ranges.iter().copied().map(|range| MeshHighlightDraw {
range,
kind: MeshHighlightKind::Selected,
}));
}
}
for handle in hovered
.iter()
.filter(|handle| !selected.contains(handle) && !edge_handles.contains(handle))
{
if let Some(ranges) = self.mesh_ranges_by_handle.get(&handle) {
out.extend(ranges.iter().copied().map(|range| MeshHighlightDraw {
range,
kind: MeshHighlightKind::Hover,
}));
}
}
self.mesh_highlight_draws = out;
}
fn active_mesh_chunks_indexed(
&self,
) -> impl Iterator<Item = (usize, &mesh_gpu::MeshBatchChunk)> {
let dynamic_offset = self.gpu_mesh_batch.len();
self.gpu_mesh_batch
.iter()
.enumerate()
.filter(|(index, _)| !self.mesh_disabled_chunks.contains(index))
.chain(
self.gpu_mesh_dynamic
.iter()
.enumerate()
.map(move |(index, chunk)| (dynamic_offset + index, chunk)),
)
}
pub fn upload_hatches(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
hatches: &[HatchModel],
) {
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
let renderable_count = hatches
.iter()
.filter(|hatch| hatch.boundary.len() >= 3)
.count();
self.hatch_gpu.upload(device, queue, hatches);
if let Some(started) = perf_started {
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
if elapsed_ms >= 1.0 {
crate::perf_record!(
"[perf] hatch-upload {:>7.1}ms models={}",
elapsed_ms,
renderable_count,
);
}
}
}
pub fn upload_preview_hatches(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
hatches: &[HatchModel],
) {
self.hatch_gpu.upload_preview(device, queue, hatches);
}
pub fn upload_wipeouts(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
wipeouts: &[HatchModel],
) {
let renderable: Vec<HatchModel> = wipeouts
.iter()
.filter(|h| h.boundary.len() >= 3)
.cloned()
.collect();
self.gpu_wipeouts = WipeoutGpu::from_models(device, queue, &renderable, &self.wipeout_bgl1);
}
pub fn upload_images(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
images: &[ImageModel],
) {
self.gpu_images = ImageGpu::from_models(device, queue, images, &self.image_bgl1);
}
pub fn upload_point_clouds(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
set: &crate::scene::model::point_cloud::PointCloudSet,
) {
self.gpu_point_clouds.upload(device, queue, set);
}
pub fn set_point_cloud_view(&mut self, queue: &wgpu::Queue, light: [f32; 12]) {
self.gpu_point_clouds.set_view(queue, light);
}
/// Upload the frame's SDF text-quad vertices, and (re)build the GPU glyph
/// atlas from the shared CPU atlas when it grew (new glyphs baked by the
/// text collector). `verts` empty (flag off) leaves nothing to draw.
pub fn upload_text(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
verts: &[text_gpu::TextVertex],
wires: &[WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) {
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() {
if self.text_atlas_gpu.is_none() || atlas.is_dirty() {
self.text_atlas_gpu = Some(text_gpu::TextAtlasGpu::upload(
device,
queue,
&atlas,
&self.text_atlas_bgl,
));
atlas.clear_dirty();
}
}
self.text_gpu = text_gpu::upload_vertices(device, queue, verts);
self.block_text_gpu = text_gpu::upload_block_vertices(device, queue, wires, depth_map);
if let Some(started) = perf_started {
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
if elapsed_ms >= 1.0 {
crate::perf_record!(
"[perf] text-upload {:>7.1}ms vertices={}",
elapsed_ms,
verts.len(),
);
}
}
}
/// Whatever is bound at the frame group's shadow slot: the real depth
/// target when this viewport casts shadows, the 1x1 placeholder otherwise.
fn shadow_view(&self) -> &wgpu::TextureView {
match &self.shadow_full {
Some((_, view)) => view,
None => &self.shadow_fallback_view,
}
}
/// Rebuild the frame bind group from whatever this slot currently holds.
///
/// Three things can change what belongs in it — a background image, an
/// environment image, and whether the shadow target exists — and it used to
/// be assembled inline at each. One builder means a shadow transition
/// cannot forget the background the slot already had.
fn rebuild_frame_bind_group(&mut self, device: &wgpu::Device) {
let background_view = self
.background_texture
.create_view(&wgpu::TextureViewDescriptor::default());
let environment_view = self
.environment_texture
.create_view(&wgpu::TextureViewDescriptor::default());
self.uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("viewer.bind_group"),
layout: &self.frame_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: self.uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&background_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&self.background_sampler),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(&environment_view),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Sampler(&self.background_sampler),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::TextureView(self.shadow_view()),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::Sampler(&self.shadow_sampler),
},
],
});
}
/// Allocate or drop the shadow target to match `shadow_enabled`.
///
/// Only on a transition: the frame bind group has to be rebuilt with the
/// new view, and a viewport that keeps its shadow setting should not pay
/// for that every frame.
fn sync_shadow_target(&mut self, device: &wgpu::Device) {
match (self.shadow_enabled, self.shadow_full.is_some()) {
(true, false) => {
let errors_before = gpu_errors_seen();
let texture = create_shadow_texture(device, SHADOW_MAP_SIZE);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
if gpu_errors_seen() == errors_before {
self.shadow_full = Some((texture, view));
}
}
(false, true) => self.shadow_full = None,
_ => return,
}
self.rebuild_frame_bind_group(device);
}
pub fn upload_uniforms(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
uniforms: &Uniforms,
) {
self.shadow_enabled = uniforms.shadow_params[0] > 0.5;
self.sync_shadow_target(device);
let mut uniforms = *uniforms;
if self.shadow_full.is_none() {
uniforms.shadow_params[0] = 0.0;
}
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
}
pub fn upload_background_images(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
background: Option<&crate::scene::model::image_model::DecodedImage>,
environment: Option<&crate::scene::model::image_model::DecodedImage>,
) {
let source_id = |image: Option<&crate::scene::model::image_model::DecodedImage>| {
image
.map(|image| std::sync::Arc::as_ptr(&image.pixels) as usize)
.unwrap_or(0)
};
let background_id = source_id(background);
let environment_id = source_id(environment);
if background_id == self.background_source_id
&& environment_id == self.environment_source_id
{
return;
}
let upload = |label: &'static str,
image: Option<&crate::scene::model::image_model::DecodedImage>,
fallback: [u8; 4]| {
let (width, height, pixels) = image
.map(|image| (image.width, image.height, image.pixels.as_slice()))
.unwrap_or((1, 1, fallback.as_slice()));
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width: width.max(1),
height: height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
texture.as_image_copy(),
pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * width.max(1)),
rows_per_image: Some(height.max(1)),
},
wgpu::Extent3d {
width: width.max(1),
height: height.max(1),
depth_or_array_layers: 1,
},
);
texture
};
self.background_texture = upload("background.texture", background, [0, 0, 0, 255]);
self.environment_texture = upload("environment.texture", environment, [128, 128, 128, 255]);
self.rebuild_frame_bind_group(device);
self.background_source_id = background_id;
self.environment_source_id = environment_id;
}
/// Write the blit shader's UV crop uniform. Call in `prepare` (the only
/// place with a `&Queue`) — `render` then just submits the draw call.
pub fn upload_blit_uv(&self, queue: &wgpu::Queue, uv_offset: [f32; 2], uv_scale: [f32; 2]) {
// The geometry passes render at `depth_texture_size` into the top-left
// corner of the (possibly larger) `alloc_size` resolve texture, so the
// rendered image occupies UV [0, render/alloc]. Scale the incoming crop
// — computed in full-image-normalized units — down into that region.
let fx = self.depth_texture_size.width as f32 / self.alloc_size.width.max(1) as f32;
let fy = self.depth_texture_size.height as f32 / self.alloc_size.height.max(1) as f32;
queue.write_buffer(
&self.blit_uniform_buffer,
0,
bytemuck::cast_slice(&[
uv_offset[0] * fx,
uv_offset[1] * fy,
uv_scale[0] * fx,
uv_scale[1] * fy,
]),
);
}
/// Render geometry at its fractional pixel rectangle and blit the resolve
/// to `surface_dest` on the swap-chain. The UV crop is read from the
/// blit uniform buffer (written by `upload_blit_uv` during `prepare`)
/// so a viewport that hangs off the canvas still composites the correct
/// sub-rectangle to the visible portion of the surface.
pub fn render(
&self,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
raster: Rectangle,
surface_dest: Rectangle<u32>,
surface_clip: Rectangle<u32>,
bg_color: [f32; 4],
mesh_wireframe: bool,
hidden_line: bool,
show_3d_edges: bool,
) {
let msaa = &self.msaa_view;
let [r, g, b, a] = bg_color;
let clear_color = if self.clip_boundary.is_some() || self.skip_background {
wgpu::Color::TRANSPARENT
} else {
wgpu::Color {
r: r as f64,
g: g as f64,
b: b as f64,
a: a as f64,
}
};
// Non-rectangular viewport clip: the boundary is stamped into the
// just-cleared (0x00) stencil with `Invert`, so an odd (interior)
// coverage becomes 0xFF. Every content pass then draws with reference
// 0xFF so only the interior survives. Rectangular / unclipped viewports
// leave the stencil at 0 and draw with reference 0 (the viewport's own
// render rectangle does the clipping).
let stencil_ref: u32 = if self.clip_boundary.is_some() {
0xFF
} else {
0
};
// Render shadows only when a full target was allocated.
if let Some(shadow_target) = self
.shadow_full
.as_ref()
.map(|(_, view)| view)
.filter(|_| self.shadow_enabled && !self.skip_geometry && !mesh_wireframe)
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("shadow.render_pass"),
color_attachments: &[],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: shadow_target,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_bind_group(0, &self.shadow_uniform_bind_group, &[]);
for (_, chunk) in self.active_mesh_chunks_indexed() {
pass.set_pipeline(if chunk.compact_vertices {
&self.shadow_plain_pipeline
} else {
&self.shadow_pipeline
});
pass.set_bind_group(
1,
chunk
.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
pass.set_vertex_buffer(0, chunk.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, chunk.instance_buffer.slice(..));
if chunk.index_count != 0 {
pass.set_index_buffer(chunk.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..chunk.index_count, 0, 0..chunk.instance_count);
}
if chunk.transp_index_count != 0 {
pass.set_index_buffer(
chunk.transp_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
pass.draw_indexed(0..chunk.transp_index_count, 0, 0..chunk.instance_count);
}
}
}
// Scene-render cache: when `prepare` found this frame pixel-identical
// to the last (unchanged view / geometry / selection / preview), the
// resolve texture already holds the image. Skip every geometry pass +
// the MSAA resolve and fall straight through to the blit below. This
// is what makes a pure cursor move cost one fullscreen blit instead of
// re-rasterizing the whole drawing every frame.
if !self.skip_geometry {
// ── Pass 1: hatch fills ────────────────────────────────────────────
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("hatch.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
// Clear MSAA to background color on the first pass.
load: wgpu::LoadOp::Clear(clear_color),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
// Clip stencil starts at 0 (= "unclipped", passes content
// bound to reference 0); clip masks stamp 1 into interiors.
stencil_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(0),
store: wgpu::StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
// Stamp the viewport clip boundary into the just-cleared stencil
// (interior → 1) before any content draws, so every pass below can
// clip to the shape with reference 1.
if let Some((vbuf, vcount)) = &self.clip_boundary {
pass.set_pipeline(&self.clip_mask_pipeline);
pass.set_vertex_buffer(0, vbuf.slice(..));
pass.draw(0..*vcount, 0..1);
}
// The background shader returns an opaque colour whatever alpha it
// is handed, so a see-through viewport cannot be asked for as a
// transparent background — the pass has to not run.
if !self.skip_background {
pass.set_pipeline(&self.background_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
pass.draw(0..3, 0..1);
}
// The capability-selected façade dispatches storage or texture
// draws before wires so outlines remain on top in either backend.
// Skipped while navigating because per-pixel hatch work dominates
// hatch-heavy drawings.
if !self.skip_hatch_frame {
self.hatch_gpu
.draw(&mut pass, &self.uniform_bind_group, stencil_ref);
}
}
// ── Pass 2: raster images ─────────────────────────────────────────
if !self.gpu_images.is_empty() {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("image.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_pipeline(&self.image_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
for img in self.gpu_images.iter() {
pass.set_bind_group(1, &img.bind_group, &[]);
pass.set_vertex_buffer(0, img.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, img.instance_buffer.slice(..));
pass.draw(0..img.vertex_count, 0..img.instance_count);
}
}
// ── Pass 3: point clouds ──────────────────────────────────────────
if !self.gpu_point_clouds.is_empty() {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("point_cloud.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_pipeline(&self.point_cloud_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
self.gpu_point_clouds.draw(&mut pass);
}
// ── Pass 4: solid meshes (batched) ────────────────────────────────
if !self.gpu_mesh_batch.is_empty() || !self.gpu_mesh_dynamic.is_empty() {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("mesh.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
// Four draw paths share this pass:
// - Solid: `mesh_pipeline` + triangle index buf.
// - Wireframe: `mesh_wireframe_pipeline` + the
// pre-built expanded wire vertex buffer.
// - HiddenLine: depth prepass (`mesh_depth_pipeline`,
// writes Z, no colour) → wire overlay.
// - Solid+Edges: `mesh_pipeline` shaded fill → wire
// overlay; LessEqual depth test on the
// wire pass keeps the edges crisp on
// top of the shaded surface.
let want_solid_with_edges = !hidden_line && !mesh_wireframe && show_3d_edges;
// Each path now binds a chunk's buffers and draws the whole chunk in
// one call — a handful of draws total instead of one per solid.
if hidden_line {
// Depth-only prepass: every solid surface occludes hidden edges,
// so both the opaque and the transparent tris write depth here.
for (_, c) in self.active_mesh_chunks_indexed() {
pass.set_pipeline(if c.compact_vertices {
&self.mesh_plain_depth_pipeline
} else {
&self.mesh_depth_pipeline
});
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
if c.index_count != 0 {
pass.set_index_buffer(
c.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
pass.draw_indexed(0..c.index_count, 0, 0..c.instance_count);
}
if c.transp_index_count != 0 {
pass.set_index_buffer(
c.transp_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
pass.draw_indexed(0..c.transp_index_count, 0, 0..c.instance_count);
}
}
pass.set_pipeline(&self.mesh_wireframe_pipeline);
pass.set_bind_group(1, &self.mesh_default_material_bind_group, &[]);
for (_, c) in self.active_mesh_chunks_indexed() {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
// Plain-mesh triangulation edges.
if c.wire_vertex_count != 0 {
pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..));
pass.draw(0..c.wire_vertex_count, 0..c.instance_count);
}
// ACIS solid B-rep feature edges (LineList, non-indexed).
if c.edge_vertex_count != 0 {
pass.set_vertex_buffer(0, c.edge_vertex_buffer.slice(..));
pass.draw(0..c.edge_vertex_count, 0..c.instance_count);
}
}
pass.set_pipeline(&self.silhouette_black_pipeline);
for chunk in &self.silhouette_chunks {
pass.set_vertex_buffer(0, chunk.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, chunk.instance_buffer.slice(..));
pass.draw(0..chunk.vertex_count, 0..chunk.instance_count);
}
} else {
if mesh_wireframe {
pass.set_pipeline(&self.mesh_wireframe_pipeline);
pass.set_bind_group(1, &self.mesh_default_material_bind_group, &[]);
for (_, c) in self.active_mesh_chunks_indexed() {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
if c.wire_vertex_count != 0 {
pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..));
pass.draw(0..c.wire_vertex_count, 0..c.instance_count);
}
if c.edge_vertex_count != 0 {
pass.set_vertex_buffer(0, c.edge_vertex_buffer.slice(..));
pass.draw(0..c.edge_vertex_count, 0..c.instance_count);
}
}
pass.set_pipeline(&self.silhouette_pipeline);
for chunk in &self.silhouette_chunks {
pass.set_vertex_buffer(0, chunk.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, chunk.instance_buffer.slice(..));
pass.draw(0..chunk.vertex_count, 0..chunk.instance_count);
}
} else {
// Opaque fills first (they write depth).
for (_, c) in self.active_mesh_chunks_indexed() {
pass.set_pipeline(if c.compact_vertices {
&self.mesh_plain_pipeline
} else {
&self.mesh_pipeline
});
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
if c.index_count == 0 {
continue;
}
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
pass.set_index_buffer(
c.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
pass.draw_indexed(0..c.index_count, 0, 0..c.instance_count);
}
// Transparent fills last, with depth writes disabled, so they
// blend over the opaque geometry behind them instead of
// culling it via the depth buffer.
for (_, c) in self.active_mesh_chunks_indexed() {
pass.set_pipeline(if c.compact_vertices {
&self.mesh_plain_transparent_pipeline
} else {
&self.mesh_transparent_pipeline
});
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
if c.transp_index_count == 0 {
continue;
}
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
pass.set_index_buffer(
c.transp_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
pass.draw_indexed(0..c.transp_index_count, 0, 0..c.instance_count);
}
}
// Selection / hover highlight reuses index ranges already
// resident in the chunk buffers; hover never uploads geometry.
for kind in [MeshHighlightKind::Selected, MeshHighlightKind::Hover] {
// Hidden-line views keep the depth-only faces colourless;
// visible selected edges are tinted in the pass below.
if hidden_line {
continue;
}
if !self
.mesh_highlight_draws
.iter()
.any(|draw| draw.kind == kind)
{
continue;
}
for draw in self
.mesh_highlight_draws
.iter()
.filter(|draw| draw.kind == kind)
{
let chunk = if draw.range.dynamic {
self.gpu_mesh_dynamic.get(draw.range.chunk)
} else {
self.gpu_mesh_batch.get(draw.range.chunk)
};
let Some(chunk) = chunk else {
continue;
};
if draw.range.index_count == 0 {
continue;
}
pass.set_pipeline(match (kind, chunk.compact_vertices) {
(MeshHighlightKind::Selected, false) => {
&self.mesh_selected_pipeline
}
(MeshHighlightKind::Selected, true) => {
&self.mesh_plain_selected_pipeline
}
(MeshHighlightKind::Hover, false) => &self.mesh_hover_pipeline,
(MeshHighlightKind::Hover, true) => &self.mesh_plain_hover_pipeline,
});
pass.set_bind_group(
1,
chunk
.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
pass.set_vertex_buffer(0, chunk.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, chunk.instance_buffer.slice(..));
if draw.range.transparent {
pass.set_index_buffer(
chunk.transp_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
} else {
pass.set_index_buffer(
chunk.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
}
pass.draw_indexed(
draw.range.index_start
..draw.range.index_start + draw.range.index_count,
0,
draw.range.instance_start
..draw.range.instance_start + draw.range.instance_count,
);
}
}
// *WithEdges variants: overlay edge segments on top of the shaded
// fill in black (mesh_edge_black_pipeline). The LessEqual depth
// test keeps the edges visible over the fragments the fill wrote.
if want_solid_with_edges {
pass.set_pipeline(&self.mesh_edge_black_pipeline);
for (_, c) in self.active_mesh_chunks_indexed() {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
if c.wire_vertex_count != 0 {
pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..));
pass.draw(0..c.wire_vertex_count, 0..c.instance_count);
}
if c.edge_vertex_count != 0 {
pass.set_vertex_buffer(0, c.edge_vertex_buffer.slice(..));
pass.draw(0..c.edge_vertex_count, 0..c.instance_count);
}
}
pass.set_pipeline(&self.silhouette_black_pipeline);
for chunk in &self.silhouette_chunks {
pass.set_vertex_buffer(0, chunk.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, chunk.instance_buffer.slice(..));
pass.draw(0..chunk.vertex_count, 0..chunk.instance_count);
}
}
}
}
// ── Pass 5a: 3DFACE fills (3D + 2D split) ─────────────────────────
// 3D quads + PolyfaceMesh face tris go through the depth-only
// pipeline in HiddenLine so wires hidden behind them disappear.
// 2D fills (text greek, MultiLeader bg) always draw with colour.
if let Some(ref fill) = self.gpu_face3d_fill {
if !fill.chunks_3d.is_empty()
|| !fill.chunks_2d.is_empty()
|| !fill.block_chunks_3d.is_empty()
|| !fill.block_chunks_2d.is_empty()
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("face3d.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
if !fill.chunks_3d.is_empty() {
if hidden_line {
pass.set_pipeline(&self.face3d_depth_pipeline);
} else {
pass.set_pipeline(&self.face3d_pipeline);
}
for c in &fill.chunks_3d {
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
pass.draw(0..c.vertex_count, 0..1);
}
}
if !fill.block_chunks_3d.is_empty() {
if hidden_line {
pass.set_pipeline(&self.block_face3d_depth_pipeline);
} else {
pass.set_pipeline(&self.block_face3d_pipeline);
}
for c in &fill.block_chunks_3d {
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
pass.draw(0..c.vertex_count, 0..c.instance_count);
}
}
if !fill.chunks_2d.is_empty() {
pass.set_pipeline(&self.face3d_pipeline);
for c in &fill.chunks_2d {
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
pass.draw(0..c.vertex_count, 0..1);
}
}
if !fill.block_chunks_2d.is_empty() {
pass.set_pipeline(&self.block_face3d_pipeline);
for c in &fill.block_chunks_2d {
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
pass.draw(0..c.vertex_count, 0..c.instance_count);
}
}
}
}
// ── Pass 5b: 3DFACE edges (batched, possibly multiple chunks) ────
// FlatShaded / GouraudShaded hide the 3DFACE outline (the user
// chose a clean shaded look); every other mode keeps it.
if show_3d_edges && !self.gpu_face3d_edges.is_empty() {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("face3d_edges.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_pipeline(&self.wire_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
for edges in &self.gpu_face3d_edges {
if edges.instance_count > 0 {
if let Some(bg) = &edges.const_bind_group {
pass.set_bind_group(1, bg.as_ref(), &[]);
}
pass.set_vertex_buffer(0, edges.instance_buffer.slice(..));
pass.draw(
0..6,
edges.first_instance..edges.first_instance + edges.instance_count,
);
}
}
}
// ── Pass 5: wires ─────────────────────────────────────────────────
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("wire.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_pipeline(&self.wire_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
// In a filled-with-edges mode the mesh outline edges frame the shaded
// fill and should read black; wireframe / hidden-line keep the entity
// colour. `wire_black_pipeline` shares the wire layout, so the switch
// needs no bind-group rebind.
let want_solid_with_edges = !hidden_line && !mesh_wireframe && show_3d_edges;
let mut black_active = false;
pass.set_stencil_reference(stencil_ref);
for wire in self.gpu_wires.iter() {
if wire.instance_count == 0 {
continue;
}
// PolyfaceMesh / PolygonMesh outline edges live in
// `gpu_wires` (their `WireModel` has both `points` and
// `fill_tris`). In FlatShaded / GouraudShaded the user
// wants a clean shaded surface, so the wire pass skips
// these instances; the *WithEdges and pure wireframe
// modes leave the flag at true and draw them.
if !show_3d_edges && wire.is_3d_mesh_edge {
continue;
}
let use_black = want_solid_with_edges && wire.is_3d_mesh_edge;
if use_black != black_active {
pass.set_pipeline(if use_black {
&self.wire_black_pipeline
} else {
&self.wire_pipeline
});
black_active = use_black;
}
if let Some(bg) = &wire.const_bind_group {
pass.set_bind_group(1, bg.as_ref(), &[]);
}
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..));
pass.draw(
0..6,
wire.first_instance..wire.first_instance + wire.instance_count,
);
}
let mut block_black_active = false;
pass.set_pipeline(&self.block_wire_pipeline);
for wire in self.gpu_block_wires.iter() {
if wire.instance_count == 0 || (!show_3d_edges && wire.is_3d_mesh_edge) {
continue;
}
let use_black = want_solid_with_edges && wire.is_3d_mesh_edge;
if use_black != block_black_active {
pass.set_pipeline(if use_black {
&self.block_wire_black_pipeline
} else {
&self.block_wire_pipeline
});
block_black_active = use_black;
}
bind_and_draw_block_wire(&mut pass, wire);
}
// Analytical GPU circles (instanced screen-space quads)
if self.gpu_circles.iter().any(|cg| cg.instance_count > 0) {
pass.set_pipeline(&self.circle_pipeline);
for cg in self.gpu_circles.iter() {
if cg.instance_count > 0 {
pass.set_vertex_buffer(0, cg.instance_buffer.slice(..));
pass.draw(0..6, 0..cg.instance_count);
}
}
}
// Analytical GPU ellipses (instanced screen-space quads)
if self.gpu_ellipses.iter().any(|eg| eg.instance_count > 0) {
pass.set_pipeline(&self.ellipse_pipeline);
for eg in self.gpu_ellipses.iter() {
if eg.instance_count > 0 {
pass.set_vertex_buffer(0, eg.instance_buffer.slice(..));
pass.draw(0..6, 0..eg.instance_count);
}
}
}
}
// ── Pass 5c: SDF text quads (drawn over wires) ────────────────────
// Selection / rollover text is drawn later with the selected-wire xray
// overlay, after wipeouts, so normal text cannot hide its own tint.
if let Some(atlas) = &self.text_atlas_gpu {
let have_base = !self.text_gpu.is_empty();
let have_blocks = !self.block_text_gpu.is_empty();
if have_base || have_blocks {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("text.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_pipeline(&self.text_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
pass.set_bind_group(1, &atlas.bind_group, &[]);
for text in &self.text_gpu {
pass.set_vertex_buffer(0, text.vertex_buffer.slice(..));
pass.draw(0..text.vertex_count, 0..1);
}
if have_blocks {
pass.set_pipeline(&self.block_text_pipeline);
for text in &self.block_text_gpu {
pass.set_vertex_buffer(0, text.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, text.instance_buffer.slice(..));
pass.draw(0..text.vertex_count, 0..text.instance_count);
}
}
}
}
// ── Pass 6: wipeout fills (drawn after wires to mask them) ────────
if !self.gpu_wipeouts.is_empty() {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("wipeout.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_pipeline(&self.wipeout_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
for (i, wipeout) in self.gpu_wipeouts.iter().enumerate() {
if self.wipeout_skip_flags.get(i).copied().unwrap_or(false) {
continue;
}
pass.set_bind_group(1, &wipeout.bind_group, &[]);
pass.set_vertex_buffer(0, wipeout.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, wipeout.instance_buffer.slice(..));
pass.draw(0..6, 0..wipeout.instance_count);
}
}
// ── Pass 6b: live command/grip preview overlay ────────────────────
// Wipeouts intentionally mask resident drawing geometry, but they must
// never mask temporary construction geometry belonging to an active
// command or grip edit. Draw those previews after the wipeout pass.
//
// The xray variants keep previews independent of resident depth while
// the shared content stencil still respects viewport/XCLIP clipping.
let have_preview_overlay = self
.gpu_preview_wires
.iter()
.any(|wire| wire.instance_count > 0)
|| self
.gpu_preview_circles
.iter()
.any(|circle| circle.instance_count > 0)
|| self
.gpu_preview_ellipses
.iter()
.any(|ellipse| ellipse.instance_count > 0)
|| !self.text_preview_gpu.is_empty();
if have_preview_overlay {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("preview_overlay.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
if self
.gpu_preview_wires
.iter()
.any(|wire| wire.instance_count > 0)
{
pass.set_pipeline(&self.wire_xray_pipeline);
for wire in &self.gpu_preview_wires {
if wire.instance_count == 0 {
continue;
}
if let Some(bg) = &wire.const_bind_group {
pass.set_bind_group(1, bg.as_ref(), &[]);
}
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..));
pass.draw(
0..6,
wire.first_instance..wire.first_instance + wire.instance_count,
);
}
}
if self
.gpu_preview_circles
.iter()
.any(|circle| circle.instance_count > 0)
{
pass.set_pipeline(&self.circle_xray_pipeline);
for circle in &self.gpu_preview_circles {
if circle.instance_count > 0 {
pass.set_vertex_buffer(0, circle.instance_buffer.slice(..));
pass.draw(0..6, 0..circle.instance_count);
}
}
}
if self
.gpu_preview_ellipses
.iter()
.any(|ellipse| ellipse.instance_count > 0)
{
pass.set_pipeline(&self.ellipse_xray_pipeline);
for ellipse in &self.gpu_preview_ellipses {
if ellipse.instance_count > 0 {
pass.set_vertex_buffer(0, ellipse.instance_buffer.slice(..));
pass.draw(0..6, 0..ellipse.instance_count);
}
}
}
if let Some(atlas) = &self.text_atlas_gpu {
if !self.text_preview_gpu.is_empty() {
// Same glyph shader as ordinary text, but depth-independent:
// previews are temporary UI geometry, not resident drawing.
pass.set_pipeline(&self.text_highlight_pipeline);
pass.set_bind_group(1, &atlas.bind_group, &[]);
for text in &self.text_preview_gpu {
pass.set_vertex_buffer(0, text.vertex_buffer.slice(..));
pass.draw(0..text.vertex_count, 0..1);
}
}
}
}
// ── Pass 7: selection overlay pass ───────────────────────────────
// Redraws selected wires and text with depth_compare=Always so both
// appear on top of all other geometry at full brightness — unless
// faces are drawn: then edges behind a surface stay hidden while
// selected, as they are when not (#1031).
let occluded = hidden_line || !mesh_wireframe;
let have_text_highlight =
(!self.text_highlight_gpu.is_empty()) || !self.block_text_highlight_gpu.is_empty();
if !self.gpu_selected_wires.is_empty()
|| !self.gpu_selected_block_wires.is_empty()
|| !self.gpu_selected_circles.is_empty()
|| !self.gpu_selected_ellipses.is_empty()
|| have_text_highlight
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("selection_xray.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
if !self.gpu_selected_wires.is_empty() {
pass.set_pipeline(if occluded {
&self.wire_pipeline
} else {
&self.wire_xray_pipeline
});
for wire in &self.gpu_selected_wires {
if wire.instance_count > 0 {
if let Some(bg) = &wire.const_bind_group {
pass.set_bind_group(1, bg.as_ref(), &[]);
}
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..));
pass.draw(
0..6,
wire.first_instance..wire.first_instance + wire.instance_count,
);
}
}
}
if !self.gpu_selected_block_wires.is_empty() {
pass.set_pipeline(if occluded {
&self.block_wire_pipeline
} else {
&self.block_wire_xray_pipeline
});
for wire in &self.gpu_selected_block_wires {
if wire.instance_count == 0 {
continue;
}
bind_and_draw_block_wire(&mut pass, wire);
}
}
if !self.gpu_selected_circles.is_empty() {
pass.set_pipeline(if occluded {
&self.circle_pipeline
} else {
&self.circle_xray_pipeline
});
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
for cg in &self.gpu_selected_circles {
if cg.instance_count > 0 {
pass.set_vertex_buffer(0, cg.instance_buffer.slice(..));
pass.draw(0..6, 0..cg.instance_count);
}
}
}
if !self.gpu_selected_ellipses.is_empty() {
pass.set_pipeline(if occluded {
&self.ellipse_pipeline
} else {
&self.ellipse_xray_pipeline
});
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
for eg in &self.gpu_selected_ellipses {
if eg.instance_count > 0 {
pass.set_vertex_buffer(0, eg.instance_buffer.slice(..));
pass.draw(0..6, 0..eg.instance_count);
}
}
}
if let Some(atlas) = &self.text_atlas_gpu {
if !self.text_highlight_gpu.is_empty() {
pass.set_pipeline(&self.text_highlight_pipeline);
pass.set_bind_group(1, &atlas.bind_group, &[]);
for text in &self.text_highlight_gpu {
pass.set_vertex_buffer(0, text.vertex_buffer.slice(..));
pass.draw(0..text.vertex_count, 0..1);
}
}
}
if let Some(atlas) = &self.text_atlas_gpu {
if !self.block_text_highlight_gpu.is_empty() {
pass.set_pipeline(&self.block_text_highlight_pipeline);
pass.set_bind_group(1, &atlas.bind_group, &[]);
for text in &self.block_text_highlight_gpu {
pass.set_vertex_buffer(0, text.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, text.instance_buffer.slice(..));
pass.draw(0..text.vertex_count, 0..text.instance_count);
}
}
}
}
// ── Resolve MSAA → resolve texture ────────────────────────────────
// Both are the same (rounded `alloc_size`) offscreen texture, so the
// resolve never touches the surface. Only the drawn [0, render_size]
// corner holds content; the rounded border stays the cleared bg color
// and is never sampled (the blit UV is scaled to render/alloc).
{
let _resolve = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("msaa.resolve_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa,
depth_slice: None,
resolve_target: Some(&self.resolve_view),
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Discard,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
// No draw calls — the pass itself triggers the MSAA resolve.
}
} // end `if !self.skip_geometry`
// ── Blit resolve texture → surface target at surface_dest position ──
// The viewport maps the NDC quad to exactly `surface_dest` in the
// swap-chain; `uv_offset` + `uv_scale` (passed through the blit
// uniform) crop the resolve so we sample only the visible portion
// of the full viewport's MSAA texture.
if surface_dest.width > 0 && surface_dest.height > 0 {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("blit.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(
surface_dest.x as f32,
surface_dest.y as f32,
surface_dest.width as f32,
surface_dest.height as f32,
0.0,
1.0,
);
pass.set_pipeline(&self.blit_pipeline);
pass.set_scissor_rect(
surface_clip.x,
surface_clip.y,
surface_clip.width,
surface_clip.height,
);
pass.set_bind_group(0, &self.blit_bind_group, &[]);
pass.draw(0..6, 0..1);
}
}
/// Reset every cache key that describes this slot's uploaded content.
pub(crate) fn forget_cached_keys(&mut self) {
self.cached_epoch = (u64::MAX, u64::MAX, u64::MAX);
self.cached_wire_id = u64::MAX;
self.cached_selection = (u64::MAX, u64::MAX);
self.cached_highlight_key = (u64::MAX, u64::MAX);
self.cached_mesh_content_id = u64::MAX;
self.cached_face3d_key = (u64::MAX, false, false, u64::MAX);
self.cached_solid_visibility = (u64::MAX, [u32::MAX; 3], u64::MAX);
self.cached_hatch_source = None;
self.cached_preview_hatch_source = None;
self.cached_wipeout_source = None;
self.cached_image_source = None;
self.cached_point_cloud_source = None;
self.cached_text_source = None;
self.cached_mesh_source = None;
self.cached_face3d_source = None;
self.cached_face3d_depth_source = None;
self.wire_cull_key = (u64::MAX, u64::MAX, 0, 0);
self.hatch_lod_key = (usize::MAX, u64::MAX, 0, 0, false);
self.wipeout_lod_key = (usize::MAX, u64::MAX, 0, 0, false);
self.silhouette_key = (usize::MAX, u64::MAX, [u32::MAX; 3], false);
self.silhouette_source_key = (usize::MAX, usize::MAX, u64::MAX);
self.render_sig = u64::MAX;
// The retained-upload record is part of the slot's cached state.
self.partition_contributors.clear();
self.partition_depth_generation = u64::MAX;
}
/// Catch errors delivered after the previous upload/submit completed.
pub(crate) fn sync_gpu_error_epoch(&mut self) {
let errors = gpu_errors_seen();
if errors == self.gpu_error_epoch {
return;
}
self.gpu_error_epoch = errors;
self.forget_cached_keys();
self.wire_arena = None;
self.wire_arena_mesh = None;
self.wire_arena_id = u64::MAX;
self.partition_contributors.clear();
self.partition_depth_generation = u64::MAX;
self.alloc_size = Size::new(0, 0);
self.shadow_full = None;
self.background_source_id = usize::MAX;
self.environment_source_id = usize::MAX;
}
/// Give this slot's device memory back and forget what it held.
///
/// A slot nobody is drawing still owns its wire arena, its render targets,
/// its text atlas and every category buffer — tens of megabytes each. Held
/// while another viewport is asking for an allocation, that is the
/// difference between a degraded frame and a session that cannot recover.
///
/// Everything released here is derived state with a rebuild path guarded by
/// a cache key, and every one of those keys is reset, so the next frame that
/// needs this slot builds it again. The cost of releasing a slot that turns
/// out to be needed is one slow frame.
pub(crate) fn release_heavy_resources(&mut self, device: &wgpu::Device) {
self.forget_cached_keys();
self.gpu_wires = std::sync::Arc::new(Vec::new());
self.gpu_block_wires = std::sync::Arc::new(Vec::new());
self.gpu_circles = std::sync::Arc::new(Vec::new());
self.gpu_ellipses = std::sync::Arc::new(Vec::new());
self.wire_handle_index = std::sync::Arc::new(rustc_hash::FxHashMap::default());
self.wire_arena = None;
self.wire_arena_mesh = None;
self.wire_arena_fallback = std::sync::Arc::new(Vec::new());
self.wire_arena_fallback_kind = None;
self.wire_arena_fallback_handles.clear();
self.wire_arena_id = u64::MAX;
self.partition_contributors.clear();
self.partition_depth_generation = u64::MAX;
self.gpu_selected_wires.clear();
self.gpu_selected_block_wires.clear();
self.gpu_selected_circles.clear();
self.gpu_selected_ellipses.clear();
self.gpu_preview_wires.clear();
self.gpu_preview_circles.clear();
self.gpu_preview_ellipses.clear();
self.hatch_gpu.clear();
self.gpu_wipeouts.clear();
self.wipeout_skip_flags.clear();
self.gpu_images.clear();
self.gpu_mesh_batch.clear();
self.gpu_mesh_dynamic.clear();
self.mesh_disabled_chunks.clear();
self.mesh_dynamic_handles.clear();
self.mesh_ranges_by_handle.clear();
self.mesh_highlight_draws.clear();
self.silhouette_chunks.clear();
self.silhouette_source_groups.clear();
self.clip_boundary = None;
// A cold slot's shadow target goes back too. `shadow_enabled` is left
// alone, so `sync_shadow_target` reallocates on the next frame that
// still wants shadows.
if self.shadow_full.take().is_some() {
self.rebuild_frame_bind_group(device);
}
self.text_atlas_gpu = None;
self.text_gpu.clear();
self.block_text_gpu.clear();
self.block_text_highlight_gpu.clear();
self.text_highlight_gpu.clear();
self.text_preview_gpu.clear();
// Back to the smallest allocation the rounding allows (128x128, about
// 0.6 MiB against the ~70 MiB a full-canvas slot holds). Going through
// `ensure_depth_texture` rather than reaching for the textures directly
// keeps the blit bind group consistent with the views it samples.
self.ensure_depth_texture(device, Size::new(1, 1));
// ...and make the next real size a mismatch, so it reallocates.
self.alloc_size = Size::new(0, 0);
}
pub fn ensure_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) {
// Record the requested render size every frame (the blit UV scale reads
// it); only reallocate the textures when the *rounded* size changes.
self.depth_texture_size = size;
let alloc = Size::new(round_up_tex(size.width), round_up_tex(size.height));
if self.alloc_size != alloc {
let size = alloc;
let depth_tex = create_depth_texture(device, size);
self.depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
let msaa_tex = create_msaa_texture(device, size, self.surface_format);
self.msaa_view = msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
let resolve_tex = create_resolve_texture(device, size, self.surface_format);
let resolve_view = resolve_tex.create_view(&wgpu::TextureViewDescriptor::default());
self.blit_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("blit.bind_group"),
layout: &self.blit_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&resolve_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.blit_sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.blit_uniform_buffer.as_entire_binding(),
},
],
});
self.resolve_view = resolve_view;
self.alloc_size = alloc;
}
}
}
/// Round a texture dimension up to a coarse grid so a live divider drag
/// doesn't recreate the depth / MSAA / resolve textures on every frame. See
/// `alloc_size` — this is the mitigation for the Windows-Firefox freeze (#191).
fn round_up_tex(n: u32) -> u32 {
const GRID: u32 = 128;
((n.max(1) + GRID - 1) / GRID) * GRID
}
/// `true` when the world-XY AABB projects entirely outside the
/// viewport rect (extended by `MARGIN_FRAC` to absorb pan inertia and
/// avoid edge pop-in). Phase 2.2 mesh-frustum / Phase 2.3 hatch +
/// wipeout cull. Equivalent to a 2D bounding-box rejection test in
/// NDC; uses the same 4-corner projection that LOD picking already
/// does, so the extra cost is negligible.
///
/// IMPORTANT: the AABB must be in the same local space (world_offset
/// subtracted) that `view_proj` expects. `WipeoutGpu.world_aabb` rebuilds
/// the absolute local-space rect from `model.world_origin + boundary
/// extents` for this reason; meshes already store an absolute rect.
fn aabb_offscreen(
aabb: [f32; 4],
view_rot: glam::Mat4,
eye: glam::DVec3,
clip_w: u32,
clip_h: u32,
) -> bool {
let [x0, y0, x1, y1] = aabb;
if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite() {
return false;
}
let w = clip_w as f32;
let h = clip_h as f32;
let corners = [
view_rot.project_point3((glam::DVec3::new(x0 as f64, y0 as f64, 0.0) - eye).as_vec3()),
view_rot.project_point3((glam::DVec3::new(x1 as f64, y0 as f64, 0.0) - eye).as_vec3()),
view_rot.project_point3((glam::DVec3::new(x0 as f64, y1 as f64, 0.0) - eye).as_vec3()),
view_rot.project_point3((glam::DVec3::new(x1 as f64, y1 as f64, 0.0) - eye).as_vec3()),
];
let mut min_px = f32::INFINITY;
let mut max_px = f32::NEG_INFINITY;
let mut min_py = f32::INFINITY;
let mut max_py = f32::NEG_INFINITY;
for c in &corners {
let px = (c.x + 1.0) * 0.5 * w;
let py = (1.0 - c.y) * 0.5 * h;
if px < min_px {
min_px = px;
}
if px > max_px {
max_px = px;
}
if py < min_py {
min_py = py;
}
if py > max_py {
max_py = py;
}
}
// 25% pad on each side — matches `view_world_aabb` (wire path),
// keeps edge geometry rendered while panning before the next
// upload reaches the GPU.
const MARGIN_FRAC: f32 = 0.25;
let mx = w * MARGIN_FRAC;
let my = h * MARGIN_FRAC;
max_px < -mx || min_px > w + mx || max_py < -my || min_py > h + my
}
/// Return `true` when the world-XY AABB's screen-space size is below the
/// given pixel threshold. Used by LOD passes (hatch skip, etc.) to drop
/// draw calls that wouldn't contribute a visible pixel.
fn aabb_below_pixel(
aabb: [f32; 4],
view_rot: glam::Mat4,
eye: glam::DVec3,
clip_w: u32,
clip_h: u32,
threshold_px: f32,
) -> bool {
let [x0, y0, x1, y1] = aabb;
if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite() {
return false;
}
let w = clip_w as f32;
let h = clip_h as f32;
let corners = [
view_rot.project_point3((glam::DVec3::new(x0 as f64, y0 as f64, 0.0) - eye).as_vec3()),
view_rot.project_point3((glam::DVec3::new(x1 as f64, y0 as f64, 0.0) - eye).as_vec3()),
view_rot.project_point3((glam::DVec3::new(x0 as f64, y1 as f64, 0.0) - eye).as_vec3()),
view_rot.project_point3((glam::DVec3::new(x1 as f64, y1 as f64, 0.0) - eye).as_vec3()),
];
let mut min_px = f32::INFINITY;
let mut max_px = f32::NEG_INFINITY;
let mut min_py = f32::INFINITY;
let mut max_py = f32::NEG_INFINITY;
for c in &corners {
let px = (c.x + 1.0) * 0.5 * w;
let py = (1.0 - c.y) * 0.5 * h;
if px < min_px {
min_px = px;
}
if px > max_px {
max_px = px;
}
if py < min_py {
min_py = py;
}
if py > max_py {
max_py = py;
}
}
(max_px - min_px).max(max_py - min_py) < threshold_px
}
/// Device bytes a renderer is holding, counted from the sizes it allocated
/// rather than from process RSS — the allocator and the driver both keep
/// high-water memory that RSS cannot distinguish from live resources.
///
/// Partial by construction: it covers the categories large enough to decide
/// where the next fix goes, and says so rather than pretending to be a total.
#[derive(Default, Clone, Copy, PartialEq)]
pub(crate) struct GpuLiveBytes {
pub slots: usize,
pub shadow: u64,
pub render_targets: u64,
pub text_atlas: u64,
pub wire_arena: u64,
pub block_geometry: u64,
}
impl GpuLiveBytes {
pub(crate) fn total(&self) -> u64 {
self.shadow + self.render_targets + self.text_atlas + self.wire_arena + self.block_geometry
}
}
impl Pipeline {
/// This slot's share. `alloc_size` is the real allocation, which is rounded
/// up from the requested size and grows only.
fn gpu_live_bytes(&self) -> GpuLiveBytes {
// 4x MSAA colour (4 B/sample) + 4x MSAA depth-stencil (4 B/sample) +
// one single-sample resolve target.
const BYTES_PER_PIXEL: u64 = (MSAA_SAMPLES as u64) * 4 + (MSAA_SAMPLES as u64) * 4 + 4;
let pixels = u64::from(self.alloc_size.width) * u64::from(self.alloc_size.height);
GpuLiveBytes {
slots: 1,
// Only while this viewport casts shadows; the placeholder that
// stands in otherwise is 4 bytes.
shadow: match &self.shadow_full {
Some(_) => u64::from(SHADOW_MAP_SIZE) * u64::from(SHADOW_MAP_SIZE) * 4,
None => 4,
},
render_targets: pixels * BYTES_PER_PIXEL,
text_atlas: self
.text_atlas_gpu
.as_ref()
.map(text_gpu::TextAtlasGpu::gpu_bytes)
.unwrap_or(0),
wire_arena: self
.wire_arena
.as_ref()
.map(wire_arena::PersistentWireArena::gpu_bytes)
.unwrap_or(0)
+ self
.wire_arena_mesh
.as_ref()
.map(wire_arena::PersistentWireArena::gpu_bytes)
.unwrap_or(0),
block_geometry: 0,
}
}
}
impl MultiPipeline {
/// Release cold slots after 64 frames, or two frames under memory pressure.
/// Current and previous frame slots remain reserved for sibling widgets.
pub(crate) fn release_idle_slots(
&mut self,
device: &wgpu::Device,
reserved: &[usize],
urgent: bool,
) -> usize {
const IDLE_FRAMES: u64 = 64;
// Other shader widgets may prepare later in this frame. Keep every
// slot used in this or the previous frame, even under memory pressure.
let idle_frames = if urgent { 2 } else { IDLE_FRAMES };
let mut released = 0;
for index in 0..self.inners.len() {
if reserved.contains(&index) {
continue;
}
let idle = self.slot_clock.saturating_sub(self.slot_last_used[index]);
if idle < idle_frames {
continue;
}
// Nothing to give back — releasing again would only churn the
// render targets it just rebuilt at their smallest size.
let held = self.inners[index].gpu_live_bytes();
if held.wire_arena == 0
&& held.shadow <= 4
&& self.inners[index].alloc_size == Size::new(0, 0)
{
continue;
}
self.inners[index].release_heavy_resources(device);
released += 1;
}
if released > 0 {
self.wire_buffer_cache.retain(|_, entry| {
std::sync::Arc::strong_count(&entry.0) > 1
|| std::sync::Arc::strong_count(&entry.1) > 1
|| std::sync::Arc::strong_count(&entry.3) > 1
});
// Block records are host memory a released slot no longer needs;
// the next build re-emits what it still draws.
self.block_geometry.clear();
}
released
}
/// Sum across slots, plus the caches held once for all of them.
pub(crate) fn gpu_live_bytes(&self) -> GpuLiveBytes {
let mut total = GpuLiveBytes::default();
// Block arenas belong to the batches, and a cached build hands the same
// arenas to several slots, so each one is counted once.
let mut counted = rustc_hash::FxHashSet::default();
for inner in &self.inners {
let slot = inner.gpu_live_bytes();
total.slots += slot.slots;
total.shadow += slot.shadow;
total.render_targets += slot.render_targets;
total.text_atlas += slot.text_atlas;
total.wire_arena += slot.wire_arena;
total.block_geometry +=
wire_gpu::block_arena_bytes(&inner.gpu_block_wires, &mut counted)
+ wire_gpu::block_arena_bytes(
&inner.gpu_selected_block_wires,
&mut counted,
);
}
for entry in self.wire_buffer_cache.values() {
total.block_geometry += wire_gpu::block_arena_bytes(&entry.1, &mut counted);
}
total
}
}
/// Bind one block-wire batch and draw it.
///
/// The two pipeline modes lay the vertex slots out differently — packed puts
/// the six-per-segment geometry in slot 0 and the placements in slot 1, while
/// storage has no geometry vertex buffer at all and the placements become slot
/// 0. Both draw loops go through here so they cannot drift apart.
/// Many definitions share one arena, so the batch addresses its own slice of
/// it: a dynamic offset for its constants, `first_vertex` for its segments and
/// `first_instance` for its placements.
fn bind_and_draw_block_wire(pass: &mut wgpu::RenderPass<'_>, wire: &wire_gpu::BlockWireGpu) {
let arena = wire.arena.as_ref();
pass.set_bind_group(1, arena.bind_group.as_ref(), &[wire.const_offset]);
match &arena.vertex_buffer {
Some(vertices) => {
pass.set_vertex_buffer(0, vertices.slice(..));
pass.set_vertex_buffer(1, arena.instance_buffer.slice(..));
}
None => pass.set_vertex_buffer(0, arena.instance_buffer.slice(..)),
}
pass.draw(
wire.first_vertex..wire.first_vertex + wire.vertex_count,
wire.first_instance..wire.first_instance + wire.instance_count,
);
}
/// A square `Depth32Float` render target for the shadow pass. `side` is 1 for
/// the placeholder that keeps the frame bind group satisfied while shadows are
/// off, and `SHADOW_MAP_SIZE` for the real thing.
fn create_shadow_texture(device: &wgpu::Device, side: u32) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("shadow.texture"),
size: wgpu::Extent3d {
width: side,
height: side,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
})
}
fn create_depth_texture(device: &wgpu::Device, size: Size<u32>) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("viewer.depth_texture"),
size: wgpu::Extent3d {
width: size.width.max(1),
height: size.height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: MSAA_SAMPLES,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth24PlusStencil8,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
}
fn create_resolve_texture(
device: &wgpu::Device,
size: Size<u32>,
format: wgpu::TextureFormat,
) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("viewer.resolve_texture"),
size: wgpu::Extent3d {
width: size.width.max(1),
height: size.height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
})
}
fn create_msaa_texture(
device: &wgpu::Device,
size: Size<u32>,
format: wgpu::TextureFormat,
) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("viewer.msaa_texture"),
size: wgpu::Extent3d {
width: size.width.max(1),
height: size.height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: MSAA_SAMPLES,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
}
/// Holds one inner `Pipeline` per viewport drawn this frame. A single
/// shader widget owns one `MultiPipeline`; the unified renderer grows the
/// `inners` vector to match the viewport count and draws each into its own
/// screen rectangle. Inner `Pipeline` code (upload / LOD / render / blit)
/// is unchanged — it just runs once per viewport.
pub struct MultiPipeline {
pub(crate) inners: Vec<Pipeline>,
format: wgpu::TextureFormat,
/// Stable viewport identity → pipeline slot map. Paper viewports used to
/// occupy slots by their current list position, so switching layouts or
/// scrolling a sheet could assign an existing viewport to a different
/// slot and throw away all of its GPU caches. Keep the association across
/// tab switches and only recycle genuinely cold slots.
pub(crate) slot_by_instance: rustc_hash::FxHashMap<u64, usize>,
slot_last_used: Vec<u64>,
slot_clock: u64,
pub(crate) frame_rendered: std::sync::atomic::AtomicBool,
pub(crate) gpu_error_epoch: usize,
/// Definition geometry shared across viewport slots.
pub(crate) block_geometry: wire_gpu::BlockGeometryCache,
/// Shared resident batches, keyed by wire content identity.
pub(crate) wire_buffer_cache: rustc_hash::FxHashMap<
u64,
(
std::sync::Arc<Vec<WireGpu>>,
std::sync::Arc<Vec<BlockWireGpu>>,
std::sync::Arc<rustc_hash::FxHashMap<u64, Vec<u32>>>,
std::sync::Arc<Vec<CircleGpu>>,
std::sync::Arc<Vec<EllipseGpu>>,
),
>,
}
impl MultiPipeline {
/// Grow slot storage without disturbing sibling shader widgets.
pub(crate) fn ensure_len(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, n: usize) {
let n = n.max(1);
while self.inners.len() < n {
self.inners.push(Pipeline::new(device, queue, self.format));
self.slot_last_used.push(0);
}
}
/// Preserve viewport slots across frames; recycle only cold slots above the limit.
pub(crate) fn resolve_slots(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
instance_ids: &[u64],
) -> Vec<usize> {
const SOFT_LIMIT: usize = 32;
const HOT_WINDOW: u64 = 8;
if self
.frame_rendered
.swap(false, std::sync::atomic::Ordering::Relaxed)
|| self.slot_clock == 0
{
self.slot_clock = self.slot_clock.wrapping_add(1).max(1);
}
let now = self.slot_clock;
let reserved: rustc_hash::FxHashSet<u64> = instance_ids.iter().copied().collect();
// `inner.slot_id` is updated later in `prepare`, after this whole
// method returns. Without a per-call claim set, two new viewports in
// the same primitive both see the freshly-grown slot as `MAX` and get
// assigned to it. Paper then blits the final occupant's resolve texture
// once as the full sheet and again as the floating viewport.
let mut claimed: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
let mut slots = Vec::with_capacity(instance_ids.len());
for &instance_id in instance_ids {
let existing = self
.slot_by_instance
.get(&instance_id)
.copied()
.filter(|slot| !claimed.contains(slot));
let slot = if let Some(slot) = existing {
slot
} else {
self.slot_by_instance.remove(&instance_id);
let vacant = self
.inners
.iter()
.enumerate()
.find(|(slot, inner)| !claimed.contains(slot) && inner.slot_id == u64::MAX)
.map(|(slot, _)| slot);
let recyclable = vacant.or_else(|| {
(self.inners.len() >= SOFT_LIMIT)
.then(|| {
self.inners
.iter()
.enumerate()
.filter(|(slot, _)| !claimed.contains(slot))
.filter(|(_, inner)| !reserved.contains(&inner.slot_id))
.filter(|(slot, _)| {
now.saturating_sub(self.slot_last_used[*slot]) > HOT_WINDOW
})
.min_by_key(|(slot, _)| self.slot_last_used[*slot])
.map(|(slot, _)| slot)
})
.flatten()
});
let slot = recyclable.unwrap_or_else(|| {
let slot = self.inners.len();
self.ensure_len(device, queue, slot + 1);
slot
});
let old_id = self.inners[slot].slot_id;
if old_id != u64::MAX {
self.slot_by_instance.remove(&old_id);
}
self.slot_by_instance.insert(instance_id, slot);
slot
};
claimed.insert(slot);
self.slot_last_used[slot] = now;
slots.push(slot);
}
slots
}
}
/// Send wgpu's uncaptured validation errors to stderr instead of the default
/// handler, which panics — and inside iced's main-thread redraw that panic
/// aborts the process, taking the user's unsaved work with it (#358). A bad
/// frame must degrade, not end the session.
///
/// The handler slot is per-device and single: this also catches iced's own draw
/// errors, and nothing else can report them, so it must stay loud enough to be
/// noticed. A validation error normally repeats on every frame, though, so a
/// plain print would flood stderr at refresh rate and bury the first (most
/// useful) message. Log the 1st, 2nd, 4th, 8th … occurrence instead: the error
/// is never hidden, the count shows it is ongoing, and the output stays finite.
///
/// Installed from `MultiPipeline::new`, which runs once per device — NOT from
/// `Pipeline::new`, which runs again for every pane `ensure_len` adds. If iced
/// ever rebuilds the device it builds a new `MultiPipeline` too, so the fresh
/// device is covered (and the throttle restarts with it).
static GPU_ERRORS_SEEN: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
/// How many uncaptured device errors have been reported since this device was
/// created.
///
/// Callers snapshot it around an allocation to find out whether what they just
/// built is usable. Best effort: wgpu surfaces some errors asynchronously, so a
/// move means "something failed", while no move is not proof that nothing did.
/// That is the right asymmetry here — the cost of believing a bad buffer is a
/// frozen viewport, the cost of rebuilding a good one is a frame.
pub(crate) fn gpu_errors_seen() -> usize {
GPU_ERRORS_SEEN.load(std::sync::atomic::Ordering::Relaxed)
}
fn install_gpu_error_handler(device: &wgpu::Device) {
use std::sync::atomic::Ordering;
GPU_ERRORS_SEEN.store(0, Ordering::Relaxed);
device.on_uncaptured_error(std::sync::Arc::new(|e: wgpu::Error| {
let n = GPU_ERRORS_SEEN.fetch_add(1, Ordering::Relaxed) + 1;
if n.is_power_of_two() {
eprintln!("[gpu] uncaptured wgpu error #{n} (frame degraded, session kept alive): {e}");
}
}));
}
/// The adapter wgpu ended up drawing on.
///
/// iced chooses it and names it only under `RUST_LOG`, so a fallback to a
/// software rasterizer used to be invisible: an NVIDIA userspace update
/// applied without a reboot left the Vulkan ICD unable to talk to the loaded
/// kernel module, wgpu quietly took Mesa's `llvmpipe`, and a day of pan/zoom
/// on the CPU read as a regression in the drawing code. `record_gpu_adapter`
/// now reports one `[gpu] adapter:` line per device and keeps a copy for the
/// app, which turns it into a [`GpuStatus`] the user can see.
#[derive(Debug, Clone, PartialEq)]
pub struct GpuAdapter {
pub name: String,
pub backend: wgpu::Backend,
pub device_type: wgpu::DeviceType,
}
impl GpuAdapter {
/// A software rasterizer: every frame is drawn by the CPU, which on a
/// large drawing is the difference between interactive and unusable.
///
/// `DeviceType::Cpu` is what Vulkan says for llvmpipe, DX12 for WARP and
/// GL for anything whose renderer string gives it away. WebGPU never says
/// it — wgpu reports every browser adapter as `Other` (gfx-rs/wgpu#8819)
/// — so the names browsers give their fallbacks are matched as well, the
/// same way wgpu's own GL backend classifies them.
pub fn is_software(&self) -> bool {
if self.device_type == wgpu::DeviceType::Cpu {
return true;
}
let name = self.name.to_ascii_lowercase();
SOFTWARE_RASTERIZER_NAMES
.iter()
.any(|marker| name.contains(marker))
}
}
/// Renderer names that mean "no GPU", lower-cased: Mesa's llvmpipe (also
/// exposed as lavapipe under Vulkan), Google's SwiftShader (Chrome's WebGL /
/// WebGPU fallback), and Microsoft's WARP, which DXGI names as a driver.
const SOFTWARE_RASTERIZER_NAMES: [&str; 5] = [
"llvmpipe",
"lavapipe",
"softpipe",
"swiftshader",
"microsoft basic render",
];
/// What is drawing the scene, as far as the app can tell.
#[derive(Debug, Clone, PartialEq)]
pub enum GpuStatus {
/// No scene frame has completed yet, so nothing is known.
Unknown,
/// A GPU, through wgpu. The normal case; nothing to say.
Hardware(GpuAdapter),
/// A CPU rasterizer, through wgpu — llvmpipe, WARP, SwiftShader. Every
/// frame works, slowly, and the driver that should be here is not.
Software(GpuAdapter),
/// wgpu found no adapter at all. iced then draws the interface with
/// tiny-skia and drops the scene primitive with a `log::warn!` nobody
/// sees, so the viewport stays blank while everything else works.
NoRenderer,
}
impl GpuStatus {
/// True when the user should be told: the scene is slow or missing.
pub fn is_degraded(&self) -> bool {
matches!(self, Self::Software(_) | Self::NoRenderer)
}
/// A stable name for "this situation", so the popup can be silenced for
/// one adapter without silencing the next one.
pub fn identity(&self) -> Option<String> {
match self {
Self::Software(adapter) => Some(format!("software:{}", adapter.name)),
Self::NoRenderer => Some("no-renderer".to_string()),
Self::Unknown | Self::Hardware(_) => None,
}
}
}
/// The adapter of the most recently created device.
static GPU_ADAPTER: std::sync::Mutex<Option<GpuAdapter>> = std::sync::Mutex::new(None);
/// How many times the viewport has asked the scene for a primitive. Counted
/// in `ViewportPane::draw`, which every renderer calls; only wgpu goes on to
/// `prepare` and build a device. Draws without a device are therefore the
/// evidence that the scene is being dropped.
static SCENE_DRAWS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
/// Draws the viewport may request before a still-missing device counts as
/// `NoRenderer`. Under wgpu the device exists before the first draw's frame
/// ends, so one would do; the margin covers a frame that was drawn but never
/// presented (an outdated surface, an occluded window).
const SCENE_DRAWS_BEFORE_NO_RENDERER: u32 = 3;
/// Bumped whenever the verdict of [`gpu_status`] can have changed: a device
/// was created, or the draw count reached its threshold. The app compares it
/// against what it last saw, so the per-message check is one atomic load.
static GPU_STATUS_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// Called by the viewport's `shader::Program::draw`, whichever renderer runs it.
pub(crate) fn note_scene_draw() {
use std::sync::atomic::Ordering;
if SCENE_DRAWS.fetch_add(1, Ordering::Relaxed) + 1 == SCENE_DRAWS_BEFORE_NO_RENDERER {
GPU_STATUS_GENERATION.fetch_add(1, Ordering::Relaxed);
}
}
/// One line for whoever reads logs: stderr natively, the browser console on
/// the web, where wasm has no stderr to write to.
pub(crate) fn report_gpu_line(line: &str) {
#[cfg(target_arch = "wasm32")]
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(line));
#[cfg(not(target_arch = "wasm32"))]
eprintln!("{line}");
}
fn record_gpu_adapter(device: &wgpu::Device) {
let info = device.adapter_info();
report_gpu_line(&format!(
"[gpu] adapter: {} ({:?}, {:?}, driver: {} {})",
info.name, info.backend, info.device_type, info.driver, info.driver_info
));
let adapter = GpuAdapter {
name: info.name,
backend: info.backend,
device_type: info.device_type,
};
if let Ok(mut slot) = GPU_ADAPTER.lock() {
*slot = Some(adapter);
}
GPU_STATUS_GENERATION.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
/// The verdict from what has been recorded so far. Pure, so it can be tested
/// without a device: `adapter` is the last device's adapter, `scene_draws`
/// how often the viewport asked to be drawn.
fn gpu_status_from(adapter: Option<GpuAdapter>, scene_draws: u32) -> GpuStatus {
match adapter {
Some(adapter) if adapter.is_software() => GpuStatus::Software(adapter),
Some(adapter) => GpuStatus::Hardware(adapter),
None if scene_draws >= SCENE_DRAWS_BEFORE_NO_RENDERER => GpuStatus::NoRenderer,
None => GpuStatus::Unknown,
}
}
/// The current verdict.
pub(crate) fn gpu_status() -> GpuStatus {
let adapter = GPU_ADAPTER.lock().ok().and_then(|slot| slot.clone());
gpu_status_from(
adapter,
SCENE_DRAWS.load(std::sync::atomic::Ordering::Relaxed),
)
}
/// The verdict, but only when it may have moved since `seen` — the app calls
/// this after every message, and almost every call is one atomic load.
pub(crate) fn gpu_status_if_changed(seen: &mut u64) -> Option<GpuStatus> {
let generation = GPU_STATUS_GENERATION.load(std::sync::atomic::Ordering::Relaxed);
if generation == *seen {
return None;
}
*seen = generation;
Some(gpu_status())
}
#[cfg(test)]
mod gpu_status_tests {
use super::*;
fn adapter(name: &str, device_type: wgpu::DeviceType) -> GpuAdapter {
GpuAdapter {
name: name.to_string(),
backend: wgpu::Backend::Vulkan,
device_type,
}
}
#[test]
fn software_is_the_device_type_or_a_known_fallback_name() {
assert!(adapter("llvmpipe (LLVM 21.1.8, 256 bits)", wgpu::DeviceType::Cpu).is_software());
// WebGPU reports every adapter as Other, so the name has to carry it.
assert!(adapter(
"ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)",
wgpu::DeviceType::Other
)
.is_software());
assert!(adapter("Microsoft Basic Render Driver", wgpu::DeviceType::Other).is_software());
assert!(!adapter("NVIDIA GeForce GT 1030", wgpu::DeviceType::DiscreteGpu).is_software());
assert!(!adapter("Apple M2", wgpu::DeviceType::IntegratedGpu).is_software());
assert!(!adapter("", wgpu::DeviceType::Other).is_software());
}
#[test]
fn the_verdict_needs_either_a_device_or_enough_dropped_draws() {
assert_eq!(gpu_status_from(None, 0), GpuStatus::Unknown);
assert_eq!(
gpu_status_from(None, SCENE_DRAWS_BEFORE_NO_RENDERER - 1),
GpuStatus::Unknown
);
assert_eq!(
gpu_status_from(None, SCENE_DRAWS_BEFORE_NO_RENDERER),
GpuStatus::NoRenderer
);
let gpu = adapter("NVIDIA GeForce GT 1030", wgpu::DeviceType::DiscreteGpu);
assert_eq!(
gpu_status_from(Some(gpu.clone()), 100),
GpuStatus::Hardware(gpu)
);
let cpu = adapter("llvmpipe", wgpu::DeviceType::Cpu);
assert_eq!(
gpu_status_from(Some(cpu.clone()), 0),
GpuStatus::Software(cpu)
);
}
#[test]
fn only_degraded_verdicts_carry_an_identity() {
assert!(GpuStatus::Unknown.identity().is_none());
assert!(
GpuStatus::Hardware(adapter("x", wgpu::DeviceType::DiscreteGpu))
.identity()
.is_none()
);
assert_eq!(
GpuStatus::NoRenderer.identity().as_deref(),
Some("no-renderer")
);
let software = GpuStatus::Software(adapter("llvmpipe", wgpu::DeviceType::Cpu));
assert!(software.is_degraded());
assert_eq!(software.identity().as_deref(), Some("software:llvmpipe"));
}
}
impl iced::widget::shader::Pipeline for MultiPipeline {
fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
install_gpu_error_handler(device);
record_gpu_adapter(device);
Self {
block_geometry: Default::default(),
inners: vec![Pipeline::new(device, queue, format)],
format,
slot_by_instance: rustc_hash::FxHashMap::default(),
slot_last_used: vec![0],
slot_clock: 0,
frame_rendered: std::sync::atomic::AtomicBool::new(false),
gpu_error_epoch: gpu_errors_seen(),
wire_buffer_cache: rustc_hash::FxHashMap::default(),
}
}
}
#[cfg(test)]
mod highlight_classification_tests {
use super::*;
use crate::scene::model::wire_model::TangentGeom;
fn plain(name: &str) -> WireModel {
WireModel::solid(
name.to_string(),
vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
[1.0; 4],
false,
)
}
fn circle(name: &str) -> WireModel {
let mut wire = plain(name);
wire.tangent_geoms.push(TangentGeom::PlanarCircle {
center: [0.0, 0.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 2.0,
});
wire
}
fn ellipse(name: &str) -> WireModel {
let mut wire = plain(name);
wire.tangent_geoms.push(TangentGeom::PlanarEllipse {
center: [0.0, 0.0, 0.0],
major_axis: [2.0, 0.0, 0.0],
normal: [0.0, 0.0, 1.0],
minor_axis_ratio: 0.5,
start_param: 0.0,
end_param: std::f64::consts::TAU,
});
wire
}
#[test]
fn each_wire_lands_in_the_bucket_the_old_predicate_chose() {
let wires = [plain("1"), circle("2"), ellipse("3"), plain("4")];
let refs: Vec<&WireModel> = wires.iter().collect();
let depth_map = rustc_hash::FxHashMap::default();
let (circles, ellipses, regular, blocks) =
Pipeline::classify_highlight_wires(&refs, None, &depth_map);
for wire in &refs {
let was_regular = wire.render_instance.is_none()
&& circle_gpu::extract_circle_instances(wire, 0.0).is_none()
&& ellipse_gpu::extract_ellipse_instances(wire, 0.0).is_none();
assert_eq!(
was_regular,
regular.iter().any(|kept| std::ptr::eq(*kept, *wire)),
"wire {} disagrees with the old predicate",
wire.name,
);
}
assert_eq!(regular.len(), 2, "two plain lines");
assert_eq!(circles.len(), 1, "one circle instance");
assert_eq!(ellipses.len(), 1, "one ellipse instance");
assert!(blocks.is_empty(), "no block wires in this sample");
}
// Hover recolours every instance; a selection only recolours when a tint is
// configured. Both go through the same argument, so it has to actually
// reach the instances.
#[test]
fn the_colour_override_reaches_the_instances() {
let wires = [circle("2"), ellipse("3")];
let refs: Vec<&WireModel> = wires.iter().collect();
let depth_map = rustc_hash::FxHashMap::default();
let (tinted_circles, tinted_ellipses, _, _) =
Pipeline::classify_highlight_wires(&refs, Some(WireModel::HOVER), &depth_map);
assert_eq!(tinted_circles[0].color, WireModel::HOVER);
assert_eq!(tinted_ellipses[0].color, WireModel::HOVER);
let (plain_circles, _, _, _) = Pipeline::classify_highlight_wires(&refs, None, &depth_map);
assert_ne!(
plain_circles[0].color,
WireModel::HOVER,
"without a tint the extractor's own colour stands",
);
}
}