Fix layout cache correctness and GPU resource recovery

This commit is contained in:
Hakan Seven committed 2026-09-05 19:56:49 +03:00
1 parent 1a4b389b0e
commit ac8a545669
11 files changed
+352 -326

No files matched your search

+30
View File
@@ -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
View File
@@ -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.
+1 -2
View File
@@ -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()
}
+1 -3
View File
@@ -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
View File
@@ -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 {