perf(render): hold model wire set static instead of re-tessellating on camera moves
The model-space wire geometry is camera-independent, yet every pan/zoom re-tessellated the whole model and re-uploaded it to the GPU. On a block/text-heavy drawing that is millions of segments, costing a ~100ms re-tessellation plus a ~150ms GPU re-upload per camera change (~258ms freeze) while the GPU itself draws the full set comfortably in real time. Tessellate the full, un-culled model once per geometry epoch, hold it resident, and return it for every camera and tile. A pan/zoom now only changes the view uniform; no frustum cull, no zoom LOD, no re-tess or re-upload on camera moves. Pan/zoom frame time drops to ~7ms (~140fps) with no stutter. Removes the now-dead per-tile cull cache, pan-invariant hash, AABB containment helper, and the reuse-during-motion / settle-regen machinery that an earlier iteration introduced. Also exempts Face3D from the sub-pixel stub LOD: a flat face is trivially cheap to tessellate, so it is drawn full at any zoom and no longer pops a cube stub at the threshold. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4e8521010b
commit
14ebdd36d1
2 changed files with 65 additions and 139 deletions
166
src/scene/mod.rs
166
src/scene/mod.rs
|
|
@ -62,13 +62,13 @@ use std::sync::Arc;
|
||||||
/// GPU Pipeline to skip re-uploading geometry when switching tabs.
|
/// GPU Pipeline to skip re-uploading geometry when switching tabs.
|
||||||
static GEOMETRY_EPOCH: AtomicU64 = AtomicU64::new(1);
|
static GEOMETRY_EPOCH: AtomicU64 = AtomicU64::new(1);
|
||||||
|
|
||||||
/// Process-wide monotonic id stamped on each Model-tile wire re-tessellation.
|
/// Process-wide monotonic id stamped each time the Model wire set is built.
|
||||||
/// Reused (not re-stamped) when a pan reuses the cached tessellation, so it
|
/// The set is held static across camera moves, so the id stays the same every
|
||||||
/// uniquely identifies a wire-buffer's *content* across frames. The GPU
|
/// frame until the geometry epoch changes — it uniquely identifies a wire
|
||||||
/// pipeline gates wire re-upload on it (Phase 3.2): a pan that reuses the
|
/// buffer's *content* across frames. The GPU pipeline gates wire re-upload on
|
||||||
/// tessellation keeps the same id, so the world-space wire buffer is not
|
/// it: an unchanged id means the world-space wire buffer is not re-sent.
|
||||||
/// re-sent. Monotonic (never reused) → free of the ABA hazard a raw `Arc`
|
/// Monotonic (never reused) → free of the ABA hazard a raw `Arc` pointer would
|
||||||
/// pointer would carry when an address is freed and reallocated.
|
/// carry when an address is freed and reallocated.
|
||||||
static WIRE_CONTENT_GEN: AtomicU64 = AtomicU64::new(1);
|
static WIRE_CONTENT_GEN: AtomicU64 = AtomicU64::new(1);
|
||||||
|
|
||||||
/// Resolve a viewport's paper-to-model scale ratio from its two
|
/// Resolve a viewport's paper-to-model scale ratio from its two
|
||||||
|
|
@ -636,35 +636,6 @@ fn transform_block_mesh_lod_set(
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
|
||||||
/// 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 = 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, 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]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// World-XY rectangle the model camera currently sees, expanded by `margin`
|
/// World-XY rectangle the model camera currently sees, expanded by `margin`
|
||||||
/// (1.0 = tight), as `[min_x, min_y, max_x, max_y]` for the entity R-tree cull.
|
/// (1.0 = tight), as `[min_x, min_y, max_x, max_y]` for the entity R-tree cull.
|
||||||
///
|
///
|
||||||
|
|
@ -761,19 +732,6 @@ 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, 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, [f32; 4]), 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.
|
||||||
|
|
@ -897,13 +855,18 @@ pub struct Scene {
|
||||||
pub(crate) last_tess_ms: std::cell::Cell<f32>,
|
pub(crate) last_tess_ms: std::cell::Cell<f32>,
|
||||||
/// Wire count produced by that most recent re-tessellation.
|
/// Wire count produced by that most recent re-tessellation.
|
||||||
pub(crate) last_tess_wires: std::cell::Cell<usize>,
|
pub(crate) last_tess_wires: std::cell::Cell<usize>,
|
||||||
/// Content id ([`WIRE_CONTENT_GEN`]) of the Model-tile wire set returned by
|
/// Content id ([`WIRE_CONTENT_GEN`]) of the Model wire set returned by the
|
||||||
/// the most recent `model_tile_wires_arc` call — stamped on a miss, reused
|
/// most recent `model_tile_wires_arc` call — stamped when the static set is
|
||||||
/// on a pan-hit. `build_primitive` reads it right after the call to gate
|
/// (re)built, otherwise the held value. `build_primitive` reads it right
|
||||||
/// GPU wire re-upload (Phase 3.2). 0 = none yet.
|
/// after the call to gate GPU wire re-upload. 0 = none yet.
|
||||||
pub(crate) last_model_wire_gen: std::cell::Cell<u64>,
|
pub(crate) last_model_wire_gen: std::cell::Cell<u64>,
|
||||||
|
/// Static-hold cache for the Model layout: the FULL, un-culled tessellation
|
||||||
|
/// held resident and reused for every camera (the geometry is
|
||||||
|
/// camera-independent — see `model_tile_wires_arc`). `(epoch, gen, wires)`;
|
||||||
|
/// rebuilt when `geometry_epoch` changes.
|
||||||
|
model_static_wires: RefCell<Option<(u64, u64, Arc<Vec<WireModel>>)>>,
|
||||||
/// Monotonic per-build nonce for wire sources that must NOT be skipped by
|
/// Monotonic per-build nonce for wire sources that must NOT be skipped by
|
||||||
/// the Phase 3.2 upload gate — the paper / per-viewport wire paths and any
|
/// the upload gate — the paper / per-viewport wire paths and any
|
||||||
/// frame carrying live preview / interim wires. High bit set so it can
|
/// frame carrying live preview / interim wires. High bit set so it can
|
||||||
/// never collide with a real [`WIRE_CONTENT_GEN`] id; incremented every
|
/// never collide with a real [`WIRE_CONTENT_GEN`] id; incremented every
|
||||||
/// use so the GPU always sees a fresh id and re-uploads.
|
/// use so the GPU always sees a fresh id and re-uploads.
|
||||||
|
|
@ -963,7 +926,6 @@ impl Scene {
|
||||||
block_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed),
|
block_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed),
|
||||||
selection_generation: 0,
|
selection_generation: 0,
|
||||||
wire_cache: RefCell::new(None),
|
wire_cache: RefCell::new(None),
|
||||||
model_tile_wire_cache: RefCell::new(HashMap::default()),
|
|
||||||
sort_cache: RefCell::new(None),
|
sort_cache: RefCell::new(None),
|
||||||
draw_depth_cache: RefCell::new(None),
|
draw_depth_cache: RefCell::new(None),
|
||||||
hatch_cache: RefCell::new(None),
|
hatch_cache: RefCell::new(None),
|
||||||
|
|
@ -996,6 +958,7 @@ impl Scene {
|
||||||
last_tess_ms: std::cell::Cell::new(0.0),
|
last_tess_ms: std::cell::Cell::new(0.0),
|
||||||
last_tess_wires: std::cell::Cell::new(0),
|
last_tess_wires: std::cell::Cell::new(0),
|
||||||
last_model_wire_gen: std::cell::Cell::new(0),
|
last_model_wire_gen: std::cell::Cell::new(0),
|
||||||
|
model_static_wires: RefCell::new(None),
|
||||||
wire_force_nonce: std::cell::Cell::new(0),
|
wire_force_nonce: std::cell::Cell::new(0),
|
||||||
split_cache: RefCell::new(None),
|
split_cache: RefCell::new(None),
|
||||||
tess_memo: RefCell::new(HashMap::default()),
|
tess_memo: RefCell::new(HashMap::default()),
|
||||||
|
|
@ -2005,83 +1968,44 @@ impl Scene {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-tile cached tessellation for the Model layout. Each tile has
|
/// Wire set for the Model layout, shared by every tile.
|
||||||
/// 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
|
/// The model wire geometry is **camera-independent**, so it is tessellated
|
||||||
/// camera's `ortho_size` it determines the world-XY rectangle culled
|
/// in full (un-culled, fixed detail) once per geometry epoch, held resident
|
||||||
/// against. Returns a clone of the cached `Arc` on a key match.
|
/// (`model_static_wires`), and returned for any camera/tile. A pan/zoom only
|
||||||
|
/// changes the view uniform — the GPU re-draws the same buffer, with no
|
||||||
|
/// frustum cull, no zoom LOD, and no re-tessellation/re-upload on camera
|
||||||
|
/// moves. The per-tile camera args are unused (kept for call-site symmetry
|
||||||
|
/// with the paper-space wire sources).
|
||||||
pub(super) fn model_tile_wires_arc(
|
pub(super) fn model_tile_wires_arc(
|
||||||
&self,
|
&self,
|
||||||
tile_idx: usize,
|
_tile_idx: usize,
|
||||||
cam: &Camera,
|
_cam: &Camera,
|
||||||
cam_aspect: f32,
|
_cam_aspect: f32,
|
||||||
tile_pixel_height: f32,
|
_tile_pixel_height: f32,
|
||||||
) -> Arc<Vec<WireModel>> {
|
) -> Arc<Vec<WireModel>> {
|
||||||
let wpp = if tile_pixel_height > 0.0 {
|
// Reuse the resident full set if it's already built for this epoch.
|
||||||
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.
|
|
||||||
// A tilted view returns no XY box (cull disabled) → require the cached
|
|
||||||
// region to be the full plane, so we only reuse a full tessellation.
|
|
||||||
let full = [f32::NEG_INFINITY, f32::NEG_INFINITY, f32::INFINITY, f32::INFINITY];
|
|
||||||
let need_region = if self.camera_generation == 0 {
|
|
||||||
full
|
|
||||||
} else {
|
|
||||||
view_cull_aabb(cam, cam_aspect, 1.0).unwrap_or(full)
|
|
||||||
};
|
|
||||||
{
|
{
|
||||||
let cache = self.model_tile_wire_cache.borrow();
|
let held = self.model_static_wires.borrow();
|
||||||
if let Some(((epoch, sig, region), gen, arc)) = cache.get(&tile_idx) {
|
if let Some((epoch, gen, arc)) = held.as_ref() {
|
||||||
if *epoch == self.geometry_epoch
|
if *epoch == self.geometry_epoch {
|
||||||
&& *sig == pan_sig
|
|
||||||
&& aabb_contains(*region, need_region)
|
|
||||||
{
|
|
||||||
// Pan-hit: reuse the tessellation AND its content id, so
|
|
||||||
// the GPU wire upload is skipped too (Phase 3.2).
|
|
||||||
self.last_model_wire_gen.set(*gen);
|
self.last_model_wire_gen.set(*gen);
|
||||||
return Arc::clone(arc);
|
return Arc::clone(arc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Miss → cull/tessellate to the 1.25×-margin region (unchanged cost)
|
// Build once: full tessellation, no cull (region = None), no zoom LOD
|
||||||
// and remember that region for the pan-reuse test above.
|
// (wpp = None). Held for the life of this geometry epoch.
|
||||||
// A tilted view yields no XY box → `None` = cull nothing (full tess),
|
|
||||||
// which is correct, just heavier, for the less common 3D/elevation case.
|
|
||||||
let tess_region = if self.camera_generation == 0 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
view_cull_aabb(cam, cam_aspect, 1.25)
|
|
||||||
};
|
|
||||||
let block = self.model_space_block_handle();
|
let block = self.model_space_block_handle();
|
||||||
let t_tess = iced::time::Instant::now();
|
let t_tess = iced::time::Instant::now();
|
||||||
let mut wires = self.wires_for_block_culled(block, tess_region, wpp, None, None);
|
let mut wires = self.wires_for_block_culled(block, None, None, None, None);
|
||||||
self.apply_refedit_fade(&mut wires, self.bg_color);
|
self.apply_refedit_fade(&mut wires, self.bg_color);
|
||||||
let arc = Arc::new(wires);
|
|
||||||
self.last_tess_ms.set(t_tess.elapsed().as_secs_f32() * 1000.0);
|
self.last_tess_ms.set(t_tess.elapsed().as_secs_f32() * 1000.0);
|
||||||
self.last_tess_wires.set(arc.len());
|
self.last_tess_wires.set(wires.len());
|
||||||
let stored_region = tess_region
|
let arc = Arc::new(wires);
|
||||||
.unwrap_or([f32::NEG_INFINITY, f32::NEG_INFINITY, f32::INFINITY, f32::INFINITY]);
|
|
||||||
// New tessellation → fresh content id; the GPU will re-upload.
|
|
||||||
let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed);
|
let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed);
|
||||||
self.last_model_wire_gen.set(gen);
|
self.last_model_wire_gen.set(gen);
|
||||||
self.model_tile_wire_cache.borrow_mut().insert(
|
*self.model_static_wires.borrow_mut() = Some((self.geometry_epoch, gen, Arc::clone(&arc)));
|
||||||
tile_idx,
|
|
||||||
((self.geometry_epoch, pan_sig, stored_region), gen, Arc::clone(&arc)),
|
|
||||||
);
|
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -8345,10 +8269,14 @@ fn tessellate_entity(
|
||||||
// (baseline-line / greek / full) and must reach it
|
// (baseline-line / greek / full) and must reach it
|
||||||
// even when projected size is sub-5 px.
|
// even when projected size is sub-5 px.
|
||||||
let is_text = matches!(e, EntityType::Text(_) | EntityType::MText(_));
|
let is_text = matches!(e, EntityType::Text(_) | EntityType::MText(_));
|
||||||
|
// Face3D is exempt from the sub-pixel stub: it is trivially
|
||||||
|
// cheap to tessellate (4 corners → 2 tris), so there is no
|
||||||
|
// cost to draw it full at any zoom, and the cube-stub
|
||||||
|
// otherwise pops/coarsens flat faces across the threshold.
|
||||||
|
let is_face3d = matches!(e, EntityType::Face3D(_));
|
||||||
let is_3d_entity = matches!(
|
let is_3d_entity = matches!(
|
||||||
e,
|
e,
|
||||||
EntityType::Face3D(_)
|
EntityType::Solid3D(_)
|
||||||
| EntityType::Solid3D(_)
|
|
||||||
| EntityType::Mesh(_)
|
| EntityType::Mesh(_)
|
||||||
| EntityType::PolyfaceMesh(_)
|
| EntityType::PolyfaceMesh(_)
|
||||||
| EntityType::PolygonMesh(_)
|
| EntityType::PolygonMesh(_)
|
||||||
|
|
@ -8356,7 +8284,7 @@ fn tessellate_entity(
|
||||||
| EntityType::Region(_)
|
| EntityType::Region(_)
|
||||||
| EntityType::Surface(_)
|
| EntityType::Surface(_)
|
||||||
);
|
);
|
||||||
if !is_text && w_px.max(h_px) / wpp < 5.0 {
|
if !is_text && !is_face3d && w_px.max(h_px) / wpp < 5.0 {
|
||||||
// Sub-pixel entity: emit a stub instead of
|
// Sub-pixel entity: emit a stub instead of
|
||||||
// nothing so it stays visible / selectable /
|
// nothing so it stays visible / selectable /
|
||||||
// hit-test'able at any zoom. 2-D entities
|
// hit-test'able at any zoom. 2-D entities
|
||||||
|
|
|
||||||
|
|
@ -80,10 +80,10 @@ pub struct ViewportData {
|
||||||
/// Camera generation captured when this Primitive was assembled. Paired
|
/// Camera generation captured when this Primitive was assembled. Paired
|
||||||
/// with `geometry_epoch` so the per-frame scissor / LOD recompute runs.
|
/// with `geometry_epoch` so the per-frame scissor / LOD recompute runs.
|
||||||
pub(in crate::scene) camera_generation: u64,
|
pub(in crate::scene) camera_generation: u64,
|
||||||
/// Content id of `wires` (Phase 3.2). Stable across a pure pan that reuses
|
/// Content id of `wires`. Stable across camera moves (the Model wire set is
|
||||||
/// the Model-tile tessellation, so `prepare` skips re-uploading the
|
/// held static), so `prepare` skips re-uploading the world-space wire buffer
|
||||||
/// world-space wire buffer when only the camera moved. Non-tile and
|
/// when only the camera moved. Non-tile and preview/interim frames carry a
|
||||||
/// preview/interim frames carry a fresh id each time → always re-upload.
|
/// fresh id each time → always re-upload.
|
||||||
pub(in crate::scene) wire_content_id: u64,
|
pub(in crate::scene) wire_content_id: u64,
|
||||||
/// Selected handles only (no hover) — solid meshes tint these blue.
|
/// Selected handles only (no hover) — solid meshes tint these blue.
|
||||||
pub(in crate::scene) selected_handles: Arc<rustc_hash::FxHashSet<acadrust::Handle>>,
|
pub(in crate::scene) selected_handles: Arc<rustc_hash::FxHashSet<acadrust::Handle>>,
|
||||||
|
|
@ -264,11 +264,11 @@ impl shader::Primitive for Primitive {
|
||||||
}
|
}
|
||||||
// Wire buffers are world-space, so a camera move alone doesn't
|
// Wire buffers are world-space, so a camera move alone doesn't
|
||||||
// change them — only the view_proj uniform (uploaded every frame).
|
// change them — only the view_proj uniform (uploaded every frame).
|
||||||
// Gate the upload on the wire content id instead of the camera tick
|
// Gate the upload on the wire content id instead of the camera tick:
|
||||||
// (Phase 3.2): a pan that reused the Model-tile tessellation keeps
|
// the Model wire set is held static, so its id is unchanged across
|
||||||
// the id, skipping the vertex re-pack + GPU write. Kept independent
|
// camera moves and the vertex re-pack + GPU write is skipped. Kept
|
||||||
// of the `cur_key` block so a preview/interim wire change still
|
// independent of the `cur_key` block so a preview/interim wire change
|
||||||
// uploads even when the camera didn't move.
|
// still uploads even when the camera didn't move.
|
||||||
if vp.wire_content_id != inner.cached_wire_id {
|
if vp.wire_content_id != inner.cached_wire_id {
|
||||||
inner.upload_wires(device, &vp.wires[..], &vp.draw_depths);
|
inner.upload_wires(device, &vp.wires[..], &vp.draw_depths);
|
||||||
inner.cached_wire_id = vp.wire_content_id;
|
inner.cached_wire_id = vp.wire_content_id;
|
||||||
|
|
@ -663,14 +663,12 @@ 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);
|
||||||
|
|
||||||
// Model tiles each get their own tessellation cache keyed off the
|
// Model tiles all share one resident, camera-independent wire set
|
||||||
// tile's camera — culling/LOD depend on per-tile zoom, not the
|
// (`model_tile_wires_arc` holds it static). `tile_wire_gen` is that
|
||||||
// active tile's. Paper-space content viewports already had a
|
// set's content id: stable across camera moves, so the GPU wire upload
|
||||||
// per-viewport cache.
|
// and the Face3D split below are skipped every frame the geometry is
|
||||||
// `tile_wire_gen` is the Model-tile content id (Phase 3.2): present
|
// unchanged. The paper-space sources have no stable id and force a
|
||||||
// only on the tiled Model path, where a pan reuses the tessellation
|
// re-upload.
|
||||||
// and its id so the GPU wire upload can be skipped. The other wire
|
|
||||||
// sources don't participate and force a re-upload below.
|
|
||||||
let (base_arc, tile_wire_gen) = if let Some(tile_idx) = inst.tile_idx {
|
let (base_arc, tile_wire_gen) = if let Some(tile_idx) = inst.tile_idx {
|
||||||
let aspect = if full.height > 0.0 {
|
let aspect = if full.height > 0.0 {
|
||||||
full.width / full.height
|
full.width / full.height
|
||||||
|
|
@ -703,9 +701,9 @@ impl Scene {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Split Face3D wires from the rest. The split is content-only (keyed
|
// Split Face3D wires from the rest. The split is content-only (keyed
|
||||||
// by the tile's wire id), so on a pan-reuse it's memoized rather than
|
// by the wire-set content id), so while the geometry is unchanged it's
|
||||||
// re-walking every wire (handle lookup + clone) each frame. Non-tile
|
// memoized rather than re-walking every wire (handle lookup + clone)
|
||||||
// paths have no stable id and split inline as before.
|
// each frame. Non-tile paths have no stable id and split inline.
|
||||||
let (face3d_wires, other_arc) = match tile_wire_gen {
|
let (face3d_wires, other_arc) = match tile_wire_gen {
|
||||||
Some(gen) => {
|
Some(gen) => {
|
||||||
let cached = {
|
let cached = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue