From 66f59f4afa6d658a837790e3e12c5a5ae0702c28 Mon Sep 17 00:00:00 2001 From: Emil <65846814+emil28092005@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:06:34 +0300 Subject: [PATCH] Plan P2 GPU visibility and mesh LOD --- .../plans/2026-09-23-p2-gpu-visibility.md | 122 ++++++++++++++++++ .../2026-09-23-p2-gpu-visibility-design.md | 29 +++++ 2 files changed, 151 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-23-p2-gpu-visibility.md create mode 100644 docs/superpowers/specs/2026-09-23-p2-gpu-visibility-design.md diff --git a/docs/superpowers/plans/2026-09-23-p2-gpu-visibility.md b/docs/superpowers/plans/2026-09-23-p2-gpu-visibility.md new file mode 100644 index 0000000..1e9face --- /dev/null +++ b/docs/superpowers/plans/2026-09-23-p2-gpu-visibility.md @@ -0,0 +1,122 @@ +# P2 GPU Visibility and Mesh LOD Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deliver all of PLAN P2: stable GPU instance IDs, GPU frustum culling, fixed indirect bins, a visible HZB debug mode, conservative two-pass occlusion, prepared mesh LOD with hysteresis, and measured comparisons to the direct renderer. + +**Architecture:** Keep the current direct path as a selectable reference. Add an opaque instanced path with local mesh geometry, instance/visible-ID storage buffers and compute-authored indirect instance counts. Split opaque rasterization around an ordinary-Z max-depth HZB; repair previous-frame occlusion guesses in a current-frame post pass. Keep shadows and ordered sprites/UI independent. + +**Tech Stack:** C++20, Vulkan 1.3, Slang/SPIR-V, CMake/Ninja, SDL3, CPU contract tests and GPU validation/image tests. + +**Spec:** `docs/superpowers/specs/2026-09-23-p2-gpu-visibility-design.md` + +## Global Constraints + +- Preserve the existing direct renderer and a runtime switch that selects it. +- Player/export contain cooked SPIR-V and metadata; no Slang compiler is required at runtime. +- MCP remains editor-only and never exposes the live Player world. +- Forward Z uses max-depth HZB, depth clear 1, and `LESS_OR_EQUAL`. +- No camera-frustum decision may remove a shadow caster; sprites and UI remain ordered. +- No required ray tracing, mesh shader, multi-draw-indirect, `drawIndirectFirstInstance`, or synchronous GPU visibility readback. +- Check actual Vulkan feature/format/limit support and retain a direct fallback if the P2 profile is unavailable. + +## Review Focus + +- Empty scene and exact bin capacity: indirect counts reset to zero and cannot address outside reserved ID ranges; Task 3 pins this. +- Door opens or disappears: a previously hidden object appears in the same final frame through Post; Task 5 pins this. +- Odd and resized viewports: HZB padding is far depth and stale history cannot be sampled; Tasks 4–5 pin this. +- Offscreen shadow caster: camera visibility leaves shadow generation intact; Task 3 pins this. +- Mesh/key reuse and LOD changes: previous transforms and occlusion are invalidated; Tasks 1 and 6 pin this. + +--- + +### Task 1: Stable instances, conservative bounds, and LOD policy + +**Files:** Modify `include/faset/render/renderer.hpp`, `src/player/SceneView.cpp`, `cmake/Renderer.cmake`; create `include/faset/render/visibility.hpp`, `src/render/visibility.cpp`, `tests/render_visibility_policy_tests.cpp`. + +**Interfaces:** Produce `InstanceTracker::update(key, meshIdentity, model, bounds, viewId)` returning slot, generation, previous bounds and `previousValid`; `select_lod(projectedPixels, previousLevel, thresholds, hysteresis)`; `transformed_bounds(mesh, model)`. Add optional `DrawItem::instance_key`, `DrawItem::lod_meshes`, `Snapshot::view_id`, `Snapshot::camera_cut` without changing existing aggregate initialization order. + +- [ ] **Step 1: Write failing CPU tests.** Assert transformed bounds for rotated, negative/nonuniform scaled meshes; stable slot through reorder; generation change after removal/reuse or mesh replacement; no previous state on camera cut; LOD hysteresis on both sides of a threshold; fallback when a level is absent. Include this core assertion: + ```cpp + auto first = tracker.update("object/primitive", meshA, identity, box, "game"); + auto second = tracker.update("object/primitive", meshA, moved, box, "game"); + require(first.slot == second.slot && second.previous_valid); + tracker.finish_frame(); + require(!tracker.update("object/primitive", meshB, moved, box, "game").previous_valid); + ``` +- [ ] **Step 2: Run the focused test target and record the expected missing-interface failure.** `cmake --build --preset linux-debug --target faset_render_visibility_policy_tests -j 6` must fail before implementation because the new interfaces/target do not exist. +- [ ] **Step 3: Implement the policy and extraction keys.** Derive keys from persistent scene object and imported primitive identity; anonymous draws remain renderable without temporal state. Use all eight local AABB corners and finite-value checks. Define an explicit LOD threshold/hysteresis contract in the header. +- [ ] **Step 4: Rebuild/run the focused CPU test, then the existing scene-view tests.** `ctest --test-dir build/linux-debug --output-on-failure -R 'visibility_policy|player_scene_contracts'` must pass. +- [ ] **Step 5: Commit** `Introduce stable render instances and prepared LOD policy`. + +### Task 2: P2 Slang bundle and checked shader metadata + +**Files:** Create `shaders/gpu_scene.slang`; modify `tools/compile_shader.py`, `cmake/Renderer.cmake`, `src/render/shader_contract.cpp`, `src/render/shader_contract.hpp`, `src/editor/build_service.cpp`, relevant shader/build tests. + +**Interfaces:** Compile and validate `gpuVertexMain`, `gpuShadowMain`, `gpuCullMain`, `gpuHzbMain`, `gpuPostCullMain` with documented set/binding layouts. Baseline `vertexMain`, `fragmentMain`, `shadowMain` and their hot-reload fingerprint contract remain valid. Export includes each new `.spv` and `.reflection.json` pair. + +- [ ] **Step 1: Write failing reflection/package tests.** A storage-buffer/storage-image Slang reflection fixture must normalize to a typed descriptor; missing or tampered P2 SPIR-V/metadata must fail validation; a packaged Player must contain all required P2 shaders. +- [ ] **Step 2: Run focused tests and verify the expected rejection or missing-artifact failure.** `ctest --test-dir build/linux-debug --output-on-failure -R 'render_shader_reload|build_schema_publication'` plus the new reflection test target. +- [ ] **Step 3: Extend the compiler's descriptor normalization and add the P2 shader entries.** Vertex resolves `visibleIds[binBase + SV_InstanceID]`; compute writes bounded per-bin IDs/counts; HZB computes max of valid children and far depth for padding. Keep C++/Slang record strides explicit and checked. +- [ ] **Step 4: Validate generated reflection and package.** Rebuild `faset_shaders`, run the focused tests and inspect each generated metadata stage/binding/fingerprint. +- [ ] **Step 5: Commit** `Add checked Slang shaders for GPU visibility and HZB`. + +### Task 3: GPU frustum culling and fixed indirect bins + +**Files:** Modify `src/render/renderer.cpp`, `include/faset/render/renderer.hpp`, `tests/render_tests.cpp`; create `tests/render_gpu_visibility_tests.cpp`. + +**Interfaces:** `RendererConfig::visibility_mode` selects direct/auto/GPU; `FrameStats` exposes submitted bin and frustum-visible counts. Renderer-owned buffers hold local vertices, instance records, candidate/bin tables, visible IDs and `VkDrawIndirectCommand` templates. One draw per bin uses `firstInstance=0`. + +- [ ] **Step 1: Write GPU integration tests.** Direct/GPU images must agree on the same opaque cube/plane scene; empty and one-instance scenes have zero/one visible instances; over-capacity growth and all-six-plane rejects remain validation-clean; an offscreen caster continues to affect a visible receiver. +- [ ] **Step 2: Run the new GPU test and verify that GPU mode is absent/fails for the intended reason.** Use `ctest --test-dir build/linux-debug --output-on-failure -R '^render_gpu_visibility$'`. +- [ ] **Step 3: Add checked device capability selection and distinct GPU scene resources.** Build mesh+texture bins, upload local vertices once per unique mesh each frame, reserve one ID range per bin, create/update descriptors, dispatch frustum cull, barrier compute writes to indirect and vertex-storage reads, and issue fixed indirect draws. Keep the direct path byte-for-byte selectable. +- [ ] **Step 4: Run the focused GPU test with Khronos validation and the existing offscreen renderer tests.** No Vulkan errors, no visible image holes, and no shadow regression. +- [ ] **Step 5: Commit** `Render opaque meshes through GPU culling and fixed indirect bins`. + +### Task 4: Current HZB and diagnostic view + +**Files:** Modify `src/render/renderer.cpp`, `include/faset/render/renderer.hpp`, `shaders/gpu_scene.slang`, `tests/render_gpu_visibility_tests.cpp`; add a focused HZB CPU oracle test if the reduction/projection contract needs isolation. + +**Interfaces:** `RendererConfig` or `Snapshot` selects HZB visualization; `FrameStats` records HZB extent, levels and build time. The current depth attachment is stored and sampled; a per-mip `R32_SFLOAT` image stores max-depth reduction. + +- [ ] **Step 1: Write failing tests.** Assert odd 319×241 extent, a far-depth hole, mip chain dimensions/padding, resized target recreation and an HZB debug image with non-uniform depth. +- [ ] **Step 2: Run the tests and confirm the missing HZB/debug capability is the failure.** `ctest --test-dir build/linux-debug --output-on-failure -R '^render_gpu_visibility$'`. +- [ ] **Step 3: Split Main opaque raster from sprites/UI, store depth, allocate sampled/storage HZB mip views, dispatch each reduction with explicit depth→compute and mip→mip barriers, and draw a selectable debug visualization.** Preserve existing forward-Z convention. +- [ ] **Step 4: Run focused GPU tests and baseline image tests under validation; compare HZB-off output with direct mode.** +- [ ] **Step 5: Commit** `Build and visualize current-frame max-depth HZB`. + +### Task 5: Previous-HZB main pass and same-frame post repair + +**Files:** Modify `src/render/renderer.cpp`, `src/render/visibility.cpp`, `shaders/gpu_scene.slang`, `tests/render_gpu_visibility_tests.cpp`. + +**Interfaces:** View history contains view identity, extent/viewport/projection, frame epoch, previous VP and HZB handle. MainCull may defer only a candidate with valid previous instance/view history; PostCull retests deferred candidates against current HZB and writes separate post args/IDs. PostRaster loads existing color and depth. + +- [ ] **Step 1: Write failing frame-sequence tests.** A wall hides an object in frame N; opening/deleting/teleporting it in N+1 reveals that object in the final N+1 image. Camera cut, projection change, view ID change and resize force history invalid; near-plane crossing fails open. Compare every frame against HZB-disabled output. +- [ ] **Step 2: Run the tests and observe the missing deferral/post behavior.** `ctest --test-dir build/linux-debug --output-on-failure -R '^render_gpu_visibility$'`. +- [ ] **Step 3: Implement previous/current projection tests, bounded deferred/post buffers, history ping-pong and invalidation, explicit compute→indirect/vertex barriers and Main/CurrentHZB/Post pass order.** Use ordinary-Z max-depth comparison with precision bias and full projected rectangle. +- [ ] **Step 4: Run the sequence and full GPU render suites with validation; confirm no same-frame holes.** +- [ ] **Step 5: Commit** `Repair temporal occlusion with current-frame post pass`. + +### Task 6: Prepared mesh LOD, editor controls and profiling + +**Files:** Modify `src/player/SceneView.cpp`, `src/editor/editor_ui.cpp`, `include/faset/render/renderer.hpp`, `src/render/renderer.cpp`, `tests/render_gpu_visibility_tests.cpp`; add a prepared-LOD example asset/scene and manual page under `docs/manual/editor/`. + +**Interfaces:** Imported or C++-supplied prepared LOD meshes are selected by projected size and Task 1 hysteresis. Changing level changes the bin but keeps the logical instance key and invalidates previous occlusion. Editor can switch direct/GPU/HZB modes and inspect culling/LOD counters without adding MCP access to the Player. + +- [ ] **Step 1: Write failing tests.** Jitter around both thresholds must retain the previous LOD; moving well across a threshold selects a different mesh/bin; missing levels fall back; both modes keep a valid image through level changes. +- [ ] **Step 2: Run focused tests and verify the missing selection/control behavior.** +- [ ] **Step 3: Hook prepared LODs into extraction and GPU binning, expose compact editor controls/statistics and a documented C++/asset authoring path.** Keep source scene IDs unchanged. +- [ ] **Step 4: Run CPU/GPU/editor UI suites and capture a representative debug screenshot.** +- [ ] **Step 5: Commit** `Expose prepared mesh LOD and GPU visibility diagnostics`. + +### Task 7: Adversarial validation, baselines and publication + +**Files:** Add `docs/validation/p2-gpu-visibility/README.md` and capture artifacts; update `PLAN.md`, `docs/manual/editor/profiling.md`, `docs/ARCHITECTURE.md`, CI GPU test registration as appropriate. + +**Interfaces:** Close P2 only with exact revision, compiler/driver/GPU/OS, scene/camera paths, commands, full-frame direct/GPU timings, pass counters, image comparison and stated coverage limits. + +- [ ] **Step 1: Add adversarial fixtures for mass deletion/reuse, near-plane, camera inside bounds, odd/offset viewport, door/wall, open scene and offscreen shadow caster; confirm at least one fails before its corresponding fix.** +- [ ] **Step 2: Run Debug and Release build/test suites, GPU validation, shader reload, two sample-game export/relaunch checks, and available Windows CI.** Record exact outputs; a platform without executed GPU coverage remains explicitly unverified. +- [ ] **Step 3: Profile the same closed and open scenes in direct and GPU modes.** Record CPU extraction/upload/submission, GPU pass and full-frame time, readback conditions, memory and culling counters. Do not turn a scene-specific result into a universal performance claim. +- [ ] **Step 4: Review all PLAN P2 criteria against evidence, update docs, run `graphify update .`, request independent code review and fix load-bearing findings.** +- [ ] **Step 5: Commit** `Validate and document P2 GPU visibility milestone`; publish only after all checks are green. diff --git a/docs/superpowers/specs/2026-09-23-p2-gpu-visibility-design.md b/docs/superpowers/specs/2026-09-23-p2-gpu-visibility-design.md new file mode 100644 index 0000000..23e4494 --- /dev/null +++ b/docs/superpowers/specs/2026-09-23-p2-gpu-visibility-design.md @@ -0,0 +1,29 @@ +# P2 GPU visibility and mesh LOD design + +This design implements [PLAN P2](../../../PLAN.md#p2-gpu-driven-visibility-и-lod) for the desktop Vulkan 1.3 renderer. The direct renderer remains a selectable reference for image and full-frame performance comparisons. The feature is limited to opaque triangle meshes. Ordered sprites, UI, and shadow casters keep their independent paths; camera visibility must never remove an offscreen shadow caster. + +## Data and ownership + +`DrawItem` receives an optional stable instance key and an optional ordered list of prepared coarser meshes. Scene extraction derives a key from persistent object/asset-primitive identity; ad-hoc draws without a key render correctly but do not use previous-frame occlusion or LOD history. The renderer assigns a slot and generation to each live key, invalidating previous transforms when the mesh identity changes or a slot is reused. An immutable frame snapshot contains current and previous world bounds, transforms, material values, selected mesh LOD, and a bin ID. A conservative world AABB is formed from all eight transformed corners of each mesh's local bounds. The bound used for culling covers the selected geometry; source content must not claim a narrower bound than its vertices. + +Prepared LODs are optional. The renderer chooses one by projected size, applies hysteresis around every threshold, and falls back to the nearest available level. A level switch retains the stable instance key but invalidates occlusion history for that instance. LOD selection never changes source asset IDs or authoring scene data. + +## GPU path + +The renderer groups opaque candidates by compatible mesh and texture into fixed bins. It uploads local mesh vertices once per unique mesh in the frame and uploads one transform/material/bounds record per instance. A compute pass tests current frustum and writes visible instance IDs into a pre-reserved range per bin, atomically updating the bin's indirect `instanceCount`. Each bin has a CPU-authored fixed draw template and one `vkCmdDrawIndirect` call; zero instances submit no geometry. The vertex shader resolves `SV_InstanceID` through the visible-ID buffer and transforms the local vertex on the GPU. The command uses `firstInstance = 0` and a pushed bin base, so it does not require optional `drawIndirectFirstInstance` or `multiDrawIndirect` features. Overflow is prevented by reserving exactly the number of bin candidates; buffer growth is checked before recording commands. + +The direct path continues to use existing CPU-transformed vertices, CPU frustum culling, and direct draws. Scene shading and texture descriptors are shared where possible. GPU and direct modes must produce equivalent final opaque images within stated raster tolerance. Scene culling does not affect the shadow pass. + +## HZB and two-pass occlusion + +Faset uses normal forward Z: depth clear is 1 and the comparison is `LESS_OR_EQUAL`. Its furthest-depth HZB therefore stores the **maximum** child depth, padding odd dimensions with 1. A candidate is marked occluded only when its conservative nearest depth is strictly beyond the maximum over its complete screen rectangle plus precision bias. Near-plane intersections, nonfinite projections, unknown previous transforms, a camera cut, a resized view, a different view identity, and an invalid history all fail open as visible. + +The pass order is: initialize fixed arguments/counters; MainCull against the current frustum and previous HZB; MainRaster into cleared color/depth; build current HZB from stored depth with per-mip barriers; PostCull deferred candidates against current HZB; PostRaster loading Main color/depth; then ordered sprites/UI and capture/presentation. Current HZB after Main is intentionally incomplete: a false previous occlusion is repaired by Post in the same frame, while absent occluders only reduce culling efficiency. Current HZB and its view metadata survive to the next rendered frame. History is per renderer/view and is invalidated by explicit cut, view/extent/projection changes, or incompatible instance generation. No GPU visibility decision requires synchronous CPU readback. + +RenderGraph names and validates pass order; Vulkan image and buffer barriers remain explicit in the backend. Compute writes become visible separately to indirect-command reads and vertex shader storage reads. Depth attachment writes become visible to HZB compute sampling only after ending the Main rendering instance. HZB mip writes become visible to the next mip's reads. The single-queue, one-frame-in-flight lifetime model is retained until measured work warrants a wider pipeline. + +## Diagnostics and acceptance + +The editor exposes GPU/direct and HZB-enabled modes and a current-HZB/debug visualization. Frame statistics distinguish submitted bins, main/deferred/post counts, frustum rejects, selected LODs, pass timings, and GPU allocation sizes. Debug counters may be read after the existing frame fence; normal culling decisions never use them. Full-frame profiles record both closed and open scenes with the same snapshot, camera path, resolution, driver, and build. Existing synchronous frame capture is accounted for or disabled in the comparison profile; no blanket speedup claim is made. + +Acceptance includes shader/reflection validation and package/export contents; CPU policy tests; GPU image comparison to direct mode; empty and capacity-boundary cases; wall/door reveal in the same final frame; camera cut/teleport, near-plane and camera-inside cases; odd viewport dimensions and resize; repeated spawn/despawn/mesh replacement; LOD threshold jitter; offscreen shadow caster; and zero Vulkan validation errors on the Linux reference GPU. Windows compile and available native GPU checks are reported separately without claiming hardware coverage that was not run.