forked from 1b-agent/opencadstudio-upstream
Option I: add per-viewport Arc wire cache so paper-space viewport rendering no longer re-tessellates model content every frame. Option J: hit_test_wires() returns Arc<Vec<WireModel>> — O(1) pointer copy in model space instead of a full Vec clone on every mouse move during commands. paper_canvas_hatches/wipeouts() now route through the epoch caches added in F. Option K: add world_snap_r + wire_in_range() pre-check to Snapper::snap(). All Endpoint/Midpoint/Nearest/Perpendicular/Intersection loops skip wires whose chord sphere is outside the snap circle using O(1) scalar comparisons, eliminating O(entities × vertices) matrix multiply cost when zoomed in. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
178 lines
6.9 KiB
Markdown
178 lines
6.9 KiB
Markdown
# Performance Optimization Plan
|
||
|
||
## Implemented
|
||
|
||
| Option | Description |
|
||
|--------|-------------|
|
||
| A | Wire tessellation cache — `Arc<Vec<WireModel>>` keyed by `geometry_epoch`; O(1) on navigation |
|
||
| B | GPU buffer cache — skip `upload_wires/hatches/images/meshes` when epoch unchanged |
|
||
| C | Rayon parallel tessellation — `tessellate_entity` free function + `par_iter()` in `wires_for_block()` |
|
||
| E | SortEntitiesTable scan cache — O(objects) linear scan replaced with O(1) HashMap lookup |
|
||
|
||
---
|
||
|
||
## Remaining per-frame CPU work (navigation path)
|
||
|
||
With Options A/B implemented, `build_primitive()` still runs every frame during navigation
|
||
(pan/zoom), even though the geometry epoch has not changed. The following work happens on every
|
||
frame and should be eliminated:
|
||
|
||
### 1. `ImageModel.pixels` cloned every frame — highest impact
|
||
|
||
`build_primitive()` calls `self.images.values().cloned().collect()`.
|
||
`ImageModel` contains `pixels: Vec<u8>` — raw RGBA pixel data for each image.
|
||
A single 2000×1500 image is ~12 MB. With three images, every mouse-move event copies ~36 MB.
|
||
|
||
### 2. `synced_hatch_models()` — full document scan + Insert explosion every frame
|
||
|
||
The inner loop `for entity in self.document.entities()` iterates every entity in the document,
|
||
and for each `Insert` calls `explode_from_document()` to find embedded hatches.
|
||
In drawings with many block references this is O(inserts × avg_block_size) per frame.
|
||
|
||
### 3. `meshes.values().cloned().collect()` — 3D geometry cloned every frame
|
||
|
||
`MeshModel` contains `verts: Vec<[f32;3]>` and `indices: Vec<u32>`.
|
||
A tessellated 3D solid can hold tens of thousands of triangles (several MB).
|
||
All mesh data is copied on every frame even during pure navigation.
|
||
|
||
### 4. `wipeout_models()` — full entity scan every frame
|
||
|
||
Iterates `self.document.entities()` to find `Wipeout` entities and rebuild their boundary
|
||
polygons. No caching; O(entities) on every frame.
|
||
|
||
---
|
||
|
||
## Option F — Cache hatch and wipeout models
|
||
|
||
**Status:** Done
|
||
|
||
Add epoch-keyed `Arc` caches for hatches and wipeouts, identical in structure to the wire cache:
|
||
|
||
```rust
|
||
hatch_cache: RefCell<Option<(u64, Arc<Vec<HatchModel>>)>>,
|
||
wipeout_cache: RefCell<Option<(u64, Arc<Vec<HatchModel>>)>>,
|
||
```
|
||
|
||
Introduce `hatch_models_arc()` and `wipeout_models_arc()` helpers that return the cached `Arc`
|
||
on an epoch hit (O(1) refcount bump) and rebuild on a miss.
|
||
|
||
`build_primitive()` stores the `Arc` directly in `Primitive`, removing the per-frame
|
||
`synced_hatch_models()` / `wipeout_models()` calls on navigation frames.
|
||
|
||
`Primitive.hatches` and `Primitive.wipeout_hatches` change from `Vec<HatchModel>` to
|
||
`Arc<Vec<HatchModel>>`.
|
||
|
||
**Impact:** Eliminates O(inserts × block_size) work per frame in drawings with block references
|
||
containing hatch entities. Navigation becomes free for hatch-heavy files.
|
||
|
||
**Difficulty:** Easy. Same pattern as the wire cache.
|
||
|
||
---
|
||
|
||
## Option G — Arc-wrap image pixels
|
||
|
||
**Status:** Done
|
||
|
||
Change `ImageModel.pixels` from `Vec<u8>` to `Arc<Vec<u8>>`.
|
||
|
||
`ImageModel::clone()` then copies only the pointer (8 bytes) instead of megabytes of pixel data.
|
||
No other code needs to change — the GPU upload path reads `pixels` by reference.
|
||
|
||
Add an epoch-keyed `Arc<Vec<ImageModel>>` cache (same pattern as wire cache) so
|
||
`build_primitive()` performs only an O(1) Arc bump per frame during navigation:
|
||
|
||
```rust
|
||
image_cache: RefCell<Option<(u64, Arc<Vec<ImageModel>>)>>,
|
||
```
|
||
|
||
**Impact:** Eliminates MB-scale copies per frame in files that contain raster images.
|
||
With a 4K image (~32 MB raw), this alone can cut per-frame CPU time by tens of milliseconds.
|
||
|
||
**Difficulty:** Easy. Change one field type + add cache field + wire up in `build_primitive()`.
|
||
|
||
---
|
||
|
||
## Option H — Arc-wrap mesh models
|
||
|
||
**Status:** Done
|
||
|
||
Apply the same `Arc<Vec<MeshModel>>` epoch cache to mesh models:
|
||
|
||
```rust
|
||
mesh_cache: RefCell<Option<(u64, Arc<Vec<MeshModel>>)>>,
|
||
```
|
||
|
||
`MeshModel` contains `verts: Vec<[f32;3]>` (vertex positions) and `indices: Vec<u32>`
|
||
(triangle list). A complex 3D solid can have hundreds of thousands of triangles.
|
||
|
||
**Impact:** Eliminates per-frame mesh data copies in files that contain 3D solids (ACIS bodies,
|
||
extruded polylines). Navigation stays free regardless of model complexity.
|
||
|
||
**Difficulty:** Easy. Same pattern as Options F and G.
|
||
|
||
---
|
||
|
||
## Option I — Per-viewport wire cache for paper space
|
||
|
||
**Status:** Done
|
||
|
||
Added `viewport_wire_cache: RefCell<HashMap<Handle, (u64, Arc<Vec<WireModel>>)>>` to `Scene`.
|
||
`model_wires_for_viewport_arc(vp_handle)` checks the cache first; on a hit it returns an
|
||
Arc clone (O(1)). On a miss it tessellates, stores the result, and returns it.
|
||
|
||
`build_viewport_primitive()` in `render.rs` now calls `model_wires_for_viewport_arc()` instead
|
||
of `model_wires_for_viewport()`, so paper-space viewport rendering no longer re-tessellates
|
||
model-space content every frame.
|
||
|
||
**Impact:** Eliminates per-frame tessellation when viewing paper space layouts. Before this fix,
|
||
every navigation frame in paper space re-tessellated the entire model-space entity set for each
|
||
visible viewport — the same bug Options A/B fixed for model space.
|
||
|
||
---
|
||
|
||
## Option J — Arc-return `hit_test_wires()` and `paper_canvas_*`
|
||
|
||
**Status:** Done
|
||
|
||
`hit_test_wires()` now returns `Arc<Vec<WireModel>>` instead of `Vec<WireModel>`.
|
||
In the model-space case it returns `entity_wires_arc()` directly — O(1), no Vec clone.
|
||
In the paper-space case it still builds a Vec (no suitable cache for those paths), but the
|
||
interface is consistent.
|
||
|
||
`paper_canvas_hatches()` and `paper_canvas_wipeouts()` now return `Arc<Vec<HatchModel>>`
|
||
via the epoch caches added in Option F, eliminating per-frame hatch rebuilds in the paper
|
||
canvas widget.
|
||
|
||
**Impact:** Every `ViewportMove` message during a command (grip drag, line drawing, etc.)
|
||
called `hit_test_wires()` up to twice. In model space this was a full Vec clone of all
|
||
tessellated wires — potentially MB-scale. Now it is a pointer copy.
|
||
|
||
---
|
||
|
||
## Option K — Snap world-space wire pre-rejection
|
||
|
||
**Status:** Done
|
||
|
||
Added `world_snap_r` (snap radius in world units, derived from `view_proj`) and a
|
||
`wire_in_range()` closure to `Snapper::snap()`. Before iterating a wire's vertices for
|
||
Endpoint, Midpoint, Nearest, Perpendicular, Intersection, and ApparentIntersection,
|
||
the closure checks whether the wire's first↔last chord sphere overlaps the snap search
|
||
circle. Wires that fail the check are skipped with `continue` — no vertex projection,
|
||
no matrix multiplies.
|
||
|
||
Closed wires (first ≈ last, e.g. tessellated circles) are always passed through since
|
||
their chord radius is ~0.
|
||
|
||
**Impact:** When zoomed in on a small region of a large drawing, the vast majority of
|
||
wires are outside the snap circle and are rejected in O(1) scalar comparisons each. The
|
||
per-mouse-move snap cost drops from O(entities × vertices) to O(entities) for the
|
||
pre-check plus O(nearby_vertices) for the actual snap work.
|
||
|
||
---
|
||
|
||
## Implementation order
|
||
|
||
F → G → H → I → J → K (all implemented).
|
||
Each option is independent; the pattern (epoch-keyed Arc cache or a cheap pre-rejection
|
||
guard) is the same throughout.
|