Files
OneCAD-Draft/RENDERING_OPTIMIZATION.md
T
Hakan SevenandClaude Opus 4.7 c61d648dc7 fix(scene): unfreeze open on huge xref Inserts
A real DWG with an xref containing ~74k sub-entities froze on the first
render: Insert tessellation exploded the block, style-resolved every sub
serially, and one bad sub could hang the loop forever.

- Insert short-circuits to an insertion marker when the referenced block
  has more than INSERT_SUB_LIMIT (5_000) sub-entities. Documented as
  temporary in RENDERING_OPTIMIZATION.md.
- Re-run purge_corrupt_entities after resolve_xrefs so parser-garbage
  inside xref content is caught (initial purge runs before xref load).
- build_derived_caches: single pass over entities collecting handles
  per kind; tessellation reads via doc.get_entity instead of cloning
  every EntityType. Image decode now parallel too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 23:02:48 +03:00

314 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Rendering Optimization Roadmap: Culling & Level of Detail
## Background
H7CAD renders all entities every frame regardless of camera position or zoom level. No spatial
acceleration, no LOD, no frustum culling. Every wire, hatch, mesh, and image is uploaded and
drawn unconditionally. This scales poorly for large drawings (100k+ entities) and dense 3D solids.
Current pipeline order:
1. Hatch fills → 2. Images → 3. Meshes → 4. Face3D fills → 5. Face3D edges →
6. Wires → 7. Wipeouts → 8. Selection overlay → 9. MSAA resolve → 10. Blit
---
## Phase 1 — Viewport Bounding-Box Culling
**Goal:** Skip CPU upload and draw calls for entities outside the camera view.
### 1.1 Entity AABB
Compute axis-aligned bounding boxes during tessellation and store alongside each model.
```rust
// Attach to WireModel, MeshModel, HatchModel, ImageModel
pub struct Aabb2 {
pub min: DVec2,
pub max: DVec2,
}
pub struct Aabb3 {
pub min: DVec3,
pub max: DVec3,
}
```
- `WireModel` / `HatchModel` → `Aabb2` (XY plane for 2D; full 3D AABB for 3D wires)
- `MeshModel` → `Aabb3`
- `ImageModel` → `Aabb2` from insertion point + extents
Compute once at tessellation time (`scene/tessellate.rs`), store in the model structs, invalidate
with the geometry epoch.
### 1.2 Camera Frustum / Viewport Rectangle
Extract view bounds from `camera.rs` each frame:
- **Orthographic (2D):** viewport rectangle in world space — a simple `Aabb2` test suffices.
- **Perspective (3D):** extract 6 frustum planes from the view-projection matrix.
```rust
pub enum ViewVolume {
Ortho(Aabb2),
Frustum([Vec4; 6]), // plane equations
}
```
### 1.3 CPU-Side Cull Before Upload
In `pipeline/mod.rs`, before collecting wire/hatch/mesh draw calls:
```rust
fn is_visible(aabb: &Aabb2, view: &ViewVolume) -> bool {
match view {
ViewVolume::Ortho(rect) => aabb_overlap_2d(aabb, rect),
ViewVolume::Frustum(planes) => aabb_inside_frustum(aabb, planes),
}
}
```
Filter entity lists before the upload loop. Skip `queue.write_buffer` entirely for invisible
entities — saves both CPU and GPU bus bandwidth.
### 1.4 Scissor Rects (Already Partial)
Viewport scissoring for wires already exists (`wire_pixel_scissors`). Extend same logic to
hatch and image passes.
**Estimated gain:** 50–90% draw call reduction for typical workflows (zoomed in on one region).
---
## Phase 2 — Spatial Acceleration Structure
**Goal:** O(log n) visibility queries instead of O(n) linear scan.
### 2.1 Quadtree (2D Documents)
Partition world space into a quadtree keyed by `Aabb2`. Build once on document load, update
incrementally on entity add/remove/modify.
```
QuadTree<EntityId>
├── query_rect(Aabb2) → Vec<EntityId>
└── insert / remove / update
```
Store in `scene/mod.rs` alongside the entity cache. Invalidate only the nodes touched by a
modified entity.
### 2.2 Octree (3D Solids)
For `MeshModel` / `solid3d` entities, use an octree with `Aabb3` keys. Query returns candidate
mesh IDs for frustum culling.
### 2.3 Integration
Replace linear entity scans in `pipeline/mod.rs` render preparation with spatial queries:
```rust
let candidates = scene.quadtree.query_rect(view_aabb);
// only upload GPU buffers for candidates
```
**Estimated gain:** near-constant frame cost regardless of total entity count when zoomed in.
---
## Phase 3 — Level of Detail
**Goal:** Reduce geometric complexity when entities are small or far away.
### 3.1 Screen-Space Size Filter (Sub-Pixel Cull)
Before drawing, compute projected pixel size of entity AABB diagonal. Skip entities whose
projected size is below a threshold (e.g. < 0.5 px).
```rust
let screen_px = project_size(aabb, camera);
if screen_px < MIN_VISIBLE_PX { continue; }
```
Applies to all entity types. Effectively free — just a float comparison after AABB cull.
### 3.2 Wire LOD — Curve Segment Reduction
Arcs, ellipses, and splines tessellate to N segments (currently fixed). Replace with
zoom-adaptive segment count.
```rust
fn arc_segments(radius_world: f64, px_per_unit: f64) -> u32 {
// target: ~1 segment per 2px of arc length on screen
let arc_px = radius_world * px_per_unit * TWO_PI;
(arc_px / 2.0).clamp(8.0, 256.0) as u32
}
```
Recompute segments when zoom crosses a threshold (epoch-based invalidation already in place).
Store LOD level per entity; retessellate only on LOD level change.
LOD levels (example):
| Zoom (px/unit) | Segments per 90° arc |
|----------------|----------------------|
| > 100 px/u | 64 (full detail) |
| 10–100 px/u | 24 |
| 1–10 px/u | 12 |
| < 1 px/u | 6 (or sub-pixel cull)|
### 3.3 Hatch LOD — Density Reduction
At low zoom, hatch line spacing appears smaller than 1px. Options:
- **Solid fill substitution:** replace hatch pattern with solid color below a density threshold.
- **Skip hatch entirely:** at very low zoom, hatches are not readable; skip the hatch pass.
Threshold: `hatch_spacing_px = hatch_spacing_world * px_per_unit`. If `< 2.0`, use solid fill.
### 3.4 Mesh LOD — Tessellation Resolution
`truck_tess.rs` / `solid3d_tess.rs` tessellate ACIS solids at a fixed tolerance. Switch to
multiple precomputed LOD meshes:
```rust
pub struct MeshModel {
pub lod: [Option<GpuMesh>; 3], // [high, mid, low]
}
```
| LOD | Tolerance | Use when projected diagonal |
|-----|-----------|------------------------------|
| 0 | 0.01 mm | > 200 px |
| 1 | 0.5 mm | 50–200 px |
| 2 | 5.0 mm | < 50 px |
Build LOD 0 eagerly; build LOD 1 & 2 lazily in a background thread (already have
async tessellation pattern from `truck_tess.rs`).
### 3.5 Text & Dimension Simplification
At low zoom, dimension text and annotation geometry become unreadable. Replace with
simplified bounding-box proxies or skip entirely below a legibility threshold.
---
## Phase 4 — GPU-Side Culling (Advanced)
**Goal:** Offload culling to the GPU; zero CPU cost for large entity counts.
### 4.1 Indirect Draw + Compute Cull
Convert per-entity draw calls to indirect draw calls (`draw_indirect` / `draw_indexed_indirect`).
Run a compute shader pre-pass that tests each entity's AABB against the frustum and writes
`DrawIndirectArgs` only for visible entities.
```wgsl
// cull.wgsl
@compute @workgroup_size(64)
fn cull_entities(@builtin(global_invocation_id) id: vec3<u32>) {
let entity = entities[id.x];
if frustum_test(entity.aabb) {
// atomically append to indirect draw buffer
let slot = atomicAdd(&draw_count, 1u);
draw_args[slot] = entity.draw_args;
}
}
```
Requires restructuring entity data into GPU-side storage buffers. High complexity; tackle after
Phases 1–3 prove insufficient.
### 4.2 Hierarchical Z-Buffer Occlusion Culling (3D Only)
For dense 3D solid scenes, use a Hi-Z buffer to cull occluded meshes:
1. Render depth-only pass for large opaque solids.
2. Downsample depth into mip chain (Hi-Z pyramid).
3. Compute shader tests each mesh AABB against Hi-Z; skips occluded meshes.
Relevant only for perspective (3D) mode with many overlapping solids.
---
## Implementation Order
```
Phase 1.1 Entity AABB computation low risk, high impact
Phase 1.2 ViewVolume extraction low risk
Phase 1.3 CPU cull before upload immediate draw call reduction
Phase 2.1 Quadtree for 2D medium complexity, scales 2D docs
Phase 3.1 Sub-pixel cull trivial, free perf
Phase 3.2 Wire curve LOD medium, affects tessellation cache
Phase 3.3 Hatch LOD low complexity
Phase 3.4 Mesh LOD high complexity, background thread
Phase 2.2 Octree for 3D medium, needed for dense 3D
Phase 3.5 Text/dim simplification low complexity
Phase 4.1 Indirect draw + GPU cull high complexity, defer
Phase 4.2 Hi-Z occlusion high complexity, 3D only, last
```
---
## Key Files to Modify
| File | Change |
|------|--------|
| `src/scene/wire_model.rs` | Add `Aabb2` field to `WireModel` |
| `src/scene/mesh_model.rs` | Add `Aabb3`, LOD mesh array |
| `src/scene/hatch_model.rs` | Add `Aabb2` field |
| `src/scene/image_model.rs` | Add `Aabb2` field |
| `src/scene/tessellate.rs` | Compute AABB during tessellation; adaptive arc segments |
| `src/scene/truck_tess.rs` | Multi-resolution mesh tessellation |
| `src/scene/camera.rs` | Expose `ViewVolume` extraction |
| `src/scene/mod.rs` | Quadtree/octree; LOD epoch tracking |
| `src/scene/pipeline/mod.rs` | Cull entities before upload loops |
| `src/shaders/cull.wgsl` | New — Phase 4 compute culling shader |
---
## Success Metrics
- **Phase 1 target:** Large drawing (100k wires), zoomed to 5% view → render time drops
proportional to visible fraction.
- **Phase 2 target:** Pan/zoom frame cost O(visible) not O(total).
- **Phase 3 target:** Complex solid scene at far zoom → mesh triangle count < 10% of full detail.
---
## TEMPORARY: Insert sub-entity count limit (revert when better fix lands)
`tessellate_entity` in `src/scene/mod.rs` has a guard that short-circuits any
Insert whose referenced block contains more than `INSERT_SUB_LIMIT` (currently
5,000) sub-entities. The Insert is rendered as an insertion marker only — no
explode, no sub tessellation.
**Why this exists:** A real-world DWG was found where an xref block contained
~74k sub-entities. Opening the file froze the UI on the first render because
`tessellate_entity` exploded the block and serially style-resolved every
sub-entity (and any single bad sub could hang tessellation forever). The guard
made the file openable.
**Why it should go away:** The limit hides legitimate geometry. Large xrefs are
common in civil/infrastructure drawings — the user shouldn't have to see them
as crosses.
**Proper fixes that would let us drop this guard:**
1. Per-block tessellation cache — tessellate a block once, then per-Insert
apply the transform to cached wire vertices. Helps when the same block is
inserted multiple times; doesn't fully solve single-Insert huge blocks.
2. Lazy / background Insert tessellation — show marker first, fill in detail
on a background thread, redraw when ready.
3. Per-sub-entity hardening — find why a single sub can hang tessellation
(likely Spline NURBS evaluation or malformed boundary edge) and fix the
root cause; combine with sub-count budget per frame instead of a hard cap.
4. xref attach metadata respected — DWG carries an "unloaded" state for xref
blocks; honour it instead of always inlining xref content on open.
When any of (1)–(3) lands, delete the `INSERT_SUB_LIMIT` block in
`tessellate_entity` (search for `INSERT_SUB_LIMIT`).
Related companion fix in `src/app/update.rs`: after `resolve_xrefs`, a second
`purge_corrupt_entities` pass runs against the newly inlined xref content.
That one is not a hack — it should stay even after the Insert guard goes.
- **Phase 4 target:** GPU-cull overhead < 0.5 ms for 1M entity scene.