feat(rhi): Phase 7 — port the full render-pass suite to Vulkan (#691) - #769
feat(rhi): Phase 7 — port the full render-pass suite to Vulkan (#691)#769drsnuggles8 wants to merge 20 commits into
Conversation
…ptor slot cache, facade draw path (#691 Phase 7, part 1) Stage 1 contracts, first slice. Everything device-tested on the 4090 with sync validation asserted at zero errors; full suite 5582/5583 (sole red is the pre-existing #754 atmosphere golden). - Resource factories on Vulkan: VulkanUniformBuffer (frame-arena-versioned addresses reproducing GL command-ordering semantics without hazard tracking), VulkanVertexBuffer/IndexBuffer (BDA, host-preferred VMA), VulkanVertexArray (CPU aggregate), file-loaded VulkanTexture2D (stbi parity), real texture uploads with mip blit chains, readbacks, SSBO data paths, VulkanOneShot blocking load-time submits. - Upload->sample seam: VulkanImageInfo::InitialLayout seeds the layout tracker so a graph's first barrier cannot legally discard uploaded pixels. - VulkanDescriptorSlotCache over VulkanResourceHeap: get-or-create slots per (image, view, kind, layout), storage-image descriptor writes, recycling gated on the deferred-reclaim delay. - Facade draw path: lazy dynamic-rendering scope (vkCmdBeginRendering deferred to first draw, Clear() folded into loadOp, auto-end on barrier/transfer/target change), process-global VulkanBindingState mirroring GL bind points, root-struct assembly against each shader's cached VulkanRootDataLayout, thin-PSO fetch with WITH_COUNT viewport/scissor emission, real draw/bind family implementations. - Fixed latent Phase 6 bug: VulkanResourceHeap ignored resourceHeapAlignment (VUID-11235) — the pilot's allocation only happened to land aligned; now vmaCreateBufferWithAlignment. - Tests: VulkanResourceFactoryTest (7) + VulkanDrawPathTest (an unmodified GL-shaped pass body renders a tinted fullscreen triangle end-to-end). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Phase 7, part 2) Stage 1.1 in full (the amendment (56) deferral) and Stage 1.3. 35/35 Vulkan device tests, full suite 5584/5585 (sole red is the pre-existing #754), zero validation errors with sync validation on. - VulkanDescriptorHeapBackend: RHI::IDescriptorHeapBackend over VulkanResourceHeap. Token model (AcquireDescriptor stages a fully-resolved view description; UploadSlots redeems tokens into vkWriteResourceDescriptorsEXT at the engine-chosen slots). Engine heap slots [0, kDescriptorHeapSlots) reserved ahead of the draw-path slot cache; fresh installs prefill the range; GL-parity HeapDesc capacities. - Nulls are REAL 1x1 black images per sampled view dimension and per storage format — validation rejected the assumed free null write (pView == NULL is robustness2-gated, off the ADR 0010 floor). The GL backend's typed-null discipline transfers to Vulkan almost verbatim; recorded for the ADR. - Poison-on-free proven by a draw: engine-heap OffsetOf indices render through root data; after DestroyView + Flush the same index samples deterministic black. Slot-cache release poisoning added on the same mechanism. VulkanTexture2D lifecycle fires RetireResource / InvalidateResource (the amendment (22)-correction pair); VulkanContext::Init installs the backend, teardown shuts the engine heap down before the heap it indexes. - Compute route: VulkanComputeShader (shaderc vulkan_1_4 + OLO_VULKAN, own .cached_vulkan14.comp tier, commit-on-success rebuilds, reverse-index pipeline invalidation), compute PSOs via a shared BuildBindingMappings (graphics/compute mapping chains cannot drift), BindImageTexture staging GENERAL-baked storage descriptors through the separate image-unit namespace (amendment (29)), DispatchCompute sharing the draw path's root assembly. Gated by a UBO-tinted imageStore dispatch verified by readback. - VulkanResourceHeap: ReserveSlotRange + storage-image descriptor writes; ToVkFormat promoted into VulkanBarrierLowering (two consumers). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he real graph, draw-path fixes, pilot → render-callback seam (#691 Phase 7 Stage 1.6b) The Wave A vehicle: VulkanPassSuiteTest runs REAL passes through the REAL render graph on the process-global Vulkan backend. First tenant is the golden-gated FXAA pass — a producer draws the pilot's hard-edge pattern via FullscreenBlit's new OLO_VULKAN vertex-pulling branch, FXAA consumes it through its ordinary versioned-input scan, and the output matches the SAME GL golden the Phase 6 pilot matched (RMSE < 0.02, zero validation errors with sync validation on). Backend gaps only a full graph frame could expose, all fixed: - EnsureRenderingScopeForDraw now transitions every attachment to its attachment layout through the layout tracker BEFORE vkCmdBeginRendering (the planner never lowers a framebuffer-kind write to an image barrier, so first uses rendered into UNDEFINED-layout images). - DrawIndexed/DrawIndexedInstanced honor the facade contract "indexCount 0 = whole index buffer" (GL derives; Vulkan passed 0 raw into vkCmdDrawIndexed — a LEGAL zero-index draw that rendered nothing; every pass body uses the no-count form). - LowerAccess attachment WRITE accesses widened to READ|WRITE (loadOp LOAD reads the attachment; the read bit is ignored on the source side). - PrepareDraw's silent PSO-failure drop now logs, and per-recording prepared/dropped draw counters give fixtures a draw-count contract. The Phase 6 pilot is replaced by its successor seam: a backend-neutral GraphicsContext::FrameRenderCallback receiving the acquired backbuffer as an RHI::ResourceHandle (barrier to Access::Present in neutral currency); swapchain images are published into the handle/info registries; 266 lines of pilot scaffolding deleted; clear-only fallback retained. Suite hygiene: RecreateForSelectedBackend only CONSTRUCTS the new api — the first fixture to swap the process-global backend must re-Init the restored GL object or its caps read zero (GL_MAX_DRAW_BUFFERS 0 broke 82 downstream tests); MeshPrimitives' fullscreen-triangle cache is now backend-aware and released in fixture teardown. Gates: 36/36 Vulkan tests, full suite 5585/1 (sole red is pre-existing #754 AtmosphereVisualEvidenceTest). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…691 Phase 7 Stage 2a) Every remaining Wave A pass shader now carries the OLO_VULKAN vertex-pulling branch (ADR 0011 §5 — binding 57, the standard 20-byte {vec3 position, vec2 uv} stream), GL branches untouched behind the #ifdef: - 25 shaders had the byte-canonical fullscreen vertex stage and took the FullscreenBlit branch verbatim (scripted sweep, zero skips). - PostProcess_TAA consumes only the position and DERIVES its UV — the pull branch reproduces that derivation rather than reading floats 3-4, so the two routes cannot disagree. - JumpFlood_Pass keeps its 11 varyings and vertex-stage JumpFloodUBO (the one vertex descriptor binding in the post suite) and defines pulled locals under the attribute names inside main. VulkanPassSuiteTest grows the Wave A tenant harness (RunSinglePassChain: producer -> pass -> caller-backed output with shared execute contracts — prepared/dropped draw counts, zero resolve failures, GetTarget guard) and two analytic tenants on it: - Vignette: white input reads white centre / black corners at intensity 1.0 (the smoothstep factor is exactly 1 at the centre, 0 at every probe corner). - ChromaticAberration: a black|white edge at x=96 — OFF-centre, because the radial split is zero at the screen centre — yields a pure red fringe (R sampled outward into white, B inward into black, G centred). Gates: 38/38 Vulkan tests; full suite 5587/1 (sole red is pre-existing #754 AtmosphereVisualEvidenceTest). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ook (#691 Phase 7 Stage 2a) Three more Wave A passes prove out through the real render graph on Vulkan, each pinned by its defining property: - ColorGrading: the pass's own imported identity LUT (Persistent heap lifetime) passes the hard-edge pattern through within +-2 — the imported- texture path's first graph tenant. - EASU: a constant field survives the kernel exactly (normalised weights), via the producer redirected to the SceneColor family — the harness's PatternProducerPass now parameterizes its target resource/texture-view names and blackboard slot. - DOF: two-sided focus contract through an imported uniform-depth stand-in (the new m_ExtraSetup auxiliary hook): focus at the near plane passes the edge through untouched; focus pushed far softens it. A DOF that never blurs — or always blurs — fails. Six of 23 Wave A passes now carry green Vulkan graph tenants; between them they exercise every harness capability the trivial remainder needs (pooled + caller-backed FBs, pass-owned UBOs, persistent imports, family redirects, aux imports, multi-chain tests). Gates: 41/41 Vulkan tests; full suite 5590/1 (sole red is pre-existing #754 AtmosphereVisualEvidenceTest). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…I/MotionBlur/Precipitation (#691 Phase 7 Stage 2a) All twelve Wave A trivial passes now carry green Vulkan graph tenants. The six new ones, each pinned by its defining property (two-sided where a cheap lever exists): - SSS: UBO flag off => passthrough with alpha forced 1; on with an 8-texel radius => the analytic 0.218 white-leak across the edge, far field black. Reads the DIRECT blackboard pair, so its aux setup declares the canonical SceneColorTexture attachment view (the production RenderPipeline shape) plus depth under SceneDepthAttachment. - AOApply: white AO stand-in => exact passthrough; mid-gray at intensity 1 => the exact mix(1, ao, I) darkening. - ContactShadow: genuine two-sided OCCLUSION — a two-plane depth stand-in with frontal normals and a toward-light ray darkens the contact pixel to ~13 while the far receiver stays lit; intensity 0 passes through. - SSGI: same two-wall geometry with the radiance split below the depth split => at least +25 bounce light at the contact probe; intensity 0 reads the base exactly. - MotionBlur: velocity decoded as raw .rg so an RGBA8 stand-in expresses it; zero velocity => hard edge intact; 0.2 uv velocity at strength 0.5 => both edge sides mix at three probes. - Precipitation: documented passthrough floor (both visual levers live outside the graph — the global system UBO is null headlessly and the pass refills its own UBO from CPU weather-sim statics every Execute); the tenant still pins byte-exact passthrough with alpha forced 1. Gates: 12/12 VulkanPassSuite, 47/47 across all Vulkan suites; full suite 5596/1 (sole red is pre-existing #754 AtmosphereVisualEvidenceTest). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ase 7 Wave B) The Vulkan SPIR-V route cannot express bare (non-block) uniforms, and VulkanComputeShader::Set* is a deliberate no-op — so ToneMap's 13 bare auto-exposure uniforms were exactly the values that would silently read zero and latch a NaN into the persistent exposure SSBO. All 13 migrate into ONE std140 AutoExposureParams block (binding 58, new UBO_AUTO_EXPOSURE) shared VERBATIM by AutoExposureHistogram.comp and AutoExposureAverage.comp, mirrored by UBOStructures::AutoExposureUBO (size static_asserted); the pass fills it once per frame before the histogram dispatch and the 13 Set* calls are gone. Legal on both routes — GL compute at 460 core takes UBO blocks; per glsl-shaders.md §5f the resource declarations fork for no backend. PostProcess_ToneMap.glsl also gains the standard OLO_VULKAN vertex-pull branch — it was the one post-process fullscreen shader missing it. Proof: 89/89 across every ToneMap operator fixture (monotonicity, black stays black, extreme HDR), all AutoExposure tests, and all 47 Vulkan tests — the GL path is behaviourally unchanged and the Vulkan set holds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Phase 7 Wave B) The last bare uniforms on the Wave B compute path — and the hard case: unlike ToneMap's once-per-frame values, these change PER DISPATCH. - HZB.comp's six "push-constant-style" uniforms move into HZBParams (binding 59, new UBO_HZB; C++ twin UBOStructures::HZBParamsUBO, static_asserted at 48 B). HZBGenerator refills it before every 4-mip batch — legal on both routes: GL re-uploads the bound buffer, the Vulkan backend's arena-versioned UBOs mint a fresh per-dispatch address on each SetData (ADR 0011 §4). Blast radius covers SSR, which shares HZBGenerator in min-reduce mode. - GTAO_Denoise.comp's per-ping-pong u_BlurHorizontal moves into GTAODenoiseParams (binding 60, new UBO_GTAO_DENOISE, 16 B), refilled each pass. Deliberately NOT folded into GTAOParams@28 — that block is declared at two different lengths across GTAO.comp and the denoiser, so extending it would have to touch both declarations. Proof: 100/100 across every GTAO, SSR, and HZB fixture plus all 47 Vulkan tests — the GL path is behaviourally unchanged. With this, every Wave B compute shader is SPIR-V-clean: FroxelFog*/ FluidSmooth were already block-based, ToneMap migrated in 40c53e8. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ave B)
The two fluid raster stages consume a NON-standard vertex stream — a bare
{vec2 a_QuadPos} unit-quad corner at 8-byte stride (particle position,
radius, and liveness all ride the PBF solver's SSBOs; gl_InstanceIndex was
already the Vulkan spelling). The pull branch reads two floats per vertex
from the engine-wide binding 57 and defines a pulled local under the
attribute name, so the body stays shared verbatim — the JumpFlood_Pass
pattern at a different stride. Binding 57 is free in the fluid include
tree (the solver SSBOs sit at 21/22/28).
With this, every raster stage in Waves A and B carries its pull branch.
Proof: 94/94 across all fluid fixtures (recompiling both shaders through
the GL cache) and all 47 Vulkan tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Wave B) The last backend primitive Wave B needs: 3D volume textures (froxel-fog volumes, noise fields). - VulkanImageInfo gains ViewType (default 2D). Both bind paths consume it: BindTexture's whole-image sampled view and BindImageTexture's storage view now build VK_IMAGE_VIEW_TYPE_3D for volumes instead of hardcoding 2D (an invalid view for a 3D image). - VulkanTexture3D (VulkanTransientResources): VMA 3D image with SAMPLED|STORAGE|TRANSFER usage — written as image3D by compute, read as sampler3D — registry entry + adopted RHI handle, facade-slot Bind, and the standard retire/reclaim destructor. - Texture3D::Create moves out of Platform/OpenGL/OpenGLTexture3D.cpp into a neutral Renderer/Texture3D.cpp with the three-arm backend switch (the factory cannot live in a GL TU once a second backend exists — rhi-abstraction-boundary.md). Proof: 97/97 — all Vulkan suites plus the GL 3D-texture consumers (cloud noise, wind, volumetric/froxel fog fixtures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… out (#691 Phase 7) All four Wave B compute-centric passes now run through the real render graph on Vulkan (16/16 pass-suite tenants, zero validation errors): - ToneMap: three chains — operator-0 byte-exact passthrough, manual exposure-2 exact doubling, and REAL auto-exposure metering (histogram + average dispatches against the graph-resolved HDR input; the draw consumes the metered exposure, ~0.21 for a uniform 0.502 field). - GTAO: full production recipe (2 HZB mip batches with per-mip storage views, GTAO, denoise ping-pong, CopyImageSubData to the AO buffer), pinned as a paired differential against a uniform-depth control: the crease darkens >=40 and decays monotonically; the near side of the step stays lit (sign correctness). - VolumetricFog: both dispatches over two frames (cross-frame ping-pong + temporal blend), raw 3D volume readback, albedo-1 analytic contracts (per-slice accumulated + transmittance == 1, monotone decay, far transmittance ~ exp(-3)). - FluidIntermediates: the reachable floor — no-draw early-out through the real graph and the not-ready guard against real particle StorageBuffers (the splat/smooth body waits on the Wave C raw-FBO slice). The tenants flushed out and fixed three backend bugs: 1. VulkanComputeShader resolved #include against the shader ROOT, not the .comp file's directory — '../include/*.glsl' never resolved (kills any compute shader with load-bearing includes). Now passes the parent dir at all three sites, the GL twin's rule. 2. Texture2DArray::Create had NO backend switch — it unconditionally built the GL class, whose glCreateTextures is a null glad pointer in a GL-less process (the fog tenant's isolated-run AV via the CSM placeholder). New VulkanTexture2DArray + the neutral three-arm factory; registers VIEW_TYPE_2D_ARRAY, fixing sampler2DArrayShadow binds too. 3. VulkanPipelineBuilder emitted every binding mapping with resourceMask ALL — two ALL-masked mappings at one (set,binding) violate VUID-11244 and pipeline creation FAILS for any shader with a sampler and a storage image at the same number (the froxel shaders were the first; dispatch silently dropped). Per-kind masks now express the disjoint-namespace model the way the extension intends. Also: shaderDemoteToHelperInvocation enabled (glslang at vulkan1.4 lowers discard to OpDemote — every discard shader failed module creation), and CopyImageSubData implemented (per-layout-run exact transitions through the tracker, vkCmdCopyImage, tracker updated). Gates: 16/16 VulkanPassSuite, 51/51 Vulkan suites, full suite 5600/1 (sole red is pre-existing #754). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 7) Bloom (11-draw mip loop over pooled scratch mips, halo spread + black stays black), SelectionOutline (first integer-sampler tenant: R32I entity-ID stand-in through JFA init + ping-pong + composite, exact outline ring), Cloudscape (3 draws at 2 resolutions + history extract, deck-with-gaps floor), Fog (exponential-extinction analytics, density-0 byte-exact passthrough, froxel disabled-fallback pinned the production way), TAA (two frames through a REAL history sink/import round trip — the 0.1/0.9 blend lands within +-3), OITResolve (in-place RMW with per-attachment blend, exact weighted composite + discard path), DepthVelocityUpscale (first MRT tenant, exact f32 nearest-upsample seam), SSR (intensity-0 passthrough while the pass-owned min-HZB compute chain runs), UIComposite (mixed int/float ClearAllAttachments + blended overlay callback + R32I readback of -1), SSAO (documented stub floor that FAILS the moment the raw-texture family gains a Vulkan arm). Production bugs the tenants exposed, fixed: 1. Clear()/ClearDepthOnly() with a STALE rendering scope cleared the PREVIOUS target (the lazy scope switches at the next draw) — a mid-pass Bind();Clear() wiped what the pass had just drawn. A scope whose target differs from the published framebuffer now ends and the clear folds into the new target's loadOp. 2. Sampling a just-rendered attachment mid-pass had no visibility seam: BindTexture now transitions ATTACHMENT/TRANSFER layout runs to SHADER_READ_ONLY per-run through the tracker (GENERAL untouched by design). Also what makes TAA's imported history sampleable. 3. Per-attachment blend GL parity: glEnablei alone does not give a buffer its own blend func — the lowering diverted factors to the never-written per-attachment array whenever the per-attachment enable was set. AttachmentBlendFuncSet[] now records the real GL semantics at both lowering sites and joins the pipeline key. 4. VulkanFramebuffer::ClearAllAttachments implemented (GL-parity per-attachment clears; RED_INTEGER via the bit-identical uint clear). Also deletes FogRenderPass's stale TEX_SHADOW bind (the shader has no shadow sampler since the raymarch moved to compute, issue #435) — the boundary ratchet improved 22 -> 21 and the baseline follows it down. Gates: 26/26 VulkanPassSuite, 61/61 Vulkan suites, ratchet 8/8, full suite 5609 passed with only the pre-existing #754 red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ometry ports (#691 Phase 7) The three decisions every Wave C shader depends on, plus port-order 1-4: - A2: TEX_DDGI_VISIBILITY 57 -> 64 (binding 57 was triple-booked with the vertex-pull SSBO and UBO_DEBUG_DRAW across GL's separate namespaces — one Vulkan set collapses them). Forced knock-ons: TEX_SHADER_GRAPH_0 63 -> 65 (63 stays sampler-free = the bone-pull number), heap-offset table re-rounded to whole uvec4s, BindlessHeap.glsl + its GPU test. - A3: the reserved vertex-pull PAIR — SSBO_VERTEX_PULL=57 (stream 0), SSBO_BONE_PULL=63 (stream 1, the skinned bone-influence VB); the root assembler maps both, kind-guarded to the StorageBuffer namespace (the old bare 57 check shadowed UBO_DEBUG_DRAW). - A8/A1: the Y-flip projection seam (RHIProjectionSeam, identity on GL) applied at every projection writer, with the analytically necessary SPLIT: full F = {y'=-y, z'=(z+w)/2} for gl_Position consumers (Vulkan stored depth then EQUALS GL's, so depth*2-1 reconstruction shaders keep working), row-flip-only + recompute-inverse-from-flipped for uv/depth-math consumers (composing inverse(F*VP) double-applies the z remap). Front-face CCW<->CW composed once in the pipeline builder so PlanarReflection's local flip survives. Cubemap-face bakes documented as needing per-face compensation when their passes port. - Ports: OITPrepare (ClearFramebufferColorAttachment/ClearFramebufferDepth /BlitFramebuffer implemented; differential depth-seed contract), FluidComposite (real V3 draw + refraction snapshot; the splat body waits on the raw-FBO slice, exact guard documented), Overdraw (empty replay + exact heat-ramp anchors), DeferredLighting (raw draw- attachment selection as a per-framebuffer persistent map consumed at scope-open and by blits; compare-off depth-array view handle; full Lambert+GGX analytic against a CPU mirror of PBRCommon +-4/255). Production fixes along the way: GLStateGuard faulted on Vulkan (glad null pointers — now inert off-GL); VUID-08917 (combined depth/stencil must mirror the stencil format into the PSO); VUID-03320 (blit transitions name both aspects from the registry); a full-suite-only IBL-handle contamination caught by the tight lighting contract. Gates: 30/30 VulkanPassSuite (FXAA golden intact under the seam), 65/65 Vulkan suites, DDGI/Bindless GL 37/37, full suite 5614/1 (sole red is pre-existing #754). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tream pulls (#691 Phase 7) Port-order items 5-8: VirtualGeometry (vkCmdDrawIndexedIndirectCount + DrawParameters + TextureBarrier; hand-authored cluster MDI tenant), ShaderDebugDraw (vkCmdDrawIndirect x7 + the host-visible readback ring: CreateBufferHandle/AllocateBufferStorage(DeviceToHost)/CopyBufferSubData/ ReadBufferSubData/DeleteBuffer, frames-in-flight indexed; primitives reach the viewport and stats read back), Foliage (V8 two-stream pull: 20B per-vertex @57 + 48B per-instance @63 — the stream-1 binding's first tenant; three tinted cards probed), ForwardOverlay (narrow/restore floor), Particle (V5/V6/V7 pulls with int bitcasts, CPU billboards/trails/OIT + GPU indirect). Backend: vertexPipelineStoresAndAtomics + fragmentStoresAndAtomics enabled (pipeline creation rejected the debug SSBO/storage-image stages without them), and the aspect-blind attachment lowering fixed: RenderGraph's framebuffer barrier fan-out reuses ONE access prototype for every attachment INCLUDING depth (documented contract: the backend derives aspect/layout per image) — NormalizeAttachmentAccessForAspect now remaps ColorAttachment* to DepthStencilAttachment* for depth-like aspects in both LowerAccess and LayoutFor, curing the VUID-01208 pair (COLOR layouts on depth images + poisoned tracker) the first two-passes-write-one-depth-FB graphs exposed. Gates: 35/35 VulkanPassSuite, 70/70 Vulkan suites, 80/80 mixed GL cross- checks, zero validation errors. Full suite: the pre-existing #754 red plus EASUVisualEvidenceTest.GTAOSurvivesRuntimeUpscaleSwitch — proven PRE-EXISTING (passes with the committed baseline test set on this same binary): GTAO's history latches a zero attractor when its resize lands on zero-initialized VRAM; test-order shifts allocation history. The known #549/#530/#563 invalidate-on-structural-event family; to be filed as its own issue with the deterministic repro. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, V1 pulls (#691 Phase 7) Port-order items 9-10. Twelve V1 vertex-pull branches (32 B engine Vertex over an 8-float stride): the six-shader Decal family, the Scene core (PBR_GBuffer, PBR_MultiLight, DepthPrepass, DepthPrepass_Mask with its SPARSE locations 0/2 preserved, OcclusionProxy) and DebugGBuffer_RMA. Skinned shaders stay for batch 4. Tenants: - Decal G-Buffer mode matrix: all four modes through the unmodified ExecuteOnGBuffer, with "untouched" asserted byte-exact IN-FRAME by comparing footprint against non-footprint texels of the SAME attachment — a clear-vs-clear compare cannot tell a masked write from a no-op draw. Batch 1's per-framebuffer selection map already handled NoAttachment holes correctly (null view + UNDEFINED format). - DeferredOpaqueDecal export half; the drain half cannot be pinned at one sample (the node hands ExecuteOnGBuffer the same FB for writing and depth sampling — the documented mid-pass layout gap). - Occlusion queries: occluder + occluded proxy (0 samples) + unoccluded proxy, 32/64-bit agreement, stale-handle guard, both conditional-render arms. - Scene deferred floor: the RMA-blit channel as an arithmetic contract over the G-Buffer clear the pass performs. Backend: VulkanQueryRegistry (one VkQueryPool per CreateQueries; per-slot reset immediately before the write so a blanket reset cannot wipe the double-buffered half the pool is about to read; scope-ended Begin/End so a pair cannot straddle a render-pass instance; a Recorded flag gating reads because a WAIT read of a never-written slot blocks forever), query pools on the deferred-reclaim queue, host-side conditional-render predicate (the engine's one caller passes the previous frame's already resolved query — the extension's buffer predicate would need a WAIT copy that hangs on an unwritten slot), and the BindTexture visibility barrier now names BOTH aspects of a combined depth-stencil image (a sampled depth attachment was leaving its stencil half in TRANSFER_DST). independentBlend is now enabled when supported: without it EVERY color blend attachment state must be identical, which made the whole per-attachment blend/mask facade family undefined — retroactively including the already-landed WB-OIT tenants. Defect found and pinned as observed behaviour (NOT fixed — it is a GL bug too): normal-mode decals write nothing on either backend. Decal_GBuffer_Normal.glsl outputs at location 0 while the mode's draw map is {NONE, 1, NONE, NONE, NONE}; the validation layer says it verbatim. The tenant asserts the current behaviour so the fix fails loudly. Gates: 39/39 VulkanPassSuite, 74/74 Vulkan suites, 400/400 GL cross-checks, full suite 5612 with exactly the two known reds (#754 and the documented GTAO-history latch). Zero validation errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…occlusion, tessellation, DDGI capture (#691 Phase 7) Port-order items 11-15; Wave C's geometry set is complete. - Shadow: two cascades of a depth ARRAY through one framebuffer, cascade 0 static and cascade 1 skinned by a bone whose translation makes a dropped bone stream land on the wrong half (bone id 0 = identity). Per-layer depth views implemented (cached per image+layer, selected by AttachDepthTextureArrayLayer); all five skinned shaders gained the V2 pull (engine Vertex @57 + bone stream @63, ids via floatBitsToInt exactly as GL reads the Int4 attribute). - PlanarReflection: the mirrored image is the exact vertical mirror of the direct one, with an independent row-half probe because a symmetric mirror check passes with or without the flip; both windings under culling for both cameras. - GPU occlusion: phase 1 rasterizes, a compute cull samples that depth and WRITES the indirect command, and the phase-2 draw honours the GPU-decided instanceCount — args pre-poisoned with a value neither arm can produce. - Water: the tessellation PSO shape (tessellation state, PATCH_LIST, patchControlPoints as a PSO key axis since the dynamic form is off-floor); the real 4-stage Water.glsl rasterizes, coverage read off DEPTH because the colour legitimately resolves to black through null-descriptor samplers. - DDGI capture: the seam gained AdjustCaptureProjectionForBackend (z remap WITHOUT the y flip) for direction-addressed cube-face bakes, closing the KNOWN LIMIT RHIProjectionSeam.h documented. Two silent engine defects, both fixed: 1. The front-face composition was INVERTED. The seam negates clip y, but Vulkan's facing determinant is computed in framebuffer coordinates whose y already points down where GL's window y points up — so the extra inversion made every solid mesh inside-out, back-face culling removing exactly the triangles GL keeps. Invisible until now because every earlier Vulkan tenant that recorded a winding drew with culling OFF. Pinned across all four {direct, mirrored} x {CCW, CW} cells, since a reflection's own handedness reversal lets a self-consistent-but-inverted mapping pass a one-sided check. 2. GLStateSnapshot::ApplyCore() had no backend gate — PlanarReflectionRenderPass calls it directly on the guard's entry snapshot, so a Vulkan-only process hit null glad pointers and a mixed process stomped the live GL state. Gates: 44/44 VulkanPassSuite, 79/79 Vulkan suites, 382/382 GL cross-checks, full suite 5627 passed with exactly the two documented pre-existing reds. Zero validation errors. Reported, not done: InstanceOcclusionCull.comp has 15 bare default-block uniforms and cannot compile on the SPIR-V route (own migration slice, same debt class as VirtualClusterCull.comp); SkyCubemapBake/IBLPrecompute are the other direction-addressed captures and need the capture seam too, left unchanged because neither has a tenant to prove it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…kan frame (#691) The editor now renders its real render graph on Vulkan. The live frame shows lit, shaded 3D geometry — water surface, hulls, lighting gradients — right way up and filling the window, with ZERO validation errors, zero sync hazards and zero fence failures. Swapchain import: "no framebuffer bound + a backbuffer published IS the default framebuffer" — exactly GL's rule, so Unbind() and BindDefaultFramebuffer() stay one operation. VulkanContext publishes the acquired image between BeginRecording and the callback; the lazy scope opens against it like any framebuffer (tracker-guessed transition, pending clear folded into loadOp, targets filled for the PSO fetch). The PRESENT transition is the BACKEND's, not the callback's — only the presenting backend knows the layout its swap path needs, and it keeps the clear-only fallback safe by declining only when nothing touched the image. A windowless tenant proves the seam by standing a framebuffer attachment in for the acquired image. Frame recorder: Application installs the callback when the context owns recording and hands over the layer work. Two guards found the hard way — the engine PRESENTS DURING STARTUP (shader-warmup progress frames swap from inside Renderer3D::Init), which drove OnUpdate on still-attaching layers; and a nested present resets the command buffer the outer call is recording into. ImGui runs platform-only under Vulkan so the editor's ImGui-touching update code can run at all. Two live-only defects the 45 offscreen tenants structurally could NOT see, because every tenant reads its output back in the same order it wrote it so a uniform flip cancels: - Vertical flip — the swapchain is the first asymmetric consumer. Fixed with a negative-height viewport confined to backbuffer draws (only fullscreen blits with culling off ever target it), now pinned by the tenant's mirror contract. - Extent — FinalRenderPass sizes from the GRAPH's spec, which the editor shrinks to its viewport panel; a swapchain image is undefined outside what the frame writes, so the frame presented in a corner with garbage bands. The acquired image's extent is now the authority. Backend: the real fence family on GpuFence timeline semaphores (FrameResourceManager::EndFrame was failing EVERY frame); blendEnable forced false on integer attachments (the R32_SINT entity-ID target, VUID-04727); transfer-layout source-scope widening in IssueBarrierBatch (a subresource in a TRANSFER layout was put there by a clear/copy the graph never declared — the WAW/WAR hazards); shaderInt64 enabled and the int64-atomics capability no longer over-promising; VulkanTextureCubemap (without it Renderer3D::Init asserted in the IBL system). Backend-blindness fixes, each an AV on null glad pointers: GPUPassTimerPool, GPUTimerQueryPool, GLStateGuard::Capture, StatisticsPanel's per-frame glGetString (the first thing to crash a Vulkan session), EditorLayer's entity picking, Renderer::Shutdown's GL framebuffer teardown. Compressed textures and cubemaps degrade to warn-once null instead of asserting, and the editor no longer tries to open "--rhi=vulkan" as a project file. Also lands the compute bare-uniform sweep: 101 bare uniforms across 13 .comp shaders migrated to std140 blocks (particles, wind, snow, terrain erosion, light culling, virtual-geometry cull/raster/debug, instance cull), each system's siblings sharing one block declared verbatim, with a GPU compile probe AND a headless text ratchet so the debt cannot return. Two GL contract tests that drove those uniforms directly were migrated with the engine. Gates: 46/46 VulkanPassSuite, 82/82 Vulkan suites, full suite 5632 passed with exactly the two documented pre-existing reds. Known remaining, live: materials render untextured (the slot-path material binds never reach the backend — CommandDispatch::DrawMesh's material half; note the "no heap descriptor" warning is a FALSE ALARM on Vulkan, it fires whenever the heap path merely is not live), no skybox/IBL (cubemap CPU face upload unimplemented), no ImGui UI (imgui_impl_vulkan is Phase 8), entity picking GL-only, fluid intermediates blocked on the raw-FBO stub family. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ADR 0011 amendments (57)-(69) from the pass-suite port: (57) a ported pass is not proven by "it ran" — the analytic tenant contract and the instrument set, each instrument existing because it once separated two indistinguishable failures. (58) port the facade's DEFAULT-ARGUMENT semantics, not just signatures. (59) the two-flavour projection seam — the full flip makes Vulkan's stored depth EQUAL GL's, so depth-reconstruction shaders need a row-flip-only form with inverses recomputed from the flipped matrix. (60) one descriptor set collapses GL's separate binding namespaces; the per-SHADER collision test; the reserved 57/63 pull pair. (61) bare uniforms cannot enter SPIR-V and the no-op setter makes it silent; the persistent-SSBO NaN amplifier; per-dispatch UBO uploads. (62) the graph's own contracts bite first (materialization gate, m_Enabled default, bind every declared binding). (63) the layout transitions the graph never emits; attachment writes lower to READ|WRITE; aspect-aware fan-out; the mid-pass visibility seam. (64) backend-blind factories and guards are the port's quiet failures; Recreate-does-not-Init. (65) core device features are discovered per shader family. (66) a descriptor mapping must name its resource KIND. (67) only a window can prove orientation and extent — the offscreen tenant is structurally blind to both; the engine presents during startup. (68) a diagnostic that cannot distinguish two states will misdiagnose one of them (the "no heap descriptor" warning's false alarm). (69) what Phase 7 leaves for Phase 8, with reasons. rhi-abstraction-boundary.md gains section 9: the five ways the boundary leaked under a second backend — the factory/guard leak no include scan can see, facade sentinel semantics, GL's namespace freedom, implicit synchronisation, the growing feature list — and what the port surfaced in the GL path itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 124 files, which is 24 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (66)
📒 Files selected for processing (124)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…e 7 port (#691) Master's distance-impostor reflection probes landed while this branch was in flight. One real conflict and two defects it exposed: - **Binding collision.** #705 claimed `UBO_REFLECTION_PROBES = 58`; this branch had independently claimed 58 for the auto-exposure block. Two different blocks, same number, developed in parallel — exactly what one Vulkan descriptor set makes fatal and GL's separate namespaces hide. Master's keeps 58 (already landed); auto-exposure moves to 72, with its two AutoExposure*.comp twins and the GL-limit assert moved with it. Every namespace re-audited: no duplicates, and TEX_SHADER_GRAPH_0's "above every engine slot" invariant still holds. - **`imageCubeArray` was never enabled.** The probe radiance chains are a cube ARRAY, so their shaders declare the SampledCubeArray SPIR-V capability and `vkCreateShaderModule` refused the module — taking out the DeferredLighting and Shadow tenants. Amendment (65) recurring from a new direction: the device-feature list grows with each shader family, this time one arriving from master rather than from a new Vulkan pass. - **`VulkanBindingState`'s mirrors were fixed at 64 entries**, so a binding at or above that is dropped and its lookup answers null — the shader then reads a zero address or the reserved null and renders wrong, not loudly. Auto-exposure at 72 hit it (the metering computes silently never ran), and it exposed a LATENT case: the earlier binding-57 renumber had already put TEX_DDGI_VISIBILITY at 64 and TEX_SHADER_GRAPH_0 at 65, both past the edge, invisible only because no tenant binds them. Capacities raised and tied to ShaderBindingLayout's own constants by static_assert, so the next binding that outgrows a mirror is a compile error instead of a black frame. Gates after the merge: 82/82 Vulkan suites; full suite 5687 passed with exactly the two documented pre-existing reds (#754 Atmosphere, and the GTAO history latch now filed as #771). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mers, storage-image layouts (#691) Six-lens self-review of PR #769 (CodeRabbit cannot review a diff this size). Eight blocker-class defects, verified: Vulkan 82/82, full suite 5687 passed with only the two known pre-existing reds (#754, #771). GL path (not Vulkan-specific): - Restore AudioEngine::Shutdown() in ~Application(). It was dropped while moving Renderer::Shutdown() out of the GL guard and survived only on the constructor's catch(...), so a normal exit never joined the audio thread before StopWorkers() ran. - GLStateGuard::kUboSlots named a hand-picked constant and had drifted twice; 13 UBO bindings were invisible to the leak detector. Derive it instead, from a single ShaderBindingLayout::UBO_BINDING_LIMIT. Projection seam — six missed consumers. The A8 sweep enumerated matrices by WRITER; the seam is defined by how a value is READ, so consumers living outside the CameraUBO writers were missed: - shadow SAMPLING matrices (render half had the seam, sampling half did not) - decal inverse-VP, planar-reflection lookup, debug-draw VP - HZB reprojection, applied at the upload boundary because three producers feed one field and a per-producer flip would double-apply - sky/IBL cube bakes moved to the capture flavour (dormant, but correct) Vulkan backend: - Storage images were never transitioned to GENERAL: BindImageTexture baked GENERAL into its descriptor and emitted no barrier, so pass-owned images reached their first imageStore in UNDEFINED. BindTexture no longer skips GENERAL/UNDEFINED runs either, now that storage binds transition back. - Depth-array view cache was keyed on a raw VkImage with no registration stamp and freed only in ~VulkanFramebuffer, so a recycled handle served a dead view. Views now retire in the reclaim pass, destroyed inline (an enqueue there would reallocate the vector DestroyEntry iterates). - A UBO binding could resolve to the SSBO at the same number; kind-guard it. - SubImage read a hardcoded oldLayout and recorded a layout on failure. - ForgetImage had zero callers: one layout row per image, forever. - Descriptor-heap slots were released ~2 frames early in three texture destructors, against the contract their own header states. Test honesty: - VolumetricFog hand-supplied the transitions production omitted, so it could not fail; removing them makes it exercise the production path. - Validation-error gate read its counter before device teardown, so it could never see the object-leak reports a tenant relies on. - Shared harness omitted the zero-stub instrument ADR (57) requires. - 11 new std140 blocks were absent from kKnownBlocks, so the GLSL-vs-C++ size test silently skipped all of them. - Bare-uniform ratchet matched only column-0 `uniform `. - Three tenants cited blockers this PR had already fixed. Docs: ADR 0011 (59) corrected (capture faces cannot compensate at the face bases; winding translation is the identity) and #691's body records the three carry-overs for Phase 8a. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|




Closes the port half of #691 Phase 7: the render-pass suite runs on Vulkan.
What this is
Every pass in
Renderer/Passes/(plusVirtualGeometryPassandDDGIProbeUpdatePass) now executes through the Vulkan backend, and each one ispinned by a device-gated tenant in
VulkanPassSuiteTestthat drives theunmodified pass body through the real render graph on the process-global
VulkanRendererAPI— transient declaration,BuildFrameGraph, plannerbarriers,
Execute.The Phase 6 checkpoint held at the first tenant and never moved:
FXAARenderPass— the pass Phase 6's pilot proved by hand — matches the same GL golden PNG
through the graph machinery that replaced that pilot, with zero validation
errors and sync validation on.
The method, because it is what found the bugs
A tenant does not assert "it ran". It asserts the pass's defining property
analytically: the vignette darkens corners; chromatic aberration splits channels
at an off-centre edge (the effect is zero at the centre — an on-centre probe
passes for a broken implementation); an identity LUT is a byte-exact
passthrough; DOF's focus gates the blur in both directions; the contact
shadow darkens the crease while the near side of the depth step stays lit;
TAA's two-frame history round trip lands the 0.1/0.9 blend; deferred lighting
matches a CPU mirror of
PBRCommonterm for term; a masked decal RT is provenuntouched in-frame by comparing footprint against non-footprint texels of
the same attachment.
That bar turned a long list of silent failures into named ones. Each fixture
also carries the instrument set that separated them: seam probes, the graph's
resolve-failure list, prepared/dropped draw counters, the transient-plan dump,
intermediate readbacks, a zero-stub assertion, and
GetValidationErrorCount() == 0in
TearDown.Backend bugs this found and fixed
Roughly twenty, of which the ones worth reading:
DrawIndexed(va)with count 0 drew nothing. The GL facade's contract is"0 = the whole index buffer" and every pass body uses that form; Vulkan passed
0 straight into
vkCmdDrawIndexed— a legal empty draw. No error, no warning.Port the default-argument semantics, not just the signature.
Clear()with a stale rendering scope cleared the previous target (thelazy scope switches at the next draw), so a mid-pass
Bind(); Clear()wipedwhat the pass had just drawn.
Executejust rendered had no barrier and no layout transition.
framebuffer-kind write to an image barrier, so first uses rendered into
UNDEFINED-layout images.across every attachment including depth, documenting that the backend
derives the aspect — the lowering did not, and emitted colour layouts on depth
images (then poisoned the tracker so the next scope-open failed inversely).
ALLresource masks, which fails pipelinecreation outright for any shader with a sampler and a storage image at the
same binding number (the froxel-fog pair was the first).
Texture3D::Create/Texture2DArray::Createhad no backend switch —they built the GL class unconditionally, i.e. a null
gladpointer in aGL-less process.
GLStateGuardfaulted the same way.independentBlendwas never enabled, which made the entire per-attachmentblend/mask facade family undefined behaviour (retroactively including the
WB-OIT work that was already "green").
#includeresolver used the wrong base directory, so any.compwith a relative include silently failed to compile.(demote-to-helper-invocation, vertex/fragment stores, draw parameters,
draw-indirect-count, multi-draw-indirect, tessellation), all enabled
when supported.
The conventions this establishes
the 20-byte fullscreen triangle, the 32-byte engine
Vertex, the 8-byteparticle/fluid quad, foliage and particle instance streams, the skinned bone
stream, and the two shaders whose vertex stages were never standard.
GL gives those separate namespaces and one Vulkan descriptor set does not.
Renumbered, with 57/63 now reserved and documented as the vertex-pull pair
(stream 0 / stream 1).
negative viewport — and it has two flavours: the full flip for matrices a
rasterizer consumes, and a row-flip-only form (with inverses recomputed from
the flipped matrix) for matrices shader math consumes, because the full flip
makes Vulkan's stored depth equal GL's and composing the inverse would
double-apply the z remap. One helper, identity on GL; the front-face flip
composes in the pipeline builder so passes with their own local winding flip
still work.
and
ComputeShader::Set*is a no-op on Vulkan — the values would have readzero and, for auto-exposure, latched a NaN into a persistent SSBO forever).
Pre-existing defects this surfaced (filed separately)
A backend port is an audit. Three of these are GL bugs, not Vulkan ones:
doesn't match the mode's draw-attachment map) — pinned as observed behaviour
so the fix fails loudly.
R32Fcolour framebuffer format exists, so the upscaled depth/velocitytarget silently loses its first attachment on the FSR1 path.
invalidation and can latch a zero attractor.
Verification
VulkanPassSuite: 46/46 tenants, zero validation errors (sync validation on).AtmosphereVisualEvidenceTest(pre-existing AtmosphereVisualEvidenceTest: NightClear/NightOvercast goldens drift (RMSE 13.08 / 9.40 vs threshold 8) — permanently red #754) and the GTAO-history latchabove (proven pre-existing: it passes with the committed baseline test set on
the same binary).
--rhi=vulkan— lit,shaded 3D geometry, right way up, filling the window, zero sync hazards,
zero validation errors, zero fence failures. Verified by OS-level screenshot
against an OpenGL capture of the same scene; MCP capture stays Vulkan-blind
until Phase 8, as Phase 4's amendments recorded.
Docs
docs/agent-rules/rhi-abstraction-boundary.md§9: the five ways the boundaryleaked under a second backend, and what the port surfaced in the GL path.
Still open (recorded, not silently skipped)
The raw texture/FBO facade family, explicit per-binding sampler state,
mid-pass
GENERALstore-then-sample, and the heap path's visibility seam —each with a tenant or a comment that fails the moment it is implemented.
Merging master's #705 mid-flight found two more
Master's distance-impostor reflection probes landed while this was in
flight, and the integration was itself informative:
UBO_REFLECTION_PROBES = 58;this branch had independently taken 58 for auto-exposure. Two different
blocks, same number, developed in parallel — invisible on OpenGL, fatal
once one Vulkan descriptor set collapses the namespaces. Master's keeps
58; auto-exposure moved to 72.
VulkanBindingState's mirrors were fixed at 64 entries, so anybinding at or above that was dropped and its lookup answered null — a
wrong render, not a loud failure. Auto-exposure at 72 hit it, and that
exposed a latent case: the earlier renumber had already pushed
TEX_DDGI_VISIBILITYto 64 andTEX_SHADER_GRAPH_0to 65, both past theedge, unnoticed only because no tenant binds them. The capacities are now
tied to
ShaderBindingLayout's constants bystatic_assert, so the nextbinding that outgrows a mirror is a compile error rather than a black
frame.
imageCubeArray, which the probe radiance chains need and nothinghad enabled — amendment (65)'s "the feature list grows per shader family"
arriving from master rather than from a new Vulkan pass.