perf(scene): reuse model-tile tessellation across pure pans

Phase 3.1 (partial — pan). The Model-tile wire cache keyed on the exact
camera hash, so every pan re-tessellated the whole tile even though pan
changes neither the curve tol nor the culled set (beyond a shift). Re-key
on (geometry_epoch, pan_invariant_hash, tessellated_region):

- pan_invariant_hash covers rotation + wpp (tol) but excludes the pan
  target, so a pure pan keeps it stable;
- the wires were culled to a 1.25x-margin region, recorded with the entry;
- a hit now also requires the tile's current visible rect to still fit
  inside that region, guaranteeing every on-screen wire is present.

Result: panning within the existing margin reuses the tessellation (tess
ms -> ~0 on the PERF HUD) with no missing geometry. Zoom, orbit and edits
change the signature/epoch and rebuild as before; the cull margin is
unchanged so miss cost — and zoom behaviour — is identical. GPU still
re-uploads per camera_generation (diff upload is Phase 3.2).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-05 09:06:12 +03:00
1 parent 5bb6a92235
commit d74cb891f9
2 files changed
+81 -39

No files matched your search

+12 -1
View File
@@ -165,7 +165,18 @@ geometry is camera-invariant.
changed),
- `frame_visible[handle] → bool` (recomputed per `camera_generation`).
**Partial (landed):** `set_hover_highlight` no longer bumps the geometry
**Partial (landed): pan reuse.** The Model-tile wire cache no longer keys on
the exact camera hash. It now keys on `(geometry_epoch, pan_invariant_hash,
tessellated_region)` where `pan_invariant_hash` covers rotation + tol (`wpp`)
but NOT the pan target. A pure pan keeps the epoch + signature and only shifts
the view, so as long as the new visible rect still fits inside the
1.25×-margin region the wires were culled to, the tessellation is reused
outright — no re-tessellation, `tess ms` drops to ~0 on the PERF HUD. Zoom
(changes `wpp`), orbit (changes rotation) and edits (change epoch) rebuild
exactly as before; the cull margin is unchanged, so miss cost is identical
(no zoom regression).
**Partial (landed): hover.** `set_hover_highlight` no longer bumps the geometry
epoch (a full re-tessellation) when the hovered entity is already selected —
the effective highlight set `selected ∪ {hover}` is then unchanged, so the
tessellation output is identical. Hovering over / between selected entities
+69 -38
View File
@@ -547,22 +547,32 @@ fn offset_mesh_lod_set(mut set: MeshLodSet, world_offset: [f64; 3]) -> MeshLodSe
/// Stable hash of a `Camera`'s pose for use as a per-tile cache key.
/// Two cameras with bit-identical target / rotation / distance hash the
/// same; any orbit / pan / zoom on a tile bumps it.
fn camera_state_hash(c: &Camera) -> u64 {
/// Hash of the camera state that affects *tessellation output* on the Model
/// tile, deliberately EXCLUDING the pan target. Rotation governs the
/// projection; `wpp` (world-per-pixel) governs the zoom-adaptive curve tol
/// and the sub-pixel LOD cull. Two cameras with the same value differ only by
/// pan, so the same tessellation is valid for both as long as the new view
/// still fits inside the region the wires were culled to (see
/// `model_tile_wires_arc`). Target is the one varying input on a pure pan.
fn camera_pan_invariant_hash(c: &Camera, wpp: Option<f32>) -> u64 {
fn h(state: u64, x: f32) -> u64 {
state.rotate_left(13) ^ x.to_bits() as u64
}
let mut s: u64 = 0xcbf2_9ce4_8422_2325;
s = h(s, c.target.x);
s = h(s, c.target.y);
s = h(s, c.target.z);
let mut s: u64 = 0x9e37_79b9_7f4a_7c15;
s = h(s, c.rotation.x);
s = h(s, c.rotation.y);
s = h(s, c.rotation.z);
s = h(s, c.rotation.w);
s = h(s, c.distance);
s = h(s, wpp.unwrap_or(-1.0));
s
}
/// `outer` fully contains `inner` (both `[min_x, min_y, max_x, max_y]`).
#[inline]
fn aabb_contains(outer: [f32; 4], inner: [f32; 4]) -> bool {
outer[0] <= inner[0] && outer[1] <= inner[1] && outer[2] >= inner[2] && outer[3] >= inner[3]
}
pub struct Scene {
pub camera: Rc<RefCell<Camera>>,
/// Model-space tiled viewport layout. One full-window tile by default;
@@ -607,11 +617,14 @@ pub struct Scene {
/// LOD / frustum culling has to run independently — the shared
/// `wire_cache` would cull every tile against whichever camera was
/// current when it was last built. Keyed by tile index; the value
/// carries `(geometry_epoch, camera_state_hash)` so a cache hit is
/// also rejected when the same tile's camera moved or the document
/// geometry changed.
/// Carries `(geometry_epoch, pan_invariant_hash, tessellated_region)`.
/// A hit needs the epoch + the rotation/tol signature to match AND the
/// tile's current visible view to still fit inside the region the wires
/// were culled to — so a pure pan within that 1.25×-margin region reuses
/// the tessellation instead of rebuilding it. Zoom / orbit / edits change
/// the epoch or signature and rebuild as before.
model_tile_wire_cache:
RefCell<HashMap<usize, ((u64, u64), Arc<Vec<WireModel>>)>>,
RefCell<HashMap<usize, ((u64, u64, [f32; 4]), Arc<Vec<WireModel>>)>>,
/// Index built from every SortEntitiesTable in the document.
/// Maps block_handle → (entity_handle.value() → sort_handle.value()).
/// Replaces the O(objects) linear scan inside `wires_for_block()` with an O(1) lookup.
@@ -1549,43 +1562,61 @@ impl Scene {
cam_aspect: f32,
tile_pixel_height: f32,
) -> Arc<Vec<WireModel>> {
let cam_key = camera_state_hash(cam);
let key = (self.geometry_epoch, cam_key);
{
let cache = self.model_tile_wire_cache.borrow();
if let Some((cached_key, ref arc)) = cache.get(&tile_idx) {
if *cached_key == key {
return Arc::clone(arc);
}
}
}
// Compute the tile's own view AABB and world-per-pixel from its
// camera + pixel size (mirrors `view_world_aabb` /
// `world_per_pixel`, neither of which knows about anything beyond
// the live `self.camera`).
let view_aabb = if self.camera_generation == 0 {
None
} else {
let h = cam.ortho_size();
let w = h * cam_aspect.max(0.01);
let margin = 1.25_f32;
let cx = cam.target.x;
let cy = cam.target.y;
Some([cx - w * margin, cy - h * margin, cx + w * margin, cy + h * margin])
};
let wpp = if tile_pixel_height > 0.0 {
Some((2.0 * cam.ortho_size()) / tile_pixel_height)
} else {
None
};
// Cache reuse is split from the exact-camera key: a pure pan keeps the
// geometry epoch and the pan-invariant signature (rotation + tol) but
// shifts the view. The cached wires were culled to a 1.25×-margin
// region; if the new *visible* view still sits inside that region they
// remain complete, so we can skip re-tessellation entirely — only the
// GPU re-uploads (camera_generation still bumps). No margin widening,
// so zoom (which changes the signature) costs exactly as before.
let pan_sig = camera_pan_invariant_hash(cam, wpp);
// Exact visible rect (margin 1.0) the reused wires must still cover.
let need_region = if self.camera_generation == 0 {
[f32::NEG_INFINITY, f32::NEG_INFINITY, f32::INFINITY, f32::INFINITY]
} else {
let h = cam.ortho_size();
let w = h * cam_aspect.max(0.01);
let (cx, cy) = (cam.target.x, cam.target.y);
[cx - w, cy - h, cx + w, cy + h]
};
{
let cache = self.model_tile_wire_cache.borrow();
if let Some(((epoch, sig, region), arc)) = cache.get(&tile_idx) {
if *epoch == self.geometry_epoch
&& *sig == pan_sig
&& aabb_contains(*region, need_region)
{
return Arc::clone(arc);
}
}
}
// Miss → cull/tessellate to the 1.25×-margin region (unchanged cost)
// and remember that region for the pan-reuse test above.
let tess_region = if self.camera_generation == 0 {
None
} else {
let h = cam.ortho_size();
let w = h * cam_aspect.max(0.01);
let margin = 1.25_f32;
let (cx, cy) = (cam.target.x, cam.target.y);
Some([cx - w * margin, cy - h * margin, cx + w * margin, cy + h * margin])
};
let block = self.model_space_block_handle();
let t_tess = std::time::Instant::now();
let arc = Arc::new(self.wires_for_block_culled(block, view_aabb, wpp, None, None));
let arc = Arc::new(self.wires_for_block_culled(block, tess_region, wpp, None, None));
self.last_tess_ms.set(t_tess.elapsed().as_secs_f32() * 1000.0);
self.last_tess_wires.set(arc.len());
self.model_tile_wire_cache
.borrow_mut()
.insert(tile_idx, (key, Arc::clone(&arc)));
let stored_region = tess_region
.unwrap_or([f32::NEG_INFINITY, f32::NEG_INFINITY, f32::INFINITY, f32::INFINITY]);
self.model_tile_wire_cache.borrow_mut().insert(
tile_idx,
((self.geometry_epoch, pan_sig, stored_region), Arc::clone(&arc)),
);
arc
}