feat(pipeline): per-instance visibility for batched hatches (Phase 4-B, step 4)
Adds a dedicated `visibility: array<u32>` storage buffer (binding 4) alongside the existing batched hatch buffers. Vertex shader reads `visibility[v.instance_index]` — 0 → emit an out-of-NDC clip position so the GPU rasterizer clips the primitive before the fragment stage runs. `compute_hatch_lod` now drives this buffer instead of (or alongside, for the per-hatch fallback) `hatch_skip_flags`. Walks a per-instance CPU AABB mirror that the builder fills at upload time (`instance_aabbs`), applies the same `aabb_below_pixel` (sub-pixel LOD) OR `aabb_offscreen` (frustum cull) rule the legacy path used, and pushes the resulting 0/1 flags via `queue.write_buffer` on a separate 4 B-per-hatch buffer — ~40 KB per pan tick for 10 k hatches, far cheaper than re-writing the 112 B-per-instance data. Step 5 will remove the legacy `gpu_hatches` Vec, the per-hatch draw loop, and `hatch_skip_flags` once the batched path has soaked. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
cfe53bd789
commit
398eee551d
4 changed files with 99 additions and 19 deletions
|
|
@ -136,14 +136,25 @@ pub struct HatchBatchedGpu {
|
||||||
pub boundary_buffer: wgpu::Buffer,
|
pub boundary_buffer: wgpu::Buffer,
|
||||||
pub family_buffer: wgpu::Buffer,
|
pub family_buffer: wgpu::Buffer,
|
||||||
pub dash_buffer: wgpu::Buffer,
|
pub dash_buffer: wgpu::Buffer,
|
||||||
|
/// Per-instance visibility flag (1=draw, 0=skip). Stored in its
|
||||||
|
/// own small storage buffer so per-frame updates don't have to
|
||||||
|
/// touch the large `instance_buffer`. Vertex shader reads
|
||||||
|
/// `visibility[instance_index]` — when 0 it emits an out-of-NDC
|
||||||
|
/// clip position so the GPU clips the primitive before the
|
||||||
|
/// fragment stage runs.
|
||||||
|
pub visibility_buffer: wgpu::Buffer,
|
||||||
pub bind_group: wgpu::BindGroup,
|
pub bind_group: wgpu::BindGroup,
|
||||||
pub instance_count: u32,
|
pub instance_count: u32,
|
||||||
/// CPU-side mirror of `HatchInstance.visible` — `compute_hatch_lod`
|
/// CPU mirror — `update_visibility` re-uploads this whole slice
|
||||||
/// writes 0/1 here, then `upload_visibility` pushes the changed
|
/// when any flag changes. ~4 B per hatch, so 40 KB / 10 k hatches
|
||||||
/// `visible` field through to the GPU `instance_buffer`. Storing
|
/// per pan tick. Far cheaper than touching the 112 B-per-instance
|
||||||
/// just the flag (not the whole HatchInstance) keeps the per-frame
|
/// data.
|
||||||
/// upload small.
|
|
||||||
pub visibility: Vec<u32>,
|
pub visibility: Vec<u32>,
|
||||||
|
/// CPU-side mirror of each instance's local-space AABB (world-
|
||||||
|
/// offset-subtracted, world_origin already added back). Used by
|
||||||
|
/// `compute_hatch_lod` to evaluate the sub-pixel + frustum cull
|
||||||
|
/// without reading back from the GPU.
|
||||||
|
pub instance_aabbs: Vec<[f32; 4]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HatchBatchedGpu {
|
impl HatchBatchedGpu {
|
||||||
|
|
@ -350,6 +361,13 @@ impl HatchBatchedGpu {
|
||||||
usage: wgpu::BufferUsages::STORAGE,
|
usage: wgpu::BufferUsages::STORAGE,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let visibility: Vec<u32> = instances.iter().map(|i| i.visible).collect();
|
||||||
|
let visibility_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("hatch_batched.visibility"),
|
||||||
|
contents: bytemuck::cast_slice(&visibility),
|
||||||
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||||
|
});
|
||||||
|
|
||||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
label: Some("hatch_batched.bg"),
|
label: Some("hatch_batched.bg"),
|
||||||
layout: bgl,
|
layout: bgl,
|
||||||
|
|
@ -370,10 +388,30 @@ impl HatchBatchedGpu {
|
||||||
binding: 3,
|
binding: 3,
|
||||||
resource: dash_buffer.as_entire_binding(),
|
resource: dash_buffer.as_entire_binding(),
|
||||||
},
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 4,
|
||||||
|
resource: visibility_buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
let visibility = instances.iter().map(|i| i.visible).collect();
|
// CPU AABB mirror — local-space rect with world_origin added
|
||||||
|
// back, ready for `aabb_offscreen` / `aabb_below_pixel` to
|
||||||
|
// compare against the camera view_proj.
|
||||||
|
let instance_aabbs: Vec<[f32; 4]> = instances
|
||||||
|
.iter()
|
||||||
|
.map(|i| {
|
||||||
|
let ox = i.world_origin[0];
|
||||||
|
let oy = i.world_origin[1];
|
||||||
|
[
|
||||||
|
i.aabb[0] + ox,
|
||||||
|
i.aabb[1] + oy,
|
||||||
|
i.aabb[2] + ox,
|
||||||
|
i.aabb[3] + oy,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
vertex_buffer,
|
vertex_buffer,
|
||||||
vertex_count: verts.len() as u32,
|
vertex_count: verts.len() as u32,
|
||||||
|
|
@ -381,12 +419,24 @@ impl HatchBatchedGpu {
|
||||||
boundary_buffer,
|
boundary_buffer,
|
||||||
family_buffer,
|
family_buffer,
|
||||||
dash_buffer,
|
dash_buffer,
|
||||||
|
visibility_buffer,
|
||||||
bind_group,
|
bind_group,
|
||||||
instance_count: instances.len() as u32,
|
instance_count: instances.len() as u32,
|
||||||
visibility,
|
visibility,
|
||||||
|
instance_aabbs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Push the CPU `visibility` slice to GPU. Call when any
|
||||||
|
/// element changes (typically per-frame from compute_hatch_lod).
|
||||||
|
pub fn upload_visibility(&self, queue: &wgpu::Queue) {
|
||||||
|
queue.write_buffer(
|
||||||
|
&self.visibility_buffer,
|
||||||
|
0,
|
||||||
|
bytemuck::cast_slice(&self.visibility),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Group-1 bind group layout — shared by the pipeline so it can be
|
/// Group-1 bind group layout — shared by the pipeline so it can be
|
||||||
/// constructed once at startup. All four bindings are read-only
|
/// constructed once at startup. All four bindings are read-only
|
||||||
/// storage and visible to both VS (AABB+visibility lookup) and FS
|
/// storage and visible to both VS (AABB+visibility lookup) and FS
|
||||||
|
|
@ -404,7 +454,7 @@ impl HatchBatchedGpu {
|
||||||
};
|
};
|
||||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
label: Some("hatch_batched.bgl"),
|
label: Some("hatch_batched.bgl"),
|
||||||
entries: &[entry(0), entry(1), entry(2), entry(3)],
|
entries: &[entry(0), entry(1), entry(2), entry(3), entry(4)],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -745,7 +745,32 @@ impl Pipeline {
|
||||||
/// fragment in the bounding quad. Pairs with the shader-side
|
/// fragment in the bounding quad. Pairs with the shader-side
|
||||||
/// solid-fill substitution for hatches between 2 px and the dense
|
/// solid-fill substitution for hatches between 2 px and the dense
|
||||||
/// pattern threshold.
|
/// pattern threshold.
|
||||||
pub fn compute_hatch_lod(&mut self, view_proj: glam::Mat4, clip_w: u32, clip_h: u32) {
|
pub fn compute_hatch_lod(
|
||||||
|
&mut self,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
view_proj: glam::Mat4,
|
||||||
|
clip_w: u32,
|
||||||
|
clip_h: u32,
|
||||||
|
) {
|
||||||
|
// Phase 4-B batched path — write the same skip rule (sub-pixel
|
||||||
|
// LOD OR offscreen) into the GPU visibility buffer. The vertex
|
||||||
|
// shader maps 0 → out-of-NDC clip → primitive culled before
|
||||||
|
// fragment stage; equivalent to the per-hatch `skip_flags`
|
||||||
|
// gate but at GPU-side cost.
|
||||||
|
if let Some(batch) = &mut self.gpu_hatch_batched {
|
||||||
|
// Same skip rule as the legacy per-hatch path: sub-pixel
|
||||||
|
// LOD (`aabb_below_pixel`) OR offscreen (`aabb_offscreen`).
|
||||||
|
// Walks the per-instance AABB mirror — no GPU readback.
|
||||||
|
for (i, aabb) in batch.instance_aabbs.iter().enumerate() {
|
||||||
|
let skip = aabb_below_pixel(*aabb, view_proj, clip_w, clip_h, 2.0)
|
||||||
|
|| aabb_offscreen(*aabb, view_proj, clip_w, clip_h);
|
||||||
|
batch.visibility[i] = if skip { 0 } else { 1 };
|
||||||
|
}
|
||||||
|
batch.upload_visibility(queue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-hatch (legacy) fallback path.
|
||||||
self.hatch_skip_flags = self
|
self.hatch_skip_flags = self
|
||||||
.gpu_hatches
|
.gpu_hatches
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ impl shader::Primitive for Primitive {
|
||||||
pipeline.compute_hatch_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
pipeline.compute_hatch_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||||
pipeline.compute_wipeout_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
pipeline.compute_wipeout_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||||
pipeline.compute_image_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
pipeline.compute_image_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||||
pipeline.compute_hatch_lod(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
pipeline.compute_hatch_lod(queue, self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||||
pipeline.compute_wipeout_lod(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
pipeline.compute_wipeout_lod(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||||
pipeline.compute_mesh_lod(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
pipeline.compute_mesh_lod(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||||
if self.show_viewcube {
|
if self.show_viewcube {
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,11 @@ struct LineFamily {
|
||||||
@group(1) @binding(1) var<storage, read> boundary: array<vec4<f32>>;
|
@group(1) @binding(1) var<storage, read> boundary: array<vec4<f32>>;
|
||||||
@group(1) @binding(2) var<storage, read> families: array<LineFamily>;
|
@group(1) @binding(2) var<storage, read> families: array<LineFamily>;
|
||||||
@group(1) @binding(3) var<storage, read> dashes: array<f32>;
|
@group(1) @binding(3) var<storage, read> dashes: array<f32>;
|
||||||
|
// Per-instance visibility (Phase 4-B sub-pixel + frustum skip).
|
||||||
|
// CPU writes `1` to draw / `0` to skip every frame; vertex shader
|
||||||
|
// emits an out-of-NDC clip position for 0-instances so the GPU
|
||||||
|
// rasterizer culls the primitive before any fragment runs.
|
||||||
|
@group(1) @binding(4) var<storage, read> visibility: array<u32>;
|
||||||
|
|
||||||
// ── Vertex shader ────────────────────────────────────────────────────────
|
// ── Vertex shader ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -100,12 +105,12 @@ fn corner_xy(c: u32, aabb: vec4<f32>) -> vec2<f32> {
|
||||||
var o: VOut;
|
var o: VOut;
|
||||||
let inst = instances[v.instance_index];
|
let inst = instances[v.instance_index];
|
||||||
|
|
||||||
// CPU-driven visibility flag (Phase 4-B frustum skip). Emit a clip
|
// Per-frame visibility (CPU-driven sub-pixel + frustum skip).
|
||||||
// position whose x/y exceed |w|, so the GPU frustum-culls the
|
// 0 → emit a clip position whose x/y exceed |w| so the GPU
|
||||||
// primitive and no fragment runs for it. (WGSL forbids literal NaN
|
// frustum-culls the primitive and no fragment runs. (WGSL
|
||||||
// so we can't go that route; pushing the vertex out of the unit
|
// forbids literal NaN so this out-of-NDC trick replaces the
|
||||||
// cube is the equivalent degenerate-triangle trick.)
|
// usual NaN-degenerate-triangle.)
|
||||||
if inst.visible == 0u {
|
if visibility[v.instance_index] == 0u {
|
||||||
o.clip = vec4<f32>(2.0, 2.0, 2.0, 1.0);
|
o.clip = vec4<f32>(2.0, 2.0, 2.0, 1.0);
|
||||||
o.xz = vec2<f32>(0.0, 0.0);
|
o.xz = vec2<f32>(0.0, 0.0);
|
||||||
o.instance_index = v.instance_index;
|
o.instance_index = v.instance_index;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue