feat(scene): PERF frame-budget HUD for wire re-tessellation cost

Phase 5.3 (CPU tessellation slice). Add a `PERF` command that toggles an
overlay on the active viewport showing the cost of the most recent wire
re-tessellation: ms, wire count, geometry epoch. Anchored top-left via
position_canvas_overlay.

Timing is captured at the wire-cache miss paths (model_tile_wires_arc,
paper_sheet_wires_arc) and stored in Cells on Scene, read directly by the
UI view (Scene is single-threaded / Rc-based, so build_primitive and view
share a thread). Reads ~0 ms on a warm cache, so it isolates the exact
work a miss costs — the slice 2.2 / 3.1 / 3.3 each move, making them
PR-to-PR comparable.

GPU upload/draw/GPU-wait spans (need wgpu timestamp queries) remain open.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-05 01:07:17 +03:00
1 parent 7111922852
commit 9b112ce12c
5 files changed
+83 -3

No files matched your search

+10 -3
View File
@@ -261,10 +261,17 @@ Opened "x.dwg" — 84321 entities — parse 1.2s, purge 80ms, caches 340ms, xref
Regressions are visible immediately.
### 5.3 Frame-budget HUD
### 5.3 Frame-budget HUD ✅ DONE (CPU tess slice)
Add a CLI `PERF` toggle (or F12) that shows per-frame breakdown: tess ms,
upload ms, draw ms, GPU wait ms. Makes PR-to-PR comparison trivial.
A CLI `PERF` toggle overlays the cost of the most recent wire
re-tessellation (ms + wire count + geometry epoch) on the active viewport,
anchored top-left. Reads ~0 ms while the wire cache is warm (idle
pan/zoom), so it isolates exactly the work a cache miss costs — the slice
every render-path change (2.2 / 3.1 / 3.3) moves. Timed at the miss paths
in `model_tile_wires_arc` / `paper_sheet_wires_arc` and stored on `Scene`.
Still open: GPU-side `upload` / `draw` / `GPU-wait` spans need wgpu
timestamp queries; the CPU tessellation slice covers the current hot path.
---
+12
View File
@@ -48,6 +48,18 @@ impl OpenCADStudio {
return iced::exit();
}
// ── Frame-budget HUD (Phase 5.3) ───────────────────────────────
// Toggle the per-rebuild wire-tessellation readout overlay.
"PERF" => {
self.perf_hud = !self.perf_hud;
self.command_line.push_info(if self.perf_hud {
"PERF HUD on — shows last wire re-tessellation cost"
} else {
"PERF HUD off"
});
return Task::none();
}
// ── Background color ───────────────────────────────────────────
// Usage: BACKGROUND <r> <g> <b> (0–255 each)
// BACKGROUND RESET (restore default)
+4
View File
@@ -186,6 +186,9 @@ pub(super) struct OpenCADStudio {
/// Selection cycling: clicking where objects overlap opens a list box
/// to pick which one; the pick is added to the current selection.
selection_cycling: bool,
/// Frame-budget HUD (Phase 5.3): overlays the last wire re-tessellation
/// cost on the active viewport. Toggled by the `PERF` command.
perf_hud: bool,
/// When set, the cycling list box is open: (canvas point, candidates).
cycle_candidates: Option<(iced::Point, Vec<acadrust::Handle>)>,
/// Which status-bar pills the user has chosen to show (persisted).
@@ -1254,6 +1257,7 @@ impl OpenCADStudio {
clean_screen: false,
quick_properties: false,
selection_cycling: false,
perf_hud: false,
cycle_candidates: None,
pre_cmd_tangent: None,
ortho_mode: false,
+42
View File
@@ -1014,6 +1014,48 @@ impl OpenCADStudio {
}
}
// Frame-budget HUD (Phase 5.3): toggle with the PERF command. Shows
// the cost of the most recent wire re-tessellation — the work avoided
// by a warm wire cache — so render-path changes can be compared
// PR-to-PR. Reads ~0 ms while panning/zooming on a hit cache.
if self.perf_hud && !tab.is_start {
let s = &tab.scene;
let label = format!(
"tess {:.1} ms · {} wires · epoch {}",
s.last_tess_ms.get(),
s.last_tess_wires.get(),
s.geometry_epoch,
);
let panel = container(text(label).size(12).color(Color {
r: 0.6,
g: 1.0,
b: 0.6,
a: 1.0,
}))
.padding(6)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(Color {
r: 0.08,
g: 0.08,
b: 0.08,
a: 0.85,
})),
border: Border {
color: Color {
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
},
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
viewport_stack = viewport_stack
.push(position_canvas_overlay(iced::Point::new(12.0, 40.0), panel.into()));
}
// Selection-cycling list box: pick among overlapping objects.
if let Some((pt, cands)) = &self.cycle_candidates {
if !tab.is_start {
+15
View File
@@ -712,6 +712,13 @@ pub struct Scene {
/// without depending on the shader widget's internal `Program::State`
/// (which can miss events under overlapping overlays).
pub viewcube_hover: std::cell::Cell<Option<usize>>,
/// Wall time (ms) of the most recent wire re-tessellation — the work done
/// on a wire-cache miss in `model_tile_wires_arc` / `paper_sheet_wires_arc`.
/// Stays at the last value while the cache is hit (idle pan/zoom on a warm
/// cache reads ~0). Surfaced by the frame-budget HUD (Phase 5.3).
pub(crate) last_tess_ms: std::cell::Cell<f32>,
/// Wire count produced by that most recent re-tessellation.
pub(crate) last_tess_wires: std::cell::Cell<usize>,
}
impl Scene {
@@ -769,6 +776,8 @@ impl Scene {
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
last_world_per_pixel: std::cell::Cell::new(0.0),
viewcube_hover: std::cell::Cell::new(None),
last_tess_ms: std::cell::Cell::new(0.0),
last_tess_wires: std::cell::Cell::new(0),
}
}
@@ -1570,7 +1579,10 @@ impl Scene {
None
};
let block = self.model_space_block_handle();
let t_tess = std::time::Instant::now();
let arc = Arc::new(self.wires_for_block_culled(block, view_aabb, wpp, None, None));
self.last_tess_ms.set(t_tess.elapsed().as_secs_f32() * 1000.0);
self.last_tess_wires.set(arc.len());
self.model_tile_wire_cache
.borrow_mut()
.insert(tile_idx, (key, Arc::clone(&arc)));
@@ -1590,7 +1602,10 @@ impl Scene {
}
}
let layout_block = self.current_layout_block_handle();
let t_tess = std::time::Instant::now();
let mut wires = self.wires_for_block(layout_block);
self.last_tess_ms.set(t_tess.elapsed().as_secs_f32() * 1000.0);
self.last_tess_wires.set(wires.len());
// The overall "sheet" viewport now IS the paper view itself, so its own
// border rectangle must not be drawn as an entity on the sheet.
let sheet = self.current_layout_sheet_viewport_handle();