perf(scene): zoom-adaptive curve tolerance for top-level wires (Phase 3.2)

Edge tessellation used a fixed 0.005 world-unit chord-height tolerance
regardless of zoom — a 1000 m radius arc viewed at 50 mm/px still emitted
hundreds of segments that all collapsed onto the same screen pixel.

`truck_tess` now reads tolerance from a process-wide AtomicU64 override.
`Scene::wires_for_block` sets the override to ~0.5 px chord height
(world_per_pixel × 0.5) before the par_iter, then a Drop guard clears it
back to the default. Floor stays at the original CURVE_TOL so extreme
zoom-in keeps full quality. Block-cache rebuilds and off-render paths
(snap, hit-test) always see the default — the override is bracketed to
the render call only.

Roadmap entry for Phase 3.2 removed; Implementation Order / Key Files
tables updated.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
HakanandClaude Opus 4.7 committed 2026-05-15 14:07:28 +03:00
1 parent 0584e3c862
commit 26271b8c63
3 files changed
+50 -29

No files matched your search

-27
View File
@@ -64,31 +64,6 @@ let candidates = scene.quadtree.query_rect(view_aabb);
**Goal:** Reduce geometric complexity when entities are small or far away.
### 3.2 Wire LOD — Curve Segment Reduction
Arcs, ellipses, and splines tessellate to N segments (currently fixed). Replace with
zoom-adaptive segment count.
```rust
fn arc_segments(radius_world: f64, px_per_unit: f64) -> u32 {
// target: ~1 segment per 2px of arc length on screen
let arc_px = radius_world * px_per_unit * TWO_PI;
(arc_px / 2.0).clamp(8.0, 256.0) as u32
}
```
Recompute segments when zoom crosses a threshold (epoch-based invalidation already in place).
Store LOD level per entity; retessellate only on LOD level change.
LOD levels (example):
| Zoom (px/unit) | Segments per 90° arc |
|----------------|----------------------|
| > 100 px/u | 64 (full detail) |
| 10–100 px/u | 24 |
| 1–10 px/u | 12 |
| < 1 px/u | 6 (or sub-pixel cull)|
### 3.3 Hatch LOD — Density Reduction
At low zoom, hatch line spacing appears smaller than 1px. Options:
@@ -163,7 +138,6 @@ Relevant only for perspective (3D) mode with many overlapping solids.
```
Phase 1.4 Scissor for hatch/image low complexity
Phase 2.1 Quadtree for 2D medium complexity, scales 2D docs
Phase 3.2 Wire curve LOD medium, affects tessellation cache
Phase 3.3 Hatch LOD low complexity
Phase 3.4 Mesh LOD high complexity, background thread
Phase 2.2 Octree for 3D medium, needed for dense 3D
@@ -180,7 +154,6 @@ Phase 4.2 Hi-Z occlusion high complexity, 3D only, last
| `src/scene/mesh_model.rs` | Add `Aabb3`, LOD mesh array |
| `src/scene/hatch_model.rs` | Add `Aabb2` field |
| `src/scene/image_model.rs` | Add `Aabb2` field |
| `src/scene/tessellate.rs` | Adaptive arc segments |
| `src/scene/truck_tess.rs` | Multi-resolution mesh tessellation |
| `src/scene/mod.rs` | Quadtree/octree; LOD epoch tracking |
| `src/scene/pipeline/mod.rs` | Cull hatch/image entities before upload loops |
+15
View File
@@ -1051,6 +1051,21 @@ impl Scene {
let blk_ref: &block_cache::BlockCache = &blk_cache;
let view_aabb = self.view_world_aabb();
let wpp = self.world_per_pixel();
// Zoom-adaptive curve sampling for top-level Edge tessellation. Target
// ~0.5 px chord height — far-out arcs that used to emit hundreds of
// segments now collapse to a handful. The guard clears the override
// when this scope exits so off-render tessellation (snap previews,
// hit-test, block_cache rebuild) sees the default.
struct CurveTolGuard;
impl Drop for CurveTolGuard {
fn drop(&mut self) {
crate::scene::truck_tess::set_curve_tol_override(None);
}
}
let _tol_guard = wpp.map(|w| {
crate::scene::truck_tess::set_curve_tol_override(Some((w * 0.5) as f64));
CurveTolGuard
});
let mut wires: Vec<WireModel> = visible
.into_par_iter()
.flat_map(|e| {
+35 -2
View File
@@ -20,6 +20,38 @@ const CURVE_TOL: f64 = 0.005;
// Triangle mesh tolerance (world units).
pub const MESH_TOL: f64 = 0.01;
// ── Zoom-adaptive curve tolerance override ────────────────────────────────
//
// The Scene sets this once per render frame to a value derived from
// `world_per_pixel` (target ≈ 0.5 px chord height). All Edge tessellations
// inside the frame — including those running on rayon worker threads — read
// from the same atomic, so callers don't need to thread a tolerance
// parameter through every entity-converter signature. Zero means "use
// `CURVE_TOL`"; that is the default, and what BlockCache::build expects.
use std::sync::atomic::{AtomicU64, Ordering};
static CURVE_TOL_BITS: AtomicU64 = AtomicU64::new(0);
/// Set the per-frame curve tolerance. `None` (or any non-finite/<=0 value)
/// reverts to the default `CURVE_TOL`.
pub fn set_curve_tol_override(tol: Option<f64>) {
let bits = match tol {
Some(t) if t > 0.0 && t.is_finite() => t.to_bits(),
_ => 0,
};
CURVE_TOL_BITS.store(bits, Ordering::Relaxed);
}
/// Resolve the active curve tolerance. Clamped to the floor `CURVE_TOL` so
/// extreme zoom-in never under-samples below the existing baseline quality.
fn current_curve_tol() -> f64 {
let bits = CURVE_TOL_BITS.load(Ordering::Relaxed);
if bits == 0 {
CURVE_TOL
} else {
f64::from_bits(bits).max(CURVE_TOL)
}
}
// ── Public result type ────────────────────────────────────────────────────
/// Output of any truck topology tessellation.
@@ -65,8 +97,9 @@ pub fn tessellate_edge(e: &Edge, offset: [f64; 3]) -> TruckTessResult {
let curve = e.oriented_curve();
let (t0, t1) = curve.range_tuple();
// parameter_division samples adaptively: fewer points on straight segments,
// more on tight curves, all within CURVE_TOL chord-height error.
let (_, pts) = curve.parameter_division((t0, t1), CURVE_TOL);
// more on tight curves, all within the active chord-height tolerance.
// The Scene scales this with zoom so far-out arcs aren't oversampled.
let (_, pts) = curve.parameter_division((t0, t1), current_curve_tol());
let lines = pts
.iter()
.map(|p| to_local(p.x, p.y, p.z, offset))