Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Public API is **mirrored** across React and Vue. Adding a hook on one side witho

**One visible `Polygon` → one leaf DOM element.** Leaves use canonical CSS primitives where possible and move scale into `matrix3d`; clipped solids use fixed primitives because their paint geometry becomes unstable when collapsed to 1px. Textured polygons still pack their local-2D bounding rect (`canvasW × canvasH`) into the atlas. The HTML tag *is* the render strategy — the renderer picks one tag per polygon based on its shape and material.

Raw MagicaVoxel `.vox` sources have a narrower baked-mode fast path: `parseVox` still returns the polygon mesh for bounds, fallback rendering, and public handles, but also preserves a `PolyVoxelSource` marker. Eligible vanilla meshes render exact visible voxel quads as hostless `<b>` leaves with canonical `matrix3d(...)` transforms and projected tile4 scanline DOM order. `.vox` normalization snaps to the nearest integer CSS cell size so direct voxel matrices use integer pixel coordinates without any scale wrapper. Brush colors still receive baked Lambert shading from the scene lights. Dynamic lighting, shadows, stable DOM animation, non-exact voxel geometry, and geometry replaced via `setPolygons` fall back to the polygon renderer.
Raw MagicaVoxel `.vox` sources have a narrower baked-mode fast path: `parseVox` still returns the polygon mesh for bounds, fallback rendering, and public handles, but also preserves a `PolyVoxelSource` marker. Eligible vanilla meshes render visible voxel quads as `<b>` leaves inside persistent signed-face wrappers (`t`, `b`, `fl`, `br`, `fr`, `bl`), with canonical `matrix3d(...)` transforms and projected tile4 scanline order inside each mounted face. Camera-facing culling mounts/removes those face wrappers instead of removing thousands of live brush children from the mesh root. `.vox` normalization snaps to the nearest integer CSS cell size so direct voxel matrices use integer pixel coordinates without any scale wrapper; same-color shared voxel edges get a tiny matrix-space overscan to hide compositor seams without fattening exterior silhouette edges. Brush colors still receive baked Lambert shading from the scene lights. Dynamic lighting, shadows, stable DOM animation, non-exact voxel geometry, and geometry replaced via `setPolygons` fall back to the polygon renderer.

Voxel-shaped meshes are the exception to "all polygons stay mounted": meshes with at most the six axis-aligned face normals, excluding helpers/auto-center-exempt meshes, automatically mount only camera-facing leaves and patch the mounted set when the camera or mesh rotation crosses a visible-normal boundary. Non-voxel meshes keep the full leaf DOM mounted; broad camera-dependent DOM culling is not worth the mutation cost.

Expand All @@ -41,7 +41,7 @@ Strategies are ordered cheapest → most expensive. The mesher's job is to maxim

Callers can opt out of specific strategies via `strategies: { disable: ["b" | "i" | "u"] }` on `RenderTextureAtlasOptions`. Disabled or unsupported strategies fall through the chain (`b → i → s`, `u → i → s`, `i → s`). Disabling `"i"` also disables the exact corner-shape solid branch even though that branch emits a bare `<u>`, because it belongs to the non-triangle clipped-solid family. `<s>` is the universal fallback and cannot be disabled. Solid seam bleed defaults to `1.5` CSS px on detected shared edges; callers can set `seamBleed`, where `"auto"` computes a fitted amount from each polygon plan and numeric values clamp the per-side CSS-pixel overscan. The renderer applies bleed only to detected shared seam edges of solid primitives, rather than inflating every side of each participating polygon.

The `.vox` fast path emits plain `<b>` elements directly inside the mesh wrapper. They intentionally reuse the cheap quad tag, but they are exact voxel quads with one `matrix3d(...)` per visible quad, ordered by projected tile4 scanline order. Desktop-class documents use a canonical 1px primitive for the cheapest transform shape; mobile-class documents (`pointer: coarse` or `hover: none`) use an 8px primitive and divide the in-plane matrix scale by 8 to preserve identical CSS-space geometry while avoiding large GPU filtering gaps.
The `.vox` fast path emits plain `<b>` elements inside `.polycss-voxel-face` wrappers. They intentionally reuse the cheap quad tag; each visible quad has one `matrix3d(...)`, with same-color shared-edge overscan folded into the local left/top/width/height before matrix generation. The face wrappers are grouping nodes for cheap add/remove and are not render-strategy leaves. Desktop-class documents use a canonical 1px primitive for the cheapest transform shape; mobile-class documents (`pointer: coarse` or `hover: none`) use an 8px primitive and divide the in-plane matrix scale by 8 to preserve identical CSS-space geometry while avoiding large GPU filtering gaps.

### Lighting modes (`PolyTextureLightingMode = "baked" | "dynamic"`)

Expand Down Expand Up @@ -85,7 +85,7 @@ If you find yourself wanting a `requestAnimationFrame` loop to update many DOM n
- **Hooks/composables:** `usePolyCamera`, `usePolyMesh`, `usePolySceneContext`, `usePolySelect`, `usePolySelectionApi`, `usePolyAnimation`.
- **Components:** `PolyPerspectiveCamera`, `PolyOrthographicCamera`, `PolyOrbitControls`, `PolyMapControls`, `PolyTransformControls`, `PolySelect`, `PolyAxesHelper`, `PolyDirectionalLightHelper`.
- **Types:** `PolyDirectionalLight`, `PolyAmbientLight`, `PolyTextureLightingMode`, `PolyAnimationMixer`, `PolyRenderStats`.
- **Functions:** `findPolyMeshHandle`, `injectPolyBaseStyles`, `collectPolyRenderStats`, `buildPolyVoxelFaceData`, `buildPolyVoxelSlicePlan`.
- **Functions:** `findPolyMeshHandle`, `injectPolyBaseStyles`, `collectPolyRenderStats`.
- **Vanilla factories:** `create*` names stay as-is (`createPolyScene`, `createTransformControls`, `createSelect`).
- **HTML custom elements:** `poly-` prefix + kebab-case. Existing tags: `<poly-scene>`, `<poly-mesh>`, `<poly-polygon>`, `<poly-perspective-camera>`, `<poly-orthographic-camera>`, `<poly-axes-helper>`, `<poly-directional-light-helper>`. Any new element follows the same shape (e.g. `<poly-transform-controls>`, `<poly-select>`).
- **Leaf DOM tags (`<b>`, `<i>`, `<s>`, `<u>`):** internal render-strategy tags. Not part of the public API and not user-facing — do not document them as such.
Expand Down
4 changes: 0 additions & 4 deletions bench/notes/BENCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ pnpm bench:animated-human # build bundles + run the animated human run bench
pnpm bench:trace # build bundles + run the trace analysis bucket profiler
pnpm bench:lossy # compare lossless / current lossy counts
pnpm bench:lossy:corpus # scan gallery GLB/OBJ lossy counts + crack diagnostics
pnpm bench:voxel-report # summarize voxel cadence results
pnpm bench:visual # screenshot diff against bench/baselines/*.png
pnpm bench:visual --record # capture new baselines (after intentional renderer changes)
pnpm bench:build # just rebuild the bench bundles (rarely needed alone)
Expand All @@ -42,7 +41,6 @@ node bench/lossy-optimizer-bench.mjs --json bench/results/lossy-optimizer.json
node bench/lossy-optimizer-bench.mjs --models ducky,shark,bicycle
node bench/lossy-corpus-bench.mjs --root /tmp/polycss-model-corpus --json /tmp/polycss-temp-corpus.json
node bench/lossy-corpus-bench.mjs --from-json bench/results/lossy-corpus.json --opportunities
node bench/voxel-report.mjs all
node .agents/skills/chrome-capture-trace/scripts/trace.mjs motion --mesh garden --runs 3 --dom-samples --report --markdown-out bench/results/garden-trace.md
node bench/perf-visual.mjs --mesh chicken --tolerance 0.005
node bench/nonvoxel-rotation-bench.mjs --models teapot,bicycle --variants baseline,order-tile4 --run-order round-robin
Expand Down Expand Up @@ -229,8 +227,6 @@ bench/
de-dupes a single instance.
perf-bench.mjs Playwright runner. Fresh chromium per scenario,
ephemeral port, structured JSON output.
voxel-report.mjs Consolidated voxel cadence/browser report over
existing bench/results JSON.
animated-human-bench.mjs
GPU-default Playwright runner for the animated
human run sequence. Reports FPS, mixer/update cost,
Expand Down
59 changes: 16 additions & 43 deletions bench/notes/PERF_INVESTIGATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ Useful commands are documented in `bench/notes/BENCH.md`; the common ones are:
pnpm bench:perf
pnpm bench:visual
pnpm bench:trace
pnpm bench:voxel-report
node bench/nonvoxel-rotation-bench.mjs --run-order random
node .agents/skills/chrome-capture-trace/scripts/trace.mjs motion --page nonvoxel --no-trace
node bench/nonvoxel-visual-compare.mjs
Expand Down Expand Up @@ -85,25 +84,26 @@ Eligible baked `.vox` meshes render through the dedicated voxel path.
| --- | --- |
| Scene | One transform/perspective scene root. |
| Mesh | One `.polycss-mesh` wrapper. |
| Voxel hosts | None. |
| Leaves | Plain hostless `<b>` direct-matrix exact voxel quads. |
| Voxel hosts | Persistent signed-face `.polycss-voxel-face-*` wrappers (`t`, `b`, `fl`, `br`, `fr`, `bl`). |
| Leaves | Plain `<b>` direct-matrix exact voxel quads inside the mounted signed-face wrappers. |
| Primitive | 1px on desktop-class documents; 8px on mobile-class documents with matrix scale divided by 8. |
| Matrix | One canonical non-degenerate `matrix3d(...)` per mounted quad, including an exact `+/-1` normal column. |
| DOM order | Projected screen-space `tile4-scanline-forward`. |
| Culling | Only camera-facing face directions are mounted. |
| DOM order | Projected screen-space tile4 scanline order inside each face wrapper. |
| Culling | Only camera-facing signed-face wrappers are mounted; camera flips patch entering/departing wrappers only. |

Accepted voxel decisions:

| ID | Decision | Why it stays |
| --- | --- | --- |
| D1 | Preserve `PolyVoxelSource` and route eligible `.vox` meshes through the dedicated path. | It avoids the general polygon path for exact voxel quads and keeps public polygon handles for fallback/bounds. |
| D2 | Keep hostless direct-matrix leaves as the default voxel shape. | Removing axis hosts and folding orientation/depth/scale into the leaf matrix is the strongest visual-correct one-shape baseline. |
| D3 | Keep `tile4-scanline-forward` as the default DOM order. | It is the broadest validated one-policy order; nearby tile sizes, traversal orders, depth orders, and face orders all have hard counterexamples. |
| D2 | Keep direct-matrix `<b>` leaves grouped by signed face. | The leaf still carries the exact surface transform, while culling mutates six coarse groups instead of thousands of brush nodes. |
| D3 | Keep tile4 scanline order inside each mounted face wrapper. | It is the current validated one-policy order for exact raw `.vox` quads. |
| D4 | Use exact parsed voxel quads, not source overpaint, for default rendering. | Source variants reduced nodes in places but did not reliably win and carry visual risk. |
| D5 | Keep camera-facing culling. | Mounting all six faces costs more than it saves. |
| D5 | Keep camera-facing culling at the signed-face-wrapper level. | Mounting all six faces costs more than it saves; patching wrappers avoids the old per-brush removal stutter. |
| D6 | Keep integer CSS cell snapping during `.vox` normalization. | It preserves direct integer matrix coordinates without adding a scale wrapper. |
| D7 | Normalize visual fit before comparing voxel FPS. | Fixed zoom can crop or resize large voxel scenes enough to change the benchmark. |
| D8 | Treat GPU as the default bench lane and software as an explicit stress lane. | Software-renderer ceilings produced false bottlenecks on medium scenes. |
| D9 | Keep same-color shared-edge matrix overscan for direct voxel quads. | It repairs compositor seams without adding atlas work or fattening isolated exterior edges. |

### Non-Voxel Polygon Path

Expand Down Expand Up @@ -240,40 +240,13 @@ plane count, especially `AncientCrashSite`, `skyscraper`, and long-window
These are the few historical tables worth keeping inline because they prevent
old ideas from being re-argued.

### Voxel Order Baseline
### Voxel Direct Path

Validation that made `tile4-scanline-forward` the one-strategy default:

| Model | Prior slice p95 | Tile4 scanline p95 | Read |
| --- | ---: | ---: | --- |
| `obj_house3.vox` | 59.9 | 113.6 | Rescues the face/locality counterexample. |
| `obj_house5.vox` | 59.9 | 113.4 | Validated win. |
| `desert2.vox` | 59.5 | 113.6 | Validated win. |
| `house.vox` | 59.9 | 114.7 | Validated win. |
| `scene_mechanic2.vox` | 40.0 | 113.5 | Validated win. |
| `Treasure.vox` | 30.5 | 58.5 | Moves into the about-60 FPS class. |
| `army.vox` | 39.8 | 42.1 | Weak; still needs better interval analysis. |
| `AncientCrashSite.vox` | 39.8 | 39.8 | Neutral; remains hard. |
| `skyscraper.vox` | 23.7 | 29.9 | Modest; still hard. |

Rejected replacements after this result: `tile4-depth-front`, tile3/tile5/tile6
scanlines, tile4 serpentine, Morton traversal, full face order, full depth
order, dense-tile face order, and centered/lookahead interval phases.

### Old Matrix-Vs-Slice Selector

Do not revive the old `.vox` matrix-vs-slice router without new GPU-hard proof.

The 86-model cadence corpus found matrix p95 wins on `desert2`,
`scene_hazmat`, `scene_house`, `scene_mechanic2`, `scene_sidewalk`, and
`Treasure`; slice p95 wins on `AncientCrashSite`, `armchair`,
`christmas_tree`, `ff1`, `mailbox`, `obj_house3`, `obj_house8`,
`obj_trashcan4`, `pyramid`, and `scene_park`; 66 models were flat or capped.

`visibleShadedColors >= 52 && visiblePlanes < 200` was the safest partial
gate. It caught several high-shaded matrix wins and avoided known p99 risk, but
it missed `desert2`, hit many capped models, and changed with browser mode.
Hostless direct matrix plus tile4 order superseded the router direction.
The current voxel path is one implementation: exact raw `.vox` quads render as
direct-matrix `<b>` leaves inside persistent signed-face wrappers. There is no
runtime matrix-vs-alternate renderer selector, no source-overpaint planner, and
no alternate voxel planner. Experiments that did not become this path should stay
out of product code and out of benchmark scripts.

### Non-Voxel Rotation

Expand Down Expand Up @@ -354,14 +327,14 @@ visual-correct implementation that changes the premise.
| Root transform spelling changes (`matrix3d`, perspective placement, inner target shell, transform-function perspective) | Longer validation was flat or worse. |
| CSS variables, registered variables, or individual `rotate` for interactive camera motion | They still hit PAC/layerize like normal JS transform mutation. |
| JS-scrubbed WAAPI or scroll-timeline camera controls | Scrubbing from JS still hit PAC once per frame. |
| Leaf `transform-style: flat` | Catastrophic regressions, including hostless direct-matrix voxel leaves. |
| Leaf `transform-style: flat` | Catastrophic regressions on direct-matrix voxel leaves. |
| Leaf or host `backface-visibility: hidden` | Fast-looking variants either failed visual checks or lost the win once oriented correctly. |

### Voxel DOM Shape

| Direction | Why closed |
| --- | --- |
| Axis hosts, host+brush matrix, or voxel slice hosts as the main shape | Hostless direct canonical matrix leaves are the transferable win. |
| Axis hosts or host+brush matrix as the main shape | The current path uses signed-face hosts only as culling groups; each brush remains a direct canonical matrix leaf. |
| Mount all six face directions | Extra active DOM dominates mutation savings. |
| Hide pooled leaves instead of removing unused faces | Flat to worse. |
| Split large brushes or source-overpaint planners | More leaves or visual risk without reliable p95/p99 wins. |
Expand Down
Loading
Loading