diff --git a/src/scene/pipeline/gpu_tests.rs b/src/scene/pipeline/gpu_tests.rs index 232393f6c..def4a18fc 100644 --- a/src/scene/pipeline/gpu_tests.rs +++ b/src/scene/pipeline/gpu_tests.rs @@ -30,19 +30,20 @@ fn block_edits_preserve_cache_coordinates_and_arena_partition() { let depth = rustc_hash::FxHashMap::from_iter([(1, [0.1, 0.01]), (2, [0.2, 0.01])]); let original = pipeline.upload_block_wires(&device, &queue, &[&first, &second], &depth); let unchanged = pipeline.upload_block_wires(&device, &queue, &[&first, &second], &depth); - assert_eq!(original[0].vertex_buffer, unchanged[0].vertex_buffer); + assert_eq!(original[0].geometry_id(), unchanged[0].geometry_id()); assert_eq!(original[0].instance_count, 2); let removed = pipeline.upload_block_wires(&device, &queue, &[&second], &depth); assert_ne!( - original[0].vertex_buffer, removed[0].vertex_buffer, + original[0].geometry_id(), + removed[0].geometry_id(), "removing the base instance must replace vertices baked at its old position" ); let moved = block(2, 30.0); let moved_gpu = pipeline.upload_block_wires(&device, &queue, &[&moved], &depth); - assert_ne!(removed[0].vertex_buffer, moved_gpu[0].vertex_buffer); + assert_ne!(removed[0].geometry_id(), moved_gpu[0].geometry_id()); let restored = pipeline.upload_block_wires(&device, &queue, &[&first, &second], &depth); - assert_ne!(moved_gpu[0].vertex_buffer, restored[0].vertex_buffer); + assert_ne!(moved_gpu[0].geometry_id(), restored[0].geometry_id()); assert_eq!(restored[0].instance_count, 2); assert_eq!( pipeline.block_geometry.len(), diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index df3e32a4d..a98627f59 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -94,6 +94,10 @@ pub struct Pipeline { /// compatibility mode. Passed to `WireGpu::from_run` / `from_batch`. pub(crate) wire_const_bgl: Option, 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. @@ -581,7 +585,8 @@ impl Pipeline { bind_group_layouts: &wire_bgls, immediate_size: 0, }); - let block_wire_const_bgl = wire_gpu::block_const_bind_group_layout(device); + 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)], @@ -602,9 +607,13 @@ impl Pipeline { }); let block_wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("block_wire.shader"), - source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!( - "../../shaders/block_wire.wgsl" - ))), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + if wire_mode.uses_storage() { + include_str!("../../shaders/block_wire_storage.wgsl") + } else { + include_str!("../../shaders/block_wire.wgsl") + }, + )), }); // Stencil test shared by every paper content pipeline: draw only where @@ -880,6 +889,14 @@ impl Pipeline { 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, @@ -892,10 +909,10 @@ impl Pipeline { vertex: wgpu::VertexState { module: &block_wire_shader, entry_point: Some("vs_main"), - buffers: &[ - wire_gpu::BlockWireVertex::layout(), - wire_gpu::BlockWireInstance::layout(), - ], + // 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 { @@ -2215,6 +2232,7 @@ impl Pipeline { block_wire_xray_pipeline, wire_const_bgl, block_wire_const_bgl, + block_wire_mode: wire_mode, wipeout_pipeline, hatch_gpu, image_pipeline, @@ -2349,7 +2367,10 @@ impl Pipeline { wires, depth_map, None, - &self.block_wire_const_bgl, + wire_gpu::BlockWireTarget { + const_bgl: &self.block_wire_const_bgl, + mode: self.block_wire_mode, + }, Some(&mut self.block_geometry), ) } @@ -2415,7 +2436,10 @@ impl Pipeline { &block_wires, depth_map, None, - &self.block_wire_const_bgl, + wire_gpu::BlockWireTarget { + const_bgl: &self.block_wire_const_bgl, + mode: self.block_wire_mode, + }, Some(&mut self.block_geometry), ); @@ -2554,7 +2578,10 @@ impl Pipeline { &selected_blocks, depth_map, Some(tint), - &self.block_wire_const_bgl, + wire_gpu::BlockWireTarget { + const_bgl: &self.block_wire_const_bgl, + mode: self.block_wire_mode, + }, None, ) } else { @@ -2566,7 +2593,10 @@ impl Pipeline { &hover_blocks, depth_map, Some(WireModel::HOVER), - &self.block_wire_const_bgl, + wire_gpu::BlockWireTarget { + const_bgl: &self.block_wire_const_bgl, + mode: self.block_wire_mode, + }, None, )); self.gpu_selected_block_wires = block_gpu; @@ -4288,10 +4318,7 @@ impl Pipeline { }); block_black_active = use_black; } - pass.set_bind_group(1, wire.const_bind_group.as_ref(), &[]); - pass.set_vertex_buffer(0, wire.vertex_buffer.slice(..)); - pass.set_vertex_buffer(1, wire.instance_buffer.slice(..)); - pass.draw(0..wire.vertex_count, 0..wire.instance_count); + bind_and_draw_block_wire(&mut pass, wire); } // Live overlay wires (command preview / interim / grip drag) always // on top: the xray pipeline (depth_compare=Always, no depth write) @@ -4480,10 +4507,7 @@ impl Pipeline { if wire.instance_count == 0 { continue; } - pass.set_bind_group(1, wire.const_bind_group.as_ref(), &[]); - pass.set_vertex_buffer(0, wire.vertex_buffer.slice(..)); - pass.set_vertex_buffer(1, wire.instance_buffer.slice(..)); - pass.draw(0..wire.vertex_count, 0..wire.instance_count); + bind_and_draw_block_wire(&mut pass, wire); } } if let Some(atlas) = &self.text_atlas_gpu { @@ -4705,6 +4729,24 @@ fn aabb_below_pixel( (max_px - min_px).max(max_py - min_py) < threshold_px } +/// 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. +fn bind_and_draw_block_wire(pass: &mut wgpu::RenderPass<'_>, wire: &wire_gpu::BlockWireGpu) { + pass.set_bind_group(1, wire.const_bind_group.as_ref(), &[]); + match &wire.vertex_buffer { + Some(vertices) => { + pass.set_vertex_buffer(0, vertices.slice(..)); + pass.set_vertex_buffer(1, wire.instance_buffer.slice(..)); + } + None => pass.set_vertex_buffer(0, wire.instance_buffer.slice(..)), + } + pass.draw(0..wire.vertex_count, 0..wire.instance_count); +} + fn create_depth_texture(device: &wgpu::Device, size: Size) -> wgpu::Texture { device.create_texture(&wgpu::TextureDescriptor { label: Some("viewer.depth_texture"), diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 01e382fb2..722452b87 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -334,7 +334,9 @@ impl BlockWireInstance { #[derive(Clone)] pub struct BlockWireGpu { - pub vertex_buffer: wgpu::Buffer, + /// Packed mode only; `None` in storage mode, where the segments reach the + /// shader through `const_bind_group` instead. + pub vertex_buffer: Option, pub instance_buffer: wgpu::Buffer, pub vertex_count: u32, pub instance_count: u32, @@ -342,19 +344,38 @@ pub struct BlockWireGpu { pub const_bind_group: std::sync::Arc, } -pub fn block_const_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("block_wire_const.bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, +/// Binding 0 is the definition's constants. In storage mode binding 1 carries +/// that chunk's segments, which is why the bind group is per-chunk there and +/// shared across chunks in packed mode. +pub fn block_const_bind_group_layout( + device: &wgpu::Device, + uses_storage: bool, +) -> wgpu::BindGroupLayout { + let mut entries = vec![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, + }]; + if uses_storage { + entries.push(wgpu::BindGroupLayoutEntry { + binding: 1, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, + ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None, }, count: None, - }], + }); + } + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("block_wire_const.bgl"), + entries: &entries, }) } @@ -669,23 +690,64 @@ pub(crate) fn wire_draw_depth( /// Vertices use the first instance's world coordinates, so changing that base /// must invalidate the geometry as well as rebuild the relative placements. pub type BlockGeometryCache = rustc_hash::FxHashMap< - (u64, bool, [u32; 4], [u64; 3]), - ( - std::sync::Arc>, - std::sync::Arc, - ), + BlockGeometryKey, + std::sync::Arc>, >; +/// How this device draws block wires. The layout and the mode are decided +/// together when the pipeline is built and are never chosen independently, so +/// they travel as one argument. +#[derive(Clone, Copy)] +pub struct BlockWireTarget<'a> { + pub const_bgl: &'a wgpu::BindGroupLayout, + pub mode: WirePipelineMode, +} + +/// `(source_id, mesh_edge, colour bits, base translation bits)`. +pub type BlockGeometryKey = (u64, bool, [u32; 4], [u64; 3]); + +/// One drawable slice of a block definition's geometry. +/// +/// The two pipeline modes put the segments in different places, so the chunk +/// carries whichever the mode produced: +/// +/// * packed — `vertex_buffer` holds six copies of each segment, one per corner +/// of its quad, and `bind_group` is the definition's constants alone; +/// * storage — the segments are in a read-only storage buffer inside +/// `bind_group`, one copy each, and `vertex_buffer` is `None`. +/// +/// `vertex_count` is six per segment either way: in storage mode the shader +/// divides `vertex_index` by six to find its segment. +pub struct BlockGeometryChunk { + pub vertex_buffer: Option, + pub vertex_count: u32, + pub bind_group: std::sync::Arc, +} + impl BlockWireGpu { + /// Identity of the geometry this batch draws, comparable across pipeline + /// modes. + /// + /// Packed mode keeps the segments in `vertex_buffer`; storage mode keeps + /// them inside `const_bind_group`, which is per-chunk there, and leaves + /// `vertex_buffer` `None`. Both are built on the same cache miss and + /// reused together on a hit, so the pair answers "is this the same + /// geometry as before?" in either mode — which `vertex_buffer` alone + /// cannot once it is always `None`. + pub fn geometry_id(&self) -> (Option<&wgpu::Buffer>, &wgpu::BindGroup) { + (self.vertex_buffer.as_ref(), self.const_bind_group.as_ref()) + } + pub fn from_wires( device: &wgpu::Device, queue: &wgpu::Queue, wires: &[&WireModel], depth_map: &rustc_hash::FxHashMap, color_override: Option<[f32; 4]>, - const_bgl: &wgpu::BindGroupLayout, + target: BlockWireTarget<'_>, mut cache: Option<&mut BlockGeometryCache>, ) -> Vec { + let BlockWireTarget { const_bgl, mode } = target; let mut slots: rustc_hash::FxHashMap<(u64, bool), usize> = rustc_hash::FxHashMap::default(); let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new(); @@ -710,7 +772,12 @@ impl BlockWireGpu { } let mut out = Vec::new(); - let mut live_keys = rustc_hash::FxHashSet::default(); + let mut live_keys: rustc_hash::FxHashSet = + rustc_hash::FxHashSet::default(); + // Geometry bytes actually sent to the device this call, so the storage + // path's saving is visible rather than assumed. + let mut uploaded_bytes = 0usize; + let mut uploaded_segments = 0usize; for (mesh_edge, group) in groups { let Some(&source) = group.first() else { continue; @@ -727,21 +794,13 @@ impl BlockWireGpu { ); live_keys.insert(cache_key); // Reuse this definition's geometry when it is already on the GPU. - if let Some((chunks, bind_group)) = cache + if let Some(chunks) = cache .as_deref() .and_then(|c| c.get(&cache_key)) - .map(|(v, b)| (std::sync::Arc::clone(v), std::sync::Arc::clone(b))) + .map(std::sync::Arc::clone) { let instances = block_instances(&group, base, mesh_edge, depth_map); - push_block_batches( - &mut out, - device, - queue, - &chunks, - &instances, - mesh_edge, - &bind_group, - ); + push_block_batches(&mut out, device, queue, &chunks, &instances, mesh_edge); continue; } let (segments, mut constant) = emit_wire_native(source, 0, color, 0.0); @@ -749,34 +808,15 @@ impl BlockWireGpu { continue; } constant.draw_depth = 0.0; - let mut vertices = Vec::with_capacity(segments.len() * 6); - for segment in segments { - let vertex = BlockWireVertex { + let records: Vec = segments + .iter() + .map(|segment| BlockWireVertex { pos_a: segment.pos_a, pos_a_low: segment.pos_a_low, pos_b: segment.pos_b, pos_b_low: segment.pos_b_low, distances: [segment.distance_a, segment.distance_b], taper_ratio: segment.taper_ratio, - }; - vertices.extend_from_slice(&[vertex; 6]); - } - // Bound uploads without splitting a segment's six vertices. - let max_verts = - super::gpu_budget::max_elements_grouped::(device, 6); - let vertex_chunks: Vec<(wgpu::Buffer, u32)> = vertices - .chunks(max_verts) - .map(|chunk| { - ( - super::gpu_upload::upload_buffer( - device, - queue, - "block_wire.vertices", - chunk, - wgpu::BufferUsages::VERTEX, - ), - chunk.len() as u32, - ) }) .collect(); // Queue uploads avoid mapping a failed allocation. @@ -787,41 +827,106 @@ impl BlockWireGpu { std::slice::from_ref(&constant), wgpu::BufferUsages::UNIFORM, ); - let const_bind_group = std::sync::Arc::new(device.create_bind_group( - &wgpu::BindGroupDescriptor { - label: Some("block_wire.const.bg"), - layout: const_bgl, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: const_buffer.as_entire_binding(), - }], - }, - )); - let vertex_chunks = std::sync::Arc::new(vertex_chunks); + // Bound uploads so a block-heavy drawing cannot ask a low-VRAM + // device for one multi-hundred-MB buffer. + let chunks: Vec = if mode.uses_storage() { + // One record per segment. A storage chunk is bounded by the + // binding size as well as the buffer budget, and needs no + // grouping: the shader indexes segments directly. + let max_records = + super::gpu_budget::max_storage_elements::(device); + records + .chunks(max_records) + .map(|chunk| { + let segments = super::gpu_upload::upload_buffer( + device, + queue, + "block_wire.segments", + chunk, + wgpu::BufferUsages::STORAGE, + ); + BlockGeometryChunk { + vertex_buffer: None, + vertex_count: chunk.len() as u32 * 6, + bind_group: std::sync::Arc::new(device.create_bind_group( + &wgpu::BindGroupDescriptor { + label: Some("block_wire.const.bg"), + layout: const_bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: const_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: segments.as_entire_binding(), + }, + ], + }, + )), + } + }) + .collect() + } else { + // Six copies per segment, one per corner of its quad, without + // splitting a segment's six vertices across two buffers. + let mut vertices = Vec::with_capacity(records.len() * 6); + for record in &records { + vertices.extend_from_slice(&[*record; 6]); + } + let max_verts = + super::gpu_budget::max_elements_grouped::(device, 6); + let shared = std::sync::Arc::new(device.create_bind_group( + &wgpu::BindGroupDescriptor { + label: Some("block_wire.const.bg"), + layout: const_bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: const_buffer.as_entire_binding(), + }], + }, + )); + vertices + .chunks(max_verts) + .map(|chunk| BlockGeometryChunk { + vertex_buffer: Some(super::gpu_upload::upload_buffer( + device, + queue, + "block_wire.vertices", + chunk, + wgpu::BufferUsages::VERTEX, + )), + vertex_count: chunk.len() as u32, + bind_group: std::sync::Arc::clone(&shared), + }) + .collect() + }; + uploaded_segments += records.len(); + uploaded_bytes += records.len() + * std::mem::size_of::() + * if mode.uses_storage() { 1 } else { 6 }; + let chunks = std::sync::Arc::new(chunks); if let Some(cache) = cache.as_deref_mut() { - cache.insert( - cache_key, - ( - std::sync::Arc::clone(&vertex_chunks), - std::sync::Arc::clone(&const_bind_group), - ), - ); + cache.insert(cache_key, std::sync::Arc::clone(&chunks)); } let instances = block_instances(&group, base, mesh_edge, depth_map); - push_block_batches( - &mut out, - device, - queue, - &vertex_chunks, - &instances, - mesh_edge, - &const_bind_group, - ); + push_block_batches(&mut out, device, queue, &chunks, &instances, mesh_edge); } // Release geometry that this build no longer uses. if let Some(cache) = cache { cache.retain(|key, _| live_keys.contains(key)); } + if crate::perf::enabled() && uploaded_segments > 0 { + crate::perf_record!( + "[perf] block-wire-geometry mode={} definitions={} segments={} bytes={} \ +bytes_if_packed={}", + if mode.uses_storage() { "storage" } else { "packed" }, + live_keys.len(), + uploaded_segments, + uploaded_bytes, + uploaded_segments * std::mem::size_of::() * 6, + ); + } out } } @@ -863,16 +968,15 @@ fn block_instances( .collect() } -/// One batch per (vertex chunk × instance chunk). Instance buffers are built -/// once and shared across vertex chunks rather than re-uploaded per chunk. +/// One batch per (geometry chunk × instance chunk). Instance buffers are built +/// once and shared across geometry chunks rather than re-uploaded per chunk. fn push_block_batches( out: &mut Vec, device: &wgpu::Device, queue: &wgpu::Queue, - vertex_chunks: &[(wgpu::Buffer, u32)], + chunks: &[BlockGeometryChunk], instances: &[BlockWireInstance], mesh_edge: bool, - const_bind_group: &std::sync::Arc, ) { let max_instances = super::gpu_budget::max_elements::(device); let instance_chunks: Vec<(wgpu::Buffer, u32)> = instances @@ -884,15 +988,15 @@ fn push_block_batches( ) }) .collect(); - for (vertex_buffer, vertex_count) in vertex_chunks { + for chunk in chunks { for (instance_buffer, instance_count) in &instance_chunks { out.push(BlockWireGpu { - vertex_buffer: vertex_buffer.clone(), + vertex_buffer: chunk.vertex_buffer.clone(), instance_buffer: instance_buffer.clone(), - vertex_count: *vertex_count, + vertex_count: chunk.vertex_count, instance_count: *instance_count, is_3d_mesh_edge: mesh_edge, - const_bind_group: const_bind_group.clone(), + const_bind_group: std::sync::Arc::clone(&chunk.bind_group), }); } } @@ -1271,3 +1375,60 @@ impl WireGpu { .collect() } } + +#[cfg(test)] +mod block_wire_storage_tests { + use super::BlockWireVertex; + + fn module(source: &str) -> naga::Module { + naga::front::wgsl::parse_str(source).expect("shader parses") + } + + fn validate(module: &naga::Module) -> naga::valid::ModuleInfo { + naga::valid::Validator::new( + naga::valid::ValidationFlags::all(), + naga::valid::Capabilities::all(), + ) + .validate(module) + .expect("shader validates") + } + + /// A layout mismatch here is invisible until a block draws as garbage on a + /// real device, so the stride is checked against the Rust record instead. + #[test] + fn segment_struct_matches_the_rust_record() { + let module = module(include_str!("../../shaders/block_wire_storage.wgsl")); + validate(&module); + let mut layouter = naga::proc::Layouter::default(); + layouter + .update(module.to_ctx()) + .expect("layouts are computable"); + let (handle, _) = module + .types + .iter() + .find(|(_, ty)| ty.name.as_deref() == Some("Segment")) + .expect("Segment is declared"); + assert_eq!( + layouter[handle].size as usize, + std::mem::size_of::(), + "WGSL Segment and Rust BlockWireVertex must agree byte for byte; \ + a vec3 member would pad to 16 and break this", + ); + assert_eq!(layouter[handle].alignment * 1u32, 4); + } + + /// The two shaders must stay interchangeable: same entry points, same + /// fragment behaviour. Only the geometry source differs. + #[test] + fn both_block_wire_shaders_expose_the_same_entry_points() { + let packed = module(include_str!("../../shaders/block_wire.wgsl")); + let storage = module(include_str!("../../shaders/block_wire_storage.wgsl")); + validate(&packed); + let names = |m: &naga::Module| { + let mut n: Vec = m.entry_points.iter().map(|e| e.name.clone()).collect(); + n.sort(); + n + }; + assert_eq!(names(&packed), names(&storage)); + } +} diff --git a/src/shaders/block_wire_storage.wgsl b/src/shaders/block_wire_storage.wgsl new file mode 100644 index 000000000..98cda4857 --- /dev/null +++ b/src/shaders/block_wire_storage.wgsl @@ -0,0 +1,323 @@ +// Block-wire vertex shader, storage-buffer variant. +// +// `block_wire.wgsl` carries the segment endpoints as vertex attributes, and a +// segment must reach the shader once per corner of its quad, so every record +// is written to VRAM six times: 360 bytes a segment. On a block-heavy drawing +// that is the buffer that runs a 2 GB card out of memory — the OOM names +// `block_wire.vertices` directly. +// +// Here the segments live in a read-only storage buffer indexed by +// `vertex_index / 6`, so each record is stored once: 60 bytes a segment. The +// two shaders are otherwise identical, and must stay so. +// +// `Segment` is declared with scalar fields rather than `vec3`, which +// would align to 16 bytes in storage and reintroduce padding. All-f32 members +// give it align 4 and stride 60, matching `BlockWireVertex` byte for byte. +// +// Requires storage buffers in the vertex stage; callers select +// `block_wire.wgsl` when `DeviceCapabilities::supports_wire_storage` is false. + +struct Uniforms { + viewport_size: vec2, + world_per_pixel: f32, + lwdisplay_enable: f32, + flat_shade: f32, + transparency_enable: f32, + linetype_scale: f32, + lineweight_scale: f32, + view_rot: mat4x4, + eye_high: vec3, + _pad_eh: f32, + eye_low: vec3, + _pad_el: f32, +} +@group(0) @binding(0) var u: Uniforms; + +struct WireConst { + color: vec4, + pat0: vec4, + pat1: vec4, + half_width: f32, + pattern_length: f32, + draw_depth: f32, + align_end: f32, + align_total: f32, + world_half_width: f32, + _pad1: f32, + _pad2: f32, + marker_origin_high: vec4, + marker_origin_low: vec4, + marker_normal_scale: vec4, +} +@group(1) @binding(0) var wire_const: WireConst; + +struct Segment { + pos_a_x: f32, pos_a_y: f32, pos_a_z: f32, + pos_a_low_x: f32, pos_a_low_y: f32, pos_a_low_z: f32, + pos_b_x: f32, pos_b_y: f32, pos_b_z: f32, + pos_b_low_x: f32, pos_b_low_y: f32, pos_b_low_z: f32, + distance_a: f32, distance_b: f32, + // Two `u16` unorms packed into one word, as `Unorm16x2` delivered them. + taper_ratio: u32, +} +@group(1) @binding(1) var segments: array; + +struct VertexIn { + @location(6) translation: vec3, + @location(7) translation_low: vec3, + @location(8) depth: vec2, +} + +const DRAW_ORDER_BIAS: f32 = 0.001; +const MODEL_LINEWEIGHT_BOOST: f32 = 2.0; +const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0; + +struct VertexOut { + @builtin(position) clip_pos: vec4, + @location(0) color: vec4, + @location(1) distance: f32, + @location(2) pattern_length: f32, + @location(3) pat0: vec4, + @location(4) pat1: vec4, + @location(5) @interpolate(flat) min_elem: f32, + @location(6) @interpolate(flat) align_end: f32, + @location(7) @interpolate(flat) align_total: f32, + @location(8) cap: vec2, + @location(9) @interpolate(flat) cap_ends: vec3, +} + +fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { + if taper_ratio > 0.0 { + return max((taper_ratio * world_hw) / u.world_per_pixel, 0.5); + } + if world_hw > 0.0 { + return max(world_hw / u.world_per_pixel, 0.5); + } + var display_hw = max(px_hw * u.lineweight_scale, 0.5); + if u.lineweight_scale < 0.0 { + let scale = -u.lineweight_scale; + let base_hw = select( + min(px_hw * MODEL_LINEWEIGHT_BOOST, MODEL_LINEWEIGHT_MAX_PX * 0.5), + 0.5, + px_hw <= 0.5, + ); + display_hw = max(base_hw * scale, 0.5); + } + return select(0.5, display_hw, u.lwdisplay_enable > 0.5); +} + +fn marker_relative( + position_high: vec3, + position_low: vec3, + translation: vec3, + translation_low: vec3, +) -> vec3 { + if wire_const.marker_normal_scale.w <= 0.0 { + return (position_high + translation - u.eye_high) + + (position_low + translation_low - u.eye_low); + } + let origin_high = wire_const.marker_origin_high.xyz; + let origin_low = wire_const.marker_origin_low.xyz; + let origin_relative = (origin_high + translation - u.eye_high) + + (origin_low + translation_low - u.eye_low); + let delta = (position_high - origin_high) + (position_low - origin_low); + let normal = normalize(wire_const.marker_normal_scale.xyz); + let axial = normal * dot(delta, normal); + let planar = delta - axial; + let origin_clip = u.view_rot * vec4(origin_relative, 1.0); + let projection_scale = max(length(vec3( + u.view_rot[0].y, + u.view_rot[1].y, + u.view_rot[2].y, + )), 1e-12); + let view_height = 2.0 * max(abs(origin_clip.w), 1e-6) / projection_scale; + let world_size = wire_const.marker_normal_scale.w * 0.01 * view_height; + return origin_relative + axial + planar * world_size; +} + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut { + let corner = vertex_index % 6u; + let seg = segments[vertex_index / 6u]; + let seg_pos_a = vec3(seg.pos_a_x, seg.pos_a_y, seg.pos_a_z); + let seg_pos_a_low = vec3(seg.pos_a_low_x, seg.pos_a_low_y, seg.pos_a_low_z); + let seg_pos_b = vec3(seg.pos_b_x, seg.pos_b_y, seg.pos_b_z); + let seg_pos_b_low = vec3(seg.pos_b_low_x, seg.pos_b_low_y, seg.pos_b_low_z); + let seg_distances = vec2(seg.distance_a, seg.distance_b); + let seg_taper_ratio = unpack2x16unorm(seg.taper_ratio); + let which_end_arr = array(0.0, 1.0, 1.0, 0.0, 1.0, 0.0); + let side_arr = array(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0); + let which_end = which_end_arr[corner]; + let side = side_arr[corner]; + + let rel_a = marker_relative(seg_pos_a, seg_pos_a_low, in.translation, in.translation_low); + let rel_b = marker_relative(seg_pos_b, seg_pos_b_low, in.translation, in.translation_low); + let clip_a = u.view_rot * vec4(rel_a, 1.0); + let clip_b = u.view_rot * vec4(rel_b, 1.0); + let screen_a = clip_a.xy / clip_a.w * u.viewport_size * 0.5; + let screen_b = clip_b.xy / clip_b.w * u.viewport_size * 0.5; + let segment = screen_b - screen_a; + let segment_length = length(segment); + var direction = vec2(1.0, 0.0); + if segment_length > 1e-4 { + direction = segment / segment_length; + } + let perpendicular = vec2(-direction.y, direction.x); + let clip_position = mix(clip_a, clip_b, which_end); + let half_width_a = resolve_hw( + seg_taper_ratio.x, + wire_const.world_half_width, + wire_const.half_width, + ); + let half_width_b = resolve_hw( + seg_taper_ratio.y, + wire_const.world_half_width, + wire_const.half_width, + ); + let half_width = mix(half_width_a, half_width_b, which_end); + let extension = which_end * 2.0 - 1.0; + let offset_px = perpendicular * half_width * side + + direction * half_width * extension; + let ndc_offset = offset_px / (u.viewport_size * 0.5); + let final_clip = clip_position + + vec4(ndc_offset * clip_position.w, 0.0, 0.0); + + let scale = u.linetype_scale; + var min_element = wire_const.pattern_length * scale; + let elements = array( + wire_const.pat0.x * scale, + wire_const.pat0.y * scale, + wire_const.pat0.z * scale, + wire_const.pat0.w * scale, + wire_const.pat1.x * scale, + wire_const.pat1.y * scale, + wire_const.pat1.z * scale, + wire_const.pat1.w * scale, + ); + for (var i = 0u; i < 8u; i++) { + let value = abs(elements[i]); + if value > 0.0 && value < min_element { + min_element = value; + } + } + + var out: VertexOut; + out.clip_pos = final_clip; + out.clip_pos.z -= in.depth.x * DRAW_ORDER_BIAS * out.clip_pos.w; + out.color = wire_const.color; + out.distance = mix(seg_distances.x, seg_distances.y, which_end) + + extension * half_width * u.world_per_pixel; + out.pattern_length = wire_const.pattern_length * scale; + out.pat0 = wire_const.pat0 * scale; + out.pat1 = wire_const.pat1 * scale; + out.min_elem = min_element; + out.align_end = wire_const.align_end * scale; + out.align_total = wire_const.align_total; + out.cap = vec2( + which_end * segment_length + extension * half_width, + half_width * side, + ); + out.cap_ends = vec3(segment_length, half_width_a, half_width_b); + return out; +} + +fn in_dash( + distance: f32, + pattern_length: f32, + pat0: vec4, + pat1: vec4, + align_end: f32, + align_total: f32, +) -> bool { + let elements = array( + pat0.x, pat0.y, pat0.z, pat0.w, + pat1.x, pat1.y, pat1.z, pat1.w, + ); + var count = 0u; + for (var i = 0u; i < 8u; i++) { + if elements[i] != 0.0 { + count = i + 1u; + } + } + var value: f32; + if align_total > 0.0 { + if distance <= align_end || distance >= align_total - align_end { + return true; + } + var first_dash = 0.0; + for (var i = 0u; i < count; i++) { + if elements[i] > 0.0 { + first_dash = elements[i]; + break; + } + } + value = ((distance - align_end + first_dash) % pattern_length + + pattern_length) % pattern_length; + } else { + value = ((distance % pattern_length) + pattern_length) % pattern_length; + } + var position = 0.0; + let dot_half = u.world_per_pixel * 0.75; + for (var i = 0u; i < count; i++) { + let element = elements[i]; + if element == 0.0 { + let delta = abs(value - position); + if min(delta, pattern_length - delta) <= dot_half { + return true; + } + } else if element > 0.0 { + if value >= position && value < position + element { + return true; + } + position += element; + } else { + position -= element; + } + } + return false; +} + +fn clipped_cap(cap: vec2, ends: vec3) -> bool { + if cap.x < 0.0 { + return length(cap) > ends.y; + } + if cap.x > ends.x { + return length(vec2(cap.x - ends.x, cap.y)) > ends.z; + } + return false; +} + +fn visible_fragment(in: VertexOut) -> bool { + if clipped_cap(in.cap, in.cap_ends) { + return false; + } + if in.pattern_length > 0.0 && in.min_elem >= u.world_per_pixel { + return in_dash( + in.distance, + in.pattern_length, + in.pat0, + in.pat1, + in.align_end, + in.align_total, + ); + } + return true; +} + +@fragment +fn fs_main(in: VertexOut) -> @location(0) vec4 { + if !visible_fragment(in) { + discard; + } + let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5); + return vec4(in.color.rgb, alpha); +} + +@fragment +fn fs_black(in: VertexOut) -> @location(0) vec4 { + if !visible_fragment(in) { + discard; + } + let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5); + return vec4(0.0, 0.0, 0.0, alpha); +}