spike: direct-mesh render path (render OCCT triangulation without the ViewportBody repack)#83
Draft
gsdali wants to merge 14 commits into
Draft
spike: direct-mesh render path (render OCCT triangulation without the ViewportBody repack)#83gsdali wants to merge 14 commits into
gsdali wants to merge 14 commits into
Conversation
Investigation only (no code changes). Findings in notes/spike-direct-brep-rendering.md: GPUs only raster triangles, OCCT already triangulates the B-Rep, so "no mesh" isn't a GPU concept. Two readings: (A) skip the redundant ViewportBody repack and render from OCCT's existing triangulation — Low/Med, layer-clean; (B) true trimmed-NURBS GPU tessellation / analytic ray-tracing — research-grade and breaks the viewport's no-OCCT design. Recommend A if cost/memory is the pain; lean on the already-shipped PN tessellation if the pain is visual smoothness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erer Proof of concept for rendering a kernel's existing triangulation without the interstitial ViewportBody interleave/repack — layer-clean (no OCCT dependency added to the viewport). - ViewportBody: de-interleaved meshPositions/meshNormals + usesDirectMesh + ViewportBody.directMesh(...) factory (also derives `vertices` for bbox/pick). This matches the shape OCCT's Mesh already exposes (vertexData/normalData/indices). - OffscreenRenderer: a second shaded pipeline reading position@buffer0 + normal@buffer2 (de-interleaved); shaders unchanged (attributes via stage_in). - DirectMeshRenderingTests: same sphere built both ways renders identically — interior pixel-identical (baseline noise 0), only ~86/2292 silhouette pixels differ by <=5/255 (edge AA). 162 tests pass. Findings + production TODO in notes/spike-direct-brep-rendering.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mid-extension of Option A into the interactive renderer (directMeshPipeline + BodyBuffers.normalBuffer + ensureBuffers direct branch + encodeShadedSurface direct branch + shadow/pick/depth/overlay skip-guards). Not yet built/tested — checkpoint to pause the spike. Resume: verify build + add a live-render test.
Finishes the interactive-renderer extension left UNVERIFIED in the prior checkpoint: - Adds the shadow / pick / depth / overlay skip-guards (normalBuffer == nil) that the interrupted sed never applied — those passes bind the stride-12 position buffer with the stride-6 descriptor and would misread a direct body, so direct bodies are excluded there (main opaque pass renders them via directMeshPipeline). - Adds a headless test constructing ViewportRenderer with a direct-mesh body: init returns nil if any pipeline fails to compile, so success proves the new two-buffer directMeshPipeline is a valid GPU object. Pixel correctness of the live draw is covered by the OffscreenRenderer differential test (identical shaders / descriptor / binding). swift build clean; 163 tests pass. Live pixel verification on device/sim is the remaining manual step (the live MTKView draw loop isn't driveable headlessly). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Examples/DirectMeshLiveDemo — viewport-only windowed app (no OCCT dep) showing the same sphere interleaved vs. directMesh side by side through the interactive ViewportRenderer. Ran under Metal API + GPU validation (assert error mode): both panes drew for 9+ s with zero validation errors / asserts / crashes, confirming the live direct-mesh command stream is valid (bindings, strides, skip-guards). Pixel-correctness is covered by the OffscreenRenderer differential test; screenshot was blocked by Screen-Recording permission. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend Option A direct-mesh path to the shadow pass in both renderers. Previously direct bodies were excluded from shadow casting via the normalBuffer == nil guard (the stride-6 shadow descriptor would misread the stride-12 direct positions). Add a sibling shadowDirectPipeline using shadow_vertex with the two-buffer descriptor (position@0 / normal@2) and route direct bodies through it; the shadow shader reads only position, the normal binding just satisfies attribute 1. - OffscreenRenderer + ViewportRenderer: shadowDirectPipeline built at init - Shadow pass: branch on buffers.normalBuffer to pick direct vs interleaved - ViewportRenderer hasMesh guard no longer excludes direct bodies - .gitignore: ignore .build-* (device/xcode/xros) build output dirs Builds clean; 165 tests green. No direct-body shadow differential test yet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Differential shadow test closing the gap on dcbb5c1 (shadowDirectPipeline was compile-verified only). A sphere caster built two ways (interleaved vs direct) over a flat ground receiver: - assert the direct caster's cast shadow matches the interleaved caster's (per-channel max diff ≤6, i.e. silhouette AA only) — proves shadowDirectPipeline writes the same depth as shadowPipeline - assert the shadow is materially present (>100 receiver pixels darker than a shadows-off control) — so equality can't pass trivially via "no shadow" Both assertions fail if the direct body is excluded from the shadow pass, so the test genuinely guards the direct shadow path. Added deinterleave() and translation() helpers. 166 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add depthOnlyDirectPipeline to ViewportRenderer and route direct-mesh bodies through it in the depth-only prepass that feeds SSAO and silhouette detection. Previously direct bodies were excluded via the normalBuffer == nil guard (the stride-6 depth descriptor would misread the stride-12 direct positions), so they got no ambient occlusion and no silhouette outlines. - depthOnlyDirectPipeline: depth_only_vertex with the two-buffer descriptor (position@0 / normal@2); depth shader reads only position - depth-only pass: hasMesh no longer excludes direct bodies; new normalBuffer != nil branch binds vb@0 + nb@2 to depthOnlyDirectPipeline - The prepass is interactive-only (OffscreenRenderer has no SSAO pass) and draw(in:) needs a live drawable, so there is no headless pixel path. Test asserts the renderer constructs with SSAO + silhouettes enabled and a direct body present (depthOnlyDirectPipeline compiled). Draw-time Metal validation is a Phase 4 live-verification item. 167 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GPU pick (wired, live-verify Phase 4): - pickShadedDirectPipeline (pick_vertex + pick_fragment, two-buffer descriptor position@0 / normal@2), so direct bodies are stamped into the R32Uint pick texture - face-pick sub-pass: hasMesh no longer excludes direct bodies; new normalBuffer != nil branch binds vb@0 + nb@2 to pickShadedDirectPipeline - No headless pick-texture readback (OffscreenRenderer has no pick pass; draw needs a live drawable), so this is compile-verified via init. CPU pick (BUG FIX — this was a crash, not a working path): - SceneRaycast read body.vertexData (stride-6 interleaved) indexed by body.indices. A direct-mesh body leaves vertexData EMPTY and carries its positions in `vertices`, so the narrowphase indexed an empty array → out-of-bounds crash on any raycast against a direct body. (The earlier assumption that "CPU pick works via vertices" was wrong — the raycaster read the wrong field.) - Fix: narrowphase reads `vertices` when `vertexData` is empty. - Test: CPU raycast hits a direct sphere at the same distance/point as its interleaved twin — fails (crashes) without the fix. 168 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aycast fix) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route .overlay-layer direct bodies through the direct pipelines in the two remaining inline overlay passes (these don't go through encodeShadedSurface / the main pick loop, so they had their own normalBuffer == nil guards): - overlay surface draw: directMeshPipeline (shared shaded_vertex/_fragment) with vb@0 + nb@2 when normalBuffer is set, else shadedPipeline - overlay pick loop: pickShadedDirectPipeline with vb@0 + nb@2, else pickShadedPipeline - both hasMesh guards no longer exclude direct bodies - updated the stale comment that claimed the aux passes keep the guard This was the last pass still skipping direct bodies; all render/shadow/ depth/pick/overlay passes now handle them. Overlay rendering is interactive- only (live drawable), so the test asserts the renderer constructs with a direct .overlay body (pipelines compile/route); overlay draw/pick is a Phase 4 live-verification item. 169 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… audit fix) OffscreenRenderer parity audit found one real gap: the transparent surface pass (translucent bodies, back-to-front) set shadedPipeline ONCE outside the loop and bound only vb@0 — it never branched on normalBuffer. A translucent direct-mesh body would bind its stride-12 position buffer against the stride-6 shaded descriptor → misread vertices (garbage triangles). The opaque main pass already branched correctly; the transparent pass did not. Fix: select the pipeline per-body inside the loop — directMeshPipeline (vb@0 + nb@2) when normalBuffer is set, else shadedPipeline. Both pipelines share the same alpha-blend config, so compositing is identical. Headlessly testable (OffscreenRenderer has the transparent pass): differential test renders a translucent panel interleaved vs direct over an opaque box — asserts pixel-parity (≤6 AA) and that the box shows through (purple), proving the direct body went through the alpha-blended pass. Fails (misread) without the fix. Audit result: shaded + shadow + transparent passes now all route direct bodies; OffscreenRenderer has no pick/SSAO/overlay passes by design. 170 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
"Directly render B-Rep solids without the interstitial mesh body." A GPU only rasterizes triangles, and OCCT already triangulates the B-rep (
BRepMesh→Poly_Triangulation, exposed asMesh.vertexData/.normalData/.indices). So "no mesh" isn't a GPU concept — what's removable is the interstitial repack: today the bridge interleaves OCCT's separate position/normal arrays intoViewportBody.vertexData(+ re-smooths normals + an extra copy). Full design analysis innotes/spike-direct-brep-rendering.md.What this implements (Option A — layer-clean, no OCCT dep added)
ViewportBody— de-interleaved storagemeshPositions/meshNormals(stride-3), ausesDirectMeshflag, and aViewportBody.directMesh(positions:normals:indices:…)factory (which also fillsverticesso bounding-box / fit / CPU picking keep working). This is exactly the shapeMeshalready hands back.OffscreenRenderer+ViewportRenderer— a second shaded pipeline (directMeshPipeline) whose vertex descriptor reads position from buffer 0 and normal from buffer 2 (de-interleaved). Shaders are unchanged (attributes arrive via[[stage_in]]).ensureBuffersuploads the two arrays to separateMTLBuffers; the opaque shaded pass draws them. Tessellation/meshlet builders and the shadow / pick / depth / overlay passes skip direct bodies (normalBuffer == nilguard) since those bind the stride-12 buffer with a stride-6 descriptor.Verification
DirectMeshRenderingTests: the same sphere built interleaved vs. direct renders interior pixel-identical (only ≤5/255 silhouette AA between two pipeline objects);ViewportRendererconstructs with a direct body (provesdirectMeshPipelinecompiles). 165 tests pass.Examples/DirectMeshLiveDemo, viewport-only windowed app) under Metal API + GPU validation (asserterror mode): both panes (interleaved | direct) drew for 9+ s with zero validation errors / asserts / crashes → the live command stream is valid.Honest framing
This removes the interstitial repack (interleave + re-smooth + one CPU copy) — a load-time/memory win on big scenes. It does not remove triangulation (OCCT still meshes), and it's not "B-Rep on the GPU" (that's the much harder Option B — trimmed-NURBS GPU tessellation — analysed in the notes). It's also complementary to the v1.1.22 #81 fix: the direct path skips
NormalSmoothingentirely.To productionize
vertices).OCCTSwiftTools.shapeToBodyAndMetadatabridge to feedmesh.vertexData/.normalDataand skip the interleave +NormalSmoothing(separate repo).🤖 Generated with Claude Code