Skip to content

feat(rhi): Phase 7 — port the full render-pass suite to Vulkan (#691) - #769

Open
drsnuggles8 wants to merge 20 commits into
masterfrom
feature/rhi-phase7-vulkan-pass-suite-691
Open

feat(rhi): Phase 7 — port the full render-pass suite to Vulkan (#691)#769
drsnuggles8 wants to merge 20 commits into
masterfrom
feature/rhi-phase7-vulkan-pass-suite-691

Conversation

@drsnuggles8

@drsnuggles8 drsnuggles8 commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Closes the port half of #691 Phase 7: the render-pass suite runs on Vulkan.

What this is

Every pass in Renderer/Passes/ (plus VirtualGeometryPass and
DDGIProbeUpdatePass) now executes through the Vulkan backend, and each one is
pinned by a device-gated tenant in VulkanPassSuiteTest that drives the
unmodified pass body through the real render graph on the process-global
VulkanRendererAPI — transient declaration, BuildFrameGraph, planner
barriers, 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 PBRCommon term for term; a masked decal RT is proven
untouched 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() == 0
in 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 (the
    lazy scope switches at the next draw), so a mid-pass Bind(); Clear() wiped
    what the pass had just drawn.
  • No mid-pass visibility seam: sampling an attachment the same Execute
    just rendered had no barrier and no layout transition.
  • Attachment transitions the graph never emits: the planner does not lower a
    framebuffer-kind write to an image barrier, so first uses rendered into
    UNDEFINED-layout images.
  • Aspect-blind attachment lowering: the graph fans one access prototype
    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).
  • Descriptor mappings emitted ALL resource masks, which fails pipeline
    creation 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::Create had no backend switch
    they built the GL class unconditionally, i.e. a null glad pointer in a
    GL-less process. GLStateGuard faulted the same way.
  • independentBlend was never enabled, which made the entire per-attachment
    blend/mask facade family undefined behaviour (retroactively including the
    WB-OIT work that was already "green").
  • The compute #include resolver used the wrong base directory, so any
    .comp with a relative include silently failed to compile.
  • Plus the core device features each shader family needed
    (demote-to-helper-invocation, vertex/fragment stores, draw parameters,
    draw-indirect-count, multi-draw-indirect, tessellation), all enabled
    when supported.

The conventions this establishes

  • Vertex pulling (ADR 0011 §5) now covers every vertex shape in the engine:
    the 20-byte fullscreen triangle, the 32-byte engine Vertex, the 8-byte
    particle/fluid quad, foliage and particle instance streams, the skinned bone
    stream, and the two shaders whose vertex stages were never standard.
  • Binding 57 was triple-booked (a UBO, a sampler, and the pull SSBO) because
    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).
  • The Y-flip decision (§5's deferred item) is a projection-seam flip, not a
    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.
  • Bare compute uniforms migrated to UBO blocks (they cannot enter SPIR-V,
    and ComputeShader::Set* is a no-op on Vulkan — the values would have read
    zero 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:

  • Normal-mode decals write nothing on either backend (shader output location
    doesn't match the mode's draw-attachment map) — pinned as observed behaviour
    so the fix fails loudly.
  • No R32F colour framebuffer format exists, so the upscaled depth/velocity
    target silently loses its first attachment on the FSR1 path.
  • GTAO's history/AO target survives a resolution-band resize without
    invalidation and can latch a zero attractor.

Verification

Docs

  • ADR 0011 amendments (57)–(69).
  • docs/agent-rules/rhi-abstraction-boundary.md §9: the five ways the boundary
    leaked 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 GENERAL store-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:

  • A real binding collision. Renderer: distance-impostor reflection probes — raymarched probe reflections with real depth #705 took 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 any
    binding 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_VISIBILITY to 64 and TEX_SHADER_GRAPH_0 to 65, both past the
    edge, unnoticed only because no tenant binds them. The capacities are now
    tied to ShaderBindingLayout's constants by static_assert, so the next
    binding that outgrows a mirror is a compile error rather than a black
    frame.
  • Plus imageCubeArray, which the probe radiance chains need and nothing
    had enabled — amendment (65)'s "the feature list grows per shader family"
    arriving from master rather than from a new Vulkan pass.

drsnuggles8 and others added 18 commits August 9, 2026 18:26
…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>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too 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 configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2c680b93-043c-4695-99d5-3e51a986aba4

📥 Commits

Reviewing files that changed from the base of the PR and between eba6173 and 044217c.

⛔ Files ignored due to path filters (66)
  • OloEditor/assets/shaders/DebugGBuffer_RMA.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Decal.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Decal_GBuffer.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Decal_GBuffer_Emissive.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Decal_GBuffer_Normal.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Decal_GBuffer_RMA.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Decal_OIT.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/DeferredLighting.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/DeferredLighting_MSAA.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/DepthPrepass.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/DepthPrepass_Mask.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/DepthPrepass_MaskSkinned.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/DepthPrepass_Skinned.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/FluidComposite.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/FluidDepthSplat.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/FluidThickness.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Foliage_Depth.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Foliage_Impostor.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Foliage_Instance.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Foliage_Instance_GBuffer.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/FullscreenBlit.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/JumpFlood_Composite.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/JumpFlood_Init.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/JumpFlood_Pass.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/OIT_Resolve.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/OcclusionProxy.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PBR_GBuffer.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PBR_GBuffer_Skinned.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PBR_MultiLight.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PBR_MultiLight_Skinned.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Particle_Billboard.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Particle_Billboard_GPU.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Particle_Billboard_OIT.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Particle_Trail.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_BloomComposite.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_BloomDownsample.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_BloomThreshold.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_BloomUpsample.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_ChromaticAberration.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_Cloudscape.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_CloudscapeComposite.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_CloudscapeResolve.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_ColorGrading.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_ContactShadow.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_DOF.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_EASU.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_Fog.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_FogUpsample.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_MotionBlur.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_OverdrawHeatmap.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_Precipitation.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_SSAOApply.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_SSGI.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_SSR.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_TAA.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_ToneMap.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_UpscaleDepthVelocity.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/PostProcess_Vignette.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/SSAO.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/SSAO_Blur.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/SSS_Blur.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/ShadowDepth.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/ShadowDepthSkinned.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/Water.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/include/BindlessHeap.glsl is excluded by !**/*.glsl
  • OloEditor/assets/shaders/include/DDGICommon.glsl is excluded by !**/*.glsl
📒 Files selected for processing (124)
  • OloEditor/assets/shaders/compute/AutoExposureAverage.comp
  • OloEditor/assets/shaders/compute/AutoExposureHistogram.comp
  • OloEditor/assets/shaders/compute/GTAO_Denoise.comp
  • OloEditor/assets/shaders/compute/HZB.comp
  • OloEditor/assets/shaders/compute/InstanceFrustumCull.comp
  • OloEditor/assets/shaders/compute/InstanceOcclusionCull.comp
  • OloEditor/assets/shaders/compute/LightCulling.comp
  • OloEditor/assets/shaders/compute/Particle_Compact.comp
  • OloEditor/assets/shaders/compute/Particle_Emit.comp
  • OloEditor/assets/shaders/compute/Particle_Simulate.comp
  • OloEditor/assets/shaders/compute/Snow_Accumulate.comp
  • OloEditor/assets/shaders/compute/Snow_Deform.comp
  • OloEditor/assets/shaders/compute/Terrain_Erosion.comp
  • OloEditor/assets/shaders/compute/VirtualClusterCull.comp
  • OloEditor/assets/shaders/compute/VirtualClusterRaster.comp
  • OloEditor/assets/shaders/compute/VirtualDebugColorize.comp
  • OloEditor/assets/shaders/compute/Wind_Generate.comp
  • OloEditor/src/EditorLayer.cpp
  • OloEditor/src/OloEditorApp.cpp
  • OloEditor/src/Panels/StatisticsPanel.cpp
  • OloEngine/src/CMakeLists.txt
  • OloEngine/src/OloEngine/Core/Application.cpp
  • OloEngine/src/OloEngine/Core/Application.h
  • OloEngine/src/OloEngine/Core/Window.h
  • OloEngine/src/OloEngine/ImGui/ImGuiLayer.cpp
  • OloEngine/src/OloEngine/Particle/GPUParticleSystem.cpp
  • OloEngine/src/OloEngine/Particle/GPUParticleSystem.h
  • OloEngine/src/OloEngine/Renderer/Commands/CommandDispatch.cpp
  • OloEngine/src/OloEngine/Renderer/ComputeShader.cpp
  • OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp
  • OloEngine/src/OloEngine/Renderer/Debug/GLStateGuard.cpp
  • OloEngine/src/OloEngine/Renderer/Debug/GLStateGuard.h
  • OloEngine/src/OloEngine/Renderer/Debug/GPUPassTimerPool.cpp
  • OloEngine/src/OloEngine/Renderer/Debug/GPUTimerQueryPool.cpp
  • OloEngine/src/OloEngine/Renderer/GraphicsContext.h
  • OloEngine/src/OloEngine/Renderer/HZBGenerator.cpp
  • OloEngine/src/OloEngine/Renderer/HZBGenerator.h
  • OloEngine/src/OloEngine/Renderer/IBLPrecompute.cpp
  • OloEngine/src/OloEngine/Renderer/IndexBuffer.cpp
  • OloEngine/src/OloEngine/Renderer/Instancing/GPUFrustumCuller.cpp
  • OloEngine/src/OloEngine/Renderer/Instancing/GPUFrustumCuller.h
  • OloEngine/src/OloEngine/Renderer/LightCulling/LightCullingPass.cpp
  • OloEngine/src/OloEngine/Renderer/LightCulling/LightCullingPass.h
  • OloEngine/src/OloEngine/Renderer/MeshPrimitives.cpp
  • OloEngine/src/OloEngine/Renderer/Passes/FogRenderPass.cpp
  • OloEngine/src/OloEngine/Renderer/Passes/GTAORenderPass.cpp
  • OloEngine/src/OloEngine/Renderer/Passes/GTAORenderPass.h
  • OloEngine/src/OloEngine/Renderer/Passes/PlanarReflectionRenderPass.cpp
  • OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.cpp
  • OloEngine/src/OloEngine/Renderer/Passes/ToneMapRenderPass.cpp
  • OloEngine/src/OloEngine/Renderer/Passes/ToneMapRenderPass.h
  • OloEngine/src/OloEngine/Renderer/Passes/VolumetricFogPass.cpp
  • OloEngine/src/OloEngine/Renderer/Preview/AssetPreviewRenderer.cpp
  • OloEngine/src/OloEngine/Renderer/RHI/RHIProjectionSeam.cpp
  • OloEngine/src/OloEngine/Renderer/RHI/RHIProjectionSeam.h
  • OloEngine/src/OloEngine/Renderer/RenderPipeline.cpp
  • OloEngine/src/OloEngine/Renderer/Renderer.cpp
  • OloEngine/src/OloEngine/Renderer/Renderer3DSpecializedDraws.cpp
  • OloEngine/src/OloEngine/Renderer/ShaderBindingLayout.h
  • OloEngine/src/OloEngine/Renderer/Shadow/ShadowMap.cpp
  • OloEngine/src/OloEngine/Renderer/SkyCubemapBake.cpp
  • OloEngine/src/OloEngine/Renderer/Texture.cpp
  • OloEngine/src/OloEngine/Renderer/Texture3D.cpp
  • OloEngine/src/OloEngine/Renderer/TextureCubemap.cpp
  • OloEngine/src/OloEngine/Renderer/UniformBuffer.cpp
  • OloEngine/src/OloEngine/Renderer/VertexArray.cpp
  • OloEngine/src/OloEngine/Renderer/VertexBuffer.cpp
  • OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryPass.cpp
  • OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryPass.h
  • OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryShadow.cpp
  • OloEngine/src/OloEngine/Snow/SnowAccumulationSystem.cpp
  • OloEngine/src/OloEngine/Snow/SnowAccumulationSystem.h
  • OloEngine/src/OloEngine/Terrain/Editor/TerrainErosion.cpp
  • OloEngine/src/OloEngine/Terrain/Editor/TerrainErosion.h
  • OloEngine/src/OloEngine/Wind/WindSystem.cpp
  • OloEngine/src/OloEngine/Wind/WindSystem.h
  • OloEngine/src/Platform/Linux/LinuxWindow.h
  • OloEngine/src/Platform/OpenGL/OpenGLTexture2DArray.cpp
  • OloEngine/src/Platform/OpenGL/OpenGLTexture3D.cpp
  • OloEngine/src/Platform/Vulkan/VulkanBarrierLowering.cpp
  • OloEngine/src/Platform/Vulkan/VulkanBarrierLowering.h
  • OloEngine/src/Platform/Vulkan/VulkanBindingState.cpp
  • OloEngine/src/Platform/Vulkan/VulkanBindingState.h
  • OloEngine/src/Platform/Vulkan/VulkanBufferResources.cpp
  • OloEngine/src/Platform/Vulkan/VulkanBufferResources.h
  • OloEngine/src/Platform/Vulkan/VulkanComputeShader.cpp
  • OloEngine/src/Platform/Vulkan/VulkanComputeShader.h
  • OloEngine/src/Platform/Vulkan/VulkanContext.cpp
  • OloEngine/src/Platform/Vulkan/VulkanContext.h
  • OloEngine/src/Platform/Vulkan/VulkanDescriptorHeapBackend.cpp
  • OloEngine/src/Platform/Vulkan/VulkanDescriptorHeapBackend.h
  • OloEngine/src/Platform/Vulkan/VulkanDescriptorSlotCache.cpp
  • OloEngine/src/Platform/Vulkan/VulkanDescriptorSlotCache.h
  • OloEngine/src/Platform/Vulkan/VulkanDevice.cpp
  • OloEngine/src/Platform/Vulkan/VulkanDevice.h
  • OloEngine/src/Platform/Vulkan/VulkanFrameArena.cpp
  • OloEngine/src/Platform/Vulkan/VulkanFrameArena.h
  • OloEngine/src/Platform/Vulkan/VulkanImageLayoutTracker.cpp
  • OloEngine/src/Platform/Vulkan/VulkanImageLayoutTracker.h
  • OloEngine/src/Platform/Vulkan/VulkanOneShot.cpp
  • OloEngine/src/Platform/Vulkan/VulkanOneShot.h
  • OloEngine/src/Platform/Vulkan/VulkanPipelineBuilder.cpp
  • OloEngine/src/Platform/Vulkan/VulkanPipelineBuilder.h
  • OloEngine/src/Platform/Vulkan/VulkanRendererAPI.cpp
  • OloEngine/src/Platform/Vulkan/VulkanRendererAPI.h
  • OloEngine/src/Platform/Vulkan/VulkanResourceHeap.cpp
  • OloEngine/src/Platform/Vulkan/VulkanResourceHeap.h
  • OloEngine/src/Platform/Vulkan/VulkanShader.cpp
  • OloEngine/src/Platform/Vulkan/VulkanShader.h
  • OloEngine/src/Platform/Vulkan/VulkanTransientResources.cpp
  • OloEngine/src/Platform/Vulkan/VulkanTransientResources.h
  • OloEngine/src/Platform/Windows/WindowsWindow.h
  • OloEngine/tests/CMakeLists.txt
  • OloEngine/tests/Rendering/PropertyTests/BindlessHeapGpuTest.cpp
  • OloEngine/tests/Rendering/PropertyTests/GPUOcclusionCullGPUTest.cpp
  • OloEngine/tests/Rendering/ShaderUBOSizeConsistencyTest.cpp
  • OloEngine/tests/Rendering/VirtualClusterCullParityTest.cpp
  • OloEngine/tests/Rendering/VulkanBarrierLoweringTest.cpp
  • OloEngine/tests/Rendering/VulkanDrawPathTest.cpp
  • OloEngine/tests/Rendering/VulkanPassSuiteTest.cpp
  • OloEngine/tests/Rendering/VulkanResourceFactoryTest.cpp
  • OloEngine/tests/Rendering/rhi_boundary_baseline.json
  • docs/adr/0011-rhi-neutral-resource-and-binding-model.md
  • docs/agent-rules/rhi-abstraction-boundary.md

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…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>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
D Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant