diff --git a/ROADMAP.md b/ROADMAP.md index e62ae9618..526c36b9f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. --- diff --git a/src/app/commands.rs b/src/app/commands.rs index 03ea4388c..b6389fa7c 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -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 (0–255 each) // BACKGROUND RESET (restore default) diff --git a/src/app/mod.rs b/src/app/mod.rs index d3637cb01..80bd69065 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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)>, /// 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, diff --git a/src/app/view.rs b/src/app/view.rs index 226809b6e..eaa358d54 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -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 { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 130381afb..0f38d799c 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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>, + /// 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, + /// Wire count produced by that most recent re-tessellation. + pub(crate) last_tess_wires: std::cell::Cell, } 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();