Skip to content

RHI Phase 2 step 3: attachment consumers + the command-layer bind cache (#691) - #736

Merged
drsnuggles8 merged 6 commits into
masterfrom
feature/vulkan-rhi-phase2-attachment-handles-691
Aug 1, 2026
Merged

RHI Phase 2 step 3: attachment consumers + the command-layer bind cache (#691)#736
drsnuggles8 merged 6 commits into
masterfrom
feature/vulkan-rhi-phase2-attachment-handles-691

Conversation

@drsnuggles8

@drsnuggles8 drsnuggles8 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Continues issue #691 Phase 2 step 3, picking up from #732. #691 stays open — this is worklist items 1 and 2 of four; Phase 2's checkbox stays unticked.

What landed

Item 1 — framebuffer attachment consumers (74955a7c). All seven bakers, the straightforward bind passes (Fog, Overdraw, SelectionOutline, Cloudscape composite, Decal, Bloom), every attachment read in DDGIProbeUpdatePass, and RenderGraph's attachment clear + NaN census.

Item 2 — the command-layer bind cache (35a66251). using RendererID = u32 is deleted. The POD command structs, the redundant-bind cache, DepthPrepassShaderIDs, InstanceGroupKey and the draw sort keys all carry RHI::ResourceHandle.

Item 3 - the leftovers (c6960d4d). All eight sites slice 5 had deferred as "blocked on the transient pool". That blocker was never real; see below.

Two MCP fixes ride along (d2df533e, 5f9fce01) as separately-reviewable commits — see "extra scope" below.

Counters

Before After
sweep_renderer_id 699 347
facade_native_id_params 68 67
sweep_gl_calls / sweep_glad_includes / backend_resolve_hatch / debug_escape_hatch 0 0

Both lowered baselines are in the same commits that earned them. facade_native_id_params falling is the notable part: the obvious mixed-currency DrawElementsIndirectRaw(handle, u32 bufferID) overload would have pushed a never-rises ratchet to 69. Its one caller had already run BindVAOIfNeeded, so the draw was re-binding the VAO behind the redundant-bind cache's back — DrawBoundElementsIndirect(u32) replaced both u32 forms, removed the redundant bind, and netted −1.

The "blocked on the transient pool" claim was wrong

Flagging it because it was recorded as fact in the worklist and posted to #691 before anyone measured it. TransientPool::AcquireTexture returns a Ref<Texture2D>, which has minted handles since slice 2; AcquiredInfo::RendererID is a diagnostics field with no role in resolution. The real constraint was one planner line that never set .Handle, and behind it a design invariant rather than a missing producer: PhysicalTexture documented TextureID and Handle as "ALTERNATIVES... exactly one is set".

That is right for an import - an importer only ever has one currency, and neither is derivable from the other. It was never true of a transient: the planner holds the pooled Ref, so it has both in hand and reads them off one pointer in one statement. Setting both unblocked all eight sites.

Generalisable: when a migration says "blocked on X", check whether X is a missing capability or an invariant someone wrote down. The first is work; the second is a decision that can be revisited once you know which case it was written for.

Five correctness fixes, not just type changes

  1. InstanceGroupKey batched draws by VAO GL name. A delete/create pair can hand two objects the same name, merging unrelated draws into one batch — one mesh rendered with another's geometry.
  2. DepthPrepassShaderIDs compared programs by GL name to decide whether a material's shader may be swapped for the depth-only one. A relinked program can inherit a freed name across a hot reload.
  3. The DDGI, shadow and IBL cache fingerprints hashed raw ids. EnsureResources / SetSettings destroy before recreating, so GL may reissue the freed names and the fingerprint sees no change — the graph then keeps an import describing the old resolution.
  4. ImportTextureHandle blinds RenderGraph::ResolveTexture, which every MCP capture endpoint reads. feat(rhi): generation-checked RHI::ResourceHandle — the mint and the first migrations (#691 Phase 2 step 3) #732 had already done this to SSAO's noise texture, so olo_render_capture_target SSAONoise cannot work on master.
  5. measure_rendererid.py hard-coded a deleted worktree path and reported a confident TOTAL 0 across 0 files from anywhere else.

A doc claim this falsified

rhi-abstraction-boundary.md said keying the cache on identities makes the Invalidate* calls "a pure optimisation". That is wrong in the direction that ships bugs, and is corrected in this PR rather than left standing. The recycled-name collision does die — but an in-place reload deliberately preserves the identity while replacing the storage, so the cache holds the very handle being rebound, concludes "already bound", and skips a bind that must happen. Under native-id keying that self-corrected because the name changed. Every site recreating a texture's storage must now call InvalidateTextureBinding.

Extra scope, flagged deliberately

  • The two MCP commits fix a bug that predates this work (feat(rhi): generation-checked RHI::ResourceHandle — the mint and the first migrations (#691 Phase 2 step 3) #732's SSAO import). They're separate commits and can be dropped or split out if you'd rather.
  • Item 3's Renderer3D.h / Scene.cpp surface landed early, not by choice: deleting the alias is a type change in headers included everywhere, and Renderer3D.h alone used it in 38 declarations (a 4551-error parse cascade on the first build). Seven resource chains the worklist never mentions came with it for the same reason.

Verification

  • 5231 tests, 5227 passed, 0 failed, 4 environmental skips — matches baseline. Goldens with OLOENGINE_GOLDEN_REBASE unset.
  • 68 guard conversions machine-checked for inverted polarity (none found); all 10 .Index uses audited as reporting/bucketing keys rather than backend calls — the compiler accepts either spelling and only one is correct. I nearly shipped DrawIndexedPatchesRaw(vao.Index, …), which would have handed GL a registry slot number as a VAO name.
  • Evidence PNGs: 15 drifted across both slices, all reverted. Noise floors measured per image on the identical binary; a third sample showed run1-vs-run3 exceeding head-vs-run3 on the fluid images. OcclusionCull_Deferred differs by the same single pixel (177,190) in both slices — pre-existing drift.
  • Live editor: zero errors in OloEngine.log, no handle-resolution warnings, olo_render_validate clean (0 hazards, 0 resolve failures, 40/40 resolved), 8 render targets captured including the ShadowCSMRaw compare-off view the new handle path creates, 3 posed screenshots inspected (PBR materials, planar reflection, terrain, water all correct).

Still to do on #691

Items 1-3 are done. Item 4 remains, and it is the tail of the migration rather than a chunk after item 3 - a u32 form can only be deleted once its last caller is gone. The 67 remaining native-id parameters: 18 buffers (VertexBuffer/IndexBuffer expose no GetRHIHandle() at all), 16 textures, 13 framebuffers, 8 VAOs, 5 occlusion queries (no identity type exists - a new resource kind, not a migration).

Suggested order: buffer producers -> framebuffer call sites -> CloudNoise -> query identities -> then item 4 falls out.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved rendering reliability when textures, shadows, reflections, water, terrain, and other graphics resources are recreated or unavailable.
    • Prevented stale or incorrectly reused resources after scene or frame changes.
    • Improved handling of missing rendering assets with safer fallbacks.
  • Rendering Improvements

    • Updated draw, texture-copy, readback, and shadow-rendering operations for more consistent resource tracking.
    • Enhanced diagnostics for identifying texture and framebuffer issues.
  • Tests

    • Expanded coverage for resource recreation, render-graph identity resolution, water textures, and command batching.

drsnuggles8 and others added 4 commits August 1, 2026 13:17
…#691)

Phase 2 step 3, worklist item 1. The framebuffer attachment getters were the
highest-yield migration unit: the producers (GetColorAttachmentHandle /
GetDepthAttachmentHandle) already shipped in #732, so consumers could move
without a new producer.

Migrated: all seven bakers (thumbnail, light probe, reflection probe, IBL,
impostor, sky cubemap, asset preview), the straightforward bind passes (Fog,
Overdraw, SelectionOutline, Cloudscape composite, Decal, Bloom), every
attachment read in DDGIProbeUpdatePass including the SetAtlasTextureParams
signature, and RenderGraph's attachment clear + NaN-census readback.

The worklist predicted "no new facade surface needed"; that was wrong. Five
virtuals were required - CopyImageSubData, CopyImageSubDataFull,
ClearTextureFloat, ReadTextureImage, ReadTextureSubImage - because the survey
behind that prediction counted only BIND sinks, while the attachment getters
also feed a copy family (bakers staging an attachment into a persistent
texture) and a readback family (probe capture). Same lesson as the previous
slice, one level up: migrate one real consumer PER SINK FAMILY and let the
compiler enumerate the rest.

Two silent defects found and fixed:

1. RenderPipeline's DDGI cache fingerprint could not observe an atlas
   recreate. EnsureResources calls DestroyResources() BEFORE creating the
   replacements, so GL may reissue the freed texture names - under which
   hashing the raw id sees no change at all, BuildFrameGraph is never
   rebuilt, and the graph keeps an import whose Width/Height still describe
   the OLD resolution (what olo_render_list_targets then reports). Now hashes
   RHI::HashKey(handle), whose generation cannot be reissued. Pinned by
   RHIResourceRegistry.HashKeyDiffersAcrossADestroyRecreateThatReusesTheNativeName.
   Worth internalising: the opposite teardown order (allocate-then-release)
   hides this completely, so whether the bug is live depends on a line of
   teardown code nowhere near the hash.

2. measure_rendererid.py hard-coded the (now-deleted) worktree it was written
   in, and reported a confident "TOTAL 0 across 0 files" from anywhere else.
   Root is now derived from the script's own location.

Eight sites are deliberately left on the native currency, each because its
other operand cannot mint yet - a graph transient, a Renderer3D setter, or an
externally-registered raw id. The reason for each is tabulated in
docs/agent-rules/rhi-abstraction-boundary.md rather than left as an
unexplained gap. DDGI's importAtlas is among them: ImportTextureHandle blinds
RenderGraph::ResolveTexture, which the MCP capture endpoints read, so an
import may only migrate once the diagnostics can read a handle-imported
resource.

Counters: sweep_renderer_id 699 -> 653 (baseline lowered here).
facade_native_id_params deliberately unchanged at 68 - this slice adds handle
overloads and deletes no u32 form, which is item 4's job and the documented
order.

Verification: 5221 passed / 0 failed / 6 environmental skips (baseline
5220/0/6, +1 for the new test); goldens run with OLOENGINE_GOLDEN_REBASE
unset. Eight evidence PNGs drifted; each image's own run-to-run noise floor
was measured on the identical binary and every drift sat within it, except a
single pixel in OcclusionCull_Deferred_VisualEvidence (177,190) belonging to
a pass that calls none of the migrated entry points. All eight reverted, not
committed. Live editor renders correctly with zero errors in OloEngine.log
and no ResolveNativeAs warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#691)

RenderGraph::ImportTextureHandle stores the identity and leaves TextureID at
0 - textureID and identity are ALTERNATIVES on a PhysicalTexture, deliberately
so, because that is what makes AllocateTextureHandle's change detection
honest. The consequence was not noticed: ResolveTexture ends at
PhysicalTexture::TextureID, so it answers 0 for a handle-imported resource,
and every id the MCP render tools report goes through it -
olo_render_list_targets' GLTextureId, olo_render_validate's identity table,
and ResolveTargetTexture, which backs olo_render_capture_target.

So migrating a resource's import silently DELETES it from the diagnostics.
It does not fail, warn, or look any different from a resource that genuinely
has no backing - the capture just reports id 0. #732 already did this to
SSAO's noise texture when it migrated that chain.

This matters more than one broken probe: CLAUDE.md's rendering-verification
rule is enforced through these endpoints, so a slice that quietly blinds them
removes the check on itself. It is also a blocker - it is the reason the
attachment slice had to leave DDGI's importAtlas on the native currency.

The fix is a fallback, not a second resolver: try the native id, and when it
is 0 ask the identity and go through RHI::GetNativeHandleForDebug - the hatch
RHIResources.h documents for exactly this, naming "the MCP capture endpoints"
as a legitimate caller. It lives in OloEditor, which RHIBoundaryRatchetTest
does not scan (it walks OloEngine/src only), so debug_escape_hatch stays
honestly 0 rather than being waived.

Renderer3D gains ResolveFrameGraphTextureHandle as the by-name sibling of
ResolveFrameGraphTexture, since ResolveTargetTexture resolves by name.

NOT reproduced live, and worth being precise about: the sandbox scene uses
GTAO, so SSAONoise is not in its graph, and switching the AO technique over
MCP is refused by the write-consent gate (which cannot be unlocked
non-interactively - see docs/agent-rules/mcp-setter-based-field-registry.md).
The defect is established by construction plus three green tests covering the
individual legs - RenderGraph.HandleImportResolvesAsAnIdentityAndNotNatively
asserts ResolveTexture returns 0 for a handle import, and
RHIResourceRegistry.DebugEscapeHatchResolvesThroughTheSameRegistry covers the
hatch. The three-line composition has no test, because it lives in OloEditor,
which the test target does not link.

Verified live that the fallback does not disturb the normal path: captures of
SceneColor, SceneDepth, ShadowMapCSM, BloomMip2 and SceneColorTexture all
still resolve (texture-backed, framebuffer-backed, depth, array-layer and mip
views).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ble (#691)

The fallback added in the previous commit was correct and in the wrong place.
It lived inline in OloEditor/src/MCP/McpToolsRender.cpp, which OloEngine-Tests
does not link — so the COMPOSITION had no test, only its individual legs did.
That is the same configuration that let the original defect through, so fixing
the bug there re-armed the identical trap one layer out: deleting the fallback
would have been silently green everywhere.

It was also passing the ratchet for the wrong reason. debug_escape_hatch stayed
0 because RHIBoundaryRatchetTest walks OloEngine/src only and therefore could
not see the call — a counter reading clean without the property being proven,
which is exactly what the baseline's own commentary warns about elsewhere.

Now Debug::NativeTextureIdForDiagnostics in
Renderer/Debug/RenderGraphResourceIdentity.{h,cpp}. Renderer/Debug/ is the only
directory that satisfies both constraints at once: RHIResources.h already names
the introspection tools and the MCP endpoints they back as legitimate callers of
the hatch (so the counter stays 0 honestly), and it is inside the engine library
(so the test target can reach it). Deliberately NOT a RenderGraph member — that
would put the hatch inside Renderer/, where backend_resolve_hatch bans it at 0,
and moving that boundary is a decision on its own merits.

Three tests in RenderGraphTest pin it: the native currency still resolves (the
fallback is additive), a handle-imported resource the native path structurally
cannot see still reports its backing object, and 0 is returned only when there
genuinely is no backing — including for a RETIRED identity, which must not
report a name the driver may since have reissued.

Mutation-checked rather than assumed: removing the fallback line fails exactly
DiagnosticsResolveAHandleImportedResourceTheNativePathCannotSee and leaves the
other two green, so the guard is targeted rather than a blanket tripwire.

Suite: 5231 tests, 5227 passed, 0 failed, 4 skipped. The skip count fell from 6
because building OloEditor (which add_dependencies on OloRuntime) made both
binaries exist, so AppLaunchSmoke's editor and runtime cases stopped skipping
and ran green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 2 step 3, worklist item 2 — the currency itself, not one producer family.
`using RendererID = u32` is deleted; the POD command structs, the redundant-bind
cache (BoundTextures / BoundUBOs / CurrentBoundShader / CurrentBoundVAO and the
six per-frame shadow fields), DepthPrepassShaderIDs, InstanceGroupKey and the
draw sort keys all carry RHI::ResourceHandle now.

sweep_renderer_id 653 -> 355. For scale, item 1 moved it 46.

THREE CORRECTNESS FIXES, not just a type change:

1. InstanceGroupKey batched draws by VAO GL NAME. A delete/create pair can hand
   two objects the same name, so two unrelated draws could merge into one batch
   and render one mesh with another's geometry. Two live handles cannot collide.

2. DepthPrepassShaderIDs compared PROGRAMS by GL name to decide whether a
   material's shader may be swapped for the depth-only one. Across a shader hot
   reload a relinked program can inherit a freed name.

3. RenderPipeline's shadow and IBL fingerprints hashed raw ids.
   ShadowMap::SetSettings calls Shutdown() BEFORE Init() on a resolution change,
   so GL may reissue the freed names and the hash sees nothing — the same defect
   the DDGI atlases had in the previous slice.

THE DOC CLAIM THIS FALSIFIED, corrected here rather than left standing.
rhi-abstraction-boundary.md said keying the cache on identities makes the
Invalidate* calls "a pure optimisation". That is wrong, in the direction that
ships bugs. The recycled-name collision does die, but an IN-PLACE RELOAD
deliberately PRESERVES the identity while replacing the storage
(ScopedResourceHandle::Sync never retires — that is what makes caching a handle
safe). The cache then holds the very handle being rebound, concludes "already
bound", and skips a bind that must happen, leaving the unit pointing at a
deleted GL name. Under native-id keying that self-corrected because the name
changed. So every site that recreates a texture's storage MUST now call
InvalidateTextureBinding, and the doc says so.

SCOPE WAS BIGGER THAN THE WORKLIST PREDICTED, and the doc now records why: 253
errors across 16 files rather than "~180 concentrated in CommandDispatch.cpp"
(that file was ~40%). Deleting the alias is a TYPE change in headers included
everywhere — Renderer3D.h alone used it in 38 declarations and produced a
4551-error parse cascade — so item 3's Renderer3D.h/Scene.cpp surface lands
here. Seven unlisted resource chains came too, each feeding the cache:
CloudShadowMap, SnowAccumulationSystem, OceanFFTField, FoliageRenderer,
DepthPrepassShaderIDs, the global IBL maps, ShadowMap's compare-off views. Nine
new facade virtuals were needed against a prediction of zero.

facade_native_id_params 68 -> 67, and the fall rather than a rise is the
interesting part. DrawElementsIndirectRaw(vaoID, bufferID) needed a handle form;
the obvious mixed (handle, u32 bufferID) overload would have pushed a
never-rises ratchet to 69. Its one caller had already run BindVAOIfNeeded, so
the draw was re-binding the VAO behind the redundant-bind cache's back —
DrawBoundElementsIndirect(u32) replaces both u32 forms, removes the redundant
bind, and nets -1. When a migration seems to require raising a ratchet, that is
usually the call site's shape being wrong, not the ratchet.

Dual currency is kept where a consumer genuinely needs the native id: ShadowMap
and the global IBL maps expose both, because RenderPipeline imports them into
the render graph and DeferredLightingPass reads them back through
context.ResolveTexture, which answers 0 for a handle import. Migrating those
imports would have silently dropped IBL from the whole deferred path.

Verification: 5231 tests, 5227 passed, 0 failed, 4 environmental skips —
matching baseline. Goldens run with OLOENGINE_GOLDEN_REBASE unset. 68 guard
conversions machine-checked for inverted polarity (none) and all 10 .Index uses
audited as reporting/bucketing keys, never backend calls — the compiler accepts
either spelling and only one is correct. Seven evidence PNGs drifted; a third
sample showed run1-vs-run3 exceeding head-vs-run3 on the fluid images, and
OcclusionCull_Deferred differs by the same single pixel (177,190) as the
previous slice, so all were reverted. Live editor: zero errors in OloEngine.log,
no handle-resolution warnings, olo_render_validate clean (0 hazards, 0 resolve
failures, 40/40 resources resolved), 8 render targets captured including the
compare-off ShadowCSMRaw view that the new handle path creates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9778bf8c-331b-4a70-b5c7-45d36b229604

📥 Commits

Reviewing files that changed from the base of the PR and between c6960d4 and e08913e.

📒 Files selected for processing (15)
  • OloEditor/src/MCP/McpToolsRender.cpp
  • OloEngine/src/CMakeLists.txt
  • OloEngine/src/OloEngine/Renderer/CloudShadowMap.h
  • OloEngine/src/OloEngine/Renderer/Commands/RenderCommand.h
  • OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.h
  • OloEngine/src/OloEngine/Renderer/Debug/CommandPacketDebugger.cpp
  • OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.h
  • OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h
  • OloEngine/src/OloEngine/Renderer/Renderer3DSpecializedDraws.cpp
  • OloEngine/src/OloEngine/Renderer/SkyCubemapBake.cpp
  • OloEngine/src/OloEngine/Renderer/Texture.h
  • OloEngine/tests/Rendering/MockRendererAPI.h
  • OloEngine/tests/Rendering/RenderGraphFingerprintTest.cpp
  • OloEngine/tests/Rendering/rhi_boundary_baseline.json
  • docs/agent-rules/rhi-abstraction-boundary.md

📝 Walkthrough

Walkthrough

The renderer migration replaces legacy numeric resource identifiers with generation-checked RHI::ResourceHandle values across renderer APIs, resource consumers, render-graph diagnostics, cache fingerprints, OpenGL operations, tests, and documentation.

Changes

RHI resource handle migration

Layer / File(s) Summary
Resource contracts and facades
OloEngine/src/OloEngine/Renderer/...
Renderer command structures, APIs, resource accessors, and public state now use RHI::ResourceHandle.
Resource lifecycle and diagnostics
OloEngine/src/Platform/OpenGL/..., OloEngine/src/OloEngine/Renderer/Debug/...
OpenGL operations resolve handles, binding invalidation uses handles, and render-graph diagnostics fall back from native IDs to resource identities.
Renderer consumers
OloEngine/src/OloEngine/Renderer/..., OloEngine/src/OloEngine/Scene/Scene.cpp
Command dispatch, draw submission, render passes, shadows, DDGI, water, terrain, foliage, and scene integration validate and bind handles.
Validation and migration records
OloEngine/tests/..., docs/..., OloEditor/...
Tests use synthetic handles, validate stale-resource behavior and generational hashing, and update migration records and asset ordering.

Sequence Diagram(s)

sequenceDiagram
  participant Renderer3D
  participant CommandDispatch
  participant RendererAPI
  participant ResourceRegistry
  Renderer3D->>CommandDispatch: submit ResourceHandle draw resources
  CommandDispatch->>CommandDispatch: validate and cache handles
  CommandDispatch->>RendererAPI: issue handle-based draw or bind
  RendererAPI->>ResourceRegistry: resolve native resources
  ResourceRegistry-->>RendererAPI: native resource
Loading
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the RHI migration of attachment consumers and the command-layer bind cache, which are the primary changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

…nt (#691)

Migrates the eight sites slice 5 deferred as "blocked on the transient pool":
SSAO's resolve chain and blur->AO copy, SceneRenderPass's depth/normals/velocity
exports, GPUDrivenOcclusion's two re-exports, and Cloudscape's raymarch source
plus its history chain. sweep_renderer_id 355 -> 347.

THE BLOCKER WAS NEVER REAL, and how it came to be recorded as fact is the part
worth keeping. TransientPool::AcquireTexture returns a Ref<Texture2D>, which has
minted handles since slice 2; AcquiredInfo::RendererID is a diagnostics-report
field with no role in resolution. What actually stood in the way was one line in
the planner that never set `.Handle`, and behind it a DESIGN INVARIANT rather
than a missing producer: PhysicalTexture documented TextureID and Handle as
"ALTERNATIVES, exactly one is set per entry".

That rule is correct for an IMPORT — an importer only ever HAS one currency, and
neither is derivable from the other. It was never true of a TRANSIENT: the
planner holds the pooled Ref itself, so it has both in hand and reads them off
one pointer in one statement. Nothing is derived, so nothing can drift. The
contract comment now says which case it was written for instead of stating the
narrower rule as universal.

Generalisable: when a migration says "blocked on X", check whether X is a
missing CAPABILITY or an INVARIANT someone wrote down. The first is work; the
second is a decision that can be revisited once you know which case it covers.
Not checking cost a slice of imagined work here and put a false claim on #691
until it was corrected.

A real behavioural win falls out. Every one of these sites guards its copy with
`if (src != dst)`. Those comparisons are between OBJECTS now, so a recycled
driver name can no longer make a source and its export look identical and skip a
copy the frame needed — the same defect class as InstanceGroupKey, in four more
places.

Also adds GBuffer::GetColorAttachmentHandle / GetDepthAttachmentHandle, and
migrates CloudscapeRenderPass::SetHistory plus its RenderPipeline caller.

Verification: 5231 tests, 5226 passed, 0 failed, 4 environmental skips (the
ratchet failed only by IMPROVING; baseline lowered here). Goldens with
OLOENGINE_GOLDEN_REBASE unset. Six evidence PNGs drifted, all within their own
measured noise floor except OcclusionCull_Deferred, which differs from HEAD by
the SAME single pixel (177,190) as the two previous slices — meaningful negative
evidence, since this slice migrated GPUDrivenOcclusionPass itself and the output
did not move. All reverted. Live editor: zero errors in OloEngine.log, no
handle-resolution warnings, olo_render_validate clean (0 hazards, 0 resolve
failures, 40/40 resolved), and the five targets on the changed paths (AOBuffer,
SceneDepth, SceneViewNormals, Velocity, GTAODenoisePing) all capture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
coderabbitai[bot]

This comment was marked as resolved.

All nine review findings verified against current code; all nine were still
valid, so none were skipped. Seven were stale prose left behind by the
identity migration. Two were defects introduced by it:

* CommandPacketDebugger passed RHI::ResourceHandle (an 8-byte struct) through
  varargs to ImGui::Text("%u") at three sites. It happens to print the Index
  on little-endian, which is why it looked fine. Now "%s" + FormatHandle.
  The earlier pass converted the Markdown export sites and missed these.

* olo_material_get emitted formatted handle strings ("#3:1") into textureIds,
  whose published schema is "Bound GL texture id per slot ... 0 = none".
  That breaks every existing consumer and makes the values incomparable with
  olo_render_list_targets' numeric glTextureId. Restored via
  Debug::NativeTextureIdForDiagnostics rather than by rewriting the schema:
  the numeric contract is the one worth keeping.

Texture::operator== compared GetRendererID(), so two genuinely different
textures could compare equal once GL recycled a name -- the exact defect the
generation exists to prevent. The review offered "fix the code or weaken the
doc"; fixing the operator is one line and is the point of the phase. All three
callers are Renderer2D texture-slot dedup with both textures alive, where
handle equality is equivalent-or-stricter.

MockRendererAPI::Native() resolved untyped, so a wrong-family handle could
succeed in the mock and return 0 in the real backend -- a green mock test that
says nothing about the shipping path. Now kind-checked like the backend's
ResolveNativeAs, with all 26 adapters routed through the kind their operation
requires.

Also: three sort keys in Renderer3DSpecializedDraws still read
shader->GetRendererID() while the command stored the same shader as a handle;
a hoisted loop-invariant in SkyCubemapBake; four stale comment blocks; four
test messages.

sweep_renderer_id 347 -> 345, and the split demonstrates the counter's
documented UNDERCOUNT: Texture::operator== accounts for the whole -2, while
the three sort-key fixes net ZERO (shaderRendererID is still a `RendererID`
spelling). The sort-key fix is the more consequential of the two and the proxy
cannot see it.

Verified: full suite 5227 passed / 0 failed; OloEditor builds clean (it is the
only target that compiles the MCP change). Seven evidence PNGs regenerated and
reverted -- six sit at or below their measured run-to-run noise floor on an
identical binary. WorldOriginRebase_far_before was the one outlier worth
chasing: deterministic run-to-run (0 px) yet 1694 px vs HEAD. An isolated A/B
with only the sort key reverted proved that change pixel-identical (0/0), and
a full-suite control reproduced the same 1694/8 without it -- so the delta is
full-suite-vs-isolated test context, not this diff. Amplified 24x it is a
1-pixel horizon line and sparse dots on receding grid lines: thin-line
rasterization at 50 km, no geometry or lighting change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@drsnuggles8
drsnuggles8 merged commit 26d2fec into master Aug 1, 2026
4 of 8 checks passed
@drsnuggles8
drsnuggles8 deleted the feature/vulkan-rhi-phase2-attachment-handles-691 branch August 1, 2026 20:59
drsnuggles8 added a commit that referenced this pull request Aug 2, 2026
…validity

All seven findings verified against current code and applied. Notes on
the three that are more than they look, and the one suggestion not
taken.

fs::exists(Dir) inside ~Cleanup() was a std::terminate, not a warning.
A destructor is implicitly noexcept, so the throwing overload cannot
report a failed cleanup — it aborts the process. Switched to the
error_code form.

Fixing that surfaced a second defect in the same retry, introduced with
it: the loop returned on `!ec || !exists`, and `!ec` short-circuits. The
race being retried is a recreate landing AFTER the removal, which leaves
remove_all reporting success while the tree survives — so the guard bailed
out in precisely the case it existed for, and a staging root leaked again.
It now decides on whether the directory is actually gone. Three
consecutive runs leave nothing behind.

Relational comparison of pointers that do not point into the same array
is unspecified behaviour, and AppendChars' self-alias test does exactly
that with an external `str`. NOT taken: the suggested fix scans the
buffer comparing `str` against each valid index, which is correct but
turns an O(1) range test into O(n) on a hot append path.
std::less/std::greater_equal are required to impose a total order over
all pointers of a type, so the test is well-defined at O(1).

Mipmap filters are now gated on mips actually holding data, not on
m_MipLevels. glTextureStorage2D ALLOCATES the chain, so m_MipLevels > 1
only means the levels exist; sampling one that was never written is
defined but returns undefined content, and Resize() recreates storage
without regenerating (there is no level-0 data to generate from). A
resized texture therefore minified against garbage. m_MipsPopulated is
set at the three generate sites and cleared by Resize().

The workflow's job pools are removed in favour of the project's own.
Both bound CMAKE_JOB_POOL_LINK, so the workflow-local pool would have
silently won and left OLO_LINK_JOBS looking effective while doing
nothing — an artefact of adding the workflow cap first and the project
cap second. The configure step now just passes -DOLO_LINK_JOBS=1.

Also: the fog wave-reach is gated on the same condition the shader runs
on (both FFT textures valid), not on m_UseFFT alone — until the field
produces them the surface is Gerstner-displaced and m_FFTAmplitude
describes waves that are not on screen; OLOENGINE_GOLDEN_VENDOR rejects
anything fs::path considers rooted, since a drive-relative "C:vendor"
contains no separator or dot yet still discards the base on `/=`; the
staging loop keeps the last REAL error rather than letting a
merely-already-exists candidate overwrite it with Success; and CLAUDE.md
now documents CMAKE_BUILD_PARALLEL_LEVEL as a valid cap (it is what the
nightly uses) and that an explicit --parallel N overrides it.

Validated on the tree merged with #736 (Vulkan RHI phase 2): 5244 tests,
5237 passed, 1 failed — Atmosphere against the stale shared baselines,
which passes under OLOENGINE_GOLDEN_VENDOR=amd as the nightly runs it,
pending the NVIDIA rebake in #735. That count is up from 5192: the RHI
work's ~52 new tests pass alongside these changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
drsnuggles8 added a commit that referenced this pull request Aug 2, 2026
…gate

The quality gate failed on Reliability C and Maintainability B for new
code. All three BUGs behind the C rating were cpp:S867 in String.h --
the C idiom of using <cctype>'s int return as the right operand of `&&`.

Rather than append `!= 0`, the ctype dependency is removed outright,
because it was a latent correctness problem of its own.
std::toupper/tolower/isspace are LOCALE-SENSITIVE: under a Turkish
locale std::toupper('i') is not 'I', so a case-insensitive comparison of
engine identifiers -- asset names, shader uniforms, scene keys -- would
change meaning with the user's system locale. FString does ASCII
identifier work, so explicit constexpr ASCII helpers (ToUpperChar,
ToLowerChar, IsSpaceChar, the last matching std::isspace's "C" locale
set exactly) are both correct here and free of the whole class of
problem. That also clears three CRITICAL M23_404 ("call a function from
<locale>") and the S810 signedness findings on the same lines, since
the unsigned-char cast those required is gone with them.

This is a behaviour change for non-ASCII input under a non-default
locale, which is why it is verified rather than assumed:
FStringTest.CaseAndTrimming and ComparisonRespectsCase cover exactly
these paths and pass.

Maintainability, in descending order of what it actually buys:

* S3358 x4 -- extracted SelectMinFilter(). The inline form was a nested
  conditional over integer-format AND mip-validity in two places, which
  was genuinely hard to read; the helper names both conditions.
* S6166 x58 -- [[nodiscard]] messages. Note the repo is 2963 bare vs 682
  messaged, so bare is the DOMINANT convention and this rule fires
  codebase-wide; these 44 in String.h (plus 11 in AtmosphereSky.cpp and
  3 in Texture.h) only count because the gate scores new code. Rather
  than a blanket "Store this!", the FString-returning methods get a
  message that flags a real mistake: ToUpper() and ToUpperInline() both
  exist, so discarding the pure one is a silent no-op.
* S6004 -- init-statement form, which CLAUDE.md §1 requires anyway.

Deliberately NOT fixed, so the next reader does not re-litigate them:

* S1709 x3, "add explicit to this constructor". FString is implicitly
  constructible from const char*/std::string_view/std::string BY
  DESIGN -- it is a port of UE's type, and making these explicit would
  break `FString s = "hello"` along with every implicit std::string
  conversion at call sites such as Material::Set.
* S923/S945/S5281 on Printf. The ellipsis, the array decay and the
  non-literal format string ARE the UE API being ported; replacing it
  with std::format changes the contract rather than fixing a defect.
* Findings in OpenGLRendererAPI.*, Renderer3DMeshSubmission.cpp and
  AssimpMeshExporter.cpp. Those arrived with the #736 Vulkan RHI merge
  and belong to that work, not this branch.

Full suite unchanged: 5244 tests, 5237 passed, 1 failed (Atmosphere
against the stale shared baselines, passing under
OLOENGINE_GOLDEN_VENDOR=amd, pending the NVIDIA rebake in #735).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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