fix(viewport): per-tile wire cache for Model layout
Tiled Model panes were all reading from the shared `entity_wires_arc`, whose tessellation runs `view_world_aabb` / `world_per_pixel` against the live `Scene::camera`. Zooming the active tile re-built that arc with the active camera's LOD / frustum cull, then every other tile rendered from the same arc — content in the inactive panes flickered between full and LOD-stub tessellations as you zoomed. - `wires_for_block` now delegates to `wires_for_block_culled`, which takes `view_aabb` + `wpp` explicitly. The original caller passes the live-camera values; tile rendering passes its own. - New `model_tile_wires_arc` builds a per-tile view AABB + wpp from the tile's camera + pixel rect and caches the result. Cache is keyed by tile index; the value holds `(geometry_epoch, camera_state_hash)`, so a stale hit (camera moved, doc changed) still misses and rebuilds. - `ViewportInstance` gains `tile_idx` so `viewport_data_for` can route Model tiles into the per-tile cache without disturbing the paper- layout content-viewport path (it still uses `model_wires_for_viewport_arc`).
This commit is contained in:
parent
168f248593
commit
0eaac008dd
2 changed files with 119 additions and 4 deletions
110
src/scene/mod.rs
110
src/scene/mod.rs
|
|
@ -421,6 +421,11 @@ pub struct ViewportInstance {
|
||||||
/// Source viewport entity handle, or `Handle::NULL` for the implicit
|
/// Source viewport entity handle, or `Handle::NULL` for the implicit
|
||||||
/// full-canvas Model view that has no backing entity yet.
|
/// full-canvas Model view that has no backing entity yet.
|
||||||
pub handle: Handle,
|
pub handle: Handle,
|
||||||
|
/// Source Model-space tile index, or `None` for paper-layout viewports
|
||||||
|
/// (they're identified by `handle` instead). Used as the cache key for
|
||||||
|
/// `Scene::model_tile_wires_arc` so each pane reuses its own entry on
|
||||||
|
/// camera moves instead of accumulating one per camera hash.
|
||||||
|
pub tile_idx: Option<usize>,
|
||||||
/// Screen rectangle (pixels, canvas-relative) this viewport fills.
|
/// Screen rectangle (pixels, canvas-relative) this viewport fills.
|
||||||
pub screen_rect: iced::Rectangle,
|
pub screen_rect: iced::Rectangle,
|
||||||
pub camera: Camera,
|
pub camera: Camera,
|
||||||
|
|
@ -472,6 +477,25 @@ fn overlap_len(a: (f32, f32), b: (f32, f32)) -> f32 {
|
||||||
(a.1.min(b.1) - a.0.max(b.0)).max(0.0)
|
(a.1.min(b.1) - a.0.max(b.0)).max(0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
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);
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Scene {
|
pub struct Scene {
|
||||||
pub camera: Rc<RefCell<Camera>>,
|
pub camera: Rc<RefCell<Camera>>,
|
||||||
/// Model-space tiled viewport layout. One full-window tile by default;
|
/// Model-space tiled viewport layout. One full-window tile by default;
|
||||||
|
|
@ -498,6 +522,16 @@ pub struct Scene {
|
||||||
/// invalidates the cull-dependent wire list as well as a geometry change.
|
/// invalidates the cull-dependent wire list as well as a geometry change.
|
||||||
/// Uses `Arc` so `build_primitive()` avoids a full Vec clone during navigation.
|
/// Uses `Arc` so `build_primitive()` avoids a full Vec clone during navigation.
|
||||||
wire_cache: RefCell<Option<((u64, u64), Arc<Vec<WireModel>>)>>,
|
wire_cache: RefCell<Option<((u64, u64), Arc<Vec<WireModel>>)>>,
|
||||||
|
/// Per-Model-tile cached tessellation. Each tile has its own camera
|
||||||
|
/// (live for the active tile, stored snapshot for the others), so
|
||||||
|
/// 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.
|
||||||
|
model_tile_wire_cache:
|
||||||
|
RefCell<HashMap<usize, ((u64, u64), Arc<Vec<WireModel>>)>>,
|
||||||
/// Index built from every SortEntitiesTable in the document.
|
/// Index built from every SortEntitiesTable in the document.
|
||||||
/// Maps block_handle → (entity_handle.value() → sort_handle.value()).
|
/// Maps block_handle → (entity_handle.value() → sort_handle.value()).
|
||||||
/// Replaces the O(objects) linear scan inside `wires_for_block()` with an O(1) lookup.
|
/// Replaces the O(objects) linear scan inside `wires_for_block()` with an O(1) lookup.
|
||||||
|
|
@ -613,6 +647,7 @@ impl Scene {
|
||||||
camera_generation: 0,
|
camera_generation: 0,
|
||||||
geometry_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed),
|
geometry_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed),
|
||||||
wire_cache: RefCell::new(None),
|
wire_cache: RefCell::new(None),
|
||||||
|
model_tile_wire_cache: RefCell::new(HashMap::new()),
|
||||||
sort_cache: RefCell::new(None),
|
sort_cache: RefCell::new(None),
|
||||||
hatch_cache: RefCell::new(None),
|
hatch_cache: RefCell::new(None),
|
||||||
wipeout_cache: RefCell::new(None),
|
wipeout_cache: RefCell::new(None),
|
||||||
|
|
@ -1126,6 +1161,59 @@ impl Scene {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-tile cached tessellation for the Model layout. Each tile has
|
||||||
|
/// its own camera (live for the active tile, stored snapshot for the
|
||||||
|
/// others), so LOD / frustum culling has to run independently — the
|
||||||
|
/// shared `entity_wires_arc` cache would cull every tile against
|
||||||
|
/// whichever camera was current when it was last built.
|
||||||
|
///
|
||||||
|
/// `cam_aspect` is the tile's pixel `width / height`; together with the
|
||||||
|
/// camera's `ortho_size` it determines the world-XY rectangle culled
|
||||||
|
/// against. Returns a clone of the cached `Arc` on a key match.
|
||||||
|
pub(super) fn model_tile_wires_arc(
|
||||||
|
&self,
|
||||||
|
tile_idx: usize,
|
||||||
|
cam: &Camera,
|
||||||
|
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
|
||||||
|
};
|
||||||
|
let block = self.model_space_block_handle();
|
||||||
|
let arc = Arc::new(self.wires_for_block_culled(block, view_aabb, wpp));
|
||||||
|
self.model_tile_wire_cache
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(tile_idx, (key, Arc::clone(&arc)));
|
||||||
|
arc
|
||||||
|
}
|
||||||
|
|
||||||
/// Cached tessellation of the current layout block's paper-space entities.
|
/// Cached tessellation of the current layout block's paper-space entities.
|
||||||
/// Shared by both `entity_wires_arc()` and `paper_canvas_wires()` so a single
|
/// Shared by both `entity_wires_arc()` and `paper_canvas_wires()` so a single
|
||||||
/// cache miss triggers only one tessellation pass, not two.
|
/// cache miss triggers only one tessellation pass, not two.
|
||||||
|
|
@ -1330,6 +1418,22 @@ impl Scene {
|
||||||
|
|
||||||
/// Tessellate all non-invisible entities owned by `block_handle`.
|
/// Tessellate all non-invisible entities owned by `block_handle`.
|
||||||
fn wires_for_block(&self, block_handle: Handle) -> Vec<WireModel> {
|
fn wires_for_block(&self, block_handle: Handle) -> Vec<WireModel> {
|
||||||
|
// Default culling is driven by the live `Scene::camera`. Multi-tile
|
||||||
|
// Model layouts call `wires_for_block_culled` directly with each
|
||||||
|
// tile's own camera so the per-tile LOD / frustum cull is independent.
|
||||||
|
self.wires_for_block_culled(
|
||||||
|
block_handle,
|
||||||
|
self.view_world_aabb(),
|
||||||
|
self.world_per_pixel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wires_for_block_culled(
|
||||||
|
&self,
|
||||||
|
block_handle: Handle,
|
||||||
|
view_aabb: Option<[f32; 4]>,
|
||||||
|
wpp: Option<f32>,
|
||||||
|
) -> Vec<WireModel> {
|
||||||
use acadrust::objects::ObjectType;
|
use acadrust::objects::ObjectType;
|
||||||
|
|
||||||
// ── Ensure sort-order index is current ────────────────────────────
|
// ── Ensure sort-order index is current ────────────────────────────
|
||||||
|
|
@ -1391,7 +1495,7 @@ impl Scene {
|
||||||
// entities (Insert/Viewport) are appended via a small linear scan.
|
// entities (Insert/Viewport) are appended via a small linear scan.
|
||||||
// Paper space and the first-frame "settle" path fall back to the
|
// Paper space and the first-frame "settle" path fall back to the
|
||||||
// full doc scan — preserving prior behaviour.
|
// full doc scan — preserving prior behaviour.
|
||||||
let visible: Vec<&EntityType> = if let Some(local_view) = self.view_world_aabb() {
|
let visible: Vec<&EntityType> = if let Some(local_view) = view_aabb {
|
||||||
let [ox, oy, _] = self.world_offset;
|
let [ox, oy, _] = self.world_offset;
|
||||||
let view_wcs: [f64; 4] = [
|
let view_wcs: [f64; 4] = [
|
||||||
local_view[0] as f64 + ox,
|
local_view[0] as f64 + ox,
|
||||||
|
|
@ -1460,8 +1564,6 @@ impl Scene {
|
||||||
};
|
};
|
||||||
let blk_cache = self.block_cache_arc();
|
let blk_cache = self.block_cache_arc();
|
||||||
let blk_ref: &block_cache::BlockCache = &blk_cache;
|
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
|
// Zoom-adaptive curve sampling for top-level Edge tessellation. Target
|
||||||
// ~0.5 px chord height — far-out arcs that used to emit hundreds of
|
// ~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
|
// segments now collapse to a handful. The guard clears the override
|
||||||
|
|
@ -4976,6 +5078,7 @@ impl Scene {
|
||||||
};
|
};
|
||||||
ViewportInstance {
|
ViewportInstance {
|
||||||
handle: Handle::NULL,
|
handle: Handle::NULL,
|
||||||
|
tile_idx: Some(i),
|
||||||
screen_rect: iced::Rectangle {
|
screen_rect: iced::Rectangle {
|
||||||
x: tile.rect.x * canvas_w,
|
x: tile.rect.x * canvas_w,
|
||||||
y: tile.rect.y * canvas_h,
|
y: tile.rect.y * canvas_h,
|
||||||
|
|
@ -5010,6 +5113,7 @@ impl Scene {
|
||||||
};
|
};
|
||||||
out.push(ViewportInstance {
|
out.push(ViewportInstance {
|
||||||
handle: h,
|
handle: h,
|
||||||
|
tile_idx: None,
|
||||||
screen_rect,
|
screen_rect,
|
||||||
camera,
|
camera,
|
||||||
render_mode: vp.render_mode,
|
render_mode: vp.render_mode,
|
||||||
|
|
|
||||||
|
|
@ -580,7 +580,18 @@ impl Scene {
|
||||||
let us = (visible_w / full.width).clamp(0.0, 1.0);
|
let us = (visible_w / full.width).clamp(0.0, 1.0);
|
||||||
let vs = (visible_h / full.height).clamp(0.0, 1.0);
|
let vs = (visible_h / full.height).clamp(0.0, 1.0);
|
||||||
|
|
||||||
let base_arc = if inst.handle == acadrust::Handle::NULL {
|
// Model tiles each get their own tessellation cache keyed off the
|
||||||
|
// tile's camera — culling/LOD depend on per-tile zoom, not the
|
||||||
|
// active tile's. Paper-space content viewports already had a
|
||||||
|
// per-viewport cache.
|
||||||
|
let base_arc = if let Some(tile_idx) = inst.tile_idx {
|
||||||
|
let aspect = if full.height > 0.0 {
|
||||||
|
full.width / full.height
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
self.model_tile_wires_arc(tile_idx, &inst.camera, aspect, full.height)
|
||||||
|
} else if inst.handle == acadrust::Handle::NULL {
|
||||||
self.entity_wires_arc()
|
self.entity_wires_arc()
|
||||||
} else {
|
} else {
|
||||||
self.model_wires_for_viewport_arc(inst.handle)
|
self.model_wires_for_viewport_arc(inst.handle)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue