forked from 1b-agent/opencadstudio-upstream
perf(scene): hatch LOD — solid-fill substitute + sub-pixel skip (Phase 3.3)
Hatch fills drew the full per-family pattern loop every frame regardless
of zoom. Two LOD tiers, neither of which existed:
* **Shader-side solid substitute.** When the densest line family's
spacing × scale projects to less than 2 px, individual lines blur
into a solid color and the per-fragment family loop is wasted ALU
(and produces moiré). Pass `world_per_pixel` through Uniforms; the
hatch fragment shader now returns `h.color` directly in that case.
* **CPU-side sub-pixel skip.** When the entire hatch AABB projects
to less than 2 px, drop the draw call. Adds `world_aabb` to
HatchGpu plus a per-frame `compute_hatch_lod` pass (mirrors the
existing scissor pass) and `hatch_skip_flags`. The render pass
skips flagged hatches.
Factored a shared `aabb_below_pixel` helper for future LOD callers.
The Uniforms layout is unchanged in size (96 B) — the trailing
`_pad: vec2<f32>` becomes `world_per_pixel: f32, _pad: f32`, so wgsl
modules that don't read the new field keep working with their old
`_pad: vec2<f32>` declarations.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
fbbac423d4
commit
e7675be0bc
5 files changed
+102
-7
No files matched your search
@@ -106,6 +106,10 @@ pub struct HatchGpu {
|
|||||||
/// frame's `compute_hatch_scissors` can translate the world rect into
|
/// frame's `compute_hatch_scissors` can translate the world rect into
|
||||||
/// a pixel scissor.
|
/// a pixel scissor.
|
||||||
pub vp_scissor: Option<[f32; 4]>,
|
pub vp_scissor: Option<[f32; 4]>,
|
||||||
|
/// World-space XY bounding rect [min_x, min_y, max_x, max_y] of the
|
||||||
|
/// boundary polygon. Used by the per-frame LOD pass to skip hatches
|
||||||
|
/// whose entire footprint projects to less than ~2 px (Phase 3.3).
|
||||||
|
pub world_aabb: [f32; 4],
|
||||||
_uniform_buf: wgpu::Buffer,
|
_uniform_buf: wgpu::Buffer,
|
||||||
_boundary_buf: wgpu::Buffer,
|
_boundary_buf: wgpu::Buffer,
|
||||||
_family_buf: wgpu::Buffer,
|
_family_buf: wgpu::Buffer,
|
||||||
@@ -273,6 +277,7 @@ impl HatchGpu {
|
|||||||
vertex_buffer,
|
vertex_buffer,
|
||||||
bind_group,
|
bind_group,
|
||||||
vp_scissor: model.vp_scissor,
|
vp_scissor: model.vp_scissor,
|
||||||
|
world_aabb: [min_x, min_y, max_x, max_y],
|
||||||
_uniform_buf,
|
_uniform_buf,
|
||||||
_boundary_buf,
|
_boundary_buf,
|
||||||
_family_buf,
|
_family_buf,
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ pub struct Pipeline {
|
|||||||
gpu_hatches: Vec<HatchGpu>,
|
gpu_hatches: Vec<HatchGpu>,
|
||||||
/// Pixel scissor rects [x, y, w, h] for viewport-clipped hatches. Recomputed each frame.
|
/// Pixel scissor rects [x, y, w, h] for viewport-clipped hatches. Recomputed each frame.
|
||||||
hatch_pixel_scissors: Vec<Option<[u32; 4]>>,
|
hatch_pixel_scissors: Vec<Option<[u32; 4]>>,
|
||||||
|
/// `true` for hatches whose AABB projects to less than ~2 px at the
|
||||||
|
/// current zoom. Render pass skips the draw call. Recomputed each
|
||||||
|
/// frame alongside the scissor pass (Phase 3.3 hatch LOD).
|
||||||
|
hatch_skip_flags: Vec<bool>,
|
||||||
/// Wipeout fills — rendered after wires in a separate pass.
|
/// Wipeout fills — rendered after wires in a separate pass.
|
||||||
gpu_wipeouts: Vec<HatchGpu>,
|
gpu_wipeouts: Vec<HatchGpu>,
|
||||||
/// Pixel scissor rects [x, y, w, h] for viewport-clipped wipeouts. Recomputed each frame.
|
/// Pixel scissor rects [x, y, w, h] for viewport-clipped wipeouts. Recomputed each frame.
|
||||||
@@ -597,6 +601,7 @@ impl Pipeline {
|
|||||||
gpu_selected_wires: vec![],
|
gpu_selected_wires: vec![],
|
||||||
gpu_hatches: vec![],
|
gpu_hatches: vec![],
|
||||||
hatch_pixel_scissors: vec![],
|
hatch_pixel_scissors: vec![],
|
||||||
|
hatch_skip_flags: vec![],
|
||||||
gpu_wipeouts: vec![],
|
gpu_wipeouts: vec![],
|
||||||
wipeout_pixel_scissors: vec![],
|
wipeout_pixel_scissors: vec![],
|
||||||
gpu_images: vec![],
|
gpu_images: vec![],
|
||||||
@@ -640,6 +645,20 @@ impl Pipeline {
|
|||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recompute per-hatch LOD skip flags. A hatch whose entire AABB
|
||||||
|
/// projects to less than ~2 px is invisible at this zoom — skip the
|
||||||
|
/// draw call entirely instead of running the polygon test for every
|
||||||
|
/// fragment in the bounding quad. Pairs with the shader-side
|
||||||
|
/// solid-fill substitution for hatches between 2 px and the dense
|
||||||
|
/// pattern threshold.
|
||||||
|
pub fn compute_hatch_lod(&mut self, view_proj: glam::Mat4, clip_w: u32, clip_h: u32) {
|
||||||
|
self.hatch_skip_flags = self
|
||||||
|
.gpu_hatches
|
||||||
|
.iter()
|
||||||
|
.map(|h| aabb_below_pixel(h.world_aabb, view_proj, clip_w, clip_h, 2.0))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
/// Recompute pixel scissor rects for viewport-clipped wipeouts.
|
/// Recompute pixel scissor rects for viewport-clipped wipeouts.
|
||||||
pub fn compute_wipeout_scissors(&mut self, view_proj: glam::Mat4, clip_w: u32, clip_h: u32) {
|
pub fn compute_wipeout_scissors(&mut self, view_proj: glam::Mat4, clip_w: u32, clip_h: u32) {
|
||||||
self.wipeout_pixel_scissors = self
|
self.wipeout_pixel_scissors = self
|
||||||
@@ -767,6 +786,10 @@ impl Pipeline {
|
|||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
let mut scissor_active = false;
|
let mut scissor_active = false;
|
||||||
for (i, hatch) in self.gpu_hatches.iter().enumerate() {
|
for (i, hatch) in self.gpu_hatches.iter().enumerate() {
|
||||||
|
// Phase 3.3 LOD: skip hatches projecting to sub-pixel size.
|
||||||
|
if self.hatch_skip_flags.get(i).copied().unwrap_or(false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
match self.hatch_pixel_scissors.get(i) {
|
match self.hatch_pixel_scissors.get(i) {
|
||||||
Some(Some([x, y, w, h])) => {
|
Some(Some([x, y, w, h])) => {
|
||||||
pass.set_scissor_rect(*x, *y, *w, *h);
|
pass.set_scissor_rect(*x, *y, *w, *h);
|
||||||
@@ -1161,6 +1184,43 @@ impl Pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return `true` when the world-XY AABB's screen-space size is below the
|
||||||
|
/// given pixel threshold. Used by LOD passes (hatch skip, etc.) to drop
|
||||||
|
/// draw calls that wouldn't contribute a visible pixel.
|
||||||
|
fn aabb_below_pixel(
|
||||||
|
aabb: [f32; 4],
|
||||||
|
view_proj: glam::Mat4,
|
||||||
|
clip_w: u32,
|
||||||
|
clip_h: u32,
|
||||||
|
threshold_px: f32,
|
||||||
|
) -> bool {
|
||||||
|
let [x0, y0, x1, y1] = aabb;
|
||||||
|
if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let w = clip_w as f32;
|
||||||
|
let h = clip_h as f32;
|
||||||
|
let corners = [
|
||||||
|
view_proj.project_point3(glam::Vec3::new(x0, y0, 0.0)),
|
||||||
|
view_proj.project_point3(glam::Vec3::new(x1, y0, 0.0)),
|
||||||
|
view_proj.project_point3(glam::Vec3::new(x0, y1, 0.0)),
|
||||||
|
view_proj.project_point3(glam::Vec3::new(x1, y1, 0.0)),
|
||||||
|
];
|
||||||
|
let mut min_px = f32::INFINITY;
|
||||||
|
let mut max_px = f32::NEG_INFINITY;
|
||||||
|
let mut min_py = f32::INFINITY;
|
||||||
|
let mut max_py = f32::NEG_INFINITY;
|
||||||
|
for c in &corners {
|
||||||
|
let px = (c.x + 1.0) * 0.5 * w;
|
||||||
|
let py = (1.0 - c.y) * 0.5 * h;
|
||||||
|
if px < min_px { min_px = px; }
|
||||||
|
if px > max_px { max_px = px; }
|
||||||
|
if py < min_py { min_py = py; }
|
||||||
|
if py > max_py { max_py = py; }
|
||||||
|
}
|
||||||
|
(max_px - min_px).max(max_py - min_py) < threshold_px
|
||||||
|
}
|
||||||
|
|
||||||
/// Project a world-space XY scissor rect through `view_proj` into the four
|
/// Project a world-space XY scissor rect through `view_proj` into the four
|
||||||
/// pixel-space corners and return the smallest aligned-rect that bounds them,
|
/// pixel-space corners and return the smallest aligned-rect that bounds them,
|
||||||
/// clamped to the clip viewport. Returns `None` when the rect is missing or
|
/// clamped to the clip viewport. Returns `None` when the rect is missing or
|
||||||
|
|||||||
@@ -7,16 +7,27 @@ pub struct Uniforms {
|
|||||||
pub view_proj: glam::Mat4,
|
pub view_proj: glam::Mat4,
|
||||||
pub camera_pos: glam::Vec4,
|
pub camera_pos: glam::Vec4,
|
||||||
pub viewport_size: [f32; 2],
|
pub viewport_size: [f32; 2],
|
||||||
pub _pad: [f32; 2],
|
/// World units per screen pixel at the current zoom. Used by the
|
||||||
|
/// hatch shader to substitute solid fill when pattern line spacing
|
||||||
|
/// falls below ~2 px (Phase 3.3 LOD).
|
||||||
|
pub world_per_pixel: f32,
|
||||||
|
pub _pad: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Uniforms {
|
impl Uniforms {
|
||||||
pub fn new(camera: &Camera, bounds: Rectangle) -> Self {
|
pub fn new(camera: &Camera, bounds: Rectangle) -> Self {
|
||||||
|
let half_h = camera.ortho_size();
|
||||||
|
let world_per_pixel = if bounds.height > 0.0 {
|
||||||
|
(2.0 * half_h) / bounds.height
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
Self {
|
Self {
|
||||||
view_proj: camera.view_proj(bounds),
|
view_proj: camera.view_proj(bounds),
|
||||||
camera_pos: camera.position_vec4(),
|
camera_pos: camera.position_vec4(),
|
||||||
viewport_size: [bounds.width, bounds.height],
|
viewport_size: [bounds.width, bounds.height],
|
||||||
_pad: [0.0; 2],
|
world_per_pixel,
|
||||||
|
_pad: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +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);
|
||||||
if self.show_viewcube {
|
if self.show_viewcube {
|
||||||
pipeline.viewcube.upload(
|
pipeline.viewcube.upload(
|
||||||
queue,
|
queue,
|
||||||
|
|||||||
+23
-5
@@ -13,10 +13,11 @@
|
|||||||
// ── Group 0: frame uniforms (shared) ──────────────────────────────────────
|
// ── Group 0: frame uniforms (shared) ──────────────────────────────────────
|
||||||
|
|
||||||
struct Uniforms {
|
struct Uniforms {
|
||||||
view_proj: mat4x4<f32>,
|
view_proj: mat4x4<f32>,
|
||||||
camera_pos: vec4<f32>,
|
camera_pos: vec4<f32>,
|
||||||
viewport_size: vec2<f32>,
|
viewport_size: vec2<f32>,
|
||||||
_pad: vec2<f32>,
|
world_per_pixel: f32,
|
||||||
|
_pad: f32,
|
||||||
}
|
}
|
||||||
@group(0) @binding(0) var<uniform> u: Uniforms;
|
@group(0) @binding(0) var<uniform> u: Uniforms;
|
||||||
|
|
||||||
@@ -195,7 +196,24 @@ fn check_family(
|
|||||||
return mix(h.color, h.color2, t);
|
return mix(h.color, h.color2, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Pattern: evaluate each line family.
|
// 3. Pattern: LOD substitution — when the densest family's spacing
|
||||||
|
// projects to less than 2 px, individual lines blur into a solid
|
||||||
|
// fill and the per-family loop just wastes ALU. Return solid color
|
||||||
|
// instead. (Phase 3.3 hatch LOD.)
|
||||||
|
if u.world_per_pixel > 0.0 && f.n_families > 0u {
|
||||||
|
var min_spacing_world: f32 = 1.0e30;
|
||||||
|
for (var i = 0u; i < f.n_families; i++) {
|
||||||
|
let s = abs(f.families[i].perp_step) * h.scale;
|
||||||
|
if s > 0.0 && s < min_spacing_world {
|
||||||
|
min_spacing_world = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if min_spacing_world / u.world_per_pixel < 2.0 {
|
||||||
|
return h.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Pattern: evaluate each line family.
|
||||||
let cos_off = cos(h.angle_offset);
|
let cos_off = cos(h.angle_offset);
|
||||||
let sin_off = sin(h.angle_offset);
|
let sin_off = sin(h.angle_offset);
|
||||||
for (var i = 0u; i < f.n_families; i++) {
|
for (var i = 0u; i < f.n_families; i++) {
|
||||||
|
|||||||
Reference in new issue
Block a user