|
| 1 | +# /body — server-side HHTL LOD + helix + slicer-fill (option 2) |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +`/body` must render the full FMA body as **filled polygons** (slicer-style infill, |
| 6 | +per material — tubes/vessels/solids), addressed on the **V3 substrate** |
| 7 | +(`classid 0x1000_0A01`, the `(part_of:is_a)` 8:8 cascade), with **LOD** driven by |
| 8 | +the HHTL depth-cascade. The earlier `/body` was wrong on every axis: raw-OBJ hollow |
| 9 | +shells, no fill, no LOD, no helix, V3 key mis-encoded as `(depth:is_a)`, renderer |
| 10 | +ignoring `classid`. |
| 11 | + |
| 12 | +**Decision (operator, 2026-06-27): option 2 — compute server-side.** The HHTL LOD |
| 13 | +(`depth_cascade`), helix-3-byte, and slicer-fill run in `cockpit-server` (x86, full |
| 14 | +`F32x16` SIMD), streaming LOD-selected geometry to a thin three.js viewer. Rationale: |
| 15 | +ndarray's **wasm** SIMD backend (`simd_wasm.rs`) is an un-wired stub — `F32x16` |
| 16 | +falls back to scalar on wasm, ~16× too slow for per-frame client-side LOD. Native is |
| 17 | +fully polyfilled (AVX-512/AVX2/NEON), so the cascade belongs server-side. (Option 1 — |
| 18 | +complete the wasm `F32x16` v128 backend — is the alternative, deferred.) |
| 19 | + |
| 20 | +`splat3d` is a gaussian raster (the rejected "confetti" path); we use ONLY its |
| 21 | +renderer-agnostic `depth_cascade` (LOD block-preselection) + `helix_orient`, and draw |
| 22 | +**polygons**, never gaussians. |
| 23 | + |
| 24 | +## Foundation — DONE (verified) |
| 25 | + |
| 26 | +`scratch-fma/lodprobe` (standalone, builds against ndarray `features=["std","splat3d"]`): |
| 27 | +body.spm1 → per-concept `BlockBounds{center,radius}` → `cascade_blocks(camera, …)`. |
| 28 | +Verified monotonic LOD: near ⇒ 1513/1658 `ProjectExact`; far ⇒ 1446/1658 `KeepCoarse`. |
| 29 | +API pinned: `depth_cascade::{BlockBounds, DepthCascadeBudget, HhtlAction(Reject/ |
| 30 | +KeepCoarse/Refine/ProjectExact/RenderExact), cascade_blocks}`, `project::Camera`. |
| 31 | + |
| 32 | +## Work items |
| 33 | + |
| 34 | +### Phase A — V3 substrate correctness (independent of LOD) |
| 35 | +- [ ] Bake the cascade as **6×(8:8) `(part_of:is_a)`** tiles: walk BOTH |
| 36 | + `partof_inclusion_relation_list.txt` AND `isa_inclusion_relation_list.txt`; each |
| 37 | + tier byte-pair = `(part_of_rank << 8) | is_a_rank`; identity tier too. (Current bake |
| 38 | + packs `(depth:is_a)` and never walked part_of — wrong.) |
| 39 | +- [ ] `body.rs`: emit the 6 tiers directly from the (part_of,is_a) pair arrays; drop |
| 40 | + the `mixin_for_depth` hack. |
| 41 | +- [ ] Renderer: dispatch on `classid` — assert `0x1000_xxxx` (V3), decode the |
| 42 | + `(part_of:is_a)` tile per node, use it (group/colour/pick by the two axes). |
| 43 | + |
| 44 | +### Phase B — multi-LOD geometry (the pyramid the cascade selects from) |
| 45 | +- [ ] Per concept, bake a decimation pyramid: L0 full-res (ProjectExact), L1/L2 |
| 46 | + vertex-cluster-decimated (KeepCoarse). Store offsets per (concept, level). |
| 47 | +- [ ] BlockBounds table (centroid + radius) per concept, baked alongside. |
| 48 | + |
| 49 | +### Phase C — slicer-fill + helix (the "3D printing slicer" infill) |
| 50 | +- [ ] Per solid material (tube/vessel/organ), generate infill geometry inside the |
| 51 | + shell (slicer-style), placed via HHTL tile coords + `helix_orient` 3-byte → exact |
| 52 | + location. Tubes get tubular infill; solids get volumetric. |
| 53 | +- [ ] Material-prototype texture per layer (tube/vessel/bone/…). |
| 54 | + |
| 55 | +### Phase D — server endpoint + streaming viewer |
| 56 | +- [ ] `cockpit-server`: dep ndarray `features=["std","splat3d"]`; `/api/body/lod` |
| 57 | + (POST camera {view,fx,fy,w,h}) → `cascade_blocks` → assemble selected (concept,LOD) |
| 58 | + blocks → SPM1 stream. |
| 59 | +- [ ] `BodyV3.tsx`: thin — throttled orbit posts the camera; swap the streamed mesh. |
| 60 | + Drop the full 168 MB client fetch. |
| 61 | + |
| 62 | +## LOCKED design (operator review, 2026-06-27) — supersedes the BSO1 AoS bake |
| 63 | + |
| 64 | +The wire is **SoA columns** (`MultiLaneColumn`, 64-byte aligned, `Arc<[u8]>`), |
| 65 | +joined by ONE SoA row identity. **Store identities/indices, never raw values; |
| 66 | +ClassView dereferences.** Three separate 16-byte GUID columns (cheap: 16 B × |
| 67 | +100k = 1.6 MB; × 396k surfels = 6.4 MB — separation beats bit-packing): |
| 68 | + |
| 69 | +| column | 16-B GUID / value | content | resolves via | |
| 70 | +|---|---|---|---| |
| 71 | +| **address** | `classid 0x1000` + `(part_of:is_a)` **8:8** cascade + identity tier | the node key (unique; routable prefix) | ClassView / registry | |
| 72 | +| **location** | `XYZ` standard 3D | position — GPU/slicer native; Z = slice, X·Y = in-slice 256² grid (256=4⁴ hierarchical) | direct | |
| 73 | +| **helix** | **2 helices** + reserved | helix-pos (dir from origin 0,0,0, 3 B) · helix-normal (self orientation, 3 B) · depth derivable by trig from XYZ | direct decode | |
| 74 | +| material | **codebook index** | Doppler flow class (low-res artery / high-res artery / portal / hepatic-vein / caval) | ClassView → material prototype | |
| 75 | +| label | **codebook index** | never raw text | ClassView → text + synonyms | |
| 76 | +| edges | **SoA-row refs** | part_of parent / branches / supplies / synonym alias | ClassView | |
| 77 | + |
| 78 | +Rules that fell out of review: |
| 79 | +- **Collusion = a location collision = same geometry ⇒ a ClassView resolution, |
| 80 | + not a bug.** (`celiac trunk ≡ celiac artery` = same lumen → ClassView aliases; |
| 81 | + the 3 celiac branches have distinct XYZ → distinct, linked as children.) |
| 82 | + Bilateral pairs are unique by x-sign for free. |
| 83 | +- **Relationships reference the SoA row (linked identity), never embed a |
| 84 | + neighbour's location/helix.** Edge says *who*; row says *where/what/oriented*. |
| 85 | +- **64k⁶ = (256³)⁴** — same space; XYZ-bytes factoring is the slicer-native one. |
| 86 | +- **Tubes:** normal is radial from the centerline ⇒ helix-normal derivable by trig |
| 87 | + from XYZ + the part_of branch-tree centerline; slicer fills cylindrically |
| 88 | + (depth-along-axis · helix-angle · radius). Explicit helix bytes only for |
| 89 | + non-radial surfaces (sheets/capsules). |
| 90 | +- **Material fill** densifies the *surface* (slicer-style), per Doppler class. |
| 91 | +- **Render:** Gouraud shading; **bgz17/Base17 (#17) palette drives the alpha / |
| 92 | + transparency** channel (17 levels). Keep **6+ M polygons** (NO decimation to |
| 93 | + 1.6 M yet — LOD pyramid is later). |
| 94 | +- compute server-side (ndarray native SIMD); deno_core/V8 is the *document* JS |
| 95 | + engine, never in the 3D path. |
| 96 | + |
| 97 | +## Shipped increments (2026-06-28) |
| 98 | + |
| 99 | +### Vessel "inflatable tube" fix — `fill_body_soa.py` |
| 100 | +The slicer-fill cores ballooned where vessels curve: the radius was the |
| 101 | +perpendicular distance from the **global** PCA axis, so a point on a bend sits |
| 102 | +far off the straight axis → radius inflates. Fixed: bin the points along the |
| 103 | +axis, then derive each ring from its **own bin** — centroid = bin's local centre |
| 104 | +(follows the curve), radius = **median** perpendicular distance from *that* bin |
| 105 | +centroid (robust to outliers), clamped to an **absolute** `[RMIN, RMAX]` |
| 106 | +diameter boundary (`RMAX=0.020` ≈ 34 mm dia, covers the aorta, kills balloons). |
| 107 | +662 vessels → +71,872 core verts / +133,152 tris. |
| 108 | + |
| 109 | +### Half-precision positions — the "A" brick |
| 110 | +> **SUPERSEDED to F16 (BSO2 ver 5).** BF16 (ver 4) was tried first and **rejected**: |
| 111 | +> its 7-bit mantissa gave a ~3 mm step near the head (y≈0.85) → a visible staircase |
| 112 | +> (Treppeneffekt) on the eye/brain. Shipped format is **F16 / IEEE half (ver 5)** — |
| 113 | +> same 6 B/vertex, 10-bit mantissa, ~0.2 mm (measured 0.21 mm max over the wire), no |
| 114 | +> staircase. Bake uses ndarray's `F16::from_f32`; renderer widens via a 64K half→f32 |
| 115 | +> LUT. `BodyV3.tsx` reads ver 3 (f32) / 4 (BF16) / 5 (F16). gz ≈ 63 MB (vs f32's 80). |
| 116 | +> The BF16 description below is retained for history. |
| 117 | +
|
| 118 | +Per-vertex `pos` column was **BF16** (3× u16 LE = 6 B/vertex), half of ver 3's |
| 119 | +12 B f32. Conversion via ndarray's sanctioned RNE batch path |
| 120 | +(`f32_to_bf16_batch_rne`) on the native bake host (AVX-512/AMX). The renderer |
| 121 | +widens back to f32 client-side (`bits << 16` — BF16 is the top 16 bits of f32, so |
| 122 | +the widening is exact). Round-trip ≈ 1.4 mm — which turned out to be visible on |
| 123 | +small smooth structures, hence the F16 upgrade above. Asset in release |
| 124 | +`fma-body-soa-v3-v1` (Dockerfile pulls it same-origin). |
| 125 | + |
| 126 | +### "B": server-side HHTL-O(1) LOD endpoint — WIRED (de-risked) |
| 127 | +cockpit-server can't build in-sandbox (quarto-core→runtimelib is a proxy-blocked |
| 128 | +git dep), so B is a blind deploy — de-risked three ways: |
| 129 | +1. **Verified core, standalone:** `scratch-fma/bodylod` builds + runs here against |
| 130 | + ndarray-only. `build_blocks(wire)` → per-concept `BlockBounds`, `concept_actions` |
| 131 | + → `cascade_blocks` (HHTL HEEL→HIP→TWIG→LEAF, O(concepts≈1658), the O(1) |
| 132 | + reference). Monotonic LOD on the real BF16 wire: near 1521 ProjectExact / 137 |
| 133 | + KeepCoarse → far 211 / 1447. The cockpit-server handler reuses this exact logic. |
| 134 | +2. **Tiny embedded asset, not the geometry:** `soabake` bakes `body.blocks` |
| 135 | + (1658×16 B = 26 KB: centroid + radius per concept, in the renderer's DISPLAY |
| 136 | + space so the client posts its three.js camera directly). cockpit-server |
| 137 | + `include_bytes!`s it — no 57 MB startup gunzip, no feature gate. |
| 138 | +3. **Opt-in client, default OFF:** `BodyV3.tsx` keeps the full render; a "server |
| 139 | + LOD" toggle (default off) posts the throttled camera to `/api/body/lod`, writes |
| 140 | + the per-concept `HhtlAction` bytes into a 1658-px R8 `DataTexture`, and the |
| 141 | + frag shader discards Reject concepts (gated by `uLodOn`). If the endpoint 404s |
| 142 | + (old deploy) or errors, it silently falls back to the full render. So a wrong |
| 143 | + cull (camera-space math is unverifiable here) only ever shows when the user |
| 144 | + flips the toggle — never by default. |
| 145 | + |
| 146 | +Files: `crates/cockpit-server/src/body_lod.rs` (+ route in `main.rs`, `splat3d` |
| 147 | +feature in `Cargo.toml`, `assets/body.blocks`); `cockpit/src/BodyV3.tsx`. |
| 148 | +**Deferred (Phase B pyramid):** with single-LOD geometry the cascade only |
| 149 | +distinguishes show/cull, so the win is frustum-culling whole concepts when zoomed |
| 150 | +in; switching KeepCoarse → a decimated mesh needs the L1/L2 decimation pyramid. |
| 151 | + |
| 152 | +## Constraints |
| 153 | +- Big baked assets (LOD pyramid, fill) → GitHub Releases (q2 `fma-body-soa-v3-*`), |
| 154 | + never git. `cockpit/public/body.soa*` gitignored. |
| 155 | +- q2 workspace cargo can't build in-sandbox (proxy-blocked `runtimed` git dep); |
| 156 | + ndarray-only crates verify standalone; the server build runs on deploy. |
| 157 | +- No model identifier in any committed artifact. |
0 commit comments