From d3986e9cba7daf9ef2e5afa1cb747d4347a8f043 Mon Sep 17 00:00:00 2001 From: Hakan Date: Sat, 23 May 2026 09:45:57 +0300 Subject: [PATCH] docs(perf): add wire pipeline cold-open optimization plan Documents the 4-step plan to reduce first-file-open latency by attacking wire vertex upload volume: mapped_at_creation, Rayon parallel emit, instanced rendering (the big lever, ~6.5x memory cut), and per-instance packing. Each step lands as its own commit. Co-Authored-By: Claude Opus 4.7 --- WIRE_OPTIMIZATION_PLAN.md | 80 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 WIRE_OPTIMIZATION_PLAN.md diff --git a/WIRE_OPTIMIZATION_PLAN.md b/WIRE_OPTIMIZATION_PLAN.md new file mode 100644 index 000000000..aa331d413 --- /dev/null +++ b/WIRE_OPTIMIZATION_PLAN.md @@ -0,0 +1,80 @@ +# Wire Pipeline — Cold-Open Optimization Plan + +Goal: reduce first-file-open latency (tessellate → upload → GPU buffer +creation) for wire entities. Steady-state draw cost is already cheap; the +bottleneck on cold open is **upload volume** and **vertex emission**. + +## Baseline + +Each wire segment currently produces **6 vertices × 96 B = 576 B**. The same +`pos_a / pos_b / color / half_width / pattern_length / pat0 / pat1` values +are duplicated across all 6 corners of the quad. For a drawing with 1 M +segments this is **~576 MB** of CPU-side `Vec` plus the same +amount uploaded over PCIe. + +Build path: `WireGpu::from_batch` (single-threaded loop) → `Vec` +→ `create_buffer_init` (one alloc + one memcpy into a staging buffer the +driver then DMAs to VRAM). + +## Step 1 — `mapped_at_creation` upload + +Replace `create_buffer_init(contents)` with `create_buffer(mapped: true)` + +write the vertex stream directly into the mapped slice. Skips the +intermediate `Vec` → staging-buffer memcpy. + +- **Files**: `src/scene/pipeline/wire_gpu.rs` +- **Risk**: low (size known ahead of time; no semantic change). +- **Expected win**: 1 alloc + 1 memcpy per buffer dropped. Modest but free. + +## Step 2 — Rayon parallel vertex emission + +`from_batch` walks wires serially. Each wire's vertex generation is +independent (no shared state, no order dependency — wires are already +keyed by style upstream in `block_cache`). Use `rayon::par_iter().flat_map` +to emit per-wire vertex slices in parallel, then concatenate. + +- **Files**: `src/scene/pipeline/wire_gpu.rs` +- **Risk**: low. Output ordering does not matter for correctness. +- **Expected win**: N-core speedup on multi-wire drawings; meaningful on + files with many short wires (typical CAD geometry). + +## Step 3 — Instanced rendering (the big win) + +Replace 6-vertex quad expansion with **1 instance per segment** + 4-vertex +unit quad shared across all instances. Per-instance struct (~88 B) holds: +`pos_a, pos_b, color, distance_a, distance_b, half_width, pattern_length, +pat0, pat1`. `which_end` and `side` are derived from `@builtin(vertex_index)` +inside the vertex shader. + +- **Files**: `src/scene/pipeline/wire_gpu.rs`, `src/shaders/wire.wgsl`, + `src/scene/pipeline/mod.rs` (pipeline `VertexBufferLayout`). +- **Risk**: medium. Two vertex buffers (per-vertex + per-instance), shader + derives `which_end` / `side` from `vertex_index`. `WireGpu::from_batch` + emits 1 instance per segment instead of 6 verts. +- **Expected win**: **~6.5× less GPU memory & upload bandwidth per + segment** (576 B → 88 B). Biggest cold-open lever. + +## Step 4 — Tighter per-instance packing + +After Step 3 the instance struct is the new hot path. Pack: + +- `color: [f32; 4]` → `[u8; 4]` (`Unorm8x4`): 16 B → 4 B, no visible + quality loss at 8-bit display. +- `which_end / side` already eliminated by Step 3. +- `pat0 / pat1` stay (encoder relies on up to 8 dash elements). + +Net: ~88 B → ~76 B per instance. ~15 % further reduction on top of Step 3. + +- **Files**: `src/scene/pipeline/wire_gpu.rs`, `src/shaders/wire.wgsl`. +- **Risk**: low (vertex-attribute format change only). +- **Expected win**: ~15 % less upload after Step 3. + +## Ordering rationale + +1-2 first — independent, low risk, immediate wins. +3 next — restructures the pipeline; depends on no prior changes but is the +single biggest lever. +4 last — only pays off after Step 3 turns the per-instance struct into the +hot path. + +Each step ends with a `cargo check` (warnings-free) and a commit.