forked from 1b-agent/opencadstudio-upstream
perf(render): cache rendered scene, skip re-raster on unchanged frames
Every cursor movement over the viewport re-rendered the entire GPU scene (all wires, hatches, meshes) even when nothing visible changed: iced redraws the whole window on each input message, and the shader's draw/prepare/render runs every time. On large drawings one such redraw exceeds the frame budget, so moving the mouse or panning stutters — and it gets worse the bigger the drawing. The pipeline already resolves each frame into a persistent, clip-sized resolve texture and blits that to the surface. This hashes the inputs that determine the rendered image (camera uniforms, geometry/selection generations, wire content id, render-mode flags, clip size, live preview coordinates) into a per-viewport signature. When a frame's signature matches the previous one the image is pixel-identical, so `prepare` skips the buffer uploads + per-frame scissor/LOD recompute and `render` skips passes 1-7 plus the MSAA resolve, re-blitting the resolve texture instead. A pure cursor move becomes a single fullscreen blit rather than an O(N) re-rasterization. The ViewCube is excluded from the signature (it renders in its own always-on pass) so cube hover still tracks while the scene is cached. Any real change — edit (geometry_epoch), select/hover (selection_generation), camera move or render-mode toggle — differs the signature and forces a full render, so nothing goes stale. Navigation still renders every frame (the view genuinely changes); this targets the wasted re-render on cursor-only movement.
This commit is contained in:
1 parent
aa63634d95
commit
f99f1cd7a5
2 files changed
+96
No files matched your search
@@ -178,6 +178,19 @@ pub struct Pipeline {
|
||||
/// gather just the highlighted entity's wires (`O(highlighted)`) instead of
|
||||
/// scanning and string-parsing every wire on each hover change.
|
||||
wire_handle_index: rustc_hash::FxHashMap<u64, Vec<u32>>,
|
||||
/// Signature of the last frame's rendered scene (camera uniforms, geometry
|
||||
/// / selection generations, wire content id, render-mode flags, clip size,
|
||||
/// live preview). When the next frame's signature matches, the image is
|
||||
/// pixel-identical, so `render` skips every geometry pass + the MSAA
|
||||
/// resolve and only re-blits the resolve texture (which still holds it).
|
||||
/// This turns a pure cursor move over a large drawing from an O(N)
|
||||
/// re-rasterization into a single fullscreen blit. `u64::MAX` = nothing
|
||||
/// rendered yet (forces the first frame to render fully).
|
||||
pub render_sig: u64,
|
||||
/// Set by `prepare` each frame: `true` when this frame's signature matched
|
||||
/// `render_sig`, so `render` may skip the geometry passes. Read (never
|
||||
/// written) by `render`, which always runs after `prepare` for the frame.
|
||||
pub skip_geometry: bool,
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
@@ -1156,6 +1169,8 @@ impl Pipeline {
|
||||
cached_mesh_key: (u64::MAX, u64::MAX),
|
||||
cached_face3d_key: (u64::MAX, false),
|
||||
wire_handle_index: rustc_hash::FxHashMap::default(),
|
||||
render_sig: u64::MAX,
|
||||
skip_geometry: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1553,6 +1568,13 @@ impl Pipeline {
|
||||
a: a as f64,
|
||||
};
|
||||
|
||||
// Scene-render cache: when `prepare` found this frame pixel-identical
|
||||
// to the last (unchanged view / geometry / selection / preview), the
|
||||
// resolve texture already holds the image. Skip every geometry pass +
|
||||
// the MSAA resolve and fall straight through to the blit below. This
|
||||
// is what makes a pure cursor move cost one fullscreen blit instead of
|
||||
// re-rasterizing the whole drawing every frame.
|
||||
if !self.skip_geometry {
|
||||
// ── Pass 1: hatch fills ────────────────────────────────────────────
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
@@ -2101,6 +2123,7 @@ impl Pipeline {
|
||||
});
|
||||
// No draw calls — the pass itself triggers the MSAA resolve.
|
||||
}
|
||||
} // end `if !self.skip_geometry`
|
||||
|
||||
// ── Blit resolve texture → surface target at surface_dest position ──
|
||||
// The viewport maps the NDC quad to exactly `surface_dest` in the
|
||||
|
||||
@@ -235,6 +235,35 @@ impl shader::Primitive for Primitive {
|
||||
let (uo_y, us_y) = uv_crop_axis(sr.y, sr.height);
|
||||
inner.upload_blit_uv(queue, [uo_x, uo_y], [us_x, us_y]);
|
||||
inner.upload_uniforms(queue, &vp.uniforms);
|
||||
|
||||
// ── Scene-render cache ────────────────────────────────────────
|
||||
// A pure cursor move — or any frame where the view, geometry,
|
||||
// selection and live preview are all unchanged — produces a
|
||||
// pixel-identical image. The resolve texture still holds it, so we
|
||||
// skip every geometry pass + the MSAA resolve (in `Pipeline::render`
|
||||
// via `skip_geometry`) and its per-frame O(N) scissor / LOD
|
||||
// recompute below, letting the frame reduce to a single blit. This
|
||||
// is the main fix for the per-mouse-move stall that scales with
|
||||
// drawing size. The ViewCube is excluded from the signature and
|
||||
// keeps updating in its own always-on pass, so cube hover still
|
||||
// tracks while the scene is cached.
|
||||
let sig = render_signature(vp, clip_size.width, clip_size.height);
|
||||
let skip = inner.render_sig != u64::MAX && sig == inner.render_sig;
|
||||
inner.render_sig = sig;
|
||||
inner.skip_geometry = skip;
|
||||
if skip {
|
||||
if vp.show_viewcube {
|
||||
inner.viewcube.upload(
|
||||
queue,
|
||||
vp.cam_rotation,
|
||||
vp.compass_rotation,
|
||||
(vp.screen_rect.width * bounds.width) as u32,
|
||||
(vp.screen_rect.height * bounds.height) as u32,
|
||||
vp.hover_region,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Third component is the *selected-set* signature (not
|
||||
// selection_generation, which also bumps on hover) so a rollover
|
||||
// doesn't re-upload the static hatch / face3d buffers.
|
||||
@@ -432,6 +461,50 @@ impl shader::Primitive for Primitive {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash of everything that determines one viewport's rendered scene image.
|
||||
/// Two consecutive frames with the same signature are pixel-identical, so the
|
||||
/// second may skip the geometry passes and re-blit the resolve texture (see the
|
||||
/// scene-render cache in `Primitive::prepare` / `Pipeline::render`).
|
||||
///
|
||||
/// Deliberately EXCLUDES `hover_region` — the ViewCube highlight renders in its
|
||||
/// own always-on pass, so cube hover must not force a full scene re-render. The
|
||||
/// live preview IS included (its coordinates), so a rubber-band tracking the
|
||||
/// cursor still renders, and the frame where the preview clears erases it
|
||||
/// instead of freezing the last overlay on screen.
|
||||
fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = rustc_hash::FxHasher::default();
|
||||
// Camera + per-view shading flags all live in the uniforms (view_rot, eye
|
||||
// high/low, viewport size, lineweight, flat_shade, transparency) — hashing
|
||||
// the raw POD bytes captures every pan / zoom / orbit / twist and toggle in
|
||||
// one shot. Identical camera state recomputes to identical bits, so a still
|
||||
// view never spuriously misses the cache.
|
||||
bytemuck::bytes_of(&vp.uniforms).hash(&mut h);
|
||||
vp.geometry_epoch.hash(&mut h);
|
||||
vp.selection_generation.hash(&mut h);
|
||||
vp.selected_sig.hash(&mut h);
|
||||
vp.wire_content_id.hash(&mut h);
|
||||
vp.fill_mode.hash(&mut h);
|
||||
vp.view_wireframe.hash(&mut h);
|
||||
vp.mesh_fill.hash(&mut h);
|
||||
vp.show_3d_edges.hash(&mut h);
|
||||
vp.hidden_line.hash(&mut h);
|
||||
clip_w.hash(&mut h);
|
||||
clip_h.hash(&mut h);
|
||||
// Live overlay (command preview / interim / grip drag). Small — a handful
|
||||
// of wires — so hashing its coordinates is cheap and catches the endpoint
|
||||
// moving with the cursor as well as the preview appearing / clearing.
|
||||
for w in vp.preview_wires.iter() {
|
||||
w.points.len().hash(&mut h);
|
||||
for p in &w.points {
|
||||
p[0].to_bits().hash(&mut h);
|
||||
p[1].to_bits().hash(&mut h);
|
||||
p[2].to_bits().hash(&mut h);
|
||||
}
|
||||
}
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// On-canvas-visible UV crop on one axis. `pos` and `size` are in the
|
||||
/// shader widget's normalized 0..1 coords. Returns `(uv_offset, uv_scale)`
|
||||
/// applied as `actual_uv = quad_uv * uv_scale + uv_offset` in the blit
|
||||
|
||||
Reference in new issue
Block a user