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 <noreply@anthropic.com>
This commit is contained in:
parent
7111922852
commit
9b112ce12c
5 changed files with 83 additions and 3 deletions
13
ROADMAP.md
13
ROADMAP.md
|
|
@ -261,10 +261,17 @@ Opened "x.dwg" — 84321 entities — parse 1.2s, purge 80ms, caches 340ms, xref
|
||||||
|
|
||||||
Regressions are visible immediately.
|
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,
|
A CLI `PERF` toggle overlays the cost of the most recent wire
|
||||||
upload ms, draw ms, GPU wait ms. Makes PR-to-PR comparison trivial.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,18 @@ impl OpenCADStudio {
|
||||||
return iced::exit();
|
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 ───────────────────────────────────────────
|
// ── Background color ───────────────────────────────────────────
|
||||||
// Usage: BACKGROUND <r> <g> <b> (0–255 each)
|
// Usage: BACKGROUND <r> <g> <b> (0–255 each)
|
||||||
// BACKGROUND RESET (restore default)
|
// BACKGROUND RESET (restore default)
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,9 @@ pub(super) struct OpenCADStudio {
|
||||||
/// Selection cycling: clicking where objects overlap opens a list box
|
/// Selection cycling: clicking where objects overlap opens a list box
|
||||||
/// to pick which one; the pick is added to the current selection.
|
/// to pick which one; the pick is added to the current selection.
|
||||||
selection_cycling: bool,
|
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).
|
/// When set, the cycling list box is open: (canvas point, candidates).
|
||||||
cycle_candidates: Option<(iced::Point, Vec<acadrust::Handle>)>,
|
cycle_candidates: Option<(iced::Point, Vec<acadrust::Handle>)>,
|
||||||
/// Which status-bar pills the user has chosen to show (persisted).
|
/// Which status-bar pills the user has chosen to show (persisted).
|
||||||
|
|
@ -1254,6 +1257,7 @@ impl OpenCADStudio {
|
||||||
clean_screen: false,
|
clean_screen: false,
|
||||||
quick_properties: false,
|
quick_properties: false,
|
||||||
selection_cycling: false,
|
selection_cycling: false,
|
||||||
|
perf_hud: false,
|
||||||
cycle_candidates: None,
|
cycle_candidates: None,
|
||||||
pre_cmd_tangent: None,
|
pre_cmd_tangent: None,
|
||||||
ortho_mode: false,
|
ortho_mode: false,
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// Selection-cycling list box: pick among overlapping objects.
|
||||||
if let Some((pt, cands)) = &self.cycle_candidates {
|
if let Some((pt, cands)) = &self.cycle_candidates {
|
||||||
if !tab.is_start {
|
if !tab.is_start {
|
||||||
|
|
|
||||||
|
|
@ -712,6 +712,13 @@ pub struct Scene {
|
||||||
/// without depending on the shader widget's internal `Program::State`
|
/// without depending on the shader widget's internal `Program::State`
|
||||||
/// (which can miss events under overlapping overlays).
|
/// (which can miss events under overlapping overlays).
|
||||||
pub viewcube_hover: std::cell::Cell<Option<usize>>,
|
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 {
|
impl Scene {
|
||||||
|
|
@ -769,6 +776,8 @@ impl Scene {
|
||||||
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
|
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
|
||||||
last_world_per_pixel: std::cell::Cell::new(0.0),
|
last_world_per_pixel: std::cell::Cell::new(0.0),
|
||||||
viewcube_hover: std::cell::Cell::new(None),
|
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
|
None
|
||||||
};
|
};
|
||||||
let block = self.model_space_block_handle();
|
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));
|
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
|
self.model_tile_wire_cache
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.insert(tile_idx, (key, Arc::clone(&arc)));
|
.insert(tile_idx, (key, Arc::clone(&arc)));
|
||||||
|
|
@ -1590,7 +1602,10 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let layout_block = self.current_layout_block_handle();
|
let layout_block = self.current_layout_block_handle();
|
||||||
|
let t_tess = std::time::Instant::now();
|
||||||
let mut wires = self.wires_for_block(layout_block);
|
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
|
// 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.
|
// border rectangle must not be drawn as an entity on the sheet.
|
||||||
let sheet = self.current_layout_sheet_viewport_handle();
|
let sheet = self.current_layout_sheet_viewport_handle();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue