forked from 1b-agent/opencadstudio-upstream
Fix layout cache correctness and GPU resource recovery
This commit is contained in:
1 parent
1a4b389b0e
commit
ac8a545669
11 files changed
+352
-326
No files matched your search
@@ -165,6 +165,7 @@ clipboard =
|
||||
.paste-to-original-coordinates = Özgün Koordinatlara Yapıştır
|
||||
|
||||
viewport =
|
||||
.preparing-layout = Yerleşim hazırlanıyor…
|
||||
.split-vertical = Görünüm Penceresini Dikey Böl
|
||||
.split-horizontal = Görünüm Penceresini Yatay Böl
|
||||
.move = Görünüm Penceresini Taşı
|
||||
|
||||
@@ -653,6 +653,36 @@ impl OpenCADStudio {
|
||||
mod tests {
|
||||
use crate::app::OpenCADStudio;
|
||||
|
||||
#[test]
|
||||
fn layout_notice_skips_grid_camera_and_scene_builds() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
assert_eq!(app.automation_op(r#"{"op":"new"}"#)["ok"], true);
|
||||
assert_eq!(app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 10,10"}"#)["ok"], true);
|
||||
let i = app.active_tab;
|
||||
let scene = &mut app.tabs[i].scene;
|
||||
scene.document.add_layout("Review").unwrap();
|
||||
scene.set_current_layout("Review".to_string());
|
||||
let mut viewport = acadrust::entities::Viewport::new();
|
||||
viewport.id = 2;
|
||||
viewport.width = 100.0;
|
||||
viewport.height = 50.0;
|
||||
viewport.status.is_on = true;
|
||||
scene.add_entity(acadrust::EntityType::Viewport(viewport));
|
||||
for entity in scene.document.entities_mut() {
|
||||
if let acadrust::EntityType::Viewport(viewport) = entity {
|
||||
viewport.status.grid_on = true;
|
||||
}
|
||||
}
|
||||
let before = scene.last_tess_wires.get();
|
||||
app.layout_settling = true;
|
||||
drop(app.view_main());
|
||||
assert_eq!(app.tabs[i].scene.last_tess_wires.get(), before);
|
||||
let _ = app.update(crate::app::Message::LayoutSettled);
|
||||
assert!(!app.layout_settling);
|
||||
drop(app.view_main());
|
||||
assert!(app.tabs[i].scene.last_tess_wires.get() > before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automation_ops_round_trip() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
|
||||
+2
-10
@@ -1055,14 +1055,7 @@ pub(super) struct OpenCADStudio {
|
||||
/// `Some` while a CAD file is loading — drives the modal overlay.
|
||||
/// Cleared when the load finishes, errors, or the user cancels.
|
||||
pub(super) opening: Option<OpenProgress>,
|
||||
/// Set the moment a layout switch changes `current_layout`, cleared on the
|
||||
/// next presented frame.
|
||||
///
|
||||
/// The switch itself costs about 10 ms; the frame after it rebuilds the
|
||||
/// whole wire set and can block the main thread for seconds. While this is
|
||||
/// set the viewport is replaced by a notice, so that frame is cheap — which
|
||||
/// gives the compositor an answer, and the user something other than a
|
||||
/// stale window, immediately before the expensive one.
|
||||
/// Show a lightweight notice until the next layout redraw.
|
||||
pub(super) layout_settling: bool,
|
||||
open_job_serial: u64,
|
||||
/// Last repair or failed-open report shown in the recovery modal.
|
||||
@@ -2292,8 +2285,7 @@ pub enum Message {
|
||||
/// Timer pulse while the cursor is dwelling on a grip; drives the
|
||||
/// dwell-to-popup transition without requiring further mouse motion.
|
||||
GripDwellTick,
|
||||
/// One frame has been presented since a layout switch; the next may do the
|
||||
/// expensive rebuild.
|
||||
/// The notice redraw allows the next frame to build the scene.
|
||||
LayoutSettled,
|
||||
/// Timer pulse while a rollover hit-test is queued; fires when the
|
||||
/// cursor has been still long enough to safely run the pick.
|
||||
|
||||
@@ -3046,8 +3046,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
Message::LayoutSettled => {
|
||||
// The notice frame has been presented, so the compositor has
|
||||
// had its answer. Let the next frame build the real thing.
|
||||
// Restore the scene after the notice redraw.
|
||||
self.layout_settling = false;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
@@ -5535,9 +5535,7 @@ impl OpenCADStudio {
|
||||
.layers
|
||||
.sync_with_viewports(&doc_layers, vp_info);
|
||||
let layers_ms = perf_phase.elapsed().as_secs_f64() * 1000.0;
|
||||
// The switch is cheap; the frame that follows it is not. Draw a notice
|
||||
// for one frame first, so the compositor gets an answer before the main
|
||||
// thread disappears into rebuilding the wire set.
|
||||
// Give the layout rebuild a visible notice first.
|
||||
self.layout_settling = true;
|
||||
if perf {
|
||||
crate::perf_record!(
|
||||
|
||||
+12
-18
@@ -36,11 +36,7 @@ pub(in crate::app) const VIEWPORT_CAPTURE_BOUNDS_ID: &str = "viewport-capture-bo
|
||||
|
||||
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_REGION_PX;
|
||||
|
||||
/// Base surface directly under the crosshair, selection marquee, and grid overlay.
|
||||
/// In paper space, drafting surface context is unconditionally the white sheet (`tab.scene.paper_bg_color`),
|
||||
/// matching AutoCAD's model (where crosshairs and overlays adapt per context — model vs. sheet/layout —
|
||||
/// rather than per-pixel cursor sampling). This avoids flicker across sheet edges and provides a clean
|
||||
/// dark crosshair that maintains ~5.85:1 contrast against the default medium-gray desk margin (#8A8A8A).
|
||||
/// Background used by drafting overlays in model or paper space.
|
||||
fn crosshair_background(tab: &DocumentTab, is_paper: bool) -> [f32; 4] {
|
||||
if !is_paper {
|
||||
return tab.scene.bg_color;
|
||||
@@ -273,10 +269,7 @@ impl OpenCADStudio {
|
||||
// borders) and the floating content viewports blit on top.
|
||||
mark("viewport_3d");
|
||||
let viewport_3d: Element<'_, Message> = if self.layout_settling && !tab.is_start {
|
||||
// One cheap frame between the switch and the rebuild. No shader
|
||||
// widget means no scene primitive, so `prepare` has nothing to do
|
||||
// and the event loop turns — which is what stops the compositor
|
||||
// deciding the application has stopped responding.
|
||||
// Omit the shader and drafting overlays for the notice frame.
|
||||
let bg = crosshair_background(tab, is_paper);
|
||||
let lum = 0.299 * bg[0] + 0.587 * bg[1] + 0.114 * bg[2];
|
||||
let ink = if lum > 0.5 {
|
||||
@@ -376,7 +369,7 @@ impl OpenCADStudio {
|
||||
// above the crosshair overlay so it actually receives mouse events, and
|
||||
// it owns the divider resize. Only built for the Model layout.
|
||||
mark("model_input");
|
||||
let model_input_layer: Option<Element<'_, Message>> = if is_paper || tab.is_start {
|
||||
let model_input_layer: Option<Element<'_, Message>> = if is_paper || tab.is_start || self.layout_settling {
|
||||
None
|
||||
} else {
|
||||
let scene = &tab.scene;
|
||||
@@ -394,7 +387,9 @@ impl OpenCADStudio {
|
||||
};
|
||||
|
||||
mark("grid");
|
||||
let grid_overlay = {
|
||||
let grid_overlay = if self.layout_settling || tab.is_start {
|
||||
Space::new().into()
|
||||
} else {
|
||||
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
||||
let model_basis = {
|
||||
let (o, ux, uy, uz) = tab.ucs_xform().axes();
|
||||
@@ -479,7 +474,9 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
};
|
||||
|
||||
mark("selection_overlay");
|
||||
let selection_overlay = {
|
||||
let selection_overlay = if self.layout_settling || tab.is_start {
|
||||
Space::new().into()
|
||||
} else {
|
||||
// Hold a single `Ref<'_, SelectionState>` for the whole overlay
|
||||
// block. The `vp_size` and `last_move_pos` reads below become
|
||||
// field accesses on `sel_ref` (no redundant `borrow()`s), and
|
||||
@@ -996,7 +993,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
};
|
||||
|
||||
mark("viewport_stack");
|
||||
let mut viewport_stack = if tab.is_start {
|
||||
let mut viewport_stack = if tab.is_start || self.layout_settling {
|
||||
// Start tab: only the welcome widget over a flat background.
|
||||
// Skip every drawing-only overlay (selection markers, snap info,
|
||||
// mouse-area capturing draw clicks, viewcube, nav toolbar, …).
|
||||
@@ -1057,7 +1054,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
.height(Fill)
|
||||
};
|
||||
|
||||
if !thumbnail_capture_clean {
|
||||
if !thumbnail_capture_clean && !self.layout_settling {
|
||||
// Per-pane input pane_grid goes ABOVE the crosshair overlay so it
|
||||
// receives mouse events (the overlay's `Hidden` cursor would otherwise
|
||||
// starve any layer beneath it). The controls bar is pushed on top of it.
|
||||
@@ -2279,10 +2276,7 @@ impl OpenCADStudio {
|
||||
// the mouse perfectly still — `ViewportMove` alone would never
|
||||
// fire again. Auto-stops once the hover clears or the popup is
|
||||
// already open.
|
||||
// One frame after a layout switch, then stop. `window::frames` fires
|
||||
// once a frame has actually been presented, which is the guarantee this
|
||||
// needs: the notice is on screen and the compositor has been answered
|
||||
// before the expensive frame starts.
|
||||
// Resume scene construction after the notice redraw.
|
||||
let layout_settle = if self.layout_settling {
|
||||
window::frames().map(|_| Message::LayoutSettled)
|
||||
} else {
|
||||
|
||||
+134
-229
@@ -1495,8 +1495,8 @@ pub enum ViewportRefreshScope {
|
||||
All,
|
||||
}
|
||||
|
||||
/// `(geometry_epoch, entities per block, entities no block claims)`.
|
||||
type BlockMembers = (u64, HashMap<Handle, Vec<Handle>>, Vec<Handle>);
|
||||
/// Entity membership in document order, keyed by geometry epoch.
|
||||
type BlockMembers = (u64, HashMap<Handle, Vec<Handle>>);
|
||||
|
||||
pub struct Scene {
|
||||
pub camera: Rc<RefCell<Camera>>,
|
||||
@@ -1836,24 +1836,9 @@ pub struct Scene {
|
||||
/// Reverse map: entity_handle → block_record_handle, built from entity_handles lists.
|
||||
/// Keyed by geometry_epoch. Eliminates the O(B) fallback scan in belongs_to_visible_block.
|
||||
entity_block_map_cache: RefCell<Option<(u64, HashMap<Handle, Handle>)>>,
|
||||
/// Candidate entities per block, keyed by `geometry_epoch`.
|
||||
///
|
||||
/// Selecting a block's contents used to walk every entity in the document
|
||||
/// and ask `belongs_to_visible_block` about each: 313 ms of a 334 ms
|
||||
/// resident build for a paper sheet holding 43 entities, on a drawing with
|
||||
/// 1.17 M. The membership rules only depend on owner handles and the block
|
||||
/// records, so one pass answers them for every block at once.
|
||||
///
|
||||
/// The second field holds entities no block claims, which
|
||||
/// `belongs_to_visible_block` treats as belonging to everything — but only
|
||||
/// when no block record enumerates its contents at all.
|
||||
/// Candidate handles per block, in document order and keyed by geometry epoch.
|
||||
block_members_cache: RefCell<Option<BlockMembers>>,
|
||||
/// Handles of entities the quadtree cannot index — Insert, Viewport, Block,
|
||||
/// BlockEnd — in document order, keyed by `geometry_epoch`.
|
||||
///
|
||||
/// The quadtree path has to append these regardless of the view, and did so
|
||||
/// by walking every entity in the document: ~300 ms of every resident
|
||||
/// rebuild on a 1.17 M-entity drawing, to find a few thousand.
|
||||
/// Insert/Viewport/Block/BlockEnd handles omitted by the spatial index.
|
||||
unindexable_cache: RefCell<Option<(u64, Vec<Handle>)>>,
|
||||
/// Distinct entity type names present in a layout, keyed by
|
||||
/// (geometry_epoch, layout block). The status bar asks for this on every
|
||||
@@ -1958,17 +1943,9 @@ pub struct Scene {
|
||||
/// Hash of the tessellation parameters `tess_memo` was built under. When
|
||||
/// the current call's parameters differ, the memo is stale and cleared.
|
||||
tess_memo_guard: std::cell::Cell<u64>,
|
||||
/// Per-entity memo for the **resident** model wire set (`model_tile_wires_arc`,
|
||||
/// the one the main GPU render holds). Kept separate from `tess_memo` because
|
||||
/// the resident set is camera-INDEPENDENT (no view cull, no zoom LOD), so its
|
||||
/// guard depends only on anno-scale — it survives pan/zoom and layout switches, and a
|
||||
/// single-entity edit re-tessellates just the changed entity instead of the
|
||||
/// whole model. Sharing `tess_memo` would let the camera-dependent culled path
|
||||
/// thrash it on every zoom. (#perf)
|
||||
/// Camera-independent model tessellation, separate from the culled memo.
|
||||
resident_tess_memo: RefCell<HashMap<Handle, Arc<Vec<WireModel>>>>,
|
||||
/// Guard hash for `resident_tess_memo` (anno-scale only). The background
|
||||
/// is not part of it: it changes colours, not geometry, and
|
||||
/// `resolve_colors_for_bg` reapplies it to the assembled set.
|
||||
/// Annotation, background, viewport and glyph-atlas signature.
|
||||
resident_tess_guard: std::cell::Cell<u64>,
|
||||
/// Per-mutation delta journal: which handles changed on each `geometry_epoch`
|
||||
/// bump. Bounded ring ([`GEOMETRY_JOURNAL_CAP`]); a derived cache replays the
|
||||
@@ -8916,7 +8893,7 @@ impl Scene {
|
||||
// the compiler enforces that. A default here is what made the first
|
||||
// version of this diagnostic report a constant.
|
||||
let visible_path: &str;
|
||||
let mut visible_candidates = 0usize;
|
||||
let mut visible_candidates: usize;
|
||||
|
||||
// Phase 2.1 — quadtree-driven candidate selection. When a view
|
||||
// AABB exists (Model layout with a settled camera), only iterate
|
||||
@@ -8978,6 +8955,7 @@ impl Scene {
|
||||
// — the legacy-DXF case. Both need the full walk, and both must
|
||||
// keep document order, which is draw order for the wire pass.
|
||||
visible_path = "full-scan";
|
||||
visible_candidates = self.document.entity_count();
|
||||
self.document
|
||||
.entities()
|
||||
.filter(|e| visibility_ok(e))
|
||||
@@ -8993,7 +8971,7 @@ impl Scene {
|
||||
// in exactly the order the full scan produced.
|
||||
visible_path = "block-index";
|
||||
let members = self.block_members();
|
||||
let (_, by_block, _) = &*members;
|
||||
let (_, by_block) = &*members;
|
||||
let claimed = by_block.get(&block_handle).map(Vec::as_slice).unwrap_or(&[]);
|
||||
visible_candidates = claimed.len();
|
||||
let mut out: Vec<&EntityType> = Vec::with_capacity(claimed.len());
|
||||
@@ -9089,11 +9067,7 @@ impl Scene {
|
||||
let mut guard_was_stale = false;
|
||||
let t_build = perf.then(iced::time::Instant::now);
|
||||
let mut wires: Vec<WireModel> = if memo_active {
|
||||
// Guard hash of everything tessellate_entity's GEOMETRY depends on
|
||||
// besides the entity itself. A mismatch (zoom/tol, view, anno,
|
||||
// offset, entered viewport) means the memo is stale. For the
|
||||
// resident path wpp/view_aabb are None, so this collapses to anno.
|
||||
// The background is excluded on purpose — see the note below.
|
||||
// Invalidate when any baked display input changes.
|
||||
let guard = {
|
||||
let mut g: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
let mut mix = |x: u64| g = g.rotate_left(13) ^ x;
|
||||
@@ -9106,13 +9080,11 @@ impl Scene {
|
||||
mix(anno.to_bits() as u64);
|
||||
mix(annotation_scale_handle.map(|handle| handle.value()).unwrap_or(0));
|
||||
mix(all_visible as u64);
|
||||
// The background is deliberately NOT mixed in. It changes
|
||||
// colours, never geometry — `local_wires_for` discards it with
|
||||
// `let _ = bg_color;` and `Batches::finalize` applies it last —
|
||||
// so a memo entry built under one background serves any other
|
||||
// once `resolve_colors_for_bg` has run over the assembled set
|
||||
// below. Keying on it made a layout switch re-tessellate the
|
||||
// whole model to arrive at identical points.
|
||||
// Direct entities and inherited/faded block colours still bake
|
||||
// the background before Batches::finalize records its inputs.
|
||||
for channel in bg {
|
||||
mix(channel.to_bits() as u64);
|
||||
}
|
||||
mix(avp.map(|h| h.value()).unwrap_or(0));
|
||||
// SDF glyph quads bake the atlas UV of each tile, so a growth or
|
||||
// a re-bake (which rescale / rewind every UV) makes memoized text
|
||||
@@ -9283,21 +9255,7 @@ guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}",
|
||||
wires
|
||||
}
|
||||
|
||||
/// Decide whether an entity should be drawn as direct content of `block_handle`.
|
||||
/// Entities each block may contain, and the ones no block claims.
|
||||
///
|
||||
/// Mirrors `belongs_to_visible_block` exactly, resolved once per
|
||||
/// `geometry_epoch` for every block instead of per entity per query:
|
||||
///
|
||||
/// * a non-null owner *is* the block, so the entity is that block's;
|
||||
/// * a null owner falls back to the reverse map built from the block
|
||||
/// records' `entity_handles`;
|
||||
/// * an entity neither names nor is named by any block is "unowned", and
|
||||
/// counts towards every block only when no block record enumerates its
|
||||
/// contents at all — the legacy-DXF case that predicate allows.
|
||||
/// Entities the quadtree cannot index, in document order, resolved once per
|
||||
/// `geometry_epoch`. Membership depends only on the entity's type, so an
|
||||
/// edit that does not add or remove one leaves the list unchanged.
|
||||
/// Unindexable entity handles in document order, cached per geometry epoch.
|
||||
fn unindexable_handles(&self) -> std::cell::Ref<'_, (u64, Vec<Handle>)> {
|
||||
{
|
||||
let cache = self.unindexable_cache.borrow();
|
||||
@@ -9318,10 +9276,11 @@ guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}",
|
||||
std::cell::Ref::map(self.unindexable_cache.borrow(), |c| c.as_ref().unwrap())
|
||||
}
|
||||
|
||||
/// Resolve owner handles and block-record membership once per geometry epoch.
|
||||
fn block_members(&self) -> std::cell::Ref<'_, BlockMembers> {
|
||||
{
|
||||
let cache = self.block_members_cache.borrow();
|
||||
if cache.as_ref().is_some_and(|(epoch, _, _)| *epoch == self.geometry_epoch) {
|
||||
if cache.as_ref().is_some_and(|(epoch, _)| *epoch == self.geometry_epoch) {
|
||||
drop(cache);
|
||||
return std::cell::Ref::map(self.block_members_cache.borrow(), |c| {
|
||||
c.as_ref().unwrap()
|
||||
@@ -9329,7 +9288,6 @@ guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}",
|
||||
}
|
||||
}
|
||||
let mut members: HashMap<Handle, Vec<Handle>> = HashMap::default();
|
||||
let mut unowned: Vec<Handle> = Vec::new();
|
||||
{
|
||||
let map = self.entity_block_map();
|
||||
for entity in self.document.entities() {
|
||||
@@ -9339,16 +9297,15 @@ guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}",
|
||||
members.entry(owner).or_default().push(common.handle);
|
||||
} else if let Some(&block) = map.get(&common.handle) {
|
||||
members.entry(block).or_default().push(common.handle);
|
||||
} else {
|
||||
unowned.push(common.handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
*self.block_members_cache.borrow_mut() =
|
||||
Some((self.geometry_epoch, members, unowned));
|
||||
Some((self.geometry_epoch, members));
|
||||
std::cell::Ref::map(self.block_members_cache.borrow(), |c| c.as_ref().unwrap())
|
||||
}
|
||||
|
||||
/// Whether the entity is direct content of this block.
|
||||
fn belongs_to_visible_block(
|
||||
&self,
|
||||
entity_handle: Handle,
|
||||
@@ -10044,15 +10001,7 @@ guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}",
|
||||
std::cell::Ref::map(self.entity_index_cache.borrow(), |c| &c.as_ref().unwrap().1)
|
||||
}
|
||||
|
||||
/// Full tessellation pipeline for one entity.
|
||||
/// The document-wide inputs `tessellate_entity` takes, resolved from the
|
||||
/// current layout and viewport.
|
||||
///
|
||||
/// Every entity in a walk gets the same four, and resolving them per entity
|
||||
/// is not free — `block_cache_arc_for` takes a `RefCell` borrow and a cache
|
||||
/// lookup. Callers that tessellate more than one entity resolve them once
|
||||
/// and pass them down; sharing the resolution keeps those callers from
|
||||
/// drifting away from `tessellate_one`.
|
||||
/// Resolve the active layout and annotation context for tessellation.
|
||||
fn tessellation_context(
|
||||
&self,
|
||||
) -> ([f32; 4], f32, Option<Handle>, Arc<cache::block_cache::BlockCache>, bool) {
|
||||
@@ -10227,167 +10176,43 @@ guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}",
|
||||
if model_block.is_null() {
|
||||
return None;
|
||||
}
|
||||
let mut min = glam::Vec3::splat(f32::INFINITY);
|
||||
let mut max = glam::Vec3::splat(f32::NEG_INFINITY);
|
||||
let mut any = false;
|
||||
|
||||
// Prefer the already-computed wire AABB cache when available — avoids re-tessellating.
|
||||
if self.current_layout == "Model" {
|
||||
let cache = self.wire_cache.borrow();
|
||||
if let Some(((epoch, _cam_gen), _gen, ref arc)) = *cache {
|
||||
if epoch == self.geometry_epoch {
|
||||
for wire in arc.iter() {
|
||||
let [ax, ay, bx, by] = wire.aabb;
|
||||
let lo = glam::Vec3::new(ax, ay, 0.0);
|
||||
let hi = glam::Vec3::new(bx, by, 0.0);
|
||||
// Reject the whole AABB unless every component is finite:
|
||||
// rays/xlines carry an unbounded AABB, and checking only
|
||||
// x let a vertical ray's infinite y poison the extents.
|
||||
if lo.is_finite() && hi.is_finite() {
|
||||
min = min.min(lo);
|
||||
max = max.max(hi);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
// 3D solids render as meshes, not wires, so fold their
|
||||
// complete 3D boxes in too — otherwise view fitting loses
|
||||
// both solid-only drawings and their depth.
|
||||
for (&handle, set) in &self.meshes {
|
||||
if !self.mesh_entity_visible(handle)
|
||||
|| !self.document.get_entity(handle).is_some_and(|entity| {
|
||||
self.belongs_to_visible_block(
|
||||
handle,
|
||||
entity.common().owner_handle,
|
||||
model_block,
|
||||
)
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let [ax, ay, bx, by] = set.world_aabb;
|
||||
let [az, bz] = set.z_aabb;
|
||||
let lo = glam::Vec3::new(ax, ay, az);
|
||||
let hi = glam::Vec3::new(bx, by, bz);
|
||||
if lo.is_finite() && hi.is_finite() {
|
||||
min = min.min(lo);
|
||||
max = max.max(hi);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
return if any { Some((min, max)) } else { None };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: tessellate (first call or paper-space context).
|
||||
// wire.key_vertices live in offset-rel coords (world_offset
|
||||
// already subtracted at tessellation time). Add it back so the
|
||||
// result matches Path 1 above and the caller's expectation —
|
||||
// callers (auto_fit_viewport) write the centroid directly to
|
||||
// `Viewport.view_target`, which is a WCS field; storing
|
||||
// offset-rel coords there silently double-subtracts world_offset
|
||||
// inside `camera_for_viewport` and points the viewport at the
|
||||
// wrong location on UTM-scale drawings.
|
||||
//
|
||||
// `tessellate_one` resolves the background, annotation scale and block
|
||||
// cache from `&self` on every call, and this walk calls it once per
|
||||
// model entity — 186 k times on a large drawing, each one taking a
|
||||
// `RefCell` borrow for a block cache that is the same every time. Those
|
||||
// inputs do not vary across the loop, so they are resolved once and the
|
||||
// shared tessellator is called directly, which also lets the walk run
|
||||
// in parallel the way the wire builder's own tessellation pass does.
|
||||
// Same arguments, same output: only who computes them changed.
|
||||
let (bg_for_tess, anno_for_tess, anno_handle_for_tess, blk_for_tess, paper_for_tess) =
|
||||
self.tessellation_context();
|
||||
let subjects: Vec<&EntityType> = self
|
||||
.document
|
||||
.entities()
|
||||
.filter(|entity| {
|
||||
let c = entity.common();
|
||||
c.owner_handle == model_block
|
||||
&& !c.invisible
|
||||
&& !self.entity_temporarily_hidden(c.handle)
|
||||
})
|
||||
.collect();
|
||||
let doc = &self.document;
|
||||
let sel = &self.selected;
|
||||
let avp = self.active_viewport;
|
||||
let blk_ref: &cache::block_cache::BlockCache = &blk_for_tess;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
let reduced = subjects
|
||||
.into_par_iter()
|
||||
.map(|entity| {
|
||||
let mut lo = glam::Vec3::splat(f32::INFINITY);
|
||||
let mut hi = glam::Vec3::splat(f32::NEG_INFINITY);
|
||||
let mut hit = false;
|
||||
for wire in tessellate_entity(
|
||||
doc,
|
||||
sel,
|
||||
avp,
|
||||
bg_for_tess,
|
||||
anno_for_tess,
|
||||
anno_handle_for_tess,
|
||||
entity,
|
||||
Some(blk_ref),
|
||||
None,
|
||||
None,
|
||||
paper_for_tess,
|
||||
) {
|
||||
for &[x, y, z] in &wire.key_vertices {
|
||||
let v = glam::Vec3::new(x as f32, y as f32, z as f32);
|
||||
// Check finiteness *after* the f32 cast: a ray/xline
|
||||
// endpoint is a huge-but-finite f64 that overflows to
|
||||
// inf in f32, which the f64 `is_finite` test would have
|
||||
// let through.
|
||||
if v.is_finite() {
|
||||
lo = lo.min(v);
|
||||
hi = hi.max(v);
|
||||
hit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
(lo, hi, hit)
|
||||
})
|
||||
.reduce(
|
||||
|| {
|
||||
(
|
||||
glam::Vec3::splat(f32::INFINITY),
|
||||
glam::Vec3::splat(f32::NEG_INFINITY),
|
||||
false,
|
||||
)
|
||||
},
|
||||
|a, b| (a.0.min(b.0), a.1.max(b.1), a.2 || b.2),
|
||||
);
|
||||
if reduced.2 {
|
||||
min = min.min(reduced.0);
|
||||
max = max.max(reduced.1);
|
||||
any = true;
|
||||
}
|
||||
// Same mesh inclusion for the tessellate fallback path.
|
||||
for (&handle, set) in &self.meshes {
|
||||
// Reuse the full resident source in every layout. The kernel bounds
|
||||
// its world-space key vertices; camera fitting never tessellates a
|
||||
// second copy or reads the current paper sheet's wire cache.
|
||||
let scale = crate::scene::annotative::scale_handle_by_name(
|
||||
&self.document,
|
||||
&self.document.header.current_annotation_scale,
|
||||
);
|
||||
let wires = self.resident_wires_for(
|
||||
model_block,
|
||||
Some(self.annotation_scale),
|
||||
scale,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let wire_points = wires.iter().flat_map(|wire| wire.key_vertices.iter().copied());
|
||||
let mesh_points = self.meshes.iter().filter_map(|(&handle, set)| {
|
||||
let entity = self.document.get_entity(handle)?;
|
||||
if !self.mesh_entity_visible(handle)
|
||||
|| !self.document.get_entity(handle).is_some_and(|entity| {
|
||||
self.belongs_to_visible_block(
|
||||
handle,
|
||||
entity.common().owner_handle,
|
||||
model_block,
|
||||
)
|
||||
})
|
||||
|| !self.belongs_to_visible_block(handle, entity.common().owner_handle, model_block)
|
||||
{
|
||||
continue;
|
||||
return None;
|
||||
}
|
||||
let [ax, ay, bx, by] = set.world_aabb;
|
||||
let [az, bz] = set.z_aabb;
|
||||
let lo = glam::Vec3::new(ax, ay, az);
|
||||
let hi = glam::Vec3::new(bx, by, bz);
|
||||
if lo.is_finite() && hi.is_finite() {
|
||||
min = min.min(lo);
|
||||
max = max.max(hi);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if any {
|
||||
return Some((min, max));
|
||||
Some([
|
||||
[ax as f64, ay as f64, az as f64],
|
||||
[bx as f64, by as f64, bz as f64],
|
||||
])
|
||||
}).flatten();
|
||||
let points = wire_points.chain(mesh_points).filter(|point| {
|
||||
point.iter().all(|coordinate| (*coordinate as f32).is_finite())
|
||||
});
|
||||
if let Some(bounds) = cadkernel::brep::bounds::Aabb::around(points) {
|
||||
return Some((
|
||||
glam::Vec3::from_array(bounds.min.map(|v| v as f32)),
|
||||
glam::Vec3::from_array(bounds.max.map(|v| v as f32)),
|
||||
));
|
||||
}
|
||||
// Last resort: saved EXTMIN/EXTMAX before the wire cache is built.
|
||||
const SANE_EXTENT: f64 = 1.0e16;
|
||||
@@ -10649,7 +10474,7 @@ mod journal_tests {
|
||||
// What the membership index reports for `block`, in order.
|
||||
fn indexed(s: &Scene, block: Handle) -> Vec<Handle> {
|
||||
let members = s.block_members();
|
||||
let (_, by_block, _) = &*members;
|
||||
let (_, by_block) = &*members;
|
||||
by_block.get(&block).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -11311,3 +11136,83 @@ mod selection_arc_tests {
|
||||
assert!(true);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod layout_cache_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn memoized_plain_line_matches_fresh_background() {
|
||||
let mut s = Scene::new();
|
||||
s.bg_color = [0.1, 0.1, 0.1, 1.0];
|
||||
let handle = s.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
|
||||
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
||||
acadrust::types::Vector3::new(10.0, 0.0, 0.0),
|
||||
)));
|
||||
let build = |s: &Scene| {
|
||||
s.wires_for_block_culled(
|
||||
s.model_space_block_handle(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.into_iter()
|
||||
.find(|wire| wire.name == handle.value().to_string())
|
||||
.unwrap()
|
||||
};
|
||||
let original = build(&s);
|
||||
assert!(s.resident_tess_memo.borrow().contains_key(&handle));
|
||||
s.paper_bg_color = [1.0; 4];
|
||||
s.document.add_layout("Review").unwrap();
|
||||
s.set_current_layout("Review".to_string());
|
||||
let cached = build(&s);
|
||||
s.resident_tess_memo.borrow_mut().clear();
|
||||
let fresh = build(&s);
|
||||
assert_ne!(
|
||||
original.color, fresh.color,
|
||||
"fixture must depend on background"
|
||||
);
|
||||
assert_eq!(
|
||||
cached.color, fresh.color,
|
||||
"memo hit should match fresh tessellation on a white background"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_extents_reuse_resident_geometry_in_every_layout() {
|
||||
let mut s = Scene::new();
|
||||
s.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
|
||||
acadrust::types::Vector3::new(1_000_000.0, 2_000_000.0, 30.0),
|
||||
acadrust::types::Vector3::new(1_000_010.0, 2_000_020.0, 40.0),
|
||||
)));
|
||||
s.document.add_layout("Review").unwrap();
|
||||
for layout in ["Model", "Review"] {
|
||||
s.set_current_layout(layout.to_string());
|
||||
*s.model_extents_cache.borrow_mut() = None;
|
||||
let scale = crate::scene::annotative::scale_handle_by_name(
|
||||
&s.document,
|
||||
&s.document.header.current_annotation_scale,
|
||||
);
|
||||
let source = s.resident_wires_for(
|
||||
s.model_space_block_handle(),
|
||||
Some(s.annotation_scale),
|
||||
scale,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let generation = s.last_model_wire_gen.get();
|
||||
let bounds = s.model_space_extents().unwrap();
|
||||
assert_eq!(bounds.0, glam::Vec3::new(1_000_000.0, 2_000_000.0, 30.0));
|
||||
assert_eq!(bounds.1, glam::Vec3::new(1_000_010.0, 2_000_020.0, 40.0));
|
||||
assert_eq!(
|
||||
s.last_model_wire_gen.get(),
|
||||
generation,
|
||||
"fitting must reuse the resident source"
|
||||
);
|
||||
assert!(!source.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,3 +319,92 @@ fn toggling_shadows_reallocates_without_upsetting_the_device() {
|
||||
"GPU validation failed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a GPU adapter"]
|
||||
fn urgent_release_preserves_visible_panes_and_frees_cold_slots() {
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||
let adapter =
|
||||
block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default())).unwrap();
|
||||
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: adapter.limits(),
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap();
|
||||
let mut pipeline = <MultiPipeline as iced::widget::shader::Pipeline>::new(
|
||||
&device,
|
||||
&queue,
|
||||
wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||
);
|
||||
let first = pipeline.resolve_slots(&device, &queue, &[10])[0];
|
||||
pipeline.inners[first].slot_id = 10;
|
||||
pipeline.inners[first].ensure_depth_texture(&device, Size::new(512, 512));
|
||||
let second = pipeline.resolve_slots(&device, &queue, &[20])[0];
|
||||
pipeline.inners[second].slot_id = 20;
|
||||
pipeline.inners[second].ensure_depth_texture(&device, Size::new(512, 512));
|
||||
pipeline.release_idle_slots(&device, &[second], true);
|
||||
assert_eq!(
|
||||
pipeline.inners[first].alloc_size,
|
||||
Size::new(512, 512),
|
||||
"a sibling pane prepared immediately before this pane is still visible"
|
||||
);
|
||||
|
||||
pipeline
|
||||
.frame_rendered
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let next = pipeline.resolve_slots(&device, &queue, &[20]);
|
||||
assert_eq!(
|
||||
pipeline.release_idle_slots(&device, &next, true),
|
||||
0,
|
||||
"a previous-frame pane may still be prepared later in this frame"
|
||||
);
|
||||
pipeline
|
||||
.frame_rendered
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let next = pipeline.resolve_slots(&device, &queue, &[20]);
|
||||
assert_eq!(pipeline.release_idle_slots(&device, &next, true), 1);
|
||||
assert_eq!(pipeline.inners[first].alloc_size, Size::new(0, 0));
|
||||
assert_eq!(pipeline.inners[second].alloc_size, Size::new(512, 512));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a GPU adapter"]
|
||||
fn late_device_errors_invalidate_uploaded_content_and_targets() {
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||
let adapter =
|
||||
block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default())).unwrap();
|
||||
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: adapter.limits(),
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap();
|
||||
let mut pipeline = <MultiPipeline as iced::widget::shader::Pipeline>::new(
|
||||
&device,
|
||||
&queue,
|
||||
wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||
);
|
||||
let inner = &mut pipeline.inners[0];
|
||||
inner.cached_wire_id = 7;
|
||||
inner.wire_arena_id = 7;
|
||||
inner.ensure_depth_texture(&device, Size::new(512, 512));
|
||||
let before = gpu_errors_seen();
|
||||
let _rejected = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("rejected test allocation"),
|
||||
size: device.limits().max_buffer_size + 4,
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
assert!(gpu_errors_seen() > before);
|
||||
inner.sync_gpu_error_epoch();
|
||||
assert_eq!(inner.cached_wire_id, u64::MAX);
|
||||
assert_eq!(inner.wire_arena_id, u64::MAX);
|
||||
assert_eq!(inner.alloc_size, Size::new(0, 0));
|
||||
assert_eq!(inner.background_source_id, usize::MAX);
|
||||
inner.cached_wire_id = 8;
|
||||
inner.sync_gpu_error_epoch();
|
||||
assert_eq!(
|
||||
inner.cached_wire_id, 8,
|
||||
"the same error must not invalidate twice"
|
||||
);
|
||||
}
|
||||
@@ -159,6 +159,19 @@ impl HatchGpu {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clear(&mut self) {
|
||||
match &mut self.backend {
|
||||
HatchBackend::Storage { resident, preview } => {
|
||||
resident.clear();
|
||||
preview.clear();
|
||||
}
|
||||
HatchBackend::Texture { resident, preview } => {
|
||||
resident.clear();
|
||||
preview.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, hatches: &[HatchModel]) {
|
||||
match &mut self.backend {
|
||||
HatchBackend::Storage { resident, .. } => {
|
||||
|
||||
+57
-49
@@ -74,6 +74,7 @@ struct MeshHighlightDraw {
|
||||
}
|
||||
|
||||
pub struct Pipeline {
|
||||
gpu_error_epoch: usize,
|
||||
background_pipeline: wgpu::RenderPipeline,
|
||||
shadow_pipeline: wgpu::RenderPipeline,
|
||||
shadow_plain_pipeline: wgpu::RenderPipeline,
|
||||
@@ -2226,6 +2227,7 @@ impl Pipeline {
|
||||
wire_const_bgl,
|
||||
block_wire_const_bgl,
|
||||
block_wire_mode: wire_mode,
|
||||
gpu_error_epoch: gpu_errors_seen(),
|
||||
wipeout_pipeline,
|
||||
hatch_gpu,
|
||||
image_pipeline,
|
||||
@@ -3557,9 +3559,12 @@ impl Pipeline {
|
||||
fn sync_shadow_target(&mut self, device: &wgpu::Device) {
|
||||
match (self.shadow_enabled, self.shadow_full.is_some()) {
|
||||
(true, false) => {
|
||||
let errors_before = gpu_errors_seen();
|
||||
let texture = create_shadow_texture(device, SHADOW_MAP_SIZE);
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
self.shadow_full = Some((texture, view));
|
||||
if gpu_errors_seen() == errors_before {
|
||||
self.shadow_full = Some((texture, view));
|
||||
}
|
||||
}
|
||||
(false, true) => self.shadow_full = None,
|
||||
_ => return,
|
||||
@@ -3575,7 +3580,11 @@ impl Pipeline {
|
||||
) {
|
||||
self.shadow_enabled = uniforms.shadow_params[0] > 0.5;
|
||||
self.sync_shadow_target(device);
|
||||
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(uniforms));
|
||||
let mut uniforms = *uniforms;
|
||||
if self.shadow_full.is_none() {
|
||||
uniforms.shadow_params[0] = 0.0;
|
||||
}
|
||||
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
|
||||
}
|
||||
|
||||
pub fn upload_background_images(
|
||||
@@ -3710,9 +3719,7 @@ impl Pipeline {
|
||||
// render rectangle does the clipping).
|
||||
let stencil_ref: u32 = if self.clip_boundary.is_some() { 0xFF } else { 0 };
|
||||
|
||||
// `shadow_enabled` says the style wants shadows; the target existing
|
||||
// says the device could give us one. Without the second test a failed
|
||||
// 16 MiB allocation would render the pass into the 1x1 placeholder.
|
||||
// Render shadows only when a full target was allocated.
|
||||
if let Some(shadow_target) = self
|
||||
.shadow_full
|
||||
.as_ref()
|
||||
@@ -4671,6 +4678,23 @@ impl Pipeline {
|
||||
self.render_sig = u64::MAX;
|
||||
}
|
||||
|
||||
/// Catch errors delivered after the previous upload/submit completed.
|
||||
pub(crate) fn sync_gpu_error_epoch(&mut self) {
|
||||
let errors = gpu_errors_seen();
|
||||
if errors == self.gpu_error_epoch {
|
||||
return;
|
||||
}
|
||||
self.gpu_error_epoch = errors;
|
||||
self.forget_cached_keys();
|
||||
self.wire_arena = None;
|
||||
self.wire_arena_mesh = None;
|
||||
self.wire_arena_id = u64::MAX;
|
||||
self.alloc_size = Size::new(0, 0);
|
||||
self.shadow_full = None;
|
||||
self.background_source_id = usize::MAX;
|
||||
self.environment_source_id = usize::MAX;
|
||||
}
|
||||
|
||||
/// Give this slot's device memory back and forget what it held.
|
||||
///
|
||||
/// A slot nobody is drawing still owns its wire arena, its render targets,
|
||||
@@ -4698,6 +4722,7 @@ impl Pipeline {
|
||||
self.gpu_selected_wires.clear();
|
||||
self.gpu_selected_block_wires.clear();
|
||||
self.gpu_preview_wires.clear();
|
||||
self.hatch_gpu.clear();
|
||||
self.gpu_wipeouts.clear();
|
||||
self.wipeout_skip_flags.clear();
|
||||
self.gpu_images.clear();
|
||||
@@ -4928,19 +4953,8 @@ impl Pipeline {
|
||||
}
|
||||
|
||||
impl MultiPipeline {
|
||||
/// Hand back the device memory of slots nobody is drawing.
|
||||
///
|
||||
/// `reserved` are the slots this frame is about to use; they are never
|
||||
/// touched. Everything else is released once it has been idle for
|
||||
/// `idle_frames` prepare calls — gradually, so an ordinary session pays
|
||||
/// nothing and a layout the user keeps switching back to stays warm.
|
||||
///
|
||||
/// `urgent` drops that patience: the device has just refused an allocation,
|
||||
/// and holding a viewport nobody can see is what turns a degraded frame
|
||||
/// into a session that never recovers. Releasing early costs one slow frame
|
||||
/// if the slot is wanted again; not releasing costs the drawing.
|
||||
///
|
||||
/// Returns how many slots were released.
|
||||
/// Release cold slots after 64 frames, or two frames under memory pressure.
|
||||
/// Current and previous frame slots remain reserved for sibling widgets.
|
||||
pub(crate) fn release_idle_slots(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
@@ -4948,7 +4962,9 @@ impl MultiPipeline {
|
||||
urgent: bool,
|
||||
) -> usize {
|
||||
const IDLE_FRAMES: u64 = 64;
|
||||
let idle_frames = if urgent { 0 } else { IDLE_FRAMES };
|
||||
// Other shader widgets may prepare later in this frame. Keep every
|
||||
// slot used in this or the previous frame, even under memory pressure.
|
||||
let idle_frames = if urgent { 2 } else { IDLE_FRAMES };
|
||||
let mut released = 0;
|
||||
for index in 0..self.inners.len() {
|
||||
if reserved.contains(&index) {
|
||||
@@ -4970,6 +4986,15 @@ impl MultiPipeline {
|
||||
self.inners[index].release_heavy_resources(device);
|
||||
released += 1;
|
||||
}
|
||||
if released > 0 {
|
||||
self.wire_buffer_cache.retain(|_, entry| {
|
||||
std::sync::Arc::strong_count(&entry.0) > 1
|
||||
|| std::sync::Arc::strong_count(&entry.1) > 1
|
||||
});
|
||||
self.block_geometry.retain(|_, chunks| {
|
||||
chunks.iter().any(|chunk| std::sync::Arc::strong_count(&chunk.bind_group) > 1)
|
||||
});
|
||||
}
|
||||
released
|
||||
}
|
||||
|
||||
@@ -5104,25 +5129,11 @@ pub struct MultiPipeline {
|
||||
pub(crate) slot_by_instance: rustc_hash::FxHashMap<u64, usize>,
|
||||
slot_last_used: Vec<u64>,
|
||||
slot_clock: u64,
|
||||
/// The resident wire batches, keyed by `wire_content_id` and shared across
|
||||
/// every slot (and every pane — one `MultiPipeline` backs all of them) that
|
||||
/// renders the same content. `prepare` builds an entry once on a cache miss
|
||||
/// then hands `Arc` clones to each slot, so N paper viewports / Model tiles
|
||||
/// showing one identical resident set upload the wire vertex buffers exactly
|
||||
/// once between them instead of once per slot. Kept trim by dropping entries
|
||||
/// no slot still references (`Arc::strong_count == 1`) once it grows past a
|
||||
/// small bound.
|
||||
/// Block geometry that survives an edit, keyed by the definition it
|
||||
/// expands rather than by anything a viewport contributes. See
|
||||
/// `wire_gpu::BlockGeometryCache`.
|
||||
///
|
||||
/// Shared across slots for the same reason `wire_buffer_cache` is: a
|
||||
/// definition's segments are identical in every viewport that draws it, and
|
||||
/// this is the largest upload the renderer makes — 59.5 MiB on the
|
||||
/// reproducer. Held per slot, zooming a paper layout out until four or five
|
||||
/// viewports were on screen uploaded it once each and ran a 2 GB card out
|
||||
/// of memory.
|
||||
pub(crate) frame_rendered: std::sync::atomic::AtomicBool,
|
||||
pub(crate) gpu_error_epoch: usize,
|
||||
/// Definition geometry shared across viewport slots.
|
||||
pub(crate) block_geometry: wire_gpu::BlockGeometryCache,
|
||||
/// Shared resident batches, keyed by wire content identity.
|
||||
pub(crate) wire_buffer_cache: rustc_hash::FxHashMap<
|
||||
u64,
|
||||
(
|
||||
@@ -5134,11 +5145,7 @@ pub struct MultiPipeline {
|
||||
}
|
||||
|
||||
impl MultiPipeline {
|
||||
/// Ensure at least `n` (≥1) inner pipelines exist, creating any missing
|
||||
/// ones. Grow-only: extra pipelines are NOT dropped, because per-pane Model
|
||||
/// shader widgets share this storage and each only touches its own slot —
|
||||
/// truncating mid-frame would destroy another pane's slot. Stale inners (a
|
||||
/// closed pane, or fewer paper viewports) are harmless idle GPU resources.
|
||||
/// Grow slot storage without disturbing sibling shader widgets.
|
||||
pub(crate) fn ensure_len(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
@@ -5152,12 +5159,7 @@ impl MultiPipeline {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve stable slots for the viewport identities in one primitive.
|
||||
/// Thirty-two hot slots cover ordinary tiled/paper drawings. A cold slot
|
||||
/// is recycled only after several other prepare calls, which prevents
|
||||
/// sibling Model panes prepared in the same frame from evicting each
|
||||
/// other. If every slot is still hot, growing is safer than a visible
|
||||
/// rebuild hitch.
|
||||
/// Preserve viewport slots across frames; recycle only cold slots above the limit.
|
||||
pub(crate) fn resolve_slots(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
@@ -5167,7 +5169,11 @@ impl MultiPipeline {
|
||||
const SOFT_LIMIT: usize = 32;
|
||||
const HOT_WINDOW: u64 = 8;
|
||||
|
||||
self.slot_clock = self.slot_clock.wrapping_add(1).max(1);
|
||||
if self.frame_rendered.swap(false, std::sync::atomic::Ordering::Relaxed)
|
||||
|| self.slot_clock == 0
|
||||
{
|
||||
self.slot_clock = self.slot_clock.wrapping_add(1).max(1);
|
||||
}
|
||||
let now = self.slot_clock;
|
||||
let reserved: rustc_hash::FxHashSet<u64> = instance_ids.iter().copied().collect();
|
||||
// `inner.slot_id` is updated later in `prepare`, after this whole
|
||||
@@ -5284,6 +5290,8 @@ impl iced::widget::shader::Pipeline for MultiPipeline {
|
||||
slot_by_instance: rustc_hash::FxHashMap::default(),
|
||||
slot_last_used: vec![0],
|
||||
slot_clock: 0,
|
||||
frame_rendered: std::sync::atomic::AtomicBool::new(false),
|
||||
gpu_error_epoch: gpu_errors_seen(),
|
||||
wire_buffer_cache: rustc_hash::FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
+12
-15
@@ -364,20 +364,20 @@ impl shader::Primitive for Primitive {
|
||||
viewport: &Viewport,
|
||||
) {
|
||||
let nav_prepare_started = iced::time::Instant::now();
|
||||
let errors_at_entry = crate::scene::pipeline::gpu_errors_seen();
|
||||
let recovering = pipeline.gpu_error_epoch != errors_at_entry;
|
||||
if recovering {
|
||||
pipeline.wire_buffer_cache.clear();
|
||||
pipeline.block_geometry.clear();
|
||||
pipeline.gpu_error_epoch = errors_at_entry;
|
||||
}
|
||||
let scale = viewport.scale_factor() as f32;
|
||||
let instance_ids: Vec<u64> = self.viewports.iter().map(|vp| vp.instance_id).collect();
|
||||
let slots = pipeline.resolve_slots(device, queue, &instance_ids);
|
||||
// Compared after the frame's uploads: a move means the device refused
|
||||
// something, which changes how patient the slot release below can be.
|
||||
let errors_at_entry = crate::scene::pipeline::gpu_errors_seen();
|
||||
|
||||
for (i, vp) in self.viewports.iter().enumerate() {
|
||||
let inner = &mut pipeline.inners[slots[i]];
|
||||
// Compared at the end of this viewport's turn. Every "we hold this
|
||||
// now" latch below is stamped after its upload, whether or not the
|
||||
// device accepted it — and the wire latch gates the whole upload
|
||||
// block, so one rejected allocation could leave a slotpermanently empty with
|
||||
// nothing that would ever rebuild it.
|
||||
inner.sync_gpu_error_epoch();
|
||||
// Do not latch rejected uploads as current content.
|
||||
let slot_errors_before = crate::scene::pipeline::gpu_errors_seen();
|
||||
// Pipeline slots are addressed by list index, but off-canvas
|
||||
// viewports are dropped from the list — so a slot can be reused by a
|
||||
@@ -1216,13 +1216,9 @@ impl shader::Primitive for Primitive {
|
||||
self.viewports.len(),
|
||||
);
|
||||
}
|
||||
// Give back what nobody is drawing. Gradual while the device is
|
||||
// healthy; immediate once it has refused an allocation, because the
|
||||
// memory a hidden viewport is holding is exactly what the visible one
|
||||
// could not get — and freeing it is what lets the operator close a
|
||||
// viewport or two and carry on instead of restarting.
|
||||
// Memory pressure shortens cold-slot retention without evicting siblings.
|
||||
let errors_now = crate::scene::pipeline::gpu_errors_seen();
|
||||
let urgent = errors_now != errors_at_entry;
|
||||
let urgent = recovering || errors_now != errors_at_entry;
|
||||
let released = pipeline.release_idle_slots(device, &slots, urgent);
|
||||
if released > 0 && crate::perf::enabled() {
|
||||
crate::perf_record!(
|
||||
@@ -1241,6 +1237,7 @@ impl shader::Primitive for Primitive {
|
||||
clip: &Rectangle<u32>,
|
||||
) {
|
||||
let nav_render_started = iced::time::Instant::now();
|
||||
pipeline.frame_rendered.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let cw = clip.width as f32;
|
||||
let ch = clip.height as f32;
|
||||
let clip_right = clip.x + clip.width;
|
||||
|
||||
Reference in new issue
Block a user