# 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 --- ## Temporary Workarounds (to be reverted) > **These are stop-gap measures to prevent crashes on large files. They must be removed > once the proper Phase 1–3 optimizations are in place, because they silently drop geometry > rather than culling it intelligently.** ### Wire Segment Hard Caps (commit 321433e) **Problem:** Some entities — particularly dense `PolyfaceMesh` / `PolygonMesh` edge meshes, high-resolution splines, or very large polylines — tessellate into hundreds of thousands of line segments. Each segment expands to 6 GPU vertices × 96 bytes = 576 bytes. A single pathological entity could allocate hundreds of megabytes; a large file with many such entities exhausts all available GPU memory and crashes the renderer. **Temporary fix applied in `src/scene/pipeline/wire_gpu.rs` and `src/scene/pipeline/mod.rs`:** 1. **Per-entity cap — `MAX_SEGS_PER_WIRE = 100_000`** Defined as a `pub const` in `wire_gpu.rs`. Applied in both `WireGpu::build()` (used by `WireGpu::new`) and `WireGpu::from_batch()`. If a single `WireModel` contains more than 100 K segments, the excess tail is silently truncated and a warning is printed to stderr: ``` [H7CAD] wire '': segments exceeds limit (100000), truncating ``` 2. **Scene-level budget — `MAX_TOTAL_SEGS = 3_000_000`** Enforced in `Pipeline::upload_wires()`. Wires are processed in the order they arrive; once the running segment total would exceed 3 M, all remaining wires are skipped for that frame: ``` [H7CAD] upload_wires: skipped wire(s) — total segment budget (3000000) exceeded ``` At 576 bytes/segment the budget caps GPU wire memory at roughly **1.6 GB**. **Why this must be reverted:** - Truncation cuts an entity mid-geometry; the rendered result is visually wrong. - Skipping by arrival order is arbitrary — important visible entities may be dropped while invisible off-screen entities are retained (no spatial awareness). - The correct solution is Phase 1 (AABB cull before upload) + Phase 3.2 (adaptive arc segments), which eliminate the excess geometry without ever creating it in the first place. **Revert when:** Phase 1.3 (CPU cull before upload) is landed and verified to keep peak GPU wire memory below ~512 MB for the pathological files that triggered these caps. --- ## 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 ├── query_rect(Aabb2) → Vec └── 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; 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) { 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. - **Phase 4 target:** GPU-cull overhead < 0.5 ms for 1M entity scene.