diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 8f878824c..68cd426c4 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -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>, + /// 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 diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 59676a50f..d9f3bd194 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -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