refactor(hatch): unify capability backends
Keep storage and texture transports behind one renderer lifecycle so Pipeline no longer branches by backend.
This commit is contained in:
parent
dc7d03dbe1
commit
d80637cda7
7 changed files with 385 additions and 322 deletions
287
src/scene/pipeline/hatch_gpu/mod.rs
Normal file
287
src/scene/pipeline/hatch_gpu/mod.rs
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
//! Capability-selected hatch renderer.
|
||||||
|
//!
|
||||||
|
//! `Pipeline` sees one [`HatchGpu`]. This module owns the renderer selection,
|
||||||
|
//! render pipeline, resident/preview uploads, visibility updates, and draw
|
||||||
|
//! dispatch. The storage and texture transports remain private implementation
|
||||||
|
//! details because their bind groups and upload layouts are fundamentally
|
||||||
|
//! different.
|
||||||
|
|
||||||
|
mod storage;
|
||||||
|
mod texture;
|
||||||
|
|
||||||
|
use super::device_capabilities::DeviceCapabilities;
|
||||||
|
use crate::scene::model::hatch_model::HatchModel;
|
||||||
|
use iced::wgpu;
|
||||||
|
|
||||||
|
use storage::StorageHatchBatch;
|
||||||
|
use texture::TextureHatch;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum HatchBackendKind {
|
||||||
|
Storage,
|
||||||
|
Texture,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HatchBackendKind {
|
||||||
|
fn select(capabilities: DeviceCapabilities, force_compatibility: bool) -> Self {
|
||||||
|
if !force_compatibility && capabilities.supports_batched_hatch() {
|
||||||
|
Self::Storage
|
||||||
|
} else {
|
||||||
|
Self::Texture
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HatchBackend {
|
||||||
|
Storage {
|
||||||
|
resident: Option<StorageHatchBatch>,
|
||||||
|
preview: Option<StorageHatchBatch>,
|
||||||
|
},
|
||||||
|
Texture {
|
||||||
|
resident: Vec<TextureHatch>,
|
||||||
|
preview: Vec<TextureHatch>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One hatch renderer whose transport is selected from the active device.
|
||||||
|
pub struct HatchGpu {
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
bind_group_layout: wgpu::BindGroupLayout,
|
||||||
|
backend: HatchBackend,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HatchGpu {
|
||||||
|
pub fn new(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
format: wgpu::TextureFormat,
|
||||||
|
frame_bind_group_layout: &wgpu::BindGroupLayout,
|
||||||
|
content_stencil: &wgpu::StencilState,
|
||||||
|
capabilities: DeviceCapabilities,
|
||||||
|
force_compatibility: bool,
|
||||||
|
) -> Self {
|
||||||
|
let backend_kind = HatchBackendKind::select(capabilities, force_compatibility);
|
||||||
|
let uses_storage = backend_kind == HatchBackendKind::Storage;
|
||||||
|
let bind_group_layout = if uses_storage {
|
||||||
|
StorageHatchBatch::bind_group_layout(device)
|
||||||
|
} else {
|
||||||
|
TextureHatch::bind_group_layout(device)
|
||||||
|
};
|
||||||
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("hatch.pipeline_layout"),
|
||||||
|
bind_group_layouts: &[frame_bind_group_layout, &bind_group_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some(if uses_storage {
|
||||||
|
"hatch.storage.shader"
|
||||||
|
} else {
|
||||||
|
"hatch.texture.shader"
|
||||||
|
}),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(if uses_storage {
|
||||||
|
include_str!("../../../shaders/hatch.wgsl")
|
||||||
|
} else {
|
||||||
|
include_str!("../../../shaders/hatch_texture.wgsl")
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
let vertex_layout = if uses_storage {
|
||||||
|
storage::HatchVertex::layout()
|
||||||
|
} else {
|
||||||
|
texture::vertex_layout()
|
||||||
|
};
|
||||||
|
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some(if uses_storage {
|
||||||
|
"hatch.storage.pipeline"
|
||||||
|
} else {
|
||||||
|
"hatch.texture.pipeline"
|
||||||
|
}),
|
||||||
|
layout: Some(&pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[vertex_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: true,
|
||||||
|
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||||
|
stencil: content_stencil.clone(),
|
||||||
|
bias: wgpu::DepthBiasState {
|
||||||
|
constant: 1,
|
||||||
|
slope_scale: 1.0,
|
||||||
|
clamp: 0.0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: super::MSAA_SAMPLES,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &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: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
let backend = match backend_kind {
|
||||||
|
HatchBackendKind::Storage => HatchBackend::Storage {
|
||||||
|
resident: None,
|
||||||
|
preview: None,
|
||||||
|
},
|
||||||
|
HatchBackendKind::Texture => HatchBackend::Texture {
|
||||||
|
resident: Vec::new(),
|
||||||
|
preview: Vec::new(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
pipeline,
|
||||||
|
bind_group_layout,
|
||||||
|
backend,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn backend_name(&self) -> &'static str {
|
||||||
|
match self.backend {
|
||||||
|
HatchBackend::Storage { .. } => "storage",
|
||||||
|
HatchBackend::Texture { .. } => "texture",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, hatches: &[HatchModel]) {
|
||||||
|
match &mut self.backend {
|
||||||
|
HatchBackend::Storage { resident, .. } => {
|
||||||
|
let renderable: Vec<HatchModel> = hatches
|
||||||
|
.iter()
|
||||||
|
.filter(|hatch| hatch.boundary.len() >= 3)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
*resident = StorageHatchBatch::build(device, &self.bind_group_layout, &renderable);
|
||||||
|
}
|
||||||
|
HatchBackend::Texture { resident, .. } => {
|
||||||
|
*resident = hatches
|
||||||
|
.iter()
|
||||||
|
.filter(|hatch| hatch.boundary.len() >= 3)
|
||||||
|
.map(|hatch| TextureHatch::new(device, queue, hatch, &self.bind_group_layout))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upload_preview(
|
||||||
|
&mut self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
hatches: &[HatchModel],
|
||||||
|
) {
|
||||||
|
match &mut self.backend {
|
||||||
|
HatchBackend::Storage { preview, .. } => {
|
||||||
|
let renderable: Vec<HatchModel> = hatches
|
||||||
|
.iter()
|
||||||
|
.filter(|hatch| hatch.boundary.len() >= 3)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
*preview = StorageHatchBatch::build(device, &self.bind_group_layout, &renderable);
|
||||||
|
}
|
||||||
|
HatchBackend::Texture { preview, .. } => {
|
||||||
|
*preview = hatches
|
||||||
|
.iter()
|
||||||
|
.filter(|hatch| hatch.boundary.len() >= 3)
|
||||||
|
.map(|hatch| TextureHatch::new(device, queue, hatch, &self.bind_group_layout))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh resident hatch visibility. Texture hatches retain their existing
|
||||||
|
/// per-draw behavior and return zero because they have no visibility buffer.
|
||||||
|
pub fn update_visibility(
|
||||||
|
&mut self,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
mut is_visible: impl FnMut([f32; 4]) -> bool,
|
||||||
|
) -> usize {
|
||||||
|
let HatchBackend::Storage {
|
||||||
|
resident: Some(batch),
|
||||||
|
..
|
||||||
|
} = &mut self.backend
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
for (index, aabb) in batch.instance_aabbs.iter().copied().enumerate() {
|
||||||
|
batch.visibility[index] = u32::from(is_visible(aabb));
|
||||||
|
}
|
||||||
|
batch.upload_visibility(queue);
|
||||||
|
batch.instance_aabbs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw<'pass>(
|
||||||
|
&'pass self,
|
||||||
|
pass: &mut wgpu::RenderPass<'pass>,
|
||||||
|
frame_bind_group: &'pass wgpu::BindGroup,
|
||||||
|
stencil_reference: u32,
|
||||||
|
) {
|
||||||
|
pass.set_pipeline(&self.pipeline);
|
||||||
|
pass.set_bind_group(0, frame_bind_group, &[]);
|
||||||
|
pass.set_stencil_reference(stencil_reference);
|
||||||
|
match &self.backend {
|
||||||
|
HatchBackend::Storage { resident, preview } => {
|
||||||
|
for batch in [resident.as_ref(), preview.as_ref()].into_iter().flatten() {
|
||||||
|
pass.set_bind_group(1, &batch.bind_group, &[]);
|
||||||
|
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
|
||||||
|
pass.draw(0..batch.vertex_count, 0..1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HatchBackend::Texture { resident, preview } => {
|
||||||
|
for hatch in resident.iter().chain(preview) {
|
||||||
|
pass.set_bind_group(1, &hatch.bind_group, &[]);
|
||||||
|
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
|
||||||
|
pass.draw(0..6, 0..1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{DeviceCapabilities, HatchBackendKind};
|
||||||
|
|
||||||
|
fn capabilities(storage_bindings: u32) -> DeviceCapabilities {
|
||||||
|
DeviceCapabilities {
|
||||||
|
max_storage_buffers_per_shader_stage: storage_bindings,
|
||||||
|
max_inter_stage_shader_components: 31,
|
||||||
|
max_vertex_attributes: 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selects_backend_from_device_limits() {
|
||||||
|
assert_eq!(
|
||||||
|
HatchBackendKind::select(capabilities(5), false),
|
||||||
|
HatchBackendKind::Storage
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
HatchBackendKind::select(capabilities(4), false),
|
||||||
|
HatchBackendKind::Texture
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compatibility_override_selects_texture_backend() {
|
||||||
|
assert_eq!(
|
||||||
|
HatchBackendKind::select(capabilities(8), true),
|
||||||
|
HatchBackendKind::Texture
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -126,13 +126,13 @@ const _: () = assert!(std::mem::size_of::<LineFamilyGpu>() == 48);
|
||||||
/// failed to tessellate emit an AABB quad here instead (see `build`).
|
/// failed to tessellate emit an AABB quad here instead (see `build`).
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct HatchVertex {
|
pub(super) struct HatchVertex {
|
||||||
pub local_xy: [f32; 2], // 0 — local-space position
|
pub local_xy: [f32; 2], // 0 — local-space position
|
||||||
pub instance_index: u32, // 8 — index into InstanceBuffer
|
pub instance_index: u32, // 8 — index into InstanceBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HatchVertex {
|
impl HatchVertex {
|
||||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
pub(super) fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||||
wgpu::VertexBufferLayout {
|
wgpu::VertexBufferLayout {
|
||||||
array_stride: std::mem::size_of::<HatchVertex>() as u64,
|
array_stride: std::mem::size_of::<HatchVertex>() as u64,
|
||||||
step_mode: wgpu::VertexStepMode::Vertex,
|
step_mode: wgpu::VertexStepMode::Vertex,
|
||||||
|
|
@ -158,7 +158,7 @@ impl HatchVertex {
|
||||||
/// buffers + the per-vertex buffer needed by `hatch.wgsl`.
|
/// buffers + the per-vertex buffer needed by `hatch.wgsl`.
|
||||||
/// Returns `None` when the input slice is empty (caller skips the
|
/// Returns `None` when the input slice is empty (caller skips the
|
||||||
/// hatch render pass entirely).
|
/// hatch render pass entirely).
|
||||||
pub struct HatchGpu {
|
pub(super) struct StorageHatchBatch {
|
||||||
pub vertex_buffer: wgpu::Buffer,
|
pub vertex_buffer: wgpu::Buffer,
|
||||||
pub vertex_count: u32,
|
pub vertex_count: u32,
|
||||||
// The four storage buffers below are referenced via `bind_group` —
|
// The four storage buffers below are referenced via `bind_group` —
|
||||||
|
|
@ -190,12 +190,12 @@ pub struct HatchGpu {
|
||||||
pub instance_aabbs: Vec<[f32; 4]>,
|
pub instance_aabbs: Vec<[f32; 4]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HatchGpu {
|
impl StorageHatchBatch {
|
||||||
/// One-time build from the full hatch list. Re-uploaded only when
|
/// One-time build from the full hatch list. Re-uploaded only when
|
||||||
/// `geometry_epoch` advances (mirrors the existing per-hatch
|
/// `geometry_epoch` advances (mirrors the existing per-hatch
|
||||||
/// upload trigger). Per-frame visibility flips go through
|
/// upload trigger). Per-frame visibility flips go through
|
||||||
/// [`upload_visibility`].
|
/// [`upload_visibility`].
|
||||||
pub fn build(
|
pub(super) fn build(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
bgl: &wgpu::BindGroupLayout,
|
bgl: &wgpu::BindGroupLayout,
|
||||||
hatches: &[HatchModel],
|
hatches: &[HatchModel],
|
||||||
|
|
@ -257,7 +257,7 @@ impl HatchGpu {
|
||||||
}
|
}
|
||||||
let n_dashes = (dashes.len() as u32 - dash_offset).min(u32::MAX);
|
let n_dashes = (dashes.len() as u32 - dash_offset).min(u32::MAX);
|
||||||
// QCAD PAT local-frame convention (mirrors
|
// QCAD PAT local-frame convention (mirrors
|
||||||
// `build_family_batch` in hatch_gpu.rs): `dy` is
|
// the storage batch convention: `dy` is
|
||||||
// the perpendicular spacing, `dx` is the along-line
|
// the perpendicular spacing, `dx` is the along-line
|
||||||
// phase shift — both in family-local coords. The
|
// phase shift — both in family-local coords. The
|
||||||
// shader applies cos_off/sin_off to rotate them.
|
// shader applies cos_off/sin_off to rotate them.
|
||||||
|
|
@ -501,7 +501,7 @@ impl HatchGpu {
|
||||||
|
|
||||||
/// Push the CPU `visibility` slice to GPU. Call when any
|
/// Push the CPU `visibility` slice to GPU. Call when any
|
||||||
/// element changes (typically per-frame from compute_hatch_lod).
|
/// element changes (typically per-frame from compute_hatch_lod).
|
||||||
pub fn upload_visibility(&self, queue: &wgpu::Queue) {
|
pub(super) fn upload_visibility(&self, queue: &wgpu::Queue) {
|
||||||
queue.write_buffer(
|
queue.write_buffer(
|
||||||
&self.visibility_buffer,
|
&self.visibility_buffer,
|
||||||
0,
|
0,
|
||||||
|
|
@ -514,7 +514,7 @@ impl HatchGpu {
|
||||||
/// storage and visible to both VS (AABB+visibility lookup) and FS
|
/// storage and visible to both VS (AABB+visibility lookup) and FS
|
||||||
/// (boundary / family / dash sampling).
|
/// (boundary / family / dash sampling).
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
|
pub(super) fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
|
||||||
let entry = |binding: u32| wgpu::BindGroupLayoutEntry {
|
let entry = |binding: u32| wgpu::BindGroupLayoutEntry {
|
||||||
binding,
|
binding,
|
||||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
// Storage-free hatch renderer — texture-backed, UNCAPPED.
|
// Storage-free hatch renderer — texture-backed, UNCAPPED.
|
||||||
//
|
//
|
||||||
// WebGL2 has no storage buffers; some native adapters also expose insufficient
|
// Devices without storage buffers use this per-hatch renderer. It reuses the
|
||||||
// limits. This per-hatch renderer reuses the WebGL2-safe hatch algorithm (see
|
// storage-free hatch algorithm (see wipeout.wgsl / hatch_texture.wgsl) but packs the
|
||||||
// wipeout.wgsl / hatch_web.wgsl) but packs the
|
|
||||||
// variable-length boundary / family / dash arrays into ONE RGBA32F data texture
|
// variable-length boundary / family / dash arrays into ONE RGBA32F data texture
|
||||||
// read via textureLoad — removing the MAX_FAMILIES / MAX_HATCH_BOUNDARY_VERTS /
|
// read via textureLoad — removing the MAX_FAMILIES / MAX_HATCH_BOUNDARY_VERTS /
|
||||||
// MAX_DASHES caps of the uniform (WipeoutGpu) path. Every hatch type — solid,
|
// MAX_DASHES caps of the uniform (WipeoutGpu) path. Every hatch type — solid,
|
||||||
// gradient, and arbitrarily complex line patterns — renders in compat mode.
|
// gradient, and arbitrarily complex line patterns — renders in compat mode.
|
||||||
//
|
//
|
||||||
// Storage-capable devices use hatch_gpu.rs; wipeout masks use wipeout_gpu.rs.
|
// Storage-capable devices use the sibling storage backend; wipeout masks use
|
||||||
|
// wipeout_gpu.rs.
|
||||||
|
|
||||||
use crate::scene::model::hatch_model::{HatchModel, HatchPattern};
|
use crate::scene::model::hatch_model::{HatchModel, HatchPattern};
|
||||||
use iced::wgpu;
|
use iced::wgpu;
|
||||||
|
|
@ -27,12 +27,24 @@ struct HatchVertex {
|
||||||
_pad: f32,
|
_pad: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn vertex_layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||||
|
wgpu::VertexBufferLayout {
|
||||||
|
array_stride: std::mem::size_of::<HatchVertex>() as u64,
|
||||||
|
step_mode: wgpu::VertexStepMode::Vertex,
|
||||||
|
attributes: &[wgpu::VertexAttribute {
|
||||||
|
offset: 0,
|
||||||
|
shader_location: 0,
|
||||||
|
format: wgpu::VertexFormat::Float32x3,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Per-hatch uniform (binding 0) — 96 bytes, matches HatchUniforms in
|
// ── Per-hatch uniform (binding 0) — 96 bytes, matches HatchUniforms in
|
||||||
// hatch_web.wgsl. ─────────────────────────────────────────────────────────
|
// hatch_texture.wgsl. ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
struct HatchWebUniform {
|
struct TextureHatchUniform {
|
||||||
color: [f32; 4], // 0
|
color: [f32; 4], // 0
|
||||||
color2: [f32; 4], // 16
|
color2: [f32; 4], // 16
|
||||||
mode: u32, // 32
|
mode: u32, // 32
|
||||||
|
|
@ -53,9 +65,9 @@ struct HatchWebUniform {
|
||||||
|
|
||||||
// ── Per-hatch GPU handle ────────────────────────────────────────────────────
|
// ── Per-hatch GPU handle ────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct HatchWebGpu {
|
pub(super) struct TextureHatch {
|
||||||
pub vertex_buffer: wgpu::Buffer,
|
pub(super) vertex_buffer: wgpu::Buffer,
|
||||||
pub bind_group: wgpu::BindGroup,
|
pub(super) bind_group: wgpu::BindGroup,
|
||||||
/// Reserved for per-frame AABB LOD (mirrors `WipeoutGpu`); not yet wired
|
/// Reserved for per-frame AABB LOD (mirrors `WipeoutGpu`); not yet wired
|
||||||
/// into the web hatch draw loop — the native batched path doesn't do
|
/// into the web hatch draw loop — the native batched path doesn't do
|
||||||
/// per-hatch LOD either.
|
/// per-hatch LOD either.
|
||||||
|
|
@ -65,12 +77,12 @@ pub struct HatchWebGpu {
|
||||||
_data_tex: wgpu::Texture,
|
_data_tex: wgpu::Texture,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HatchWebGpu {
|
impl TextureHatch {
|
||||||
/// Group-1 layout: uniform header (binding 0) + non-filterable float data
|
/// Group-1 layout: uniform header (binding 0) + non-filterable float data
|
||||||
/// texture (binding 1).
|
/// texture (binding 1).
|
||||||
pub fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
|
pub(super) fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
|
||||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
label: Some("hatch_web.bgl1"),
|
label: Some("hatch.texture.bgl1"),
|
||||||
entries: &[
|
entries: &[
|
||||||
wgpu::BindGroupLayoutEntry {
|
wgpu::BindGroupLayoutEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
|
|
@ -96,7 +108,7 @@ impl HatchWebGpu {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub(super) fn new(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
queue: &wgpu::Queue,
|
queue: &wgpu::Queue,
|
||||||
model: &HatchModel,
|
model: &HatchModel,
|
||||||
|
|
@ -143,7 +155,7 @@ impl HatchWebGpu {
|
||||||
let pad = (diag * 0.8 + max_spacing * 2.0 * model.scale).max(1.0);
|
let pad = (diag * 0.8 + max_spacing * 2.0 * model.scale).max(1.0);
|
||||||
|
|
||||||
// Anchor pattern phase at `world_origin` with the boundary stored raw,
|
// Anchor pattern phase at `world_origin` with the boundary stored raw,
|
||||||
// matching the desktop batched renderer (hatch_gpu.rs) — NOT WipeoutGpu,
|
// matching the storage backend — NOT WipeoutGpu,
|
||||||
// whose f64 origin grid-snap is dead code (wipeouts are always solid)
|
// whose f64 origin grid-snap is dead code (wipeouts are always solid)
|
||||||
// and would phase-shift every line pattern relative to desktop. No drift.
|
// and would phase-shift every line pattern relative to desktop. No drift.
|
||||||
let origin = model.world_origin;
|
let origin = model.world_origin;
|
||||||
|
|
@ -164,7 +176,7 @@ impl HatchWebGpu {
|
||||||
HatchVertex { pos: [x0, y1, 0.0], _pad: 0.0 },
|
HatchVertex { pos: [x0, y1, 0.0], _pad: 0.0 },
|
||||||
];
|
];
|
||||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
label: Some("hatch_web.vbuf"),
|
label: Some("hatch.texture.vbuf"),
|
||||||
contents: bytemuck::cast_slice(&quad),
|
contents: bytemuck::cast_slice(&quad),
|
||||||
usage: wgpu::BufferUsages::VERTEX,
|
usage: wgpu::BufferUsages::VERTEX,
|
||||||
});
|
});
|
||||||
|
|
@ -182,7 +194,7 @@ impl HatchWebGpu {
|
||||||
} else {
|
} else {
|
||||||
let proj_min = projs.iter().cloned().fold(f32::INFINITY, f32::min);
|
let proj_min = projs.iter().cloned().fold(f32::INFINITY, f32::min);
|
||||||
let proj_max = projs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
|
let proj_max = projs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
|
||||||
// Floor matches the desktop hatch renderer (hatch_gpu.rs).
|
// Floor matches the storage backend.
|
||||||
(proj_min, (proj_max - proj_min).max(1.0))
|
(proj_min, (proj_max - proj_min).max(1.0))
|
||||||
}
|
}
|
||||||
} else if mode == 3 {
|
} else if mode == 3 {
|
||||||
|
|
@ -260,7 +272,7 @@ impl HatchWebGpu {
|
||||||
let height = ((texels.len() as u32).div_ceil(width)).max(1);
|
let height = ((texels.len() as u32).div_ceil(width)).max(1);
|
||||||
texels.resize((width * height) as usize, [0.0; 4]);
|
texels.resize((width * height) as usize, [0.0; 4]);
|
||||||
let data_tex = device.create_texture(&wgpu::TextureDescriptor {
|
let data_tex = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
label: Some("hatch_web.data_tex"),
|
label: Some("hatch.texture.data_tex"),
|
||||||
size: wgpu::Extent3d {
|
size: wgpu::Extent3d {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
|
@ -290,7 +302,7 @@ impl HatchWebGpu {
|
||||||
let tex_view = data_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
let tex_view = data_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
// ── Uniform header ────────────────────────────────────────────────
|
// ── Uniform header ────────────────────────────────────────────────
|
||||||
let uniform_data = HatchWebUniform {
|
let uniform_data = TextureHatchUniform {
|
||||||
color: model.color,
|
color: model.color,
|
||||||
color2,
|
color2,
|
||||||
mode,
|
mode,
|
||||||
|
|
@ -314,13 +326,13 @@ impl HatchWebGpu {
|
||||||
tex_width: width,
|
tex_width: width,
|
||||||
};
|
};
|
||||||
let _uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
let _uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
label: Some("hatch_web.uniform"),
|
label: Some("hatch.texture.uniform"),
|
||||||
contents: bytemuck::bytes_of(&uniform_data),
|
contents: bytemuck::bytes_of(&uniform_data),
|
||||||
usage: wgpu::BufferUsages::UNIFORM,
|
usage: wgpu::BufferUsages::UNIFORM,
|
||||||
});
|
});
|
||||||
|
|
||||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
label: Some("hatch_web.bind_group1"),
|
label: Some("hatch.texture.bind_group1"),
|
||||||
layout: bgl1,
|
layout: bgl1,
|
||||||
entries: &[
|
entries: &[
|
||||||
wgpu::BindGroupEntry {
|
wgpu::BindGroupEntry {
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
mod device_capabilities;
|
mod device_capabilities;
|
||||||
pub mod face3d_gpu;
|
pub mod face3d_gpu;
|
||||||
pub mod hatch_gpu;
|
pub mod hatch_gpu;
|
||||||
/// Texture-backed compatibility hatch renderer. Used on WebGL2 and on native
|
|
||||||
/// adapters that cannot support the storage-buffer batch.
|
|
||||||
pub mod hatch_web_gpu;
|
|
||||||
pub mod wipeout_gpu;
|
pub mod wipeout_gpu;
|
||||||
pub mod image_gpu;
|
pub mod image_gpu;
|
||||||
pub mod mesh_gpu;
|
pub mod mesh_gpu;
|
||||||
|
|
@ -91,14 +88,9 @@ pub struct Pipeline {
|
||||||
/// compatibility mode. Passed to `WireGpu::from_run` / `from_batch`.
|
/// compatibility mode. Passed to `WireGpu::from_run` / `from_batch`.
|
||||||
pub(crate) wire_const_bgl: Option<wgpu::BindGroupLayout>,
|
pub(crate) wire_const_bgl: Option<wgpu::BindGroupLayout>,
|
||||||
wipeout_pipeline: wgpu::RenderPipeline,
|
wipeout_pipeline: wgpu::RenderPipeline,
|
||||||
/// Phase 4-B — single-draw batched hatch pipeline. Per-instance
|
/// Capability-selected hatch renderer. Storage and texture transports are
|
||||||
/// data lives in storage buffers; one draw call covers every
|
/// private backends behind one upload/LOD/draw lifecycle.
|
||||||
/// hatch in the frame. `None` in compatibility mode.
|
hatch_gpu: hatch_gpu::HatchGpu,
|
||||||
hatch_pipeline: Option<wgpu::RenderPipeline>,
|
|
||||||
/// Texture-backed compatibility hatch pipeline + its group-1 layout.
|
|
||||||
/// Present whenever the storage-buffer batch is unavailable or forced off.
|
|
||||||
hatch_compat_bgl1: Option<wgpu::BindGroupLayout>,
|
|
||||||
hatch_compat_pipeline: Option<wgpu::RenderPipeline>,
|
|
||||||
image_pipeline: wgpu::RenderPipeline,
|
image_pipeline: wgpu::RenderPipeline,
|
||||||
/// SDF text-quad pipeline (Phase 2b): draws per-glyph quads sampling the
|
/// 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).
|
/// shared glyph atlas. Fed only when `OCS_TEXT_SDF` is set (else no verts).
|
||||||
|
|
@ -140,11 +132,6 @@ pub struct Pipeline {
|
||||||
uniform_buffer: wgpu::Buffer,
|
uniform_buffer: wgpu::Buffer,
|
||||||
uniform_bind_group: wgpu::BindGroup,
|
uniform_bind_group: wgpu::BindGroup,
|
||||||
wipeout_bgl1: wgpu::BindGroupLayout,
|
wipeout_bgl1: wgpu::BindGroupLayout,
|
||||||
/// Group-1 layout for the batched hatch pipeline (storage buffers
|
|
||||||
/// for instances / boundary / families / dashes). `None` in compatibility
|
|
||||||
/// mode, where hatches render through `hatch_web_gpu`.
|
|
||||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
|
||||||
hatch_bgl1: Option<wgpu::BindGroupLayout>,
|
|
||||||
image_bgl1: wgpu::BindGroupLayout,
|
image_bgl1: wgpu::BindGroupLayout,
|
||||||
/// Group-1 layout for the text pipeline (atlas texture + sampler).
|
/// Group-1 layout for the text pipeline (atlas texture + sampler).
|
||||||
text_atlas_bgl: wgpu::BindGroupLayout,
|
text_atlas_bgl: wgpu::BindGroupLayout,
|
||||||
|
|
@ -245,16 +232,6 @@ pub struct Pipeline {
|
||||||
/// frame they are present (small), drawn on top of the base wire pass — so
|
/// frame they are present (small), drawn on top of the base wire pass — so
|
||||||
/// a live drag never re-uploads the resident base buffer.
|
/// a live drag never re-uploads the resident base buffer.
|
||||||
gpu_preview_wires: Vec<WireGpu>,
|
gpu_preview_wires: Vec<WireGpu>,
|
||||||
/// The canonical hatch fills — a single batched GPU resource drawn in one
|
|
||||||
/// call with per-instance visibility masking the rest. All pattern line
|
|
||||||
/// families are uploaded (no cap). `None` in compatibility mode.
|
|
||||||
gpu_hatch: Option<hatch_gpu::HatchGpu>,
|
|
||||||
/// Compatibility hatch fills — one texture-backed GPU object per hatch.
|
|
||||||
gpu_hatches_compat: Vec<hatch_web_gpu::HatchWebGpu>,
|
|
||||||
/// Tiny live hatch batch used by grip previews. Separate from the resident
|
|
||||||
/// batch so an interactive edit uploads only the hatch being changed.
|
|
||||||
gpu_preview_hatch: Option<hatch_gpu::HatchGpu>,
|
|
||||||
gpu_preview_hatches_compat: Vec<hatch_web_gpu::HatchWebGpu>,
|
|
||||||
/// Wipeout masks — solid fills rendered after wires in a separate pass via
|
/// Wipeout masks — solid fills rendered after wires in a separate pass via
|
||||||
/// the legacy per-primitive `WipeoutGpu` renderer.
|
/// the legacy per-primitive `WipeoutGpu` renderer.
|
||||||
gpu_wipeouts: Vec<WipeoutGpu>,
|
gpu_wipeouts: Vec<WipeoutGpu>,
|
||||||
|
|
@ -398,32 +375,8 @@ impl Pipeline {
|
||||||
let force_compat_renderer = crate::cli::gui_config().compat_renderer;
|
let force_compat_renderer = crate::cli::gui_config().compat_renderer;
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let force_compat_renderer = false;
|
let force_compat_renderer = false;
|
||||||
let wire_mode = wire_gpu::WirePipelineMode::select(
|
let wire_mode =
|
||||||
device_caps,
|
wire_gpu::WirePipelineMode::select(device_caps, force_compat_renderer);
|
||||||
force_compat_renderer,
|
|
||||||
);
|
|
||||||
let hatch_uses_storage =
|
|
||||||
!force_compat_renderer && device_caps.supports_batched_hatch();
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
if std::env::var_os("RUST_LOG").is_some() {
|
|
||||||
eprintln!(
|
|
||||||
"renderer pipelines: wire={} hatch={} mesh={} compute-cull={} (storage buffers/stage: {})",
|
|
||||||
if wire_mode.uses_storage() { "storage" } else { "packed" },
|
|
||||||
if hatch_uses_storage { "storage" } else { "texture" },
|
|
||||||
if device_caps.supports_mesh_storage_instancing() { "storage" } else { "uniform" },
|
|
||||||
if device_caps.supports_mesh_compute_culling() { "gpu" } else { "cpu" },
|
|
||||||
device.limits().max_storage_buffers_per_shader_stage
|
|
||||||
);
|
|
||||||
}
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
log::info!(
|
|
||||||
"renderer pipelines: wire={} hatch={} mesh={} compute-cull={} (storage buffers/stage: {})",
|
|
||||||
if wire_mode.uses_storage() { "storage" } else { "packed" },
|
|
||||||
if hatch_uses_storage { "storage" } else { "texture" },
|
|
||||||
if device_caps.supports_mesh_storage_instancing() { "storage" } else { "uniform" },
|
|
||||||
if device_caps.supports_mesh_compute_culling() { "gpu" } else { "cpu" },
|
|
||||||
device.limits().max_storage_buffers_per_shader_stage
|
|
||||||
);
|
|
||||||
let wire_const_bgl = wire_mode
|
let wire_const_bgl = wire_mode
|
||||||
.uses_storage()
|
.uses_storage()
|
||||||
.then(|| wire_gpu::WireConst::bind_group_layout(device));
|
.then(|| wire_gpu::WireConst::bind_group_layout(device));
|
||||||
|
|
@ -761,138 +714,36 @@ impl Pipeline {
|
||||||
cache: None,
|
cache: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Hatch pipelines ────────────────────────────────────────────────
|
// ── Hatch renderer ─────────────────────────────────────────────────
|
||||||
// Fast mode batches every hatch through five storage buffers. Compat
|
// The façade owns capability selection plus both backend lifecycles.
|
||||||
// mode uses one data texture per hatch and therefore needs no storage.
|
let hatch_gpu = hatch_gpu::HatchGpu::new(
|
||||||
let (hatch_bgl1, hatch_pipeline) = if hatch_uses_storage {
|
device,
|
||||||
let hatch_bgl1 = hatch_gpu::HatchGpu::bind_group_layout(device);
|
format,
|
||||||
let hatch_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
&frame_bgl,
|
||||||
label: Some("hatch.pipeline_layout"),
|
&content_stencil,
|
||||||
bind_group_layouts: &[&frame_bgl, &hatch_bgl1],
|
device_caps,
|
||||||
push_constant_ranges: &[],
|
force_compat_renderer,
|
||||||
});
|
);
|
||||||
let hatch_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
label: Some("hatch.shader"),
|
if std::env::var_os("RUST_LOG").is_some() {
|
||||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
|
eprintln!(
|
||||||
"../../shaders/hatch.wgsl"
|
"renderer pipelines: wire={} hatch={} mesh={} compute-cull={} (storage buffers/stage: {})",
|
||||||
))),
|
if wire_mode.uses_storage() { "storage" } else { "packed" },
|
||||||
});
|
hatch_gpu.backend_name(),
|
||||||
let hatch_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
if device_caps.supports_mesh_storage_instancing() { "storage" } else { "uniform" },
|
||||||
label: Some("hatch.pipeline"),
|
if device_caps.supports_mesh_compute_culling() { "gpu" } else { "cpu" },
|
||||||
layout: Some(&hatch_layout),
|
device.limits().max_storage_buffers_per_shader_stage
|
||||||
vertex: wgpu::VertexState {
|
);
|
||||||
module: &hatch_shader,
|
}
|
||||||
entry_point: Some("vs_main"),
|
#[cfg(target_arch = "wasm32")]
|
||||||
buffers: &[hatch_gpu::HatchVertex::layout()],
|
log::info!(
|
||||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
"renderer pipelines: wire={} hatch={} mesh={} compute-cull={} (storage buffers/stage: {})",
|
||||||
},
|
if wire_mode.uses_storage() { "storage" } else { "packed" },
|
||||||
primitive: wgpu::PrimitiveState {
|
hatch_gpu.backend_name(),
|
||||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
if device_caps.supports_mesh_storage_instancing() { "storage" } else { "uniform" },
|
||||||
cull_mode: None,
|
if device_caps.supports_mesh_compute_culling() { "gpu" } else { "cpu" },
|
||||||
..Default::default()
|
device.limits().max_storage_buffers_per_shader_stage
|
||||||
},
|
);
|
||||||
depth_stencil: Some(wgpu::DepthStencilState {
|
|
||||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
|
||||||
depth_write_enabled: true,
|
|
||||||
depth_compare: 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: &hatch_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: None,
|
|
||||||
cache: None,
|
|
||||||
});
|
|
||||||
(Some(hatch_bgl1), Some(hatch_pipeline))
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
let (hatch_compat_bgl1, hatch_compat_pipeline) = if !hatch_uses_storage {
|
|
||||||
let bgl1 = hatch_web_gpu::HatchWebGpu::bind_group_layout(device);
|
|
||||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
|
||||||
label: Some("hatch_compat.pipeline_layout"),
|
|
||||||
bind_group_layouts: &[&frame_bgl, &bgl1],
|
|
||||||
push_constant_ranges: &[],
|
|
||||||
});
|
|
||||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
||||||
label: Some("hatch_compat.shader"),
|
|
||||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
|
|
||||||
"../../shaders/hatch_web.wgsl"
|
|
||||||
))),
|
|
||||||
});
|
|
||||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
||||||
label: Some("hatch_compat.pipeline"),
|
|
||||||
layout: Some(&layout),
|
|
||||||
vertex: wgpu::VertexState {
|
|
||||||
module: &shader,
|
|
||||||
entry_point: Some("vs_main"),
|
|
||||||
buffers: &[wgpu::VertexBufferLayout {
|
|
||||||
array_stride: 16,
|
|
||||||
step_mode: wgpu::VertexStepMode::Vertex,
|
|
||||||
attributes: &[wgpu::VertexAttribute {
|
|
||||||
offset: 0,
|
|
||||||
shader_location: 0,
|
|
||||||
format: wgpu::VertexFormat::Float32x3,
|
|
||||||
}],
|
|
||||||
}],
|
|
||||||
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: true,
|
|
||||||
depth_compare: 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: &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: None,
|
|
||||||
cache: None,
|
|
||||||
});
|
|
||||||
(Some(bgl1), Some(pipeline))
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Mesh pipeline ──────────────────────────────────────────────────
|
// ── Mesh pipeline ──────────────────────────────────────────────────
|
||||||
let mesh_storage_instancing = device_caps.supports_mesh_storage_instancing();
|
let mesh_storage_instancing = device_caps.supports_mesh_storage_instancing();
|
||||||
|
|
@ -1758,9 +1609,7 @@ impl Pipeline {
|
||||||
wire_xray_pipeline,
|
wire_xray_pipeline,
|
||||||
wire_const_bgl,
|
wire_const_bgl,
|
||||||
wipeout_pipeline,
|
wipeout_pipeline,
|
||||||
hatch_pipeline,
|
hatch_gpu,
|
||||||
hatch_compat_bgl1,
|
|
||||||
hatch_compat_pipeline,
|
|
||||||
image_pipeline,
|
image_pipeline,
|
||||||
text_pipeline,
|
text_pipeline,
|
||||||
text_highlight_pipeline,
|
text_highlight_pipeline,
|
||||||
|
|
@ -1798,7 +1647,6 @@ impl Pipeline {
|
||||||
uniform_buffer,
|
uniform_buffer,
|
||||||
uniform_bind_group,
|
uniform_bind_group,
|
||||||
wipeout_bgl1,
|
wipeout_bgl1,
|
||||||
hatch_bgl1,
|
|
||||||
image_bgl1,
|
image_bgl1,
|
||||||
depth_texture_size: Size::new(1, 1),
|
depth_texture_size: Size::new(1, 1),
|
||||||
// (0, 0) forces the first `ensure_depth_texture` to allocate at the
|
// (0, 0) forces the first `ensure_depth_texture` to allocate at the
|
||||||
|
|
@ -1827,10 +1675,6 @@ impl Pipeline {
|
||||||
clip_boundary: None,
|
clip_boundary: None,
|
||||||
gpu_selected_wires: vec![],
|
gpu_selected_wires: vec![],
|
||||||
gpu_preview_wires: vec![],
|
gpu_preview_wires: vec![],
|
||||||
gpu_hatch: None,
|
|
||||||
gpu_hatches_compat: vec![],
|
|
||||||
gpu_preview_hatch: None,
|
|
||||||
gpu_preview_hatches_compat: vec![],
|
|
||||||
gpu_wipeouts: vec![],
|
gpu_wipeouts: vec![],
|
||||||
wipeout_skip_flags: vec![],
|
wipeout_skip_flags: vec![],
|
||||||
gpu_images: vec![],
|
gpu_images: vec![],
|
||||||
|
|
@ -2352,22 +2196,20 @@ impl Pipeline {
|
||||||
clip_h: u32,
|
clip_h: u32,
|
||||||
) {
|
) {
|
||||||
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
||||||
let Some(batch) = &mut self.gpu_hatch else {
|
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;
|
return;
|
||||||
};
|
|
||||||
for (i, aabb) in batch.instance_aabbs.iter().enumerate() {
|
|
||||||
let skip = aabb_below_pixel(*aabb, view_rot, eye, clip_w, clip_h, 2.0)
|
|
||||||
|| aabb_offscreen(*aabb, view_rot, eye, clip_w, clip_h);
|
|
||||||
batch.visibility[i] = if skip { 0 } else { 1 };
|
|
||||||
}
|
}
|
||||||
batch.upload_visibility(queue);
|
|
||||||
if let Some(started) = perf_started {
|
if let Some(started) = perf_started {
|
||||||
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||||
if elapsed_ms >= 1.0 {
|
if elapsed_ms >= 1.0 {
|
||||||
crate::perf_record!(
|
crate::perf_record!(
|
||||||
"[perf] hatch-lod {:>7.1}ms instances={}",
|
"[perf] hatch-lod {:>7.1}ms instances={}",
|
||||||
elapsed_ms,
|
elapsed_ms,
|
||||||
batch.instance_aabbs.len(),
|
instance_count,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2851,32 +2693,18 @@ impl Pipeline {
|
||||||
hatches: &[HatchModel],
|
hatches: &[HatchModel],
|
||||||
) {
|
) {
|
||||||
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
||||||
if let Some(bgl1) = &self.hatch_compat_bgl1 {
|
let renderable_count = hatches
|
||||||
self.gpu_hatches_compat = hatches
|
.iter()
|
||||||
.iter()
|
.filter(|hatch| hatch.boundary.len() >= 3)
|
||||||
.filter(|h| h.boundary.len() >= 3)
|
.count();
|
||||||
.map(|h| hatch_web_gpu::HatchWebGpu::new(device, queue, h, bgl1))
|
self.hatch_gpu.upload(device, queue, hatches);
|
||||||
.collect();
|
|
||||||
self.gpu_hatch = None;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.gpu_hatches_compat.clear();
|
|
||||||
let _ = queue;
|
|
||||||
let Some(bgl1) = &self.hatch_bgl1 else {
|
|
||||||
self.gpu_hatch = None;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let renderable: Vec<HatchModel> =
|
|
||||||
hatches.iter().filter(|h| h.boundary.len() >= 3).cloned().collect();
|
|
||||||
self.gpu_hatch = hatch_gpu::HatchGpu::build(device, bgl1, &renderable);
|
|
||||||
if let Some(started) = perf_started {
|
if let Some(started) = perf_started {
|
||||||
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||||
if elapsed_ms >= 1.0 {
|
if elapsed_ms >= 1.0 {
|
||||||
crate::perf_record!(
|
crate::perf_record!(
|
||||||
"[perf] hatch-upload {:>7.1}ms models={}",
|
"[perf] hatch-upload {:>7.1}ms models={}",
|
||||||
elapsed_ms,
|
elapsed_ms,
|
||||||
renderable.len(),
|
renderable_count,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2888,27 +2716,7 @@ impl Pipeline {
|
||||||
queue: &wgpu::Queue,
|
queue: &wgpu::Queue,
|
||||||
hatches: &[HatchModel],
|
hatches: &[HatchModel],
|
||||||
) {
|
) {
|
||||||
if let Some(bgl1) = &self.hatch_compat_bgl1 {
|
self.hatch_gpu.upload_preview(device, queue, hatches);
|
||||||
self.gpu_preview_hatches_compat = hatches
|
|
||||||
.iter()
|
|
||||||
.filter(|h| h.boundary.len() >= 3)
|
|
||||||
.map(|h| hatch_web_gpu::HatchWebGpu::new(device, queue, h, bgl1))
|
|
||||||
.collect();
|
|
||||||
self.gpu_preview_hatch = None;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.gpu_preview_hatches_compat.clear();
|
|
||||||
let Some(bgl1) = &self.hatch_bgl1 else {
|
|
||||||
self.gpu_preview_hatch = None;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let renderable: Vec<HatchModel> = hatches
|
|
||||||
.iter()
|
|
||||||
.filter(|h| h.boundary.len() >= 3)
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
self.gpu_preview_hatch = hatch_gpu::HatchGpu::build(device, bgl1, &renderable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upload_wipeouts(&mut self, device: &wgpu::Device, wipeouts: &[HatchModel]) {
|
pub fn upload_wipeouts(&mut self, device: &wgpu::Device, wipeouts: &[HatchModel]) {
|
||||||
|
|
@ -3077,57 +2885,13 @@ impl Pipeline {
|
||||||
pass.set_vertex_buffer(0, vbuf.slice(..));
|
pass.set_vertex_buffer(0, vbuf.slice(..));
|
||||||
pass.draw(0..*vcount, 0..1);
|
pass.draw(0..*vcount, 0..1);
|
||||||
}
|
}
|
||||||
// Phase 4-B — single batched draw covers every hatch.
|
// The capability-selected façade dispatches storage or texture
|
||||||
// Vertex shader culls per-instance via the `visibility`
|
// draws before wires so outlines remain on top in either backend.
|
||||||
// buffer (sub-pixel LOD + frustum cull written each frame
|
// Skipped while navigating because per-pixel hatch work dominates
|
||||||
// by `compute_hatch_lod`). Per-hatch viewport scissor
|
// hatch-heavy drawings.
|
||||||
// (paper-space MSPACE) isn't ported to the batched path
|
|
||||||
// yet — follow-up if it shows up as a visual issue.
|
|
||||||
if let (Some(batch), Some(pipeline)) =
|
|
||||||
(&self.gpu_hatch, &self.hatch_pipeline)
|
|
||||||
{
|
|
||||||
// Skipped while navigating (interaction LOD) — the per-pixel
|
|
||||||
// hatch pass dominates the GPU frame on hatch-heavy drawings.
|
|
||||||
if !self.skip_hatch_frame {
|
|
||||||
pass.set_pipeline(pipeline);
|
|
||||||
pass.set_stencil_reference(stencil_ref);
|
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
|
||||||
pass.set_bind_group(1, &batch.bind_group, &[]);
|
|
||||||
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
|
|
||||||
pass.draw(0..batch.vertex_count, 0..1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let (Some(batch), Some(pipeline)) =
|
|
||||||
(&self.gpu_preview_hatch, &self.hatch_pipeline)
|
|
||||||
{
|
|
||||||
if !self.skip_hatch_frame {
|
|
||||||
pass.set_pipeline(pipeline);
|
|
||||||
pass.set_stencil_reference(stencil_ref);
|
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
|
||||||
pass.set_bind_group(1, &batch.bind_group, &[]);
|
|
||||||
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
|
|
||||||
pass.draw(0..batch.vertex_count, 0..1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storage-free compatibility path. Draw before wires so outlines
|
|
||||||
// remain on top, matching the batched path.
|
|
||||||
if !self.skip_hatch_frame {
|
if !self.skip_hatch_frame {
|
||||||
if let Some(pipeline) = &self.hatch_compat_pipeline {
|
self.hatch_gpu
|
||||||
pass.set_pipeline(pipeline);
|
.draw(&mut pass, &self.uniform_bind_group, stencil_ref);
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
|
||||||
pass.set_stencil_reference(stencil_ref);
|
|
||||||
for hatch in &self.gpu_hatches_compat {
|
|
||||||
pass.set_bind_group(1, &hatch.bind_group, &[]);
|
|
||||||
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
|
|
||||||
pass.draw(0..6, 0..1);
|
|
||||||
}
|
|
||||||
for hatch in &self.gpu_preview_hatches_compat {
|
|
||||||
pass.set_bind_group(1, &hatch.bind_group, &[]);
|
|
||||||
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
|
|
||||||
pass.draw(0..6, 0..1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Wipeout GPU buffers — the legacy per-primitive fill renderer, now used ONLY
|
// Wipeout GPU buffers — the legacy per-primitive fill renderer, now used ONLY
|
||||||
// for wipeout masks (solid fills drawn after wires to hide them). Real hatch
|
// for wipeout masks (solid fills drawn after wires to hide them). Real hatch
|
||||||
// fills go through the canonical `hatch_gpu.rs` batched renderer.
|
// fills go through the capability-selected `hatch_gpu` renderer.
|
||||||
//
|
//
|
||||||
// It retains a general pattern/gradient capability (mode 0/2 below) and the
|
// It retains a general pattern/gradient capability (mode 0/2 below) and the
|
||||||
// `MAX_FAMILIES = 16` cap, but wipeouts are always solid (mode 1, zero pattern
|
// `MAX_FAMILIES = 16` cap, but wipeouts are always solid (mode 1, zero pattern
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
// positions so we don't depend on @builtin(instance_index) edge cases
|
// positions so we don't depend on @builtin(instance_index) edge cases
|
||||||
// across backends).
|
// across backends).
|
||||||
//
|
//
|
||||||
// Layout — matches `hatch_gpu.rs`:
|
// Layout — matches `hatch_gpu/storage.rs`:
|
||||||
// group 1 binding 0 InstanceBuffer HatchInstance[] (128 B / inst)
|
// group 1 binding 0 InstanceBuffer HatchInstance[] (128 B / inst)
|
||||||
// group 1 binding 1 BoundaryBuffer vec4<f32>[] (xy in .xy)
|
// group 1 binding 1 BoundaryBuffer vec4<f32>[] (xy in .xy)
|
||||||
// group 1 binding 2 FamilyBuffer LineFamilyGpu[] (48 B / fam)
|
// group 1 binding 2 FamilyBuffer LineFamilyGpu[] (48 B / fam)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Hatch shader (WebGL2) — texture-backed, UNCAPPED variant of wipeout.wgsl.
|
// Hatch texture shader — storage-free, UNCAPPED variant of wipeout.wgsl.
|
||||||
//
|
//
|
||||||
// Devices without vertex/fragment storage buffers cannot use the batched
|
// Devices without vertex/fragment storage buffers cannot use the batched
|
||||||
// storage-buffer renderer (hatch.wgsl). This shader keeps the exact
|
// storage-buffer renderer (hatch.wgsl). This shader keeps the exact
|
||||||
Loading…
Reference in a new issue