From 74955a7cbc77e195524723dfc13578e6bb618ee9 Mon Sep 17 00:00:00 2001 From: Ole Bueker Date: Sat, 1 Aug 2026 13:17:19 +0200 Subject: [PATCH 1/6] refactor(rhi): migrate framebuffer attachment consumers to identities (#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) --- OloEngine/src/CMakeLists.txt | 11 +- .../Renderer/DDGI/DDGIProbeUpdatePass.cpp | 69 ++++++--- .../Renderer/DDGI/DDGIProbeUpdatePass.h | 18 +++ .../src/OloEngine/Renderer/IBLPrecompute.cpp | 14 +- .../Renderer/Impostor/ImpostorBaker.cpp | 8 +- .../OloEngine/Renderer/LightProbeBaker.cpp | 7 +- .../Renderer/Passes/BloomRenderPass.cpp | 12 +- .../Renderer/Passes/CloudscapeRenderPass.cpp | 2 +- .../Renderer/Passes/DecalRenderPass.cpp | 4 +- .../Renderer/Passes/FogRenderPass.cpp | 4 +- .../Renderer/Passes/OverdrawRenderPass.cpp | 2 +- .../Passes/SelectionOutlineRenderPass.cpp | 4 +- .../Renderer/Preview/AssetPreviewRenderer.cpp | 4 +- .../src/OloEngine/Renderer/RHI/RHIResources.h | 8 +- .../src/OloEngine/Renderer/RHI/RHITypes.h | 13 +- .../Renderer/ReflectionProbeBaker.cpp | 7 +- .../src/OloEngine/Renderer/RenderCommand.h | 42 ++++++ .../src/OloEngine/Renderer/RenderGraph.cpp | 12 +- .../src/OloEngine/Renderer/RenderPipeline.cpp | 21 ++- .../src/OloEngine/Renderer/RendererAPI.h | 28 ++++ .../src/OloEngine/Renderer/SkyCubemapBake.cpp | 4 +- .../OloEngine/SaveGame/ThumbnailCapture.cpp | 10 +- .../src/Platform/OpenGL/OpenGLRendererAPI.cpp | 56 ++++++++ .../src/Platform/OpenGL/OpenGLRendererAPI.h | 15 ++ OloEngine/tests/Rendering/MockRendererAPI.h | 35 +++++ .../Rendering/RHIResourceRegistryTest.cpp | 39 ++++++ .../Rendering/rhi_boundary_baseline.json | 53 ++++++- OloEngine/tests/scripts/measure_rendererid.py | 5 +- docs/agent-rules/rhi-abstraction-boundary.md | 131 ++++++++++++++++-- 29 files changed, 537 insertions(+), 101 deletions(-) diff --git a/OloEngine/src/CMakeLists.txt b/OloEngine/src/CMakeLists.txt index 79c01da7c..792ba7850 100644 --- a/OloEngine/src/CMakeLists.txt +++ b/OloEngine/src/CMakeLists.txt @@ -591,10 +591,13 @@ "OloEngine/Renderer/PlanarReflection.cpp" "OloEngine/Renderer/RendererAPI.cpp" "OloEngine/Renderer/RendererAPI.h" - # Declaration-only RHI vocabulary (issue #691 Phase 1, ADR 0011). No .cpp - # and no consumers yet — these exist so the Phase 2 sweep has a fixed - # target to convert toward. Listed here only so they show up in the IDE - # project tree; RHIBoundaryRatchetTest is what actually compiles them. + # RHI vocabulary (issue #691 Phases 1-2, ADR 0011). No longer + # declaration-only: RHIResourceRegistry.{h,cpp} mints the generation-checked + # RHI::ResourceHandle (defined in RHITypes.h) that the Platform/OpenGL + # resource classes, RenderCommand and the render graph now carry. + # RHIResources.h is the exception — still forward-looking vocabulary that + # only RHIResourceRegistry.cpp includes so far. + # RHIBoundaryRatchetTest guards the no-backend-types boundary. "OloEngine/Renderer/RHI/RHITypes.h" "OloEngine/Renderer/RHI/RHIResources.h" "OloEngine/Renderer/RHI/RHIResourceRegistry.h" diff --git a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp index e1ab88388..c38b9be1e 100644 --- a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp +++ b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp @@ -68,6 +68,19 @@ namespace OloEngine static_assert(sizeof(DDGIPassDataUBO) == 160, "DDGIPassDataUBO std140 size drifted from GLSL expectation (160 B)"); static_assert(sizeof(DDGIPassDataUBO) % 16 == 0, "DDGIPassDataUBO must be 16-byte aligned for std140"); + // Overloaded rather than converted (issue #691 step 3, slice 5). The + // atlases are framebuffer ATTACHMENTS and migrated to identities; the + // 1x1 placeholder/white/probe-data textures this pass creates itself are + // still native and belong to a later resource-grain slice (they are also + // graph-imported, and ImportTextureHandle is the blocker — see the + // comment on importAtlas in Setup). Two complete chains on two + // currencies, not one half-migrated chain. + void SetAtlasTextureParams(RHI::ResourceHandle texture, RHI::Filter filter) + { + RenderCommand::SetTextureFilter(texture, filter, filter); + RenderCommand::SetTextureWrap(texture, RHI::AddressMode::ClampToEdge); + } + void SetAtlasTextureParams(u32 texID, RHI::Filter filter) { RenderCommand::SetTextureFilter(texID, filter, filter); @@ -318,6 +331,20 @@ namespace OloEngine : 0; } + RHI::ResourceHandle DDGIProbeUpdatePass::GetIrradianceAtlasHandle(const u32 pingIndex) const + { + if (pingIndex >= 2u || !m_IrradianceFB[pingIndex]) + return RHI::NullResource; + return m_IrradianceFB[pingIndex]->GetColorAttachmentHandle(0); + } + + RHI::ResourceHandle DDGIProbeUpdatePass::GetVisibilityAtlasHandle(const u32 pingIndex) const + { + if (pingIndex >= 2u || !m_VisibilityFB[pingIndex]) + return RHI::NullResource; + return m_VisibilityFB[pingIndex]->GetColorAttachmentHandle(0); + } + u32 DDGIProbeUpdatePass::GetProbeDataTextureID() const { return m_ProbeDataTexture; @@ -413,7 +440,7 @@ namespace OloEngine for (u32 i = 0; i < 2; ++i) { m_IrradianceFB[i] = makeAtlasFB(FramebufferTextureFormat::RGBA16F, DDGI::kIrradianceTileTexels); - SetAtlasTextureParams(m_IrradianceFB[i]->GetColorAttachmentRendererID(0), RHI::Filter::Linear); + SetAtlasTextureParams(m_IrradianceFB[i]->GetColorAttachmentHandle(0), RHI::Filter::Linear); m_IrradianceFB[i]->Bind(); m_IrradianceFB[i]->ClearAllAttachments(glm::vec4(0.0f), -1); } @@ -422,7 +449,7 @@ namespace OloEngine for (u32 i = 0; i < 2; ++i) { m_VisibilityFB[i] = makeAtlasFB(FramebufferTextureFormat::RG16F, DDGI::kVisibilityTileTexels); - SetAtlasTextureParams(m_VisibilityFB[i]->GetColorAttachmentRendererID(0), RHI::Filter::Linear); + SetAtlasTextureParams(m_VisibilityFB[i]->GetColorAttachmentHandle(0), RHI::Filter::Linear); m_VisibilityFB[i]->Bind(); m_VisibilityFB[i]->ClearAllAttachments(glm::vec4(0.0f), -1); } @@ -430,7 +457,7 @@ namespace OloEngine // Radiance cache (RGBA16F, HitCacheTexels tiles, no border) — NEAREST // (texelFetch-only consumer). m_RadianceFB = makeAtlasFB(FramebufferTextureFormat::RGBA16F, t); - SetAtlasTextureParams(m_RadianceFB->GetColorAttachmentRendererID(0), RHI::Filter::Nearest); + SetAtlasTextureParams(m_RadianceFB->GetColorAttachmentHandle(0), RHI::Filter::Nearest); m_RadianceFB->Bind(); m_RadianceFB->ClearAllAttachments(glm::vec4(0.0f), -1); @@ -443,8 +470,8 @@ namespace OloEngine spec.Height = static_cast(tileDims.y * t); spec.Attachments = { FramebufferTextureFormat::RGBA8, FramebufferTextureFormat::RGBA16F }; m_HitFB = Framebuffer::Create(spec); - SetAtlasTextureParams(m_HitFB->GetColorAttachmentRendererID(0), RHI::Filter::Nearest); - SetAtlasTextureParams(m_HitFB->GetColorAttachmentRendererID(1), RHI::Filter::Nearest); + SetAtlasTextureParams(m_HitFB->GetColorAttachmentHandle(0), RHI::Filter::Nearest); + SetAtlasTextureParams(m_HitFB->GetColorAttachmentHandle(1), RHI::Filter::Nearest); m_HitFB->Bind(); m_HitFB->ClearAttachment(0, glm::vec4(0.0f)); // Geo cleared to "sky" so never-resampled tiles read as misses. @@ -462,8 +489,8 @@ namespace OloEngine spec.Attachments = { FramebufferTextureFormat::RGBA8, FramebufferTextureFormat::RGBA16F, FramebufferTextureFormat::ShadowDepth }; m_CaptureFB = Framebuffer::Create(spec); - SetAtlasTextureParams(m_CaptureFB->GetColorAttachmentRendererID(0), RHI::Filter::Nearest); - SetAtlasTextureParams(m_CaptureFB->GetColorAttachmentRendererID(1), RHI::Filter::Nearest); + SetAtlasTextureParams(m_CaptureFB->GetColorAttachmentHandle(0), RHI::Filter::Nearest); + SetAtlasTextureParams(m_CaptureFB->GetColorAttachmentHandle(1), RHI::Filter::Nearest); } // Probe data: one texel per probe (xyz = relocation offset normalized @@ -697,8 +724,8 @@ namespace OloEngine static_cast(t), static_cast(t)); m_ResampleShader->Bind(); - RenderCommand::BindTexture(0, m_CaptureFB->GetColorAttachmentRendererID(0)); - RenderCommand::BindTexture(1, m_CaptureFB->GetColorAttachmentRendererID(1)); + RenderCommand::BindTexture(0, m_CaptureFB->GetColorAttachmentHandle(0)); + RenderCommand::BindTexture(1, m_CaptureFB->GetColorAttachmentHandle(1)); SetPassDataProbe(probeIdx, glm::vec3(0.0f)); const auto va = MeshPrimitives::GetFullscreenTriangle(); @@ -712,7 +739,7 @@ namespace OloEngine const i32 t = m_Desc.HitCacheTexels; const glm::ivec2 tile = DDGI::ProbeTileCoord(probeIdx, m_Desc.Resolution); - const u32 geoTex = m_HitFB->GetColorAttachmentRendererID(1); + const RHI::ResourceHandle geoTex = m_HitFB->GetColorAttachmentHandle(1); // Read the probe's hit-geo tile back (rg = octNormal, b = distance // [< 0 = sky], a = DDGI_HIT_* flag). RGBA16F -> GL converts to float. @@ -812,8 +839,8 @@ namespace OloEngine const u32 prevIdx = m_VisibilityCurrent; const u32 currIdx = 1u - m_VisibilityCurrent; - const u32 prevTex = m_VisibilityFB[prevIdx]->GetColorAttachmentRendererID(0); - const u32 currTex = m_VisibilityFB[currIdx]->GetColorAttachmentRendererID(0); + const RHI::ResourceHandle prevTex = m_VisibilityFB[prevIdx]->GetColorAttachmentHandle(0); + const RHI::ResourceHandle currTex = m_VisibilityFB[currIdx]->GetColorAttachmentHandle(0); const glm::ivec2 visSize = m_TileDims * DDGI::kVisibilityTileTexels; // Carry every un-recaptured tile forward, then overwrite only the @@ -826,8 +853,8 @@ namespace OloEngine SetFullscreenPassState(); m_BlendVisibilityShader->Bind(); - RenderCommand::BindTexture(0, m_HitFB->GetColorAttachmentRendererID(1)); // hit geo (dist + flag) - RenderCommand::BindTexture(1, prevTex); // EMA history + RenderCommand::BindTexture(0, m_HitFB->GetColorAttachmentHandle(1)); // hit geo (dist + flag) + RenderCommand::BindTexture(1, prevTex); // EMA history const auto va = MeshPrimitives::GetFullscreenTriangle(); va->Bind(); @@ -859,10 +886,10 @@ namespace OloEngine RenderCommand::SetViewport(0, 0, static_cast(radianceSize.x), static_cast(radianceSize.y)); m_RelightShader->Bind(); - RenderCommand::BindTexture(0, m_HitFB->GetColorAttachmentRendererID(0)); // hit albedo - RenderCommand::BindTexture(1, m_HitFB->GetColorAttachmentRendererID(1)); // hit geo - RenderCommand::BindTexture(2, m_IrradianceFB[m_IrradianceCurrent]->GetColorAttachmentRendererID(0)); // prev irradiance (bounce) - RenderCommand::BindTexture(3, m_VisibilityFB[m_VisibilityCurrent]->GetColorAttachmentRendererID(0)); // current visibility + RenderCommand::BindTexture(0, m_HitFB->GetColorAttachmentHandle(0)); // hit albedo + RenderCommand::BindTexture(1, m_HitFB->GetColorAttachmentHandle(1)); // hit geo + RenderCommand::BindTexture(2, m_IrradianceFB[m_IrradianceCurrent]->GetColorAttachmentHandle(0)); // prev irradiance (bounce) + RenderCommand::BindTexture(3, m_VisibilityFB[m_VisibilityCurrent]->GetColorAttachmentHandle(0)); // current visibility RenderCommand::BindTexture(4, m_ProbeDataTexture); // Global environment cubemap for sky texels, at the engine's canonical @@ -939,9 +966,9 @@ namespace OloEngine RenderCommand::SetViewport(0, 0, static_cast(irrSize.x), static_cast(irrSize.y)); m_BlendIrradianceShader->Bind(); - RenderCommand::BindTexture(0, m_RadianceFB->GetColorAttachmentRendererID(0)); - RenderCommand::BindTexture(1, m_HitFB->GetColorAttachmentRendererID(1)); // hit geo (backface flags) - RenderCommand::BindTexture(2, m_IrradianceFB[prevIdx]->GetColorAttachmentRendererID(0)); + RenderCommand::BindTexture(0, m_RadianceFB->GetColorAttachmentHandle(0)); + RenderCommand::BindTexture(1, m_HitFB->GetColorAttachmentHandle(1)); // hit geo (backface flags) + RenderCommand::BindTexture(2, m_IrradianceFB[prevIdx]->GetColorAttachmentHandle(0)); RenderCommand::BindTexture(3, m_ProbeDataTexture); const auto va = MeshPrimitives::GetFullscreenTriangle(); diff --git a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.h b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.h index c9ad83d22..bc73c2707 100644 --- a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.h +++ b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.h @@ -117,6 +117,24 @@ namespace OloEngine // ids so atlas (re)creation triggers the rebuild that re-imports them. [[nodiscard]] u32 GetIrradianceAtlasID(u32 pingIndex) const; [[nodiscard]] u32 GetVisibilityAtlasID(u32 pingIndex) const; + + // Identity siblings of the two accessors above (issue #691 step 3). + // The FINGERPRINT reads these, not the raw ids, and that is a + // correctness fix rather than a type change: EnsureResources calls + // DestroyResources BEFORE recreating, so a Resolution / HitCacheTexels + // edit frees every atlas texture and GL is then free to hand the new + // ones the same names. Hashing the driver name therefore could not see + // the recreate — the fingerprint stayed put, BuildFrameGraph was not + // rebuilt, and the graph kept an import whose Width/Height still + // described the OLD resolution (what olo_render_list_targets and + // olo_render_capture_target then reported). A handle's generation + // cannot be recycled, so the rebuild now always happens. + // + // The raw-id accessors stay: Setup still imports natively, because + // ImportTextureHandle leaves RenderGraph::ResolveTexture answering 0 + // and that is what the MCP capture endpoints read. + [[nodiscard]] RHI::ResourceHandle GetIrradianceAtlasHandle(u32 pingIndex) const; + [[nodiscard]] RHI::ResourceHandle GetVisibilityAtlasHandle(u32 pingIndex) const; [[nodiscard]] u32 GetIrradianceCurrentIndex() const { return m_IrradianceCurrent; diff --git a/OloEngine/src/OloEngine/Renderer/IBLPrecompute.cpp b/OloEngine/src/OloEngine/Renderer/IBLPrecompute.cpp index 326b0fa17..c631f15dd 100644 --- a/OloEngine/src/OloEngine/Renderer/IBLPrecompute.cpp +++ b/OloEngine/src/OloEngine/Renderer/IBLPrecompute.cpp @@ -280,12 +280,12 @@ namespace OloEngine vertexArray->Bind(); RenderCommand::DrawIndexed(vertexArray); - // Now copy from framebuffer to cubemap face - u32 framebufferColorTexture = framebuffer->GetColorAttachmentRendererID(0); - + // Now copy from framebuffer to cubemap face. Both operands are + // identities (issue #691 step 3): the source is the framebuffer's + // colour attachment, the destination the cubemap's own object. RenderCommand::CopyImageSubDataFull( - framebufferColorTexture, RendererAPI::TextureTargetType::Texture2D, 0, 0, - cubemap->GetRendererID(), RendererAPI::TextureTargetType::TextureCubeMap, static_cast(mipLevel), static_cast(i), + framebuffer->GetColorAttachmentHandle(0), RendererAPI::TextureTargetType::Texture2D, 0, 0, + cubemap->GetRHIHandle(), RendererAPI::TextureTargetType::TextureCubeMap, static_cast(mipLevel), static_cast(i), mipWidth, mipHeight); } @@ -333,8 +333,8 @@ namespace OloEngine // Copy from framebuffer color attachment to the output texture RenderCommand::CopyImageSubDataFull( - framebuffer->GetColorAttachmentRendererID(0), RendererAPI::TextureTargetType::Texture2D, 0, 0, - texture->GetRendererID(), RendererAPI::TextureTargetType::Texture2D, 0, 0, + framebuffer->GetColorAttachmentHandle(0), RendererAPI::TextureTargetType::Texture2D, 0, 0, + texture->GetRHIHandle(), RendererAPI::TextureTargetType::Texture2D, 0, 0, texture->GetWidth(), texture->GetHeight()); // Restore previous stencil state diff --git a/OloEngine/src/OloEngine/Renderer/Impostor/ImpostorBaker.cpp b/OloEngine/src/OloEngine/Renderer/Impostor/ImpostorBaker.cpp index 94d10e7f1..79a80c18d 100644 --- a/OloEngine/src/OloEngine/Renderer/Impostor/ImpostorBaker.cpp +++ b/OloEngine/src/OloEngine/Renderer/Impostor/ImpostorBaker.cpp @@ -178,12 +178,12 @@ namespace OloEngine atlas.NormalDepth = CreateAtlasTexture(atlasSize); RenderCommand::CopyImageSubDataFull( - framebuffer->GetColorAttachmentRendererID(0), RendererAPI::TextureTargetType::Texture2D, 0, 0, - atlas.Albedo->GetRendererID(), RendererAPI::TextureTargetType::Texture2D, 0, 0, + framebuffer->GetColorAttachmentHandle(0), RendererAPI::TextureTargetType::Texture2D, 0, 0, + atlas.Albedo->GetRHIHandle(), RendererAPI::TextureTargetType::Texture2D, 0, 0, atlasSize, atlasSize); RenderCommand::CopyImageSubDataFull( - framebuffer->GetColorAttachmentRendererID(1), RendererAPI::TextureTargetType::Texture2D, 0, 0, - atlas.NormalDepth->GetRendererID(), RendererAPI::TextureTargetType::Texture2D, 0, 0, + framebuffer->GetColorAttachmentHandle(1), RendererAPI::TextureTargetType::Texture2D, 0, 0, + atlas.NormalDepth->GetRHIHandle(), RendererAPI::TextureTargetType::Texture2D, 0, 0, atlasSize, atlasSize); atlas.FramesPerAxis = N; diff --git a/OloEngine/src/OloEngine/Renderer/LightProbeBaker.cpp b/OloEngine/src/OloEngine/Renderer/LightProbeBaker.cpp index ef5ed9730..051d0a6ed 100644 --- a/OloEngine/src/OloEngine/Renderer/LightProbeBaker.cpp +++ b/OloEngine/src/OloEngine/Renderer/LightProbeBaker.cpp @@ -66,10 +66,11 @@ namespace OloEngine // Render the full scene from this cubemap face's perspective scene->RenderScene3D(captureCamera, transform); - // Read back RGBA16F pixel data from the color attachment - u32 const colorAttachmentID = fbo->GetColorAttachmentRendererID(0); + // Read back RGBA16F pixel data from the color attachment, by + // identity rather than driver name (issue #691 step 3). + RHI::ResourceHandle const colorAttachment = fbo->GetColorAttachmentHandle(0); const bool readOk = RenderCommand::ReadTextureImage( - colorAttachmentID, 0, RHI::Format::RGBA32Float, + colorAttachment, 0, RHI::Format::RGBA32Float, rgbaBuffer.size() * sizeof(f32), rgbaBuffer.data()); fbo->Unbind(); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/BloomRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/BloomRenderPass.cpp index 4b7cc436e..f4e01bfb2 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/BloomRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/BloomRenderPass.cpp @@ -265,8 +265,8 @@ namespace OloEngine context.Clear(); m_BloomDownsampleShader->Bind(); - const u32 srcID = srcMip->GetColorAttachmentRendererID(0); - context.BindTexture(0, srcID); + const RHI::ResourceHandle srcTexture = srcMip->GetColorAttachmentHandle(0); + context.BindTexture(0, srcTexture); if (m_GPUData && m_PostProcessUBO) { @@ -309,8 +309,8 @@ namespace OloEngine // No clear — we're additively accumulating into existing content. m_BloomUpsampleShader->Bind(); - const u32 srcID = srcMip->GetColorAttachmentRendererID(0); - context.BindTexture(0, srcID); + const RHI::ResourceHandle srcTexture = srcMip->GetColorAttachmentHandle(0); + context.BindTexture(0, srcTexture); if (m_GPUData && m_PostProcessUBO) { @@ -364,8 +364,8 @@ namespace OloEngine context.BindTexture(0, inputColorTextureID); m_BloomCompositeShader->SetInt("u_SceneColor", 0); - const u32 bloomColorID = bloomMips[0]->GetColorAttachmentRendererID(0); - context.BindTexture(1, bloomColorID); + const RHI::ResourceHandle bloomColor = bloomMips[0]->GetColorAttachmentHandle(0); + context.BindTexture(1, bloomColor); m_BloomCompositeShader->SetInt("u_BloomColor", 1); const auto va = MeshPrimitives::GetFullscreenTriangle(); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/CloudscapeRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/CloudscapeRenderPass.cpp index a6388b5c1..eac5b7da0 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/CloudscapeRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/CloudscapeRenderPass.cpp @@ -344,7 +344,7 @@ namespace OloEngine // Upstream scene colour at unit 0, resolved clouds at unit 1, // full-res depth at TEX_POSTPROCESS_DEPTH (all layout-qualified). context.BindTexture(0, inputColorTextureID); - context.BindTexture(1, cloudsResolvedFramebuffer->GetColorAttachmentRendererID(0)); + context.BindTexture(1, cloudsResolvedFramebuffer->GetColorAttachmentHandle(0)); context.BindTexture(ShaderBindingLayout::TEX_POSTPROCESS_DEPTH, sceneDepthTextureID); { diff --git a/OloEngine/src/OloEngine/Renderer/Passes/DecalRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/DecalRenderPass.cpp index 0878ee908..6312968c4 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/DecalRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/DecalRenderPass.cpp @@ -343,8 +343,8 @@ namespace OloEngine // plain sampler2D regardless of the write target's sample count. // Safe to sample the currently-bound depth since decal render state // disables depth writes. - const u32 depthTextureID = depthSamplingFB->GetDepthAttachmentRendererID(); - RenderCommand::BindTexture(ShaderBindingLayout::TEX_POSTPROCESS_DEPTH, depthTextureID); + const RHI::ResourceHandle depthTexture = depthSamplingFB->GetDepthAttachmentHandle(); + RenderCommand::BindTexture(ShaderBindingLayout::TEX_POSTPROCESS_DEPTH, depthTexture); m_CommandBucket.SortCommands(); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/FogRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/FogRenderPass.cpp index 67a628c1e..fdd934f96 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/FogRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/FogRenderPass.cpp @@ -264,8 +264,8 @@ namespace OloEngine context.BindTexture(0, inputColorTextureID); m_FogUpsampleShader->SetInt("u_SceneColor", 0); - const u32 fogID = fogHalfResFramebuffer->GetColorAttachmentRendererID(0); - context.BindTexture(1, fogID); + const RHI::ResourceHandle fogTexture = fogHalfResFramebuffer->GetColorAttachmentHandle(0); + context.BindTexture(1, fogTexture); m_FogUpsampleShader->SetInt("u_FogTexture", 1); // Full-res depth for bilateral edge detection. diff --git a/OloEngine/src/OloEngine/Renderer/Passes/OverdrawRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/OverdrawRenderPass.cpp index fd5c072fe..c6fb7abbf 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/OverdrawRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/OverdrawRenderPass.cpp @@ -176,7 +176,7 @@ namespace OloEngine context.Clear(); m_HeatmapShader->Bind(); - context.BindTexture(0, m_AccumFramebuffer->GetColorAttachmentRendererID(0)); + context.BindTexture(0, m_AccumFramebuffer->GetColorAttachmentHandle(0)); const auto va = MeshPrimitives::GetFullscreenTriangle(); va->Bind(); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/SelectionOutlineRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/SelectionOutlineRenderPass.cpp index b4fff046d..d2f7a4ce6 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/SelectionOutlineRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/SelectionOutlineRenderPass.cpp @@ -256,7 +256,7 @@ namespace OloEngine context.Clear(); // Bind previous JFA result - context.BindTexture(0, jfaFBs[readIndex]->GetColorAttachmentRendererID(0)); + context.BindTexture(0, jfaFBs[readIndex]->GetColorAttachmentHandle(0)); m_JFAPassShader->Bind(); m_JFAPassShader->SetInt("u_Texture", 0); @@ -290,7 +290,7 @@ namespace OloEngine // Slot 0: scene color from the selected dynamic post-chain texture view. context.BindTexture(0, inputColorTextureID); // Slot 1: final JFA distance field from the graph-owned ping-pong scratch - context.BindTexture(1, jfaFBs[readIndex]->GetColorAttachmentRendererID(0)); + context.BindTexture(1, jfaFBs[readIndex]->GetColorAttachmentHandle(0)); m_JFACompositeShader->Bind(); m_JFACompositeShader->SetInt("u_SceneColor", 0); diff --git a/OloEngine/src/OloEngine/Renderer/Preview/AssetPreviewRenderer.cpp b/OloEngine/src/OloEngine/Renderer/Preview/AssetPreviewRenderer.cpp index 77d032788..8ab154671 100644 --- a/OloEngine/src/OloEngine/Renderer/Preview/AssetPreviewRenderer.cpp +++ b/OloEngine/src/OloEngine/Renderer/Preview/AssetPreviewRenderer.cpp @@ -383,10 +383,10 @@ namespace OloEngine Ref target = CreateTargetTexture(); if (target) { - const u32 fbColor = s_Framebuffer->GetColorAttachmentRendererID(0); + const RHI::ResourceHandle fbColor = s_Framebuffer->GetColorAttachmentHandle(0); RenderCommand::CopyImageSubDataFull( fbColor, RendererAPI::TextureTargetType::Texture2D, 0, 0, - target->GetRendererID(), RendererAPI::TextureTargetType::Texture2D, 0, 0, + target->GetRHIHandle(), RendererAPI::TextureTargetType::Texture2D, 0, 0, kThumbnailSize, kThumbnailSize); } else diff --git a/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h b/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h index 2a11732d4..fe224ff96 100644 --- a/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h +++ b/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h @@ -6,9 +6,11 @@ // // Issue #691 Phase 1, ADR 0011 (docs/adr/0011-rhi-neutral-resource-and-binding-model.md). // -// **Declaration-only. Nothing consumes this yet.** Same two rules as RHITypes.h: -// no backend headers, no backend types, and these are engine enums that a -// backend converts explicitly. +// **Not yet load-bearing: only RHIResourceRegistry.cpp includes these so far.** +// (Phase 2 step 3 minted RHI::ResourceHandle, but that lives in RHITypes.h — the +// resource *descriptions* below are still waiting on their consumers.) Same two +// rules as RHITypes.h: no backend headers, no backend types, and these are engine +// enums that a backend converts explicitly. // // The descriptions below are heap+offset shaped from day one because the // binding model is heap-bindless-only (ADR 0010) — there is no classic diff --git a/OloEngine/src/OloEngine/Renderer/RHI/RHITypes.h b/OloEngine/src/OloEngine/Renderer/RHI/RHITypes.h index 9b69a8f92..18ebf9821 100644 --- a/OloEngine/src/OloEngine/Renderer/RHI/RHITypes.h +++ b/OloEngine/src/OloEngine/Renderer/RHI/RHITypes.h @@ -5,10 +5,15 @@ // // Issue #691 Phase 1, ADR 0011 (docs/adr/0011-rhi-neutral-resource-and-binding-model.md). // -// **This header is declaration-only and nothing consumes it yet.** It exists so -// that the Phase 2 sweep of ~313 raw `glXxx()` call sites has a fixed target to -// convert *toward*, instead of inventing a vocabulary one file at a time and -// discovering the disagreements at merge time. +// Written declaration-only in Phase 1, so that the Phase 2 sweep of ~313 raw +// `glXxx()` call sites had a fixed target to convert *toward* instead of +// inventing a vocabulary one file at a time and discovering the disagreements at +// merge time. **That is history now** — `ResourceHandle` below is the live +// identity currency: minted by RHI::ResourceRegistry, carried by the +// Platform/OpenGL resource classes, RenderCommand's handle-taking siblings, the +// render graph, and the framebuffer attachment getters. `ViewHandle` / +// `HeapOffset` are still forward-looking and land in Phase 3 as a matched pair +// (ADR 0011 amendment (11)). // // Two hard rules, both enforced by RHIBoundaryRatchetTest: // diff --git a/OloEngine/src/OloEngine/Renderer/ReflectionProbeBaker.cpp b/OloEngine/src/OloEngine/Renderer/ReflectionProbeBaker.cpp index 3b7cebc6a..be585f3eb 100644 --- a/OloEngine/src/OloEngine/Renderer/ReflectionProbeBaker.cpp +++ b/OloEngine/src/OloEngine/Renderer/ReflectionProbeBaker.cpp @@ -170,7 +170,10 @@ namespace OloEngine } else { - u32 const colorAttachmentID = sceneFb->GetColorAttachmentRendererID(0); + // Hoisted outside the face loop, as the native id was: SceneColor's + // attachment is not recreated between faces, and the handle stays + // valid across an in-place recreate (issue #691 step 3). + RHI::ResourceHandle const colorAttachment = sceneFb->GetColorAttachmentHandle(0); for (u32 face = 0; face < 6; ++face) { glm::mat4 const view = glm::lookAt(position, position + s_FaceTargets[face], s_FaceUps[face]); @@ -181,7 +184,7 @@ namespace OloEngine // Read back this face's lit HDR radiance from the graph's // SceneColor RT0. The readback reads the texture directly // (no FBO-bound restriction) and lets the driver pick its path. - if (!RenderCommand::ReadTextureImage(colorAttachmentID, 0, RHI::Format::RGBA32Float, + if (!RenderCommand::ReadTextureImage(colorAttachment, 0, RHI::Format::RGBA32Float, faceBytes, pixelBuffer.data())) { OLO_CORE_WARN("ReflectionProbeBaker: cubemap face readback failed"); diff --git a/OloEngine/src/OloEngine/Renderer/RenderCommand.h b/OloEngine/src/OloEngine/Renderer/RenderCommand.h index fdb63ae12..4a1d10f6a 100644 --- a/OloEngine/src/OloEngine/Renderer/RenderCommand.h +++ b/OloEngine/src/OloEngine/Renderer/RenderCommand.h @@ -334,6 +334,14 @@ namespace OloEngine s_RendererAPI->CopyImageSubData(srcID, srcTarget, dstID, dstTarget, width, height); } + // Handle form — both operands together (issue #691 step 3, slice 5). + static void CopyImageSubData(RHI::ResourceHandle src, RendererAPI::TextureTargetType srcTarget, + RHI::ResourceHandle dst, RendererAPI::TextureTargetType dstTarget, + u32 width, u32 height) + { + s_RendererAPI->CopyImageSubData(src, srcTarget, dst, dstTarget, width, height); + } + // Full image copy with source/dest z offsets (cubemap face copies) static void CopyImageSubDataFull(u32 srcID, RendererAPI::TextureTargetType srcTarget, i32 srcLevel, i32 srcZ, u32 dstID, RendererAPI::TextureTargetType dstTarget, i32 dstLevel, i32 dstZ, @@ -344,6 +352,18 @@ namespace OloEngine width, height); } + // Handle form — both operands together (issue #691 step 3, slice 5). + static void CopyImageSubDataFull(RHI::ResourceHandle src, RendererAPI::TextureTargetType srcTarget, + i32 srcLevel, i32 srcZ, + RHI::ResourceHandle dst, RendererAPI::TextureTargetType dstTarget, + i32 dstLevel, i32 dstZ, + u32 width, u32 height) + { + s_RendererAPI->CopyImageSubDataFull(src, srcTarget, srcLevel, srcZ, + dst, dstTarget, dstLevel, dstZ, + width, height); + } + // Copy from currently-bound READ framebuffer to a named texture static void CopyFramebufferToTexture(u32 textureID, u32 width, u32 height) { @@ -710,6 +730,11 @@ namespace OloEngine s_RendererAPI->ClearTextureFloat(textureID, mipLevel, color); } + static void ClearTextureFloat(RHI::ResourceHandle texture, u32 mipLevel, const glm::vec4& color) + { + s_RendererAPI->ClearTextureFloat(texture, mipLevel, color); + } + static void ClearTextureUInt(u32 textureID, u32 mipLevel, u32 value) { s_RendererAPI->ClearTextureUInt(textureID, mipLevel, value); @@ -737,6 +762,13 @@ namespace OloEngine return s_RendererAPI->ReadTextureImage(textureID, mipLevel, destFormat, destSizeBytes, dest); } + [[nodiscard("Store this!")]] static bool ReadTextureImage(RHI::ResourceHandle texture, u32 mipLevel, + RHI::Format destFormat, + sizet destSizeBytes, void* dest) + { + return s_RendererAPI->ReadTextureImage(texture, mipLevel, destFormat, destSizeBytes, dest); + } + [[nodiscard("Store this!")]] static bool ReadTextureSubImage(u32 textureID, u32 mipLevel, i32 x, i32 y, i32 z, u32 width, u32 height, u32 depth, @@ -747,6 +779,16 @@ namespace OloEngine destFormat, destSizeBytes, dest); } + [[nodiscard("Store this!")]] static bool ReadTextureSubImage(RHI::ResourceHandle texture, u32 mipLevel, + i32 x, i32 y, i32 z, + u32 width, u32 height, u32 depth, + RHI::Format destFormat, + sizet destSizeBytes, void* dest) + { + return s_RendererAPI->ReadTextureSubImage(texture, mipLevel, x, y, z, width, height, depth, + destFormat, destSizeBytes, dest); + } + static void GetTextureDimensions(u32 textureID, u32 mipLevel, u32& outWidth, u32& outHeight) { s_RendererAPI->GetTextureDimensions(textureID, mipLevel, outWidth, outHeight); diff --git a/OloEngine/src/OloEngine/Renderer/RenderGraph.cpp b/OloEngine/src/OloEngine/Renderer/RenderGraph.cpp index ee81b3758..573949e45 100644 --- a/OloEngine/src/OloEngine/Renderer/RenderGraph.cpp +++ b/OloEngine/src/OloEngine/Renderer/RenderGraph.cpp @@ -162,15 +162,15 @@ namespace OloEngine if (!hasColor) return; - const u32 textureID = target->GetColorAttachmentRendererID(0); + const RHI::ResourceHandle colorAttachment = target->GetColorAttachmentHandle(0); const auto& spec = target->GetSpecification(); - if (textureID == 0 || spec.Width == 0 || spec.Height == 0) + if (!colorAttachment.IsValid() || spec.Width == 0 || spec.Height == 0) return; const sizet texelCount = static_cast(spec.Width) * spec.Height; static thread_local std::vector s_Scratch; s_Scratch.resize(texelCount * 4u); - if (!RenderCommand::ReadTextureSubImage(textureID, 0, 0, 0, 0, + if (!RenderCommand::ReadTextureSubImage(colorAttachment, 0, 0, 0, 0, spec.Width, spec.Height, 1, RHI::Format::RGBA32Float, s_Scratch.size() * sizeof(f32), s_Scratch.data())) @@ -201,7 +201,7 @@ namespace OloEngine OLO_CORE_ERROR("BLACKSQUARE HUNT NAN: after pass '{}' {}FB#{} tex#{} has {} NaN channel value(s), first at texel ({}, {})", passName, watchLabel ? watchLabel : "target ", - target->GetRendererID(), textureID, + target->GetRendererID(), colorAttachment, nanCount, texel % spec.Width, texel / spec.Width); } @@ -241,7 +241,7 @@ namespace OloEngine OLO_CORE_ERROR("BLACKSQUARE HUNT: after pass '{}' {}FB#{} tex#{} has a >=64px black block at ({}, {}) [{}x{}]", passName, watchLabel ? watchLabel : "target ", - target->GetRendererID(), textureID, + target->GetRendererID(), colorAttachment, bx * kBlock, by * kBlock, spec.Width, spec.Height); return; // one report per pass per frame is enough } @@ -342,7 +342,7 @@ namespace OloEngine case FramebufferTextureFormat::RG16F: case FramebufferTextureFormat::RG32F: RenderCommand::ClearTextureFloat( - framebuffer->GetColorAttachmentRendererID(colorIndex), 0, + framebuffer->GetColorAttachmentHandle(colorIndex), 0, glm::vec4(color.RGBA[0], color.RGBA[1], color.RGBA[2], color.RGBA[3])); ++colorIndex; break; diff --git a/OloEngine/src/OloEngine/Renderer/RenderPipeline.cpp b/OloEngine/src/OloEngine/Renderer/RenderPipeline.cpp index 534bd371d..13968dcfe 100644 --- a/OloEngine/src/OloEngine/Renderer/RenderPipeline.cpp +++ b/OloEngine/src/OloEngine/Renderer/RenderPipeline.cpp @@ -1565,16 +1565,25 @@ namespace OloEngine // DDGI atlas imports (issue #607): DDGIProbeUpdatePass::Setup imports // the ping-pong atlases + probe-data texture, which are created lazily // (first submitted volume) and recreated on a Resolution / - // HitCacheTexels edit — the ids change with NO pass-enable change. - // Hash the raw ids so the rebuild that (re)imports them actually + // HitCacheTexels edit — the resources change with NO pass-enable + // change. Hash them so the rebuild that (re)imports them actually // happens — the exact VirtualGeometryDebug rule below. + // + // The four atlases hash by IDENTITY, not by driver name (issue #691 + // step 3). EnsureResources calls DestroyResources BEFORE recreating, so + // the old attachment textures are gone by the time the new ones are + // made and GL may reissue the same names — under which a raw-id hash + // sees no change at all and the graph keeps an import still describing + // the OLD resolution. A generation cannot be reissued. m_ProbeDataTexture + // has no identity yet (it is a pass-owned native texture, deferred to a + // later slice) and keeps its raw id. if (FrameCorePasses.DDGIProbeUpdate) { const auto& ddgiPass = *FrameCorePasses.DDGIProbeUpdate; - HashU32(h, ddgiPass.GetIrradianceAtlasID(0u)); - HashU32(h, ddgiPass.GetIrradianceAtlasID(1u)); - HashU32(h, ddgiPass.GetVisibilityAtlasID(0u)); - HashU32(h, ddgiPass.GetVisibilityAtlasID(1u)); + HashU64(h, RHI::HashKey(ddgiPass.GetIrradianceAtlasHandle(0u))); + HashU64(h, RHI::HashKey(ddgiPass.GetIrradianceAtlasHandle(1u))); + HashU64(h, RHI::HashKey(ddgiPass.GetVisibilityAtlasHandle(0u))); + HashU64(h, RHI::HashKey(ddgiPass.GetVisibilityAtlasHandle(1u))); HashU32(h, ddgiPass.GetProbeDataTextureID()); } HashPassState(h, SceneCompositePasses.DeferredLighting); diff --git a/OloEngine/src/OloEngine/Renderer/RendererAPI.h b/OloEngine/src/OloEngine/Renderer/RendererAPI.h index 0ead5bf66..eb75269ff 100644 --- a/OloEngine/src/OloEngine/Renderer/RendererAPI.h +++ b/OloEngine/src/OloEngine/Renderer/RendererAPI.h @@ -163,10 +163,24 @@ namespace OloEngine // GPU-side image copy (used for staging textures to avoid read-write hazards) virtual void CopyImageSubData(u32 srcID, TextureTargetType srcTarget, u32 dstID, TextureTargetType dstTarget, u32 width, u32 height) = 0; + // Handle form — both operands together, same reasoning as + // CopyImageSubDataFull below. + virtual void CopyImageSubData(RHI::ResourceHandle src, TextureTargetType srcTarget, + RHI::ResourceHandle dst, TextureTargetType dstTarget, + u32 width, u32 height) = 0; // Full image copy with source/dest offsets (needed for cubemap face copies) virtual void CopyImageSubDataFull(u32 srcID, TextureTargetType srcTarget, i32 srcLevel, i32 srcZ, u32 dstID, TextureTargetType dstTarget, i32 dstLevel, i32 dstZ, u32 width, u32 height) = 0; + // Handle form (issue #691 step 3, slice 5 — attachment consumers). BOTH + // operands take handles together, deliberately: every caller is + // "framebuffer attachment -> persistent texture", so a mixed + // handle/native overload pair would only exist to serve a half-migrated + // chain, which is the state this migration is meant to make + // unrepresentable. + virtual void CopyImageSubDataFull(RHI::ResourceHandle src, TextureTargetType srcTarget, i32 srcLevel, i32 srcZ, + RHI::ResourceHandle dst, TextureTargetType dstTarget, i32 dstLevel, i32 dstZ, + u32 width, u32 height) = 0; // Copy from currently-bound READ framebuffer to a named texture virtual void CopyFramebufferToTexture(u32 textureID, u32 width, u32 height) = 0; @@ -351,6 +365,7 @@ namespace OloEngine // VkClearColorValue's float/uint union members. `mipLevel` clears one // level of every layer/face, matching glClearTexImage. virtual void ClearTextureFloat(u32 textureID, u32 mipLevel, const glm::vec4& color) = 0; + virtual void ClearTextureFloat(RHI::ResourceHandle texture, u32 mipLevel, const glm::vec4& color) = 0; virtual void ClearTextureUInt(u32 textureID, u32 mipLevel, u32 value) = 0; // Offset overloads of the whole-image UploadTextureSubImage2D above. // `sourceFormat` is the HOST buffer's layout, not the texture's storage @@ -369,11 +384,24 @@ namespace OloEngine [[nodiscard("Store this!")]] virtual bool ReadTextureImage(u32 textureID, u32 mipLevel, RHI::Format destFormat, sizet destSizeBytes, void* dest) = 0; + // A stale handle resolves to 0, and a readback of texture 0 fails — + // so this reports false rather than silently handing back an + // uninitialised buffer. LightProbeBaker depends on that: its + // coefficients are PERSISTED, so a bad read must abandon the bake, not + // write wrong lighting to disk. + [[nodiscard("Store this!")]] virtual bool ReadTextureImage(RHI::ResourceHandle texture, u32 mipLevel, + RHI::Format destFormat, + sizet destSizeBytes, void* dest) = 0; [[nodiscard("Store this!")]] virtual bool ReadTextureSubImage(u32 textureID, u32 mipLevel, i32 x, i32 y, i32 z, u32 width, u32 height, u32 depth, RHI::Format destFormat, sizet destSizeBytes, void* dest) = 0; + [[nodiscard("Store this!")]] virtual bool ReadTextureSubImage(RHI::ResourceHandle texture, u32 mipLevel, + i32 x, i32 y, i32 z, + u32 width, u32 height, u32 depth, + RHI::Format destFormat, + sizet destSizeBytes, void* dest) = 0; virtual void GetTextureDimensions(u32 textureID, u32 mipLevel, u32& outWidth, u32& outHeight) = 0; // Orders a texture's use as a render target against a subsequent sample // of it in the same pass. Vulkan expresses this as a pipeline barrier. diff --git a/OloEngine/src/OloEngine/Renderer/SkyCubemapBake.cpp b/OloEngine/src/OloEngine/Renderer/SkyCubemapBake.cpp index 36f1c927f..01d413164 100644 --- a/OloEngine/src/OloEngine/Renderer/SkyCubemapBake.cpp +++ b/OloEngine/src/OloEngine/Renderer/SkyCubemapBake.cpp @@ -112,10 +112,10 @@ namespace OloEngine::SkyBake vao->Bind(); RenderCommand::DrawIndexed(vao); - const u32 fbColor = framebuffer->GetColorAttachmentRendererID(0); + const RHI::ResourceHandle fbColor = framebuffer->GetColorAttachmentHandle(0); RenderCommand::CopyImageSubDataFull( fbColor, RendererAPI::TextureTargetType::Texture2D, 0, 0, - cubemap->GetRendererID(), RendererAPI::TextureTargetType::TextureCubeMap, 0, static_cast(i), + cubemap->GetRHIHandle(), RendererAPI::TextureTargetType::TextureCubeMap, 0, static_cast(i), face, face); } diff --git a/OloEngine/src/OloEngine/SaveGame/ThumbnailCapture.cpp b/OloEngine/src/OloEngine/SaveGame/ThumbnailCapture.cpp index 2ada0998e..b3b2a2cb8 100644 --- a/OloEngine/src/OloEngine/SaveGame/ThumbnailCapture.cpp +++ b/OloEngine/src/OloEngine/SaveGame/ThumbnailCapture.cpp @@ -38,9 +38,11 @@ namespace OloEngine return {}; } - // Read the color attachment (index 0) pixels via GL - u32 texID = framebuffer->GetColorAttachmentRendererID(0); - if (texID == 0) + // Read the colour attachment (index 0) back by IDENTITY, not by driver + // name (issue #691 step 3): the attachment is a distinct GPU object + // from the framebuffer, and a resize destroys and recreates it. + const RHI::ResourceHandle colorAttachment = framebuffer->GetColorAttachmentHandle(0); + if (!colorAttachment.IsValid()) { OLO_CORE_ERROR("[ThumbnailCapture] No color attachment"); return {}; @@ -50,7 +52,7 @@ namespace OloEngine // The readback reports its own success — the backend owns the error // model (a sticky global flag on GL, a per-call result on Vulkan), so // there is deliberately no facade-level GetError() to ask afterwards. - if (!RenderCommand::ReadTextureImage(texID, 0, RHI::Format::RGBA8UNorm, + if (!RenderCommand::ReadTextureImage(colorAttachment, 0, RHI::Format::RGBA8UNorm, pixelData.size(), pixelData.data())) { OLO_CORE_ERROR("[ThumbnailCapture] Failed to read back the framebuffer colour attachment"); diff --git a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp index 157c9a204..e0e52816f 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp +++ b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp @@ -1992,4 +1992,60 @@ namespace OloEngine UploadTextureSubImage2D(Utils::ResolveNativeAs(texture, RHI::ResourceKind::Texture), width, height, sourceFormat, data); } + // ------------------------------------------------------------------------- + // Handle-taking siblings of the texture copy / clear / upload-at-offset / + // readback family (issue #691 step 3, slice 5 — attachment consumers). + // + // Same shape as the block above: resolve here, delegate to the one u32 form + // that talks to GL. What made these necessary was migrating the framebuffer + // attachment getters' consumers — the bakers copy an attachment into a + // persistent Texture2D/Cubemap and the probe bakers read one back, and + // neither family appeared in the bind or create/delete survey that produced + // the earlier additions. + // ------------------------------------------------------------------------- + void OpenGLRendererAPI::CopyImageSubData(RHI::ResourceHandle src, TextureTargetType srcTarget, + RHI::ResourceHandle dst, TextureTargetType dstTarget, + u32 width, u32 height) + { + CopyImageSubData(Utils::ResolveNativeAs(src, RHI::ResourceKind::Texture), srcTarget, + Utils::ResolveNativeAs(dst, RHI::ResourceKind::Texture), dstTarget, + width, height); + } + + void OpenGLRendererAPI::CopyImageSubDataFull(RHI::ResourceHandle src, TextureTargetType srcTarget, + i32 srcLevel, i32 srcZ, + RHI::ResourceHandle dst, TextureTargetType dstTarget, + i32 dstLevel, i32 dstZ, + u32 width, u32 height) + { + CopyImageSubDataFull(Utils::ResolveNativeAs(src, RHI::ResourceKind::Texture), srcTarget, srcLevel, srcZ, + Utils::ResolveNativeAs(dst, RHI::ResourceKind::Texture), dstTarget, dstLevel, dstZ, + width, height); + } + + void OpenGLRendererAPI::ClearTextureFloat(RHI::ResourceHandle texture, u32 mipLevel, const glm::vec4& color) + { + ClearTextureFloat(Utils::ResolveNativeAs(texture, RHI::ResourceKind::Texture), mipLevel, color); + } + + bool OpenGLRendererAPI::ReadTextureImage(RHI::ResourceHandle texture, u32 mipLevel, + RHI::Format destFormat, sizet destSizeBytes, void* dest) + { + // A stale handle resolves to 0 and the u32 form reports failure for + // texture 0, so the "unbind" degradation the bind family relies on + // becomes an honest `false` here — the caller must not treat `dest` as + // populated. + return ReadTextureImage(Utils::ResolveNativeAs(texture, RHI::ResourceKind::Texture), mipLevel, + destFormat, destSizeBytes, dest); + } + + bool OpenGLRendererAPI::ReadTextureSubImage(RHI::ResourceHandle texture, u32 mipLevel, + i32 x, i32 y, i32 z, + u32 width, u32 height, u32 depth, + RHI::Format destFormat, sizet destSizeBytes, void* dest) + { + return ReadTextureSubImage(Utils::ResolveNativeAs(texture, RHI::ResourceKind::Texture), mipLevel, + x, y, z, width, height, depth, destFormat, destSizeBytes, dest); + } + } // namespace OloEngine diff --git a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h index 54d166347..ce4e6696c 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h +++ b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h @@ -84,9 +84,15 @@ namespace OloEngine void SetBlendFuncForAttachment(u32 attachment, RHI::BlendFactor src, RHI::BlendFactor dst) override; void CopyImageSubData(u32 srcID, TextureTargetType srcTarget, u32 dstID, TextureTargetType dstTarget, u32 width, u32 height) override; + void CopyImageSubData(RHI::ResourceHandle src, TextureTargetType srcTarget, + RHI::ResourceHandle dst, TextureTargetType dstTarget, + u32 width, u32 height) override; void CopyImageSubDataFull(u32 srcID, TextureTargetType srcTarget, i32 srcLevel, i32 srcZ, u32 dstID, TextureTargetType dstTarget, i32 dstLevel, i32 dstZ, u32 width, u32 height) override; + void CopyImageSubDataFull(RHI::ResourceHandle src, TextureTargetType srcTarget, i32 srcLevel, i32 srcZ, + RHI::ResourceHandle dst, TextureTargetType dstTarget, i32 dstLevel, i32 dstZ, + u32 width, u32 height) override; void CopyFramebufferToTexture(u32 textureID, u32 width, u32 height) override; void SetDrawBuffers(std::span attachments) override; void RestoreAllDrawBuffers(u32 colorAttachmentCount) override; @@ -173,6 +179,7 @@ namespace OloEngine void DeleteVertexArray(u32 vaoID) override; void ClearTextureFloat(u32 textureID, u32 mipLevel, const glm::vec4& color) override; + void ClearTextureFloat(RHI::ResourceHandle texture, u32 mipLevel, const glm::vec4& color) override; void ClearTextureUInt(u32 textureID, u32 mipLevel, u32 value) override; // Offset overload; the whole-image one is declared above. void UploadTextureSubImage2D(u32 textureID, i32 xOffset, i32 yOffset, @@ -184,11 +191,19 @@ namespace OloEngine [[nodiscard("Store this!")]] bool ReadTextureImage(u32 textureID, u32 mipLevel, RHI::Format destFormat, sizet destSizeBytes, void* dest) override; + [[nodiscard("Store this!")]] bool ReadTextureImage(RHI::ResourceHandle texture, u32 mipLevel, + RHI::Format destFormat, + sizet destSizeBytes, void* dest) override; [[nodiscard("Store this!")]] bool ReadTextureSubImage(u32 textureID, u32 mipLevel, i32 x, i32 y, i32 z, u32 width, u32 height, u32 depth, RHI::Format destFormat, sizet destSizeBytes, void* dest) override; + [[nodiscard("Store this!")]] bool ReadTextureSubImage(RHI::ResourceHandle texture, u32 mipLevel, + i32 x, i32 y, i32 z, + u32 width, u32 height, u32 depth, + RHI::Format destFormat, + sizet destSizeBytes, void* dest) override; void GetTextureDimensions(u32 textureID, u32 mipLevel, u32& outWidth, u32& outHeight) override; void TextureBarrier() override; diff --git a/OloEngine/tests/Rendering/MockRendererAPI.h b/OloEngine/tests/Rendering/MockRendererAPI.h index 04d7bd78f..8485eef47 100644 --- a/OloEngine/tests/Rendering/MockRendererAPI.h +++ b/OloEngine/tests/Rendering/MockRendererAPI.h @@ -481,6 +481,41 @@ namespace OloEngine::Testing UploadTextureSubImage2D(Native(texture), width, height, sourceFormat, data); } + // Copy / clear / upload-at-offset / readback handle forms (slice 5). + // Same "record under the u32 sibling's name" rule as the block above. + void CopyImageSubData(RHI::ResourceHandle src, TextureTargetType srcTarget, + RHI::ResourceHandle dst, TextureTargetType dstTarget, + u32 width, u32 height) override + { + CopyImageSubData(Native(src), srcTarget, Native(dst), dstTarget, width, height); + } + [[nodiscard("Store this!")]] bool ReadTextureSubImage(RHI::ResourceHandle texture, u32 mipLevel, + i32 x, i32 y, i32 z, + u32 width, u32 height, u32 depth, + RHI::Format destFormat, + sizet destSizeBytes, void* dest) override + { + return ReadTextureSubImage(Native(texture), mipLevel, x, y, z, width, height, depth, + destFormat, destSizeBytes, dest); + } + void CopyImageSubDataFull(RHI::ResourceHandle src, TextureTargetType srcTarget, i32 srcLevel, i32 srcZ, + RHI::ResourceHandle dst, TextureTargetType dstTarget, i32 dstLevel, i32 dstZ, + u32 width, u32 height) override + { + CopyImageSubDataFull(Native(src), srcTarget, srcLevel, srcZ, + Native(dst), dstTarget, dstLevel, dstZ, width, height); + } + void ClearTextureFloat(RHI::ResourceHandle texture, u32 mipLevel, const glm::vec4& color) override + { + ClearTextureFloat(Native(texture), mipLevel, color); + } + [[nodiscard("Store this!")]] bool ReadTextureImage(RHI::ResourceHandle texture, u32 mipLevel, + RHI::Format destFormat, + sizet destSizeBytes, void* dest) override + { + return ReadTextureImage(Native(texture), mipLevel, destFormat, destSizeBytes, dest); + } + private: [[nodiscard]] static u32 Native(RHI::ResourceHandle handle) noexcept { diff --git a/OloEngine/tests/Rendering/RHIResourceRegistryTest.cpp b/OloEngine/tests/Rendering/RHIResourceRegistryTest.cpp index d4702a49a..35877c246 100644 --- a/OloEngine/tests/Rendering/RHIResourceRegistryTest.cpp +++ b/OloEngine/tests/Rendering/RHIResourceRegistryTest.cpp @@ -123,6 +123,45 @@ namespace OloEngine::Tests registry.Unregister(second); } + // The cache-fingerprint corollary of the test above, and the reason + // RenderPipeline::ComputeBlackboardFingerprint hashes RHI::HashKey(handle) + // rather than the raw id (issue #691 step 3, slice 5). + // + // The concrete bug: DDGIProbeUpdatePass::EnsureResources calls + // DestroyResources() BEFORE creating the replacement atlases, so every + // attachment texture is freed first and GL is free to reissue the same + // names. Hashing those names therefore could not observe a + // Resolution/HitCacheTexels edit at all — the fingerprint never changed, + // BuildFrameGraph was never rebuilt, and the render graph kept an import + // whose Width/Height still described the old resolution (which is what + // olo_render_list_targets then reported). + // + // Note this is a STRICTLY stronger claim than `first != second` above: a + // fingerprint mixes a single integer, so it needs the *keyed* form to + // differ, not merely the handles. + TEST(RHIResourceRegistry, HashKeyDiffersAcrossADestroyRecreateThatReusesTheNativeName) + { + auto& registry = Registry(); + + constexpr u64 kRecycledNativeName = 909090u; + + const auto before = registry.Register(RHI::ResourceKind::Texture, kRecycledNativeName, RHI::Backend::OpenGL); + registry.Unregister(before); + const auto after = registry.Register(RHI::ResourceKind::Texture, kRecycledNativeName, RHI::Backend::OpenGL); + + ASSERT_EQ(before.Index, after.Index) << "Test precondition: the slot must actually be recycled"; + ASSERT_EQ(registry.ResolveNativeForBackend(after), kRecycledNativeName) + << "Test precondition: the recreate must genuinely reuse the freed native name — " + "that is the case a raw-id hash cannot see"; + + EXPECT_NE(RHI::HashKey(before), RHI::HashKey(after)) + << "A cache keyed on 'did this GPU object change' must observe a destroy/recreate even " + "when the driver reissues the name. If this ever compares equal, every fingerprint " + "built from HashKey silently stops invalidating."; + + registry.Unregister(after); + } + TEST(RHIResourceRegistry, UpdateNativeKeepsIdentityAcrossAnInPlaceReload) { // Models texture hot-reload (issue #544 Part B): the C++ object survives, diff --git a/OloEngine/tests/Rendering/rhi_boundary_baseline.json b/OloEngine/tests/Rendering/rhi_boundary_baseline.json index e2bac281e..c0d3de225 100644 --- a/OloEngine/tests/Rendering/rhi_boundary_baseline.json +++ b/OloEngine/tests/Rendering/rhi_boundary_baseline.json @@ -205,19 +205,64 @@ " slot keeps its generation, so a handle to a destroyed object goes on", " resolving to a name the driver may reissue — the recycled-name failure the", " whole layer exists to prevent, reintroduced at the site most likely to be", - " treated as mechanical." + " treated as mechanical.", + "", + "PHASE 2 STEP 3, SLICE 5 (2026-08-01): the framebuffer ATTACHMENT consumers.", + "sweep_renderer_id 699 -> 653. facade_native_id_params deliberately UNCHANGED", + "at 68: this slice adds handle overloads and deletes no u32 form, which is the", + "documented order (add overloads, migrate callers, delete the u32 forms last).", + "", + "Migrated: 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.", + "Eight sites were deliberately left native; the reason for each is tabulated in", + "docs/agent-rules/rhi-abstraction-boundary.md ('What slice 5 actually moved').", + "", + "SIX NEW FACADE VIRTUALS were needed, against a worklist that predicted zero:", + "CopyImageSubData, CopyImageSubDataFull, ClearTextureFloat, the offset form of", + "UploadTextureSubImage2D, ReadTextureImage, ReadTextureSubImage. The prediction", + "was wrong because the survey behind it counted only BIND sinks, and the", + "attachment getters also feed a copy family (bakers staging an attachment into a", + "persistent texture) and a readback family (thumbnail / probe capture). Same", + "lesson as the SetTextureFilter/SetTextureWrap/UploadTextureSubImage2D discovery", + "one slice earlier: migrate one real consumer PER SINK FAMILY and let the", + "compiler enumerate the rest.", + "", + "TWO SILENT DEFECTS FOUND, both worth knowing beyond this issue:", + "", + "1. Hashing a driver name into a cache fingerprint cannot see a", + " destroy-then-recreate. RenderPipeline hashed DDGI's atlas ids so a", + " Resolution/HitCacheTexels edit would rebuild the frame graph — but", + " EnsureResources calls DestroyResources() BEFORE creating the replacements,", + " so GL may reissue the same names and the fingerprint never changed. The", + " graph then kept an import whose Width/Height described the OLD resolution.", + " Now hashes RHI::HashKey(handle); pinned by RHIResourceRegistry's", + " HashKeyDiffersAcrossADestroyRecreateThatReusesTheNativeName. Note the", + " opposite teardown order (allocate-then-release) hides this completely, so", + " whether the bug is live depends on a line nowhere near the hash.", + "", + "2. ImportTextureHandle BLINDS RenderGraph::ResolveTexture, and that is what the", + " MCP capture endpoints read. textureID and identity are alternatives on a", + " PhysicalTexture, so a handle-imported resource resolves natively to 0 —", + " olo_render_list_targets reports id 0 and olo_render_capture_target cannot", + " find it. #732 already did this to SSAO's noise texture. Fixed with a", + " fallback through RHI::GetNativeHandleForDebug in OloEditor (which this", + " scanner does not walk, so debug_escape_hatch stays honestly 0 rather than", + " being waived). CONSEQUENCE FOR LATER SLICES: a resource's import may only", + " move to ImportTextureHandle after the diagnostics can read one — which is", + " why DDGI's importAtlas is still native here." ], "measured_on_commit": "11d7cea7", "measured_on_date": "2026-07-30", - "step3_counters_measured_on_commit": "211b64e1", - "step3_counters_measured_on_date": "2026-07-31", + "step3_counters_measured_on_commit": "5850a6a8", + "step3_counters_measured_on_date": "2026-08-01", "sweep_gl_calls": 0, "sweep_glad_includes": 0, "tools_gl_calls": 236, "debug_escape_hatch": 0, - "sweep_renderer_id": 699, + "sweep_renderer_id": 653, "facade_native_id_params": 68, "backend_resolve_hatch": 0 } diff --git a/OloEngine/tests/scripts/measure_rendererid.py b/OloEngine/tests/scripts/measure_rendererid.py index ecc1e6697..cb09cd638 100644 --- a/OloEngine/tests/scripts/measure_rendererid.py +++ b/OloEngine/tests/scripts/measure_rendererid.py @@ -20,7 +20,10 @@ import re import collections -ROOT = r"e:\repos\OloEngine-vulkan-rhi-phase2-resource-handles-691" +# Derived from this file's location (OloEngine/tests/scripts/), never hard-coded: +# the first version pinned the worktree it was written in, and reported a +# confident "TOTAL 0 across 0 files" from every other checkout. +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) PAT = re.compile(r'\w*RendererID\w*') by_spelling = collections.Counter() diff --git a/docs/agent-rules/rhi-abstraction-boundary.md b/docs/agent-rules/rhi-abstraction-boundary.md index 272465bb2..328c9a19e 100644 --- a/docs/agent-rules/rhi-abstraction-boundary.md +++ b/docs/agent-rules/rhi-abstraction-boundary.md @@ -505,29 +505,43 @@ distribution is the part that matters — the unit of work must intersect what y are counting, which twice it did not. Reproduce with `python OloEngine/tests/scripts/measure_rendererid.py` -(`\w*RendererID\w*` over `OloEngine/src` — 1196 raw across 118 files at the -time of writing; the ratchet's 699 is the same thing after stripping comments, -strings and the exempt backend): +(`\w*RendererID\w*` over `OloEngine/src`; the ratchet's `sweep_renderer_id` is +the same thing after stripping comments, strings and the exempt backend). The +script derives the repo root from its own location — the first version pinned an +absolute worktree path and reported a confident `TOTAL 0 across 0 files` +everywhere else, so **if it prints 0, check that before believing it.** + +Counts are *after* slice 5 (was 1196 across 118 files; now **1150 across 110**): | Where | Count | Note | | --- | ---: | --- | | `Platform/OpenGL/` | 412 | **Exempt.** The backend may name GL ids. | -| `Renderer/Commands/` (`RenderCommand.h` 72, `CommandDispatch.cpp` 67) | 141 | The bind-cache unit below. | -| `Renderer3DMeshSubmission.cpp` | 71 | Same dataflow as above. | -| `Scene/Scene.cpp` | 66 | | -| `Renderer3D.h` | 62 | | -| `Get{Color,Depth}AttachmentRendererID` call sites | 72 | Producer already ships (slice 3). | +| `Renderer/Commands/` (`RenderCommand.h` 72, `CommandDispatch.cpp` 67) | 141 | The bind-cache unit below. Untouched. | +| `Renderer3DMeshSubmission.cpp` | 71 | Same dataflow as above. Untouched. | +| `Scene/Scene.cpp` | 66 | Untouched (item 3). | +| `Renderer3D.h` | 62 | Untouched (item 3). | +| `Get{Color,Depth}AttachmentRendererID` call sites | 72 → **31** | Slice 5. The remainder is the deferred table below plus the native accessors kept on purpose. | By spelling: `m_RendererID` 439 (mostly backend-internal, exempt), -`GetRendererID` 325 (**the real target** — consumers), `shaderRendererID` 110, -attachment getters 72. +`GetRendererID` 325 → **319** (**the real target** — consumers), +`shaderRendererID` 110, attachment getters 71 → **31**. Suggested order, each a buildable commit: -1. **Attachment consumers** — 72 sites, producer (`GetColorAttachmentHandle`) - already exists, no new facade surface needed. Highest yield per unit of risk; - `DDGIProbeUpdatePass`'s `SetAtlasTextureParams` alone is 8 behind one - signature. +1. ~~**Attachment consumers**~~ — **DONE (slice 5)** for every site whose sink + was reachable; see "What slice 5 actually moved" below for the eight that + were not, and why leaving them is the correct call rather than a shortfall. + One prediction in this list was wrong and is worth keeping: *"no new facade + surface needed"*. Six new virtuals were needed + (`CopyImageSubData` / `CopyImageSubDataFull` / `ClearTextureFloat` / + `ReadTextureImage` / `ReadTextureSubImage` / offset-`UploadTextureSubImage2D`), + because the survey behind this table counted only *bind* sinks. The + attachment getters also feed a **copy** family (the bakers stage an + attachment into a persistent `Texture2D`/cubemap) and a **readback** family + (thumbnail / light-probe / reflection-probe capture). Same lesson as §4's + `SetTextureFilter`/`SetTextureWrap`/`UploadTextureSubImage2D` discovery, one + slice later: **you cannot enumerate a migration's facade needs by reading; + migrate one real consumer per SINK FAMILY and let the compiler tell you.** 2. **The command-layer bind cache** — one indivisible unit, see below. 3. `Scene.cpp` / `Renderer3D.h`, then the remaining passes (`ColorGrading` is a near-clone of the migrated SSAO; `Cloudscape` needs `CloudNoise` migrated @@ -543,6 +557,95 @@ Two producer gaps to close before their consumers can move: `VertexBuffer` / `GetAtlasRendererID` + raw/placeholder variants need handle siblings (blocks item 2). +### `ImportTextureHandle` BLINDS `ResolveTexture`, and `ResolveTexture` is what the MCP capture endpoints read + +**Read this before migrating any `builder.ImportTexture` call.** It is the one +finding from slice 5 that changes how later slices must be scoped, and it is +already live in `master`. + +`ImportTextureCommon` treats the native id and the identity as **alternatives, +never both** — `ImportTexture(name, id, desc)` passes `identity = {}` and +`ImportTextureHandle(name, handle, desc)` passes `textureID = 0u`. That +invariant is deliberate and correct (it is what makes `AllocateTextureHandle`'s +change detection honest — see the section below on stamping identity on +afterwards). The consequence is not: + +- `RenderGraph::ResolveTexture` ends at `m_PhysicalTextures[i].TextureID`, so it + returns **0** for a handle-imported resource; +- `Renderer3D::ResolveFrameGraphTexture` forwards to it, and +- `McpToolsRender.cpp` resolves *every* reported id through those two + (`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 removes it from the diagnostics**. +It does not fail, warn, or look different from a resource that genuinely has no +backing — the capture just reports id 0. #732 already did this to SSAO's noise +texture, which is why `olo_render_capture_target SSAONoise` cannot work today. +That 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. + +The fix is a fallback, not a new resolver: try the native id, and when it is 0 +ask the identity and go through `RHI::GetNativeHandleForDebug` — the hatch +documented in `RHIResources.h` for exactly "the introspection tools in +`Renderer/Debug/` and the MCP capture endpoints they back". It belongs in the +editor, which `RHIBoundaryRatchetTest` does not scan (it walks `OloEngine/src` +only), so `debug_escape_hatch` legitimately stays 0. + +**Sequencing rule this gives you:** a resource's import may only move to +`ImportTextureHandle` *after* the diagnostics can read a handle-imported +resource. Slice 5 therefore migrated DDGI's atlas *consumers* while leaving +`importAtlas` on the native id, and left `m_ProbeDataTexture` native entirely — +two complete chains on two currencies, which is fine, rather than one chain that +compiles and blinds a tool. + +### What slice 5 actually moved, and the eight sites it deliberately did not + +Migrated: all seven bakers (`ThumbnailCapture`, `LightProbeBaker`, +`ReflectionProbeBaker`, `IBLPrecompute`, `ImpostorBaker`, `SkyCubemapBake`, +`AssetPreviewRenderer`), the straightforward bind passes (`Fog`, `Overdraw`, +`SelectionOutline`, `Cloudscape`'s composite, `Decal`, `Bloom`), all of +`DDGIProbeUpdatePass`'s attachment reads including the `SetAtlasTextureParams` +signature, and `RenderGraph`'s attachment clear + NaN-census readback. + +Deliberately **not** migrated, each for a stated reason rather than for size: + +| Site | Why it cannot move yet | +| --- | --- | +| `SSAO` blur→AO copy, `SceneRenderPass`'s three exports, `GPUDrivenOcclusion`'s two | The *other* copy operand comes from `context.ResolveTexture` on a **transient**, and a graph transient has only a native id (the pool hands out `AcquiredInfo::RendererID`). Blocked on the transient-pool slice, not on facade surface. | +| `Cloudscape`'s raymarch source | Compared against a history id that is native (`m_HistoryTextureID` plus a `ResolveTexture`). The doc's own note — "`Cloudscape` needs `CloudNoise` migrated first" — is the same dependency. | +| `Renderer3DFrameExecution`'s HZB depth, `PlanarReflection`, `Water` | Feed `Renderer3D::` setters/`BuildCurrentOcclusionHZB`, i.e. worklist item 3. | +| `RenderGraph`'s three external-sink copies | The sink's `TextureID` is registered from outside the graph as a raw `u32`. | +| `RenderGraph`'s JSON topology dump | Reports native ids on purpose, for external tooling. | +| `Renderer/Debug/`'s two | Phase 8 relocation. | + +**Counters:** `sweep_renderer_id` 699 → **653**; `facade_native_id_params` +deliberately unchanged at 68, because this slice *adds* handle overloads and +deletes no `u32` form — that is worklist item 4's job, and the documented order. +The 46 is real but flatters the slice: the accessor name survives wherever a +native sibling is kept on purpose (DDGI's `GetIrradianceAtlasID` is still there +so `importAtlas` can call it), and 40 of the 46 are the attachment getters +themselves. Read §4's "do not use it as a progress meter" note before reading +either number as a completion fraction. + +### Hashing a driver name into a cache fingerprint cannot see a destroy-then-recreate + +Found while migrating DDGI, and general: `RenderPipeline::ComputeBlackboardFingerprint` +hashed `GetIrradianceAtlasID(ping)` so that recreating the atlases would rebuild +the frame graph and re-import them. But `DDGIProbeUpdatePass::EnsureResources` +calls `DestroyResources()` **before** creating the replacements, so every atlas +texture is freed first and GL is then free to reissue the same names — under +which the fingerprint does not change at all. The rebuild never happens and the +graph keeps an import whose `Width`/`Height` still describe the *old* resolution, +which is exactly what `olo_render_list_targets` then reports. + +Hashing `RHI::HashKey(handle)` fixes it, because a generation cannot be +reissued. **Any cache keyed on "did this GPU object change" has this bug if it +keys on the driver name and the owner frees before it allocates** — and note the +opposite ordering (allocate-then-release, as `m_IrradianceFB[i] = makeAtlasFB(…)` +would be on its own) hides it completely, so whether the bug is live depends on +a line of teardown code nowhere near the hash. + ### The command layer's bind cache is ONE unit, and its GL-name keying has already shipped a bug Scoping note for whoever migrates `Renderer/Commands/`. It looks like several From d2df533eaf8cc091c61fb0c61455c11294980641 Mon Sep 17 00:00:00 2001 From: Ole Bueker Date: Sat, 1 Aug 2026 13:17:43 +0200 Subject: [PATCH 2/6] fix(mcp): a handle-imported graph resource reported no texture at all (#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) --- OloEditor/src/MCP/McpToolsRender.cpp | 43 ++++++++++++++++++- OloEngine/src/OloEngine/Renderer/Renderer3D.h | 18 ++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/OloEditor/src/MCP/McpToolsRender.cpp b/OloEditor/src/MCP/McpToolsRender.cpp index 5605da308..1f0afc68d 100644 --- a/OloEditor/src/MCP/McpToolsRender.cpp +++ b/OloEditor/src/MCP/McpToolsRender.cpp @@ -38,6 +38,7 @@ #include "OloEngine/Renderer/Passes/CommandBufferRenderPass.h" #include "OloEngine/Renderer/Passes/VolumetricFogPass.h" #include "OloEngine/Renderer/RenderGraph.h" +#include "OloEngine/Renderer/RHI/RHIResources.h" #include "OloEngine/Renderer/TransientPool.h" #include "OloEngine/Renderer/Renderer2D.h" #include "OloEngine/Renderer/Renderer3D.h" @@ -272,6 +273,32 @@ namespace OloEngine::MCP return "Graphics"; } + // "Whichever currency this graph resource carries" (issue #691 step 3). + // + // A PhysicalTexture holds a native id OR an identity, never both, so + // RenderGraph::ResolveTexture answers 0 for anything imported through + // ImportTextureHandle — and every id this file reports went through it. + // Without this fallback, migrating a resource's import DELETES it from + // olo_render_list_targets / olo_render_capture_target with no warning + // and no visible difference from a resource that has no backing at all. + // (#732 did exactly that to SSAO's noise texture.) + // + // GetNativeHandleForDebug is the sanctioned hatch for precisely this — + // see RHIResources.h, which names "the MCP capture endpoints" as a + // legitimate caller. It is used HERE, in the editor, and not in + // Renderer/: RHIBoundaryRatchetTest walks OloEngine/src only, so + // debug_escape_hatch stays honest at 0 rather than being waived. + u32 NativeIdOfGraphTexture(const RenderGraph& graph, RGTextureHandle handle) + { + if (const u32 nativeId = graph.ResolveTexture(handle); nativeId != 0) + return nativeId; + + const RHI::ResourceHandle identity = graph.ResolveTextureHandle(handle); + if (!identity.IsValid()) + return 0; + return static_cast(RHI::GetNativeHandleForDebug(identity).Value); + } + // Layers addressable through one render-graph resource name, and the layer // it addresses inside its parent texture object (issue #607). Shared by // olo_render_list_targets (which reports the count so an agent can @@ -414,7 +441,7 @@ namespace OloEngine::MCP // frame's (transients can re-alias next frame). if (resource.TextureHandle.IsValid()) { - info.GLTextureId = graph->ResolveTexture(resource.TextureHandle); + info.GLTextureId = NativeIdOfGraphTexture(*graph, resource.TextureHandle); info.ViewOfParentLayer = graph->GetTextureViewLayerIndex(resource.Name); } if (resource.FramebufferHandle.IsValid()) @@ -474,6 +501,18 @@ namespace OloEngine::MCP if (const u32 textureId = Renderer3D::ResolveFrameGraphTexture(name); textureId != 0) return textureId; + // Same fallback as NativeIdOfGraphTexture, by name rather than by + // handle — this path is what olo_render_capture_target uses. + if (const RHI::ResourceHandle identity = Renderer3D::ResolveFrameGraphTextureHandle(name); + identity.IsValid()) + { + if (const auto nativeId = static_cast(RHI::GetNativeHandleForDebug(identity).Value); + nativeId != 0) + { + return nativeId; + } + } + const Ref framebuffer = Renderer3D::ResolveFrameGraphFramebuffer(name); if (!framebuffer) return 0; @@ -3696,7 +3735,7 @@ namespace OloEngine::MCP RenderValidate::ResourceIdentity identity; identity.Name = resource.Name; if (resource.TextureHandle.IsValid()) - identity.GLTextureId = graph->ResolveTexture(resource.TextureHandle); + identity.GLTextureId = NativeIdOfGraphTexture(*graph, resource.TextureHandle); else if (resource.FramebufferHandle.IsValid()) identity.GLTextureId = ResolveTargetTexture(resource.Name); if (resource.BufferHandle.IsValid()) diff --git a/OloEngine/src/OloEngine/Renderer/Renderer3D.h b/OloEngine/src/OloEngine/Renderer/Renderer3D.h index ce3954332..ecbc9200d 100644 --- a/OloEngine/src/OloEngine/Renderer/Renderer3D.h +++ b/OloEngine/src/OloEngine/Renderer/Renderer3D.h @@ -814,6 +814,24 @@ namespace OloEngine return s_Data.RGraph->ResolveTexture(s_Data.RGraph->GetTextureHandle(resourceName)); } + // Identity sibling of the above (issue #691 step 3). NOT interchangeable + // with it: `textureID` and `identity` are ALTERNATIVES on a + // PhysicalTexture, so a resource imported through ImportTextureHandle + // has an identity and NO native id — ResolveTexture answers 0 for it — + // while a natively-imported one is the other way round. A caller that + // wants "whichever this resource happens to carry" must try both; the + // MCP diagnostics do exactly that, and must, or migrating an import + // silently deletes the resource from olo_render_capture_target. + static RHI::ResourceHandle ResolveFrameGraphTextureHandle(std::string_view resourceName) + { + if (!s_Data.RGraph) + { + return RHI::NullResource; + } + + return s_Data.RGraph->ResolveTextureHandle(s_Data.RGraph->GetTextureHandle(resourceName)); + } + // Dynamic Resolution Scaling. // scale is clamped to [0.25, 1.0]; use 1.0 to disable DRS. // The render graph forwards the scale to all registered render passes From 5f9fce01b3e377af8d7282da5ba00eb70c999550 Mon Sep 17 00:00:00 2001 From: Ole Bueker Date: Sat, 1 Aug 2026 14:13:24 +0200 Subject: [PATCH 3/6] refactor(mcp): move the diagnostics identity fallback somewhere testable (#691) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- OloEditor/src/MCP/McpToolsRender.cpp | 48 +++---------- OloEngine/src/CMakeLists.txt | 2 + .../Debug/RenderGraphResourceIdentity.cpp | 31 ++++++++ .../Debug/RenderGraphResourceIdentity.h | 65 +++++++++++++++++ OloEngine/tests/Rendering/RenderGraphTest.cpp | 70 +++++++++++++++++++ docs/agent-rules/rhi-abstraction-boundary.md | 20 +++++- 6 files changed, 195 insertions(+), 41 deletions(-) create mode 100644 OloEngine/src/OloEngine/Renderer/Debug/RenderGraphResourceIdentity.cpp create mode 100644 OloEngine/src/OloEngine/Renderer/Debug/RenderGraphResourceIdentity.h diff --git a/OloEditor/src/MCP/McpToolsRender.cpp b/OloEditor/src/MCP/McpToolsRender.cpp index 1f0afc68d..4ff80a441 100644 --- a/OloEditor/src/MCP/McpToolsRender.cpp +++ b/OloEditor/src/MCP/McpToolsRender.cpp @@ -38,7 +38,7 @@ #include "OloEngine/Renderer/Passes/CommandBufferRenderPass.h" #include "OloEngine/Renderer/Passes/VolumetricFogPass.h" #include "OloEngine/Renderer/RenderGraph.h" -#include "OloEngine/Renderer/RHI/RHIResources.h" +#include "OloEngine/Renderer/Debug/RenderGraphResourceIdentity.h" #include "OloEngine/Renderer/TransientPool.h" #include "OloEngine/Renderer/Renderer2D.h" #include "OloEngine/Renderer/Renderer3D.h" @@ -273,32 +273,6 @@ namespace OloEngine::MCP return "Graphics"; } - // "Whichever currency this graph resource carries" (issue #691 step 3). - // - // A PhysicalTexture holds a native id OR an identity, never both, so - // RenderGraph::ResolveTexture answers 0 for anything imported through - // ImportTextureHandle — and every id this file reports went through it. - // Without this fallback, migrating a resource's import DELETES it from - // olo_render_list_targets / olo_render_capture_target with no warning - // and no visible difference from a resource that has no backing at all. - // (#732 did exactly that to SSAO's noise texture.) - // - // GetNativeHandleForDebug is the sanctioned hatch for precisely this — - // see RHIResources.h, which names "the MCP capture endpoints" as a - // legitimate caller. It is used HERE, in the editor, and not in - // Renderer/: RHIBoundaryRatchetTest walks OloEngine/src only, so - // debug_escape_hatch stays honest at 0 rather than being waived. - u32 NativeIdOfGraphTexture(const RenderGraph& graph, RGTextureHandle handle) - { - if (const u32 nativeId = graph.ResolveTexture(handle); nativeId != 0) - return nativeId; - - const RHI::ResourceHandle identity = graph.ResolveTextureHandle(handle); - if (!identity.IsValid()) - return 0; - return static_cast(RHI::GetNativeHandleForDebug(identity).Value); - } - // Layers addressable through one render-graph resource name, and the layer // it addresses inside its parent texture object (issue #607). Shared by // olo_render_list_targets (which reports the count so an agent can @@ -441,7 +415,7 @@ namespace OloEngine::MCP // frame's (transients can re-alias next frame). if (resource.TextureHandle.IsValid()) { - info.GLTextureId = NativeIdOfGraphTexture(*graph, resource.TextureHandle); + info.GLTextureId = Debug::NativeTextureIdForDiagnostics(*graph, resource.TextureHandle); info.ViewOfParentLayer = graph->GetTextureViewLayerIndex(resource.Name); } if (resource.FramebufferHandle.IsValid()) @@ -501,16 +475,14 @@ namespace OloEngine::MCP if (const u32 textureId = Renderer3D::ResolveFrameGraphTexture(name); textureId != 0) return textureId; - // Same fallback as NativeIdOfGraphTexture, by name rather than by - // handle — this path is what olo_render_capture_target uses. - if (const RHI::ResourceHandle identity = Renderer3D::ResolveFrameGraphTextureHandle(name); - identity.IsValid()) + // Same fallback, by name rather than by handle — this path is what + // olo_render_capture_target uses, and the by-name lookups live on + // Renderer3D rather than on RenderGraph. + if (const u32 nativeId = + Debug::NativeTextureIdForDiagnostics(Renderer3D::ResolveFrameGraphTextureHandle(name)); + nativeId != 0) { - if (const auto nativeId = static_cast(RHI::GetNativeHandleForDebug(identity).Value); - nativeId != 0) - { - return nativeId; - } + return nativeId; } const Ref framebuffer = Renderer3D::ResolveFrameGraphFramebuffer(name); @@ -3735,7 +3707,7 @@ namespace OloEngine::MCP RenderValidate::ResourceIdentity identity; identity.Name = resource.Name; if (resource.TextureHandle.IsValid()) - identity.GLTextureId = NativeIdOfGraphTexture(*graph, resource.TextureHandle); + identity.GLTextureId = Debug::NativeTextureIdForDiagnostics(*graph, resource.TextureHandle); else if (resource.FramebufferHandle.IsValid()) identity.GLTextureId = ResolveTargetTexture(resource.Name); if (resource.BufferHandle.IsValid()) diff --git a/OloEngine/src/CMakeLists.txt b/OloEngine/src/CMakeLists.txt index 792ba7850..b8e620f31 100644 --- a/OloEngine/src/CMakeLists.txt +++ b/OloEngine/src/CMakeLists.txt @@ -727,6 +727,8 @@ "OloEngine/Renderer/Debug/RenderGraphFrameCapture.cpp" "OloEngine/Renderer/Debug/RenderGraphPassSnapshot.h" "OloEngine/Renderer/Debug/RenderGraphPassSnapshot.cpp" + "OloEngine/Renderer/Debug/RenderGraphResourceIdentity.h" + "OloEngine/Renderer/Debug/RenderGraphResourceIdentity.cpp" "OloEngine/Renderer/Debug/CapturedFrameData.h" "OloEngine/Renderer/Debug/CapturedFrameData.cpp" "OloEngine/Renderer/Debug/CommandPacketDebugger.h" diff --git a/OloEngine/src/OloEngine/Renderer/Debug/RenderGraphResourceIdentity.cpp b/OloEngine/src/OloEngine/Renderer/Debug/RenderGraphResourceIdentity.cpp new file mode 100644 index 000000000..8a3cbb905 --- /dev/null +++ b/OloEngine/src/OloEngine/Renderer/Debug/RenderGraphResourceIdentity.cpp @@ -0,0 +1,31 @@ +#include "OloEnginePCH.h" +#include "OloEngine/Renderer/Debug/RenderGraphResourceIdentity.h" + +#include "OloEngine/Renderer/RHI/RHIResources.h" +#include "OloEngine/Renderer/RenderGraph.h" + +namespace OloEngine::Debug +{ + u32 NativeTextureIdForDiagnostics(const RenderGraph& graph, const RGTextureHandle handle) + { + // Native first, deliberately: it is the currency the overwhelming + // majority of resources still carry, and asking the registry costs a + // bounds + generation check per resource on a path that runs over every + // registered resource in the graph. + if (const u32 nativeId = graph.ResolveTexture(handle); nativeId != 0) + return nativeId; + + return NativeTextureIdForDiagnostics(graph.ResolveTextureHandle(handle)); + } + + u32 NativeTextureIdForDiagnostics(const RHI::ResourceHandle identity) + { + if (!identity.IsValid()) + return 0; + + // A stale handle resolves to 0 here rather than to a name the driver + // may since have reissued — so a diagnostic never reports a live object + // for a dead resource, which would be worse than reporting nothing. + return static_cast(RHI::GetNativeHandleForDebug(identity).Value); + } +} // namespace OloEngine::Debug diff --git a/OloEngine/src/OloEngine/Renderer/Debug/RenderGraphResourceIdentity.h b/OloEngine/src/OloEngine/Renderer/Debug/RenderGraphResourceIdentity.h new file mode 100644 index 000000000..6cdb8b45e --- /dev/null +++ b/OloEngine/src/OloEngine/Renderer/Debug/RenderGraphResourceIdentity.h @@ -0,0 +1,65 @@ +#pragma once + +// ============================================================================= +// RenderGraphResourceIdentity.h +// +// "What backend-native object backs this render-graph texture resource?" — +// asked by the introspection tools and the MCP capture endpoints they back +// (issue #691 Phase 2 step 3). +// +// WHY THIS EXISTS AS A SHARED FUNCTION RATHER THAN AN INLINE AT EACH CALLER. +// A PhysicalTexture carries a native id OR an identity, never both: +// ImportTexture supplies the first and leaves the identity null, +// ImportTextureHandle supplies the second and leaves TextureID at 0. That rule +// is deliberate — it is what keeps AllocateTextureHandle's change detection +// honest (see the ImportTextureCommon comment). Its consequence is that +// RenderGraph::ResolveTexture answers **0** for a handle-imported resource, so +// a caller that wants "whichever currency this resource happens to carry" has +// to try both, and one that does not SILENTLY LOSES the resource: it reports +// id 0, which is indistinguishable from a resource with no backing at all. +// +// That already happened. #732 migrated SSAO's noise texture to +// ImportTextureHandle and thereby removed it from olo_render_list_targets and +// olo_render_capture_target, with no warning and no failing test — and those +// endpoints are how CLAUDE.md's rendering-verification rule is enforced, so +// blinding them removes the check on the very slices doing the migrating. +// +// WHY IT LIVES IN Renderer/Debug/ SPECIFICALLY. Two constraints intersect here +// and this is the only directory that satisfies both: +// +// * RHI::GetNativeHandleForDebug is baselined at zero uses outside +// Renderer/Debug/ and Platform/ (`debug_escape_hatch` in +// rhi_boundary_baseline.json). RHIResources.h names the introspection +// tools and the MCP endpoints as its legitimate callers — this is them. +// * The first home for this logic was OloEditor/src/MCP/, which +// OloEngine-Tests does not link. That left the composition untestable, +// which is precisely the configuration that let the original defect +// through. Renderer/Debug/ 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. Moving that boundary is a +// decision to take on its own merits, not a side effect of a bug fix. +// ============================================================================= + +#include "OloEngine/Core/Base.h" +#include "OloEngine/Renderer/RHI/RHITypes.h" +#include "OloEngine/Renderer/ResourceHandle.h" + +namespace OloEngine +{ + class RenderGraph; +} + +namespace OloEngine::Debug +{ + // The composition: try the native id, fall back to the identity. Returns 0 + // only when the resource genuinely has no backing in either currency — + // which is the answer a diagnostic tool should print, and the ONLY case in + // which it should. + [[nodiscard]] u32 NativeTextureIdForDiagnostics(const RenderGraph& graph, RGTextureHandle handle); + + // The identity leg on its own, for a caller that already resolved one by + // name (the graph's by-name lookups live on Renderer3D, not on RenderGraph). + [[nodiscard]] u32 NativeTextureIdForDiagnostics(RHI::ResourceHandle identity); +} // namespace OloEngine::Debug diff --git a/OloEngine/tests/Rendering/RenderGraphTest.cpp b/OloEngine/tests/Rendering/RenderGraphTest.cpp index bc0786825..0c5e81a4c 100644 --- a/OloEngine/tests/Rendering/RenderGraphTest.cpp +++ b/OloEngine/tests/Rendering/RenderGraphTest.cpp @@ -6,6 +6,7 @@ #include "RenderingTestUtils.h" #include "PropertyTests/RenderPropertyTest.h" #include "TestDeclarativeNode.h" +#include "OloEngine/Renderer/Debug/RenderGraphResourceIdentity.h" #include "OloEngine/Renderer/RGCommandContext.h" #include "OloEngine/Renderer/RenderGraph.h" #include "OloEngine/Renderer/ResourceHandle.h" @@ -872,6 +873,75 @@ TEST(RenderGraph, HandleImportResolvesAsAnIdentityAndNotNatively) registry.Unregister(identity); } +// ============================================================================= +// The DIAGNOSTICS side of the two tests above, and the reason it needs its own. +// +// Both currencies being correct is not enough: every id the introspection tools +// and the MCP capture endpoints report goes through ONE resolution, and if that +// resolution only knows the native currency then migrating a resource's import +// deletes it from the tooling. It reports 0, which is indistinguishable from a +// resource that has no backing — no warning, no failing test. #732 did exactly +// that to SSAO's noise texture. +// +// Debug::NativeTextureIdForDiagnostics is the composition that must not regress. +// It was originally written inline in OloEditor/src/MCP/, which OloEngine-Tests +// does not link — so the composition could not be tested at all, which is the +// same configuration that let the original defect through. It lives in +// Renderer/Debug/ now precisely so these two tests can exist. +// ============================================================================= +TEST(RenderGraph, DiagnosticsResolveANativelyImportedResource) +{ + RenderGraph graph; + const auto imported = graph.ImportTexture( + ResourceNames::AOBuffer, 4242u, + RGResourceDesc::FromHandleKind(RGResourceHandle::Kind::Texture2D, ResourceNames::AOBuffer)); + ASSERT_TRUE(imported.IsValid()); + + EXPECT_EQ(Debug::NativeTextureIdForDiagnostics(graph, imported), 4242u) + << "The native currency must still work — the fallback is additive, not a replacement."; +} + +TEST(RenderGraph, DiagnosticsResolveAHandleImportedResourceTheNativePathCannotSee) +{ + auto& registry = RHI::ResourceRegistry::Get(); + const auto identity = registry.Register(RHI::ResourceKind::Texture, 6161u, RHI::Backend::OpenGL); + + RenderGraph graph; + const auto imported = graph.ImportTextureHandle( + ResourceNames::AOBuffer, identity, + RGResourceDesc::FromHandleKind(RGResourceHandle::Kind::Texture2D, ResourceNames::AOBuffer)); + ASSERT_TRUE(imported.IsValid()); + + ASSERT_EQ(graph.ResolveTexture(imported), 0u) + << "Test precondition: this is the case the native path structurally cannot answer."; + + EXPECT_EQ(Debug::NativeTextureIdForDiagnostics(graph, imported), 6161u) + << "A handle-imported resource must still report its backing object to the tooling. " + "Returning 0 here is the regression that silently removes a migrated resource from " + "olo_render_list_targets and olo_render_capture_target."; + + registry.Unregister(identity); +} + +TEST(RenderGraph, DiagnosticsReportZeroOnlyWhenThereIsGenuinelyNoBacking) +{ + RenderGraph graph; + + EXPECT_EQ(Debug::NativeTextureIdForDiagnostics(graph, RGTextureHandle{}), 0u) + << "An invalid handle names nothing."; + EXPECT_EQ(Debug::NativeTextureIdForDiagnostics(RHI::NullResource), 0u) + << "A null identity names nothing."; + + // A RETIRED identity must also report nothing rather than the name the + // driver may since have reissued — a diagnostic showing a live object for a + // dead resource is worse than one showing none. + auto& registry = RHI::ResourceRegistry::Get(); + const auto identity = registry.Register(RHI::ResourceKind::Texture, 7373u, RHI::Backend::OpenGL); + ASSERT_EQ(Debug::NativeTextureIdForDiagnostics(identity), 7373u); + registry.Unregister(identity); + EXPECT_EQ(Debug::NativeTextureIdForDiagnostics(identity), 0u); +} + // Re-importing the SAME name with a DIFFERENT identity must retire the old // RGTextureHandle, exactly as re-importing with a different native id does. // diff --git a/docs/agent-rules/rhi-abstraction-boundary.md b/docs/agent-rules/rhi-abstraction-boundary.md index 328c9a19e..0008d189b 100644 --- a/docs/agent-rules/rhi-abstraction-boundary.md +++ b/docs/agent-rules/rhi-abstraction-boundary.md @@ -588,9 +588,23 @@ removes the check on itself. The fix is a fallback, not a new resolver: try the native id, and when it is 0 ask the identity and go through `RHI::GetNativeHandleForDebug` — the hatch documented in `RHIResources.h` for exactly "the introspection tools in -`Renderer/Debug/` and the MCP capture endpoints they back". It belongs in the -editor, which `RHIBoundaryRatchetTest` does not scan (it walks `OloEngine/src` -only), so `debug_escape_hatch` legitimately stays 0. +`Renderer/Debug/` and the MCP capture endpoints they back". It lives in +`Renderer/Debug/RenderGraphResourceIdentity.{h,cpp}` as +`Debug::NativeTextureIdForDiagnostics`. + +**Where it lives is the load-bearing part, and it was wrong first.** The +obvious home is the caller — `OloEditor/src/MCP/`, which `RHIBoundaryRatchetTest` +does not scan, so `debug_escape_hatch` stays 0 for free. That is what this fix +did initially and it is a trap: `OloEngine-Tests` does not link `OloEditor`, so +the *composition* had no test — only its individual legs did. That is precisely +the configuration that let the original defect through, so "fixing" it there +re-arms the same trap one layer out. `Renderer/Debug/` satisfies both +constraints at once: it is a sanctioned home for the hatch **and** it is inside +the engine library, so `RenderGraph.Diagnostics*` can pin it. + +Do **not** make it a `RenderGraph` member. That puts the hatch inside +`Renderer/`, where `backend_resolve_hatch` bans it at 0 — moving that boundary +is a decision on its own merits, not a side effect of a bug fix. **Sequencing rule this gives you:** a resource's import may only move to `ImportTextureHandle` *after* the diagnostics can read a handle-imported From 35a662513911b5993527198ba0be6867e4c7961f Mon Sep 17 00:00:00 2001 From: Ole Bueker Date: Sat, 1 Aug 2026 19:01:11 +0200 Subject: [PATCH 4/6] refactor(rhi): migrate the command-layer bind cache to identities (#691) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- OloEditor/src/MCP/McpToolsRender.cpp | 22 +- .../src/OloEngine/Renderer/CloudShadowMap.cpp | 36 +- .../src/OloEngine/Renderer/CloudShadowMap.h | 10 +- .../Renderer/Commands/CommandBucket.h | 9 +- .../Renderer/Commands/CommandDispatch.cpp | 361 ++++++++++-------- .../Renderer/Commands/CommandDispatch.h | 12 +- .../Renderer/Commands/RenderCommand.h | 130 ++++--- .../Renderer/DDGI/DDGIProbeUpdatePass.cpp | 17 +- .../Renderer/DDGI/DDGIProbeUpdatePass.h | 6 +- .../Renderer/Debug/CommandPacketDebugger.cpp | 29 +- .../Renderer/Instancing/GPUFrustumCuller.h | 4 +- .../Renderer/Ocean/OceanFFTField.cpp | 10 + .../OloEngine/Renderer/Ocean/OceanFFTField.h | 5 + .../Renderer/Passes/DecalRenderPass.cpp | 4 +- .../Renderer/Passes/DeferredLightingPass.cpp | 4 +- .../Renderer/Passes/FluidCompositePass.cpp | 10 +- .../Passes/PlanarReflectionRenderPass.cpp | 6 +- .../Renderer/Passes/ShadowRenderPass.cpp | 20 +- .../Renderer/Passes/ShadowRenderPass.h | 24 +- .../Renderer/Passes/ToneMapRenderPass.cpp | 4 +- .../Renderer/Passes/WaterRenderPass.cpp | 6 +- .../src/OloEngine/Renderer/RenderCommand.h | 31 +- .../src/OloEngine/Renderer/RenderPipeline.cpp | 52 +-- OloEngine/src/OloEngine/Renderer/Renderer3D.h | 159 +++++--- .../Renderer/Renderer3DMeshSubmission.cpp | 100 ++--- .../Renderer/Renderer3DSpecializedDraws.cpp | 26 +- .../OloEngine/Renderer/Renderer3DState.cpp | 41 +- .../Renderer/Renderer3DUtilityDraws.cpp | 42 +- .../src/OloEngine/Renderer/RendererAPI.h | 21 +- .../OloEngine/Renderer/Shadow/ShadowMap.cpp | 98 ++++- .../src/OloEngine/Renderer/Shadow/ShadowMap.h | 39 ++ OloEngine/src/OloEngine/Scene/Scene.cpp | 152 ++++---- .../OloEngine/Snow/SnowAccumulationSystem.cpp | 11 + .../OloEngine/Snow/SnowAccumulationSystem.h | 4 + .../Terrain/Foliage/FoliageRenderer.cpp | 8 +- .../Terrain/Foliage/FoliageRenderer.h | 9 +- .../src/Platform/OpenGL/OpenGLRendererAPI.cpp | 50 ++- .../src/Platform/OpenGL/OpenGLRendererAPI.h | 10 +- .../src/Platform/OpenGL/OpenGLTexture.cpp | 6 +- .../Platform/OpenGL/OpenGLTextureCubemap.cpp | 2 +- .../tests/Rendering/CommandBucketTest.cpp | 44 +-- .../tests/Rendering/FrameDataBufferTest.cpp | 68 ++-- OloEngine/tests/Rendering/MockRendererAPI.h | 34 +- OloEngine/tests/Rendering/PODCommandTest.cpp | 16 +- .../WaterStaleTexturePublicationTest.cpp | 14 +- .../Rendering/RenderGraphFingerprintTest.cpp | 27 +- .../tests/Rendering/RenderingTestUtils.h | 20 +- .../tests/Rendering/WaterRenderingTest.cpp | 25 +- .../Rendering/rhi_boundary_baseline.json | 49 ++- docs/agent-rules/rhi-abstraction-boundary.md | 146 +++++-- 50 files changed, 1301 insertions(+), 732 deletions(-) diff --git a/OloEditor/src/MCP/McpToolsRender.cpp b/OloEditor/src/MCP/McpToolsRender.cpp index 4ff80a441..2c0cdb39e 100644 --- a/OloEditor/src/MCP/McpToolsRender.cpp +++ b/OloEditor/src/MCP/McpToolsRender.cpp @@ -4133,18 +4133,18 @@ namespace OloEngine::MCP u32 submeshIndex, std::string_view source) { Json textures; - textures["albedo"] = data.albedoMapID; - textures["metallicRoughness"] = data.metallicRoughnessMapID; - textures["normal"] = data.normalMapID; - textures["ao"] = data.aoMapID; - textures["emissive"] = data.emissiveMapID; + textures["albedo"] = fmt::format("{}", data.albedoMapID); + textures["metallicRoughness"] = fmt::format("{}", data.metallicRoughnessMapID); + textures["normal"] = fmt::format("{}", data.normalMapID); + textures["ao"] = fmt::format("{}", data.aoMapID); + textures["emissive"] = fmt::format("{}", data.emissiveMapID); Json useMaps; - useMaps["useAlbedoMap"] = data.albedoMapID != 0; - useMaps["useMetallicRoughnessMap"] = data.metallicRoughnessMapID != 0; - useMaps["useNormalMap"] = data.normalMapID != 0; - useMaps["useAOMap"] = data.aoMapID != 0; - useMaps["useEmissiveMap"] = data.emissiveMapID != 0; + useMaps["useAlbedoMap"] = data.albedoMapID.IsValid(); + useMaps["useMetallicRoughnessMap"] = data.metallicRoughnessMapID.IsValid(); + useMaps["useNormalMap"] = data.normalMapID.IsValid(); + useMaps["useAOMap"] = data.aoMapID.IsValid(); + useMaps["useEmissiveMap"] = data.emissiveMapID.IsValid(); Json j; j["submesh"] = submeshIndex; @@ -4265,7 +4265,7 @@ namespace OloEngine::MCP source = "MeshSource imported material (per-submesh)"; } - const PODMaterialData data = Renderer3D::CreatePODMaterialDataForMaterial(*material, 0); + const PODMaterialData data = Renderer3D::CreatePODMaterialDataForMaterial(*material, RHI::NullResource); submeshes.push_back(ResolvedMaterialJson(*material, data, index, source)); } diff --git a/OloEngine/src/OloEngine/Renderer/CloudShadowMap.cpp b/OloEngine/src/OloEngine/Renderer/CloudShadowMap.cpp index 2f13483b0..3b3feaed7 100644 --- a/OloEngine/src/OloEngine/Renderer/CloudShadowMap.cpp +++ b/OloEngine/src/OloEngine/Renderer/CloudShadowMap.cpp @@ -31,15 +31,15 @@ namespace OloEngine // Lazy-create GPU resources on the first call (raw-id handling // mirrors SSAORenderPass::CreateNoiseTexture; the render pipeline // owns the call site so a live GL context is guaranteed). - if (s_Data.m_TextureID == 0 || !s_Data.m_GenerateShader) + if (!s_Data.m_Texture.IsValid() || !s_Data.m_GenerateShader) { - if (s_Data.m_TextureID == 0) + if (!s_Data.m_Texture.IsValid()) { - s_Data.m_TextureID = RenderCommand::CreateTexture2D(kShadowResolution, kShadowResolution, RHI::Format::R8UNorm); - if (s_Data.m_TextureID != 0) + s_Data.m_Texture = RenderCommand::CreateTexture2DHandle(kShadowResolution, kShadowResolution, RHI::Format::R8UNorm); + if (s_Data.m_Texture.IsValid()) { - RenderCommand::SetTextureFilter(s_Data.m_TextureID, RHI::Filter::Linear, RHI::Filter::Linear); - RenderCommand::SetTextureWrap(s_Data.m_TextureID, RHI::AddressMode::ClampToEdge); + RenderCommand::SetTextureFilter(s_Data.m_Texture, RHI::Filter::Linear, RHI::Filter::Linear); + RenderCommand::SetTextureWrap(s_Data.m_Texture, RHI::AddressMode::ClampToEdge); } } if (!s_Data.m_GenerateShader) @@ -47,17 +47,17 @@ namespace OloEngine s_Data.m_GenerateShader = ComputeShader::Create("assets/shaders/compute/CloudShadow_Generate.comp"); } - const bool textureValid = s_Data.m_TextureID != 0; + const bool textureValid = s_Data.m_Texture.IsValid(); const bool shaderValid = s_Data.m_GenerateShader && s_Data.m_GenerateShader->IsValid(); if (!textureValid || !shaderValid) { OLO_CORE_ERROR("CloudShadowMap::Update failed — {}", !shaderValid ? "CloudShadow_Generate.comp could not be loaded/compiled" : "R8 shadow texture could not be created"); - if (s_Data.m_TextureID != 0) + if (s_Data.m_Texture.IsValid()) { - RenderCommand::DeleteTexture(s_Data.m_TextureID); - s_Data.m_TextureID = 0; + RenderCommand::DeleteTexture(s_Data.m_Texture); + s_Data.m_Texture = {}; } s_Data.m_GenerateShader = nullptr; s_Data.m_CreationFailed = true; @@ -80,7 +80,7 @@ namespace OloEngine s_Data.m_GenerateShader->SetFloat("u_ShadowWorldSize", worldSize); s_Data.m_GenerateShader->SetInt("u_ShadowResolution", static_cast(kShadowResolution)); - RenderCommand::BindImageTexture(0, s_Data.m_TextureID, 0, false, 0, RHI::Access::StorageWrite, RHI::Format::R8UNorm); + RenderCommand::BindImageTexture(0, s_Data.m_Texture, 0, false, 0, RHI::Access::StorageWrite, RHI::Format::R8UNorm); constexpr u32 kGroups = (kShadowResolution + kLocalSize - 1) / kLocalSize; RenderCommand::DispatchCompute(kGroups, kGroups, 1); @@ -98,9 +98,9 @@ namespace OloEngine { OLO_PROFILE_FUNCTION(); - const bool hadState = s_Data.m_TextureID != 0 || s_Data.m_GenerateShader || s_Data.m_CreationFailed; + const bool hadState = s_Data.m_Texture.IsValid() || s_Data.m_GenerateShader || s_Data.m_CreationFailed; - if (s_Data.m_TextureID != 0) + if (s_Data.m_Texture.IsValid()) { // The shadow map is bound through the PBR mesh dispatch's TRACKED // path (CommandDispatch::SetCloudShadowTextureID), so drop any @@ -108,11 +108,11 @@ namespace OloEngine // deleted — a future bind with a recycled GL ID must not be // skipped against stale tracking (the same contract the // OpenGLTexture2D destructor honors). - CommandDispatch::InvalidateTextureBinding(s_Data.m_TextureID); - RenderCommand::DeleteTexture(s_Data.m_TextureID); + CommandDispatch::InvalidateTextureBinding(s_Data.m_Texture); + RenderCommand::DeleteTexture(s_Data.m_Texture); } s_Data.m_GenerateShader = nullptr; - s_Data.m_TextureID = 0; + s_Data.m_Texture = {}; s_Data.m_Center = glm::vec2(0.0f, 0.0f); s_Data.m_WorldSize = 0.0f; s_Data.m_Ready = false; @@ -129,9 +129,9 @@ namespace OloEngine return s_Data.m_Ready; } - u32 CloudShadowMap::GetTextureID() + RHI::ResourceHandle CloudShadowMap::GetTextureHandle() { - return s_Data.m_Ready ? s_Data.m_TextureID : 0; + return s_Data.m_Ready ? s_Data.m_Texture : RHI::NullResource; } glm::vec2 CloudShadowMap::GetCenter() diff --git a/OloEngine/src/OloEngine/Renderer/CloudShadowMap.h b/OloEngine/src/OloEngine/Renderer/CloudShadowMap.h index 795cf7883..096305bae 100644 --- a/OloEngine/src/OloEngine/Renderer/CloudShadowMap.h +++ b/OloEngine/src/OloEngine/Renderer/CloudShadowMap.h @@ -1,6 +1,7 @@ #pragma once #include "OloEngine/Core/Base.h" +#include "OloEngine/Renderer/RHI/RHITypes.h" #include "OloEngine/Core/Ref.h" #include @@ -59,7 +60,7 @@ namespace OloEngine [[nodiscard]] static bool IsReady(); /// @return GL renderer id of the R8 512² shadow map (0 when not ready). - [[nodiscard]] static u32 GetTextureID(); + [[nodiscard]] static RHI::ResourceHandle GetTextureHandle(); /// @return world-XZ center of the current map (texel-snapped). [[nodiscard]] static glm::vec2 GetCenter(); @@ -71,7 +72,12 @@ namespace OloEngine struct CloudShadowMapData { Ref m_GenerateShader; - u32 m_TextureID = 0; // raw GL R8 512², owned (RenderCommand::CreateTexture2D / DeleteTexture) + // Owned R8 512² identity (issue #691 step 3, slice 6). Migrated off the + // raw GL name because it is bound through CommandDispatch's redundant-bind + // cache, which now keys on identities — feeding it a native id would not + // compile, and half-migrating the chain would leave a step that cannot + // reach the currency the next one wants. + RHI::ResourceHandle m_Texture{}; glm::vec2 m_Center{ 0.0f, 0.0f }; f32 m_WorldSize = 0.0f; bool m_Ready = false; diff --git a/OloEngine/src/OloEngine/Renderer/Commands/CommandBucket.h b/OloEngine/src/OloEngine/Renderer/Commands/CommandBucket.h index 8d7a66461..88a378676 100644 --- a/OloEngine/src/OloEngine/Renderer/Commands/CommandBucket.h +++ b/OloEngine/src/OloEngine/Renderer/Commands/CommandBucket.h @@ -1,6 +1,7 @@ #pragma once #include "OloEngine/Core/Base.h" +#include "OloEngine/Renderer/RHI/RHITypes.h" #include "OloEngine/Memory/Platform.h" #include "CommandPacket.h" #include "CommandAllocator.h" @@ -35,7 +36,11 @@ namespace OloEngine // right — which is what made this look like a distance-dependent LOD bug). struct InstanceGroupKey { - u32 vertexArrayID = 0; + // Identity, not driver name (issue #691 step 3, slice 6): batching two + // draws together because their VAOs share a recycled GL name would + // render one mesh with the other's geometry. Two LIVE handles cannot + // collide, so this is a correctness improvement, not a retype. + RHI::ResourceHandle vertexArrayID{}; u32 indexCount = 0; u32 baseIndex = 0; u16 materialDataIndex = 0; @@ -48,7 +53,7 @@ namespace OloEngine { sizet operator()(const InstanceGroupKey& key) const { - sizet h = std::hash{}(key.vertexArrayID); + sizet h = std::hash{}(RHI::HashKey(key.vertexArrayID)); h ^= std::hash{}(key.indexCount) + 0x9e3779b9 + (h << 6) + (h >> 2); h ^= std::hash{}(key.baseIndex) + 0x9e3779b9 + (h << 6) + (h >> 2); h ^= std::hash{}(key.materialDataIndex) + 0x9e3779b9 + (h << 6) + (h >> 2); diff --git a/OloEngine/src/OloEngine/Renderer/Commands/CommandDispatch.cpp b/OloEngine/src/OloEngine/Renderer/Commands/CommandDispatch.cpp index 23042b2bc..07b18b004 100644 --- a/OloEngine/src/OloEngine/Renderer/Commands/CommandDispatch.cpp +++ b/OloEngine/src/OloEngine/Renderer/Commands/CommandDispatch.cpp @@ -72,31 +72,38 @@ namespace OloEngine // subtracts this origin so the GPU renders near 0. (0,0,0) near origin. glm::vec3 RenderOrigin = glm::vec3(0.0f); - u32 CurrentBoundShaderID = 0; - u32 CurrentBoundVAO = 0; + // The redundant-bind cache keys on IDENTITIES, not driver names + // (issue #691 step 3, slice 6). That is a correctness change, not a + // type change: GL reissues object names, so a deleted texture and a + // newly created one could compare equal here and the cache would SKIP + // a bind that genuinely had to happen. Two live handles cannot + // collide, so the Invalidate* calls below stop being load-bearing + // correctness and become the pure optimisation they read as. + RHI::ResourceHandle CurrentBoundShader{}; + RHI::ResourceHandle CurrentBoundVAO{}; u16 LastRenderStateIndex = INVALID_RENDER_STATE_INDEX; u16 LastMaterialDataIndex = INVALID_MATERIAL_DATA_INDEX; - std::array BoundTextureIDs = { 0 }; + std::array BoundTextures{}; u32 CurrentViewportWidth = 0; u32 CurrentViewportHeight = 0; // Track currently bound UBO renderer IDs per binding point to avoid // redundant glBindBufferBase calls. Indexed by ShaderBindingLayout::UBO_*. static constexpr u32 MAX_TRACKED_UBO_BINDINGS = 8; - std::array BoundUBOIDs = { 0 }; + std::array BoundUBOs{}; - // Shadow texture renderer IDs (set per-frame) - u32 CSMShadowTextureID = 0; - u32 AtlasShadowTextureID = 0; + // Shadow texture identities (set per-frame) + RHI::ResourceHandle CSMShadowTexture{}; + RHI::ResourceHandle AtlasShadowTexture{}; // Comparison-OFF raw-depth views of the CSM array / shadow atlas (PCSS blocker search) - u32 CSMRawShadowTextureID = 0; - u32 AtlasRawShadowTextureID = 0; + RHI::ResourceHandle CSMRawShadowTexture{}; + RHI::ResourceHandle AtlasRawShadowTexture{}; // Snow accumulation depth texture (set per-frame) - u32 SnowDepthTextureID = 0; + RHI::ResourceHandle SnowDepthTexture{}; // Cloud shadow transmittance map (set per-frame, issue #633) - u32 CloudShadowTextureID = 0; + RHI::ResourceHandle CloudShadowTexture{}; // Depth prepass override: when true, ApplyPODRenderState forces depth-only state bool DepthPrepassActive = false; @@ -127,7 +134,7 @@ namespace OloEngine { if (bindingPoint < CommandDispatchData::MAX_TRACKED_UBO_BINDINGS) { - s_Data.BoundUBOIDs[bindingPoint] = 0; + s_Data.BoundUBOs[bindingPoint] = RHI::NullResource; } } @@ -135,56 +142,74 @@ namespace OloEngine { // For a pass that binds a texture unit with RAW GL (glBindTextureUnit) behind this // cache's back. The cache would otherwise still claim the slot holds whatever it last - // put there, and BindTrackedTextureUnit would SKIP the real bind if the next texture - // happens to have that same GL ID — leaving the raw-bound texture live in the slot. + // put there, and BindTrackedTextureUnit would SKIP the real bind — leaving the + // raw-bound texture live in the slot. // // That is exactly what VirtualGeometryPass hit: it binds the Hi-Z pyramid to unit 0 // for the cull compute, and unit 0 is also u_AlbedoMap. Any material whose albedo ID // matched the stale cache entry silently sampled the HZB depth texture as its albedo. - if (slot < s_Data.BoundTextureIDs.size()) + // + // Still required after the identity migration: the raw binder bypasses this cache + // entirely, so the cache's claim about the slot is simply untrue and no keying + // scheme can detect that from the inside. + if (slot < s_Data.BoundTextures.size()) { - s_Data.BoundTextureIDs[slot] = 0; + s_Data.BoundTextures[slot] = RHI::NullResource; } } - void CommandDispatch::InvalidateTextureBinding(u32 textureID) + void CommandDispatch::InvalidateTextureBinding(RHI::ResourceHandle texture) { - if (textureID == 0) + if (!texture.IsValid()) return; - // Clear every slot that still claims this GL ID. After glDeleteTextures - // the driver unbinds the texture, but our cached BoundTextureIDs would - // otherwise still say it's bound — causing the next BindTrackedTexture - // call (with a recycled GL ID) to skip the actual glBindTextureUnit. - for (auto& slot : s_Data.BoundTextureIDs) + + // MORE load-bearing after the identity migration, not less — the one + // place where keying on handles is WEAKER than keying on driver names, + // so it is worth being explicit about. + // + // The old hazard is gone: a deleted texture's handle is retired, so it + // can never compare equal to a live one, and the recycled-GL-name skip + // this used to guard against is now unrepresentable. + // + // But an IN-PLACE RELOAD deliberately PRESERVES the identity while + // replacing the storage behind it (ScopedResourceHandle::Sync never + // retires — see OpenGLTexture::InvalidateImpl, which is exactly this + // path). The cache would then still hold this very handle, conclude + // "already bound", and skip a bind that genuinely must happen, leaving + // the unit pointing at the deleted GL name. Under the old native-id + // keying that self-corrected, because the name changed. + // + // So: every site that recreates a texture's storage MUST call this. + for (auto& slot : s_Data.BoundTextures) { - if (slot == textureID) - slot = 0; + if (slot == texture) + slot = RHI::NullResource; } } // Conditionally bind a UBO only when the binding point has changed, // avoiding a redundant binding-point update each draw. - static void BindUBOIfNeeded(u32 bindingPoint, u32 rendererID) + static void BindUBOIfNeeded(u32 bindingPoint, RHI::ResourceHandle buffer) { if (bindingPoint < CommandDispatchData::MAX_TRACKED_UBO_BINDINGS) { - if (s_Data.BoundUBOIDs[bindingPoint] == rendererID) + if (s_Data.BoundUBOs[bindingPoint] == buffer) return; - s_Data.BoundUBOIDs[bindingPoint] = rendererID; + s_Data.BoundUBOs[bindingPoint] = buffer; } - RenderCommand::BindUniformBuffer(bindingPoint, rendererID); + RenderCommand::BindUniformBuffer(bindingPoint, buffer); } // Conditionally bind a VAO only when it differs from the currently bound one. // This cache is why the draws below use the DrawBound* family rather than the // DrawIndexedRaw(vaoID, ...) one: the latter binds the VAO itself, which would // make the cache pointless. - static void BindVAOIfNeeded(u32 vaoID) + static void BindVAOIfNeeded(RHI::ResourceHandle vertexArray) { - if (s_Data.CurrentBoundVAO != vaoID) + if (s_Data.CurrentBoundVAO != vertexArray) { - RenderCommand::BindVertexArrayRaw(vaoID); - s_Data.CurrentBoundVAO = vaoID; + RenderCommand::BindVertexArrayRaw(vertexArray); + s_Data.CurrentBoundVAO = vertexArray; } } @@ -413,12 +438,12 @@ namespace OloEngine // DSA form, where the target is a property of the texture object itself. The // 2D-vs-cubemap distinction was therefore never carrying information the // driver did not already have (issue #691 Phase 2 step 2). - static void BindTrackedTexture(RendererID textureID, u32 slot) + static void BindTrackedTexture(RHI::ResourceHandle texture, u32 slot) { - if (textureID != 0 && s_Data.BoundTextureIDs[slot] != textureID) + if (texture.IsValid() && s_Data.BoundTextures[slot] != texture) { - RenderCommand::BindTexture(slot, textureID); - s_Data.BoundTextureIDs[slot] = textureID; + RenderCommand::BindTexture(slot, texture); + s_Data.BoundTextures[slot] = texture; ++s_Data.Stats.TextureBinds; } } @@ -463,11 +488,11 @@ namespace OloEngine pbrMaterialData.RoughnessFactor = mat.roughnessFactor; pbrMaterialData.NormalScale = mat.normalScale; pbrMaterialData.OcclusionStrength = mat.occlusionStrength; - pbrMaterialData.UseAlbedoMap = mat.albedoMapID != 0 ? 1 : 0; - pbrMaterialData.UseNormalMap = mat.normalMapID != 0 ? 1 : 0; - pbrMaterialData.UseMetallicRoughnessMap = mat.metallicRoughnessMapID != 0 ? 1 : 0; - pbrMaterialData.UseAOMap = mat.aoMapID != 0 ? 1 : 0; - pbrMaterialData.UseEmissiveMap = mat.emissiveMapID != 0 ? 1 : 0; + pbrMaterialData.UseAlbedoMap = mat.albedoMapID.IsValid() ? 1 : 0; + pbrMaterialData.UseNormalMap = mat.normalMapID.IsValid() ? 1 : 0; + pbrMaterialData.UseMetallicRoughnessMap = mat.metallicRoughnessMapID.IsValid() ? 1 : 0; + pbrMaterialData.UseAOMap = mat.aoMapID.IsValid() ? 1 : 0; + pbrMaterialData.UseEmissiveMap = mat.emissiveMapID.IsValid() ? 1 : 0; pbrMaterialData.EnableIBL = mat.enableIBL ? 1 : 0; pbrMaterialData.ApplyGammaCorrection = 1; pbrMaterialData.AlphaCutoff = mat.alphaCutoff; @@ -484,14 +509,14 @@ namespace OloEngine constexpr u32 expectedSize = ShaderBindingLayout::PBRMaterialUBO::GetSize(); static_assert(sizeof(ShaderBindingLayout::PBRMaterialUBO) == expectedSize, "PBRMaterialUBO size mismatch"); s_Data.MaterialUBO->SetData(&pbrMaterialData, expectedSize); - BindUBOIfNeeded(ShaderBindingLayout::UBO_MATERIAL, s_Data.MaterialUBO->GetRendererID()); + BindUBOIfNeeded(ShaderBindingLayout::UBO_MATERIAL, s_Data.MaterialUBO->GetRHIHandle()); } } else if (s_Data.MaterialUBO) { // Even when material data hasn't changed, re-establish the binding // point (other subsystems may have overwritten it). - BindUBOIfNeeded(ShaderBindingLayout::UBO_MATERIAL, s_Data.MaterialUBO->GetRendererID()); + BindUBOIfNeeded(ShaderBindingLayout::UBO_MATERIAL, s_Data.MaterialUBO->GetRHIHandle()); } else { @@ -521,7 +546,7 @@ namespace OloEngine constexpr u32 expectedSize = ShaderBindingLayout::MaterialUBO::GetSize(); static_assert(sizeof(ShaderBindingLayout::MaterialUBO) == expectedSize, "MaterialUBO size mismatch"); s_Data.MaterialUBO->SetData(&materialData, expectedSize); - BindUBOIfNeeded(ShaderBindingLayout::UBO_MATERIAL, s_Data.MaterialUBO->GetRendererID()); + BindUBOIfNeeded(ShaderBindingLayout::UBO_MATERIAL, s_Data.MaterialUBO->GetRHIHandle()); } } else if (s_Data.MaterialUBO) @@ -529,7 +554,7 @@ namespace OloEngine // Even when material data hasn't changed, re-establish the // binding point — other subsystems (e.g. ParticleBatchRenderer) // may have overwritten UBO_MATERIAL. - BindUBOIfNeeded(ShaderBindingLayout::UBO_MATERIAL, s_Data.MaterialUBO->GetRendererID()); + BindUBOIfNeeded(ShaderBindingLayout::UBO_MATERIAL, s_Data.MaterialUBO->GetRHIHandle()); } else { @@ -547,14 +572,14 @@ namespace OloEngine // binding, updating the redundant-bind tracker and the bind stat. A 0 id is a // no-op (no texture for that slot this frame). Shared by every tracked bind so // the check/update/increment logic lives in exactly one place. - static void BindTrackedTextureUnit(u32 slot, u32 textureID) + static void BindTrackedTextureUnit(u32 slot, RHI::ResourceHandle texture) { - if (textureID == 0) + if (!texture.IsValid()) return; - if (s_Data.BoundTextureIDs[slot] != textureID) + if (s_Data.BoundTextures[slot] != texture) { - RenderCommand::BindTexture(slot, textureID); - s_Data.BoundTextureIDs[slot] = textureID; + RenderCommand::BindTexture(slot, texture); + s_Data.BoundTextures[slot] = texture; ++s_Data.Stats.TextureBinds; } } @@ -563,20 +588,20 @@ namespace OloEngine // Relies on BoundTextureIDs tracking to avoid redundant binds. static void BindShadowTextures() { - BindTrackedTextureUnit(ShaderBindingLayout::TEX_SHADOW, s_Data.CSMShadowTextureID); - BindTrackedTextureUnit(ShaderBindingLayout::TEX_SHADOW_ATLAS, s_Data.AtlasShadowTextureID); + BindTrackedTextureUnit(ShaderBindingLayout::TEX_SHADOW, s_Data.CSMShadowTexture); + BindTrackedTextureUnit(ShaderBindingLayout::TEX_SHADOW_ATLAS, s_Data.AtlasShadowTexture); // Comparison-OFF raw-depth views for the PCSS blocker search (plain // sampler2DArray at TEX_SHADOW_CSM_RAW / TEX_SHADOW_ATLAS_RAW). - BindTrackedTextureUnit(ShaderBindingLayout::TEX_SHADOW_CSM_RAW, s_Data.CSMRawShadowTextureID); - BindTrackedTextureUnit(ShaderBindingLayout::TEX_SHADOW_ATLAS_RAW, s_Data.AtlasRawShadowTextureID); + BindTrackedTextureUnit(ShaderBindingLayout::TEX_SHADOW_CSM_RAW, s_Data.CSMRawShadowTexture); + BindTrackedTextureUnit(ShaderBindingLayout::TEX_SHADOW_ATLAS_RAW, s_Data.AtlasRawShadowTexture); - BindTrackedTextureUnit(ShaderBindingLayout::TEX_SNOW_DEPTH, s_Data.SnowDepthTextureID); + BindTrackedTextureUnit(ShaderBindingLayout::TEX_SNOW_DEPTH, s_Data.SnowDepthTexture); // Cloud shadow transmittance map (issue #633). A 0 id binds nothing — // the AtmosphereShadingUBO enabled flag gates the shader-side sample, // so an unbound-but-declared sampler is never actually read. - BindTrackedTextureUnit(ShaderBindingLayout::TEX_CLOUD_SHADOW, s_Data.CloudShadowTextureID); + BindTrackedTextureUnit(ShaderBindingLayout::TEX_CLOUD_SHADOW, s_Data.CloudShadowTexture); } // Helper: resolve the program to bind for a mesh draw during the depth @@ -589,7 +614,7 @@ namespace OloEngine // alpha test keeps carving the same depth coverage as the color pass. // Anything else (custom shaders) keeps its own program: its vertex path is // unknown, so only it is guaranteed to reproduce its color-pass depth. - static u32 ResolveDepthPrepassShader(const PODMaterialData& mat) + static RHI::ResourceHandle ResolveDepthPrepassShader(const PODMaterialData& mat) { const auto& ids = s_Data.DepthPrepassShaders; const bool isStatic = (mat.shaderRendererID == ids.PBRStatic || @@ -601,10 +626,10 @@ namespace OloEngine return mat.shaderRendererID; const bool isMask = (mat.alphaMode == 1); - const u32 depthID = isStatic - ? (isMask ? ids.DepthMaskStatic : ids.DepthStatic) - : (isMask ? ids.DepthMaskSkinned : ids.DepthSkinned); - return depthID != 0 ? depthID : mat.shaderRendererID; + const RHI::ResourceHandle depthShader = isStatic + ? (isMask ? ids.DepthMaskStatic : ids.DepthStatic) + : (isMask ? ids.DepthMaskSkinned : ids.DepthSkinned); + return depthShader.IsValid() ? depthShader : mat.shaderRendererID; } // Helper: Upload bone matrices from FrameDataBuffer. @@ -627,7 +652,7 @@ namespace OloEngine if (boneMatrices) { s_Data.BoneMatricesUBO->SetData(boneMatrices, static_cast(count * sizeof(glm::mat4))); - BindUBOIfNeeded(ShaderBindingLayout::UBO_ANIMATION, s_Data.BoneMatricesUBO->GetRendererID()); + BindUBOIfNeeded(ShaderBindingLayout::UBO_ANIMATION, s_Data.BoneMatricesUBO->GetRHIHandle()); } // Previous-frame bone matrices for per-bone velocity. Both the forward @@ -650,7 +675,7 @@ namespace OloEngine if (sourceData) { s_Data.PrevBoneMatricesUBO->SetData(sourceData, static_cast(count * sizeof(glm::mat4))); - BindUBOIfNeeded(ShaderBindingLayout::UBO_ANIMATION_PREV, s_Data.PrevBoneMatricesUBO->GetRendererID()); + BindUBOIfNeeded(ShaderBindingLayout::UBO_ANIMATION_PREV, s_Data.PrevBoneMatricesUBO->GetRHIHandle()); } } } @@ -821,7 +846,7 @@ namespace OloEngine { if (s_Data.CameraUBO) { - BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRendererID()); + BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRHIHandle()); } if (s_Data.ForwardPlus) @@ -837,20 +862,20 @@ namespace OloEngine void CommandDispatch::ResetState() { - s_Data.CurrentBoundShaderID = 0; - s_Data.CurrentBoundVAO = 0; + s_Data.CurrentBoundShader = {}; + s_Data.CurrentBoundVAO = {}; s_Data.LastRenderStateIndex = INVALID_RENDER_STATE_INDEX; s_Data.LastMaterialDataIndex = INVALID_MATERIAL_DATA_INDEX; - s_Data.BoundTextureIDs.fill(0); + s_Data.BoundTextures.fill(RHI::NullResource); s_Data.CurrentViewportWidth = 0; s_Data.CurrentViewportHeight = 0; - s_Data.BoundUBOIDs.fill(0); - s_Data.CSMShadowTextureID = 0; - s_Data.AtlasShadowTextureID = 0; - s_Data.CSMRawShadowTextureID = 0; - s_Data.AtlasRawShadowTextureID = 0; - s_Data.SnowDepthTextureID = 0; - s_Data.CloudShadowTextureID = 0; + s_Data.BoundUBOs.fill(RHI::NullResource); + s_Data.CSMShadowTexture = {}; + s_Data.AtlasShadowTexture = {}; + s_Data.CSMRawShadowTexture = {}; + s_Data.AtlasRawShadowTexture = {}; + s_Data.SnowDepthTexture = {}; + s_Data.CloudShadowTexture = {}; s_Data.DepthPrepassActive = false; s_Data.DepthPrepassColorPassActive = false; s_Data.OverdrawActive = false; @@ -1000,26 +1025,26 @@ namespace OloEngine cameraData.PrevViewProjection = MakeViewProjectionRelative(s_Data.PrevViewProjectionMatrix, origin); cameraData.RenderOrigin = origin; // for pattern shaders (triplanar/noise/etc.) s_Data.CameraUBO->SetData(&cameraData, ShaderBindingLayout::CameraUBO::GetSize()); - BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRendererID()); + BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRHIHandle()); } - void CommandDispatch::SetShadowTextureIDs(u32 csmTextureID, u32 atlasTextureID, - u32 csmRawTextureID, u32 atlasRawTextureID) + void CommandDispatch::SetShadowTextures(RHI::ResourceHandle csmTexture, RHI::ResourceHandle atlasTexture, + RHI::ResourceHandle csmRawTexture, RHI::ResourceHandle atlasRawTexture) { - s_Data.CSMShadowTextureID = csmTextureID; - s_Data.AtlasShadowTextureID = atlasTextureID; - s_Data.CSMRawShadowTextureID = csmRawTextureID; - s_Data.AtlasRawShadowTextureID = atlasRawTextureID; + s_Data.CSMShadowTexture = csmTexture; + s_Data.AtlasShadowTexture = atlasTexture; + s_Data.CSMRawShadowTexture = csmRawTexture; + s_Data.AtlasRawShadowTexture = atlasRawTexture; } - void CommandDispatch::SetSnowDepthTextureID(u32 textureID) + void CommandDispatch::SetSnowDepthTexture(RHI::ResourceHandle texture) { - s_Data.SnowDepthTextureID = textureID; + s_Data.SnowDepthTexture = texture; } - void CommandDispatch::SetCloudShadowTextureID(u32 textureID) + void CommandDispatch::SetCloudShadowTexture(RHI::ResourceHandle texture) { - s_Data.CloudShadowTextureID = textureID; + s_Data.CloudShadowTexture = texture; } CommandDispatch::Statistics& CommandDispatch::GetStatistics() @@ -1247,7 +1272,7 @@ namespace OloEngine { auto const* cmd = static_cast(data); - if (cmd->vertexArrayID == 0) + if (!cmd->vertexArrayID.IsValid()) { OLO_CORE_ERROR("CommandDispatch::DrawIndexed: Invalid vertex array ID"); return; @@ -1262,7 +1287,7 @@ namespace OloEngine { auto const* cmd = static_cast(data); - if (cmd->vertexArrayID == 0) + if (!cmd->vertexArrayID.IsValid()) { OLO_CORE_ERROR("CommandDispatch::DrawIndexedInstanced: Invalid vertex array ID"); return; @@ -1278,7 +1303,7 @@ namespace OloEngine { auto const* cmd = static_cast(data); - if (cmd->vertexArrayID == 0) + if (!cmd->vertexArrayID.IsValid()) { OLO_CORE_ERROR("CommandDispatch::DrawArrays: Invalid vertex array ID"); return; @@ -1293,7 +1318,7 @@ namespace OloEngine { auto const* cmd = static_cast(data); - if (cmd->vertexArrayID == 0) + if (!cmd->vertexArrayID.IsValid()) { OLO_CORE_ERROR("CommandDispatch::DrawLines: Invalid vertex array ID"); return; @@ -1313,7 +1338,7 @@ namespace OloEngine const auto& mat = FrameDataBufferManager::Get().GetMaterialData(cmd->materialDataIndex); // Validate POD renderer IDs - if (cmd->vertexArrayID == 0 || mat.shaderRendererID == 0) + if (!cmd->vertexArrayID.IsValid() || !mat.shaderRendererID.IsValid()) { if (static std::atomic s_InvalidDrawMeshLogCount{ 0 }; s_InvalidDrawMeshLogCount.fetch_add(1, std::memory_order_relaxed) < 16) { @@ -1330,7 +1355,7 @@ namespace OloEngine // standard PBR programs are swapped for minimal depth-only ones — the // prepass exists to eliminate overdraw, not to run the lighting FS // once more per covered fragment. - u32 shaderToBind = mat.shaderRendererID; + RHI::ResourceHandle shaderToBind = mat.shaderRendererID; bool prepassDepthOnly = false; if (s_Data.DepthPrepassActive) { @@ -1357,10 +1382,10 @@ namespace OloEngine material's own shaderToBind and prepassDepthOnly=false set above already describe the normal colour-pass draw. */ } - if (s_Data.CurrentBoundShaderID != shaderToBind) + if (s_Data.CurrentBoundShader != shaderToBind) { api.BindShaderProgram(shaderToBind); - s_Data.CurrentBoundShaderID = shaderToBind; + s_Data.CurrentBoundShader = shaderToBind; ++s_Data.Stats.ShaderBinds; } @@ -1373,7 +1398,7 @@ namespace OloEngine // Camera UBO is still needed for vertex transform (u_ViewProjection) if (s_Data.CameraUBO) { - BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRendererID()); + BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRHIHandle()); } if (s_Data.ModelInstanceBuffer) @@ -1416,7 +1441,7 @@ namespace OloEngine // Re-establish the binding so shaders read the correct scene-camera buffer. if (s_Data.CameraUBO) { - BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRendererID()); + BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRHIHandle()); } // Update model matrix UBO @@ -1493,7 +1518,7 @@ namespace OloEngine const auto& mat = FrameDataBufferManager::Get().GetMaterialData(cmd->materialDataIndex); // Validate POD renderer IDs - if (cmd->vertexArrayID == 0 || mat.shaderRendererID == 0) + if (!cmd->vertexArrayID.IsValid() || !mat.shaderRendererID.IsValid()) { if (static std::atomic s_InvalidDrawMeshInstancedLogCount{ 0 }; s_InvalidDrawMeshInstancedLogCount.fetch_add(1, std::memory_order_relaxed) < 16) { @@ -1509,7 +1534,7 @@ namespace OloEngine // Bind shader using renderer ID directly. During the depth prepass the // standard PBR programs are swapped for minimal depth-only ones — see // ResolveDepthPrepassShader (mirrors DrawMesh). - u32 shaderToBind = mat.shaderRendererID; + RHI::ResourceHandle shaderToBind = mat.shaderRendererID; bool prepassDepthOnly = false; if (s_Data.DepthPrepassActive) { @@ -1536,10 +1561,10 @@ namespace OloEngine material's own shaderToBind and prepassDepthOnly=false set above already describe the normal colour-pass draw. */ } - if (s_Data.CurrentBoundShaderID != shaderToBind) + if (s_Data.CurrentBoundShader != shaderToBind) { api.BindShaderProgram(shaderToBind); - s_Data.CurrentBoundShaderID = shaderToBind; + s_Data.CurrentBoundShader = shaderToBind; ++s_Data.Stats.ShaderBinds; } @@ -1547,7 +1572,7 @@ namespace OloEngine // overwrote the binding point. Mirrors the logic in DrawMesh's color path. if (s_Data.CameraUBO) { - BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRendererID()); + BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRHIHandle()); } // Material UBO + texture bindings (skipped when material unchanged). @@ -1586,7 +1611,9 @@ namespace OloEngine BindVAOIfNeeded(cmd->vertexArrayID); ++s_Data.Stats.DrawCalls; - api.DrawElementsIndirectRaw(cmd->vertexArrayID, cmd->cullIndirectBufferID); + // The VAO is already bound by BindVAOIfNeeded above — draw from it + // rather than re-binding behind the redundant-bind cache's back. + api.DrawBoundElementsIndirect(cmd->cullIndirectBufferID); // Profiler stats — we DON'T know the surviving instance count // without a CPU readback (which would stall the GPU pipeline), @@ -1613,7 +1640,7 @@ namespace OloEngine { profiler.RecordInstancedDraw( static_cast(cmd->meshHandle), - cmd->vertexArrayID, + cmd->vertexArrayID.Index, cmd->indexCount, preCullCount, /*entityIDs=*/nullptr, @@ -1746,7 +1773,7 @@ namespace OloEngine const bool fromAutoBatching = (cmd->entityIDBufferOffset != UINT32_MAX) && (instanceCount > 1); profiler.RecordInstancedDraw( static_cast(cmd->meshHandle), - cmd->vertexArrayID, + cmd->vertexArrayID.Index, cmd->indexCount, static_cast(instanceCount), entityIDs, @@ -1761,7 +1788,7 @@ namespace OloEngine auto const* cmd = static_cast(data); // Validate POD renderer IDs - if (cmd->vertexArrayID == 0 || cmd->shaderRendererID == 0 || cmd->skyboxTextureID == 0) + if (!cmd->vertexArrayID.IsValid() || !cmd->shaderRendererID.IsValid() || !cmd->skyboxTextureID.IsValid()) { OLO_CORE_ERROR("CommandDispatch::DrawSkybox: Invalid vertex array ID, shader ID, or skybox texture ID"); return; @@ -1771,24 +1798,24 @@ namespace OloEngine ApplyPODRenderState(cmd->renderStateIndex, api); // Bind skybox shader using renderer ID directly - if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) + if (s_Data.CurrentBoundShader != cmd->shaderRendererID) { api.BindShaderProgram(cmd->shaderRendererID); - s_Data.CurrentBoundShaderID = cmd->shaderRendererID; + s_Data.CurrentBoundShader = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } // Re-establish camera UBO binding (may be overwritten by shadow pass) if (s_Data.CameraUBO) { - BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRendererID()); + BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRHIHandle()); } // Bind skybox cubemap texture using renderer ID directly - if (s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_ENVIRONMENT] != cmd->skyboxTextureID) + if (s_Data.BoundTextures[ShaderBindingLayout::TEX_ENVIRONMENT] != cmd->skyboxTextureID) { api.BindTexture(ShaderBindingLayout::TEX_ENVIRONMENT, cmd->skyboxTextureID); - s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_ENVIRONMENT] = cmd->skyboxTextureID; + s_Data.BoundTextures[ShaderBindingLayout::TEX_ENVIRONMENT] = cmd->skyboxTextureID; ++s_Data.Stats.TextureBinds; } @@ -1807,13 +1834,13 @@ namespace OloEngine auto const* cmd = static_cast(data); // Validate POD renderer IDs - if (cmd->quadVAID == 0 || cmd->shaderRendererID == 0) + if (!cmd->quadVAID.IsValid() || !cmd->shaderRendererID.IsValid()) { OLO_CORE_ERROR("CommandDispatch::DrawQuad: Invalid vertex array ID or shader ID"); return; } - if (cmd->textureID == 0) + if (!cmd->textureID.IsValid()) { OLO_CORE_ERROR("CommandDispatch::DrawQuad: Missing texture for quad"); return; @@ -1823,10 +1850,10 @@ namespace OloEngine ApplyPODRenderState(cmd->renderStateIndex, api); // Bind shader using renderer ID directly - if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) + if (s_Data.CurrentBoundShader != cmd->shaderRendererID) { api.BindShaderProgram(cmd->shaderRendererID); - s_Data.CurrentBoundShaderID = cmd->shaderRendererID; + s_Data.CurrentBoundShader = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } @@ -1846,10 +1873,10 @@ namespace OloEngine } // Bind texture using renderer ID directly - if (s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_DIFFUSE] != cmd->textureID) + if (s_Data.BoundTextures[ShaderBindingLayout::TEX_DIFFUSE] != cmd->textureID) { api.BindTexture(ShaderBindingLayout::TEX_DIFFUSE, cmd->textureID); - s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_DIFFUSE] = cmd->textureID; + s_Data.BoundTextures[ShaderBindingLayout::TEX_DIFFUSE] = cmd->textureID; ++s_Data.Stats.TextureBinds; } @@ -1866,7 +1893,7 @@ namespace OloEngine auto const* cmd = static_cast(data); // Validate POD renderer IDs - if (cmd->quadVAOID == 0 || cmd->shaderRendererID == 0) + if (!cmd->quadVAOID.IsValid() || !cmd->shaderRendererID.IsValid()) { OLO_CORE_ERROR("CommandDispatch::DrawInfiniteGrid: Invalid VAO ID or shader ID"); return; @@ -1876,10 +1903,10 @@ namespace OloEngine ApplyPODRenderState(cmd->renderStateIndex, api); // Bind grid shader using renderer ID directly - if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) + if (s_Data.CurrentBoundShader != cmd->shaderRendererID) { api.BindShaderProgram(cmd->shaderRendererID); - s_Data.CurrentBoundShaderID = cmd->shaderRendererID; + s_Data.CurrentBoundShader = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } @@ -1887,7 +1914,7 @@ namespace OloEngine // Re-establish the binding (may be overwritten by shadow pass) if (s_Data.CameraUBO) { - BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRendererID()); + BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRHIHandle()); } // Set grid scale uniform if the shader supports it. @@ -1912,7 +1939,7 @@ namespace OloEngine auto const* cmd = static_cast(data); - if (cmd->vertexArrayID == 0 || cmd->shaderRendererID == 0) + if (!cmd->vertexArrayID.IsValid() || !cmd->shaderRendererID.IsValid()) { OLO_CORE_ERROR("CommandDispatch::DrawTerrainPatch: Invalid vertex array ID or shader ID"); return; @@ -1922,10 +1949,10 @@ namespace OloEngine ApplyPODRenderState(cmd->renderStateIndex, api); // Bind shader - if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) + if (s_Data.CurrentBoundShader != cmd->shaderRendererID) { api.BindShaderProgram(cmd->shaderRendererID); - s_Data.CurrentBoundShaderID = cmd->shaderRendererID; + s_Data.CurrentBoundShader = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } @@ -1956,27 +1983,27 @@ namespace OloEngine } // Bind terrain textures - if (cmd->heightmapTextureID != 0) + if (cmd->heightmapTextureID.IsValid()) { api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_HEIGHTMAP, cmd->heightmapTextureID); } - if (cmd->splatmapTextureID != 0) + if (cmd->splatmapTextureID.IsValid()) { api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_SPLATMAP, cmd->splatmapTextureID); } - if (cmd->splatmap1TextureID != 0) + if (cmd->splatmap1TextureID.IsValid()) { api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_SPLATMAP_1, cmd->splatmap1TextureID); } - if (cmd->albedoArrayTextureID != 0) + if (cmd->albedoArrayTextureID.IsValid()) { api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_ALBEDO_ARRAY, cmd->albedoArrayTextureID); } - if (cmd->normalArrayTextureID != 0) + if (cmd->normalArrayTextureID.IsValid()) { api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_NORMAL_ARRAY, cmd->normalArrayTextureID); } - if (cmd->armArrayTextureID != 0) + if (cmd->armArrayTextureID.IsValid()) { api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_ARM_ARRAY, cmd->armArrayTextureID); } @@ -2002,7 +2029,7 @@ namespace OloEngine auto const* cmd = static_cast(data); - if (cmd->vertexArrayID == 0 || cmd->shaderRendererID == 0) + if (!cmd->vertexArrayID.IsValid() || !cmd->shaderRendererID.IsValid()) { OLO_CORE_ERROR("CommandDispatch::DrawVoxelMesh: Invalid vertex array ID or shader ID"); return; @@ -2012,10 +2039,10 @@ namespace OloEngine ApplyPODRenderState(cmd->renderStateIndex, api); // Bind shader - if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) + if (s_Data.CurrentBoundShader != cmd->shaderRendererID) { api.BindShaderProgram(cmd->shaderRendererID); - s_Data.CurrentBoundShaderID = cmd->shaderRendererID; + s_Data.CurrentBoundShader = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } @@ -2039,15 +2066,15 @@ namespace OloEngine } // Bind textures for triplanar sampling - if (cmd->albedoArrayTextureID != 0) + if (cmd->albedoArrayTextureID.IsValid()) { api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_ALBEDO_ARRAY, cmd->albedoArrayTextureID); } - if (cmd->normalArrayTextureID != 0) + if (cmd->normalArrayTextureID.IsValid()) { api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_NORMAL_ARRAY, cmd->normalArrayTextureID); } - if (cmd->armArrayTextureID != 0) + if (cmd->armArrayTextureID.IsValid()) { api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_ARM_ARRAY, cmd->armArrayTextureID); } @@ -2069,7 +2096,7 @@ namespace OloEngine auto const* cmd = static_cast(data); - if (cmd->vertexArrayID == 0 || cmd->shaderRendererID == 0) + if (!cmd->vertexArrayID.IsValid() || !cmd->shaderRendererID.IsValid()) { OLO_CORE_ERROR("CommandDispatch::DrawDecal: Invalid vertex array ID or shader ID"); return; @@ -2084,13 +2111,13 @@ namespace OloEngine // graph-owned OIT MRT layout without requiring resubmission of the // bucket. Reading the override from the command keeps the queue // stateless and replay-safe. - u32 decalProgramID = (cmd->oitProgramOverride != 0) - ? cmd->oitProgramOverride - : cmd->shaderRendererID; - if (s_Data.CurrentBoundShaderID != decalProgramID) + const RHI::ResourceHandle decalProgram = cmd->oitProgramOverride.IsValid() + ? cmd->oitProgramOverride + : cmd->shaderRendererID; + if (s_Data.CurrentBoundShader != decalProgram) { - api.BindShaderProgram(decalProgramID); - s_Data.CurrentBoundShaderID = decalProgramID; + api.BindShaderProgram(decalProgram); + s_Data.CurrentBoundShader = decalProgram; ++s_Data.Stats.ShaderBinds; } @@ -2124,12 +2151,12 @@ namespace OloEngine } // Bind albedo texture (with redundancy check) - if (cmd->albedoTextureID != 0) + if (cmd->albedoTextureID.IsValid()) { - if (s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_USER_0] != cmd->albedoTextureID) + if (s_Data.BoundTextures[ShaderBindingLayout::TEX_USER_0] != cmd->albedoTextureID) { api.BindTexture(ShaderBindingLayout::TEX_USER_0, cmd->albedoTextureID); - s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_USER_0] = cmd->albedoTextureID; + s_Data.BoundTextures[ShaderBindingLayout::TEX_USER_0] = cmd->albedoTextureID; ++s_Data.Stats.TextureBinds; } } @@ -2137,18 +2164,18 @@ namespace OloEngine // Bind optional decal normal + RMA textures (used by Decal_GBuffer_Normal // and Decal_GBuffer_RMA variants). Unused modes pass 0 and the slot is // left alone — the variant shader only samples the slot it needs. - if (cmd->normalTextureID != 0 && - s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_USER_1] != cmd->normalTextureID) + if (cmd->normalTextureID.IsValid() && + s_Data.BoundTextures[ShaderBindingLayout::TEX_USER_1] != cmd->normalTextureID) { api.BindTexture(ShaderBindingLayout::TEX_USER_1, cmd->normalTextureID); - s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_USER_1] = cmd->normalTextureID; + s_Data.BoundTextures[ShaderBindingLayout::TEX_USER_1] = cmd->normalTextureID; ++s_Data.Stats.TextureBinds; } - if (cmd->rmaTextureID != 0 && - s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_USER_2] != cmd->rmaTextureID) + if (cmd->rmaTextureID.IsValid() && + s_Data.BoundTextures[ShaderBindingLayout::TEX_USER_2] != cmd->rmaTextureID) { api.BindTexture(ShaderBindingLayout::TEX_USER_2, cmd->rmaTextureID); - s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_USER_2] = cmd->rmaTextureID; + s_Data.BoundTextures[ShaderBindingLayout::TEX_USER_2] = cmd->rmaTextureID; ++s_Data.Stats.TextureBinds; } @@ -2163,10 +2190,11 @@ namespace OloEngine OLO_PROFILE_FUNCTION(); const auto* cmd = static_cast(data); - if (!cmd || cmd->vertexArrayID == 0 || cmd->shaderRendererID == 0 || cmd->instanceCount == 0 || cmd->indexCount == 0) + if (!cmd || !cmd->vertexArrayID.IsValid() || !cmd->shaderRendererID.IsValid() || cmd->instanceCount == 0 || cmd->indexCount == 0) { OLO_CORE_ERROR("CommandDispatch::DrawFoliageLayer: Invalid foliage command (VAO={}, shader={}, instances={}, indices={})", - cmd ? cmd->vertexArrayID : 0, cmd ? cmd->shaderRendererID : 0, + cmd ? cmd->vertexArrayID : RHI::NullResource, + cmd ? cmd->shaderRendererID : RHI::NullResource, cmd ? cmd->instanceCount : 0, cmd ? cmd->indexCount : 0); return; } @@ -2175,10 +2203,10 @@ namespace OloEngine ApplyPODRenderState(cmd->renderStateIndex, api); // Bind shader (cached) - if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) + if (s_Data.CurrentBoundShader != cmd->shaderRendererID) { api.BindShaderProgram(cmd->shaderRendererID); - s_Data.CurrentBoundShaderID = cmd->shaderRendererID; + s_Data.CurrentBoundShader = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } @@ -2215,12 +2243,12 @@ namespace OloEngine // Bind albedo texture (with redundancy check). On the impostor path this // is the octahedral albedo atlas. - if (cmd->albedoTextureID != 0) + if (cmd->albedoTextureID.IsValid()) { - if (s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_DIFFUSE] != cmd->albedoTextureID) + if (s_Data.BoundTextures[ShaderBindingLayout::TEX_DIFFUSE] != cmd->albedoTextureID) { api.BindTexture(ShaderBindingLayout::TEX_DIFFUSE, cmd->albedoTextureID); - s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_DIFFUSE] = cmd->albedoTextureID; + s_Data.BoundTextures[ShaderBindingLayout::TEX_DIFFUSE] = cmd->albedoTextureID; ++s_Data.Stats.TextureBinds; } } @@ -2240,10 +2268,11 @@ namespace OloEngine OLO_PROFILE_FUNCTION(); const auto* cmd = static_cast(data); - if (!cmd || cmd->vertexArrayID == 0 || cmd->shaderRendererID == 0 || cmd->indexCount == 0) + if (!cmd || !cmd->vertexArrayID.IsValid() || !cmd->shaderRendererID.IsValid() || cmd->indexCount == 0) { OLO_CORE_ERROR("CommandDispatch::DrawWater: Invalid water command (VAO={}, shader={}, indices={})", - cmd ? cmd->vertexArrayID : 0, cmd ? cmd->shaderRendererID : 0, + cmd ? cmd->vertexArrayID : RHI::NullResource, + cmd ? cmd->shaderRendererID : RHI::NullResource, cmd ? cmd->indexCount : 0); return; } @@ -2251,10 +2280,10 @@ namespace OloEngine ApplyPODRenderState(cmd->renderStateIndex, api); // Bind shader (cached). - if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) + if (s_Data.CurrentBoundShader != cmd->shaderRendererID) { api.BindShaderProgram(cmd->shaderRendererID); - s_Data.CurrentBoundShaderID = cmd->shaderRendererID; + s_Data.CurrentBoundShader = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } @@ -2318,14 +2347,14 @@ namespace OloEngine // environment map, deterministically clear the slot rather than leaving a // stale cubemap from a previous frame/scene (BindTrackedTexture skips id 0, // so clear it directly and update the tracking). - if (const auto envMapID = Renderer3D::GetGlobalEnvironmentMapID(); envMapID != 0) + if (const auto envMap = Renderer3D::GetGlobalEnvironmentMapHandle(); envMap.IsValid()) { - BindTrackedTexture(envMapID, ShaderBindingLayout::TEX_ENVIRONMENT); + BindTrackedTexture(envMap, ShaderBindingLayout::TEX_ENVIRONMENT); } - else if (s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_ENVIRONMENT] != 0) + else if (s_Data.BoundTextures[ShaderBindingLayout::TEX_ENVIRONMENT].IsValid()) { api.BindTexture(ShaderBindingLayout::TEX_ENVIRONMENT, 0); - s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_ENVIRONMENT] = 0; + s_Data.BoundTextures[ShaderBindingLayout::TEX_ENVIRONMENT] = {}; } // Bind VAO (cached) and draw water. diff --git a/OloEngine/src/OloEngine/Renderer/Commands/CommandDispatch.h b/OloEngine/src/OloEngine/Renderer/Commands/CommandDispatch.h index 28ca2f405..a9b9c0d19 100644 --- a/OloEngine/src/OloEngine/Renderer/Commands/CommandDispatch.h +++ b/OloEngine/src/OloEngine/Renderer/Commands/CommandDispatch.h @@ -1,6 +1,7 @@ #pragma once #include "RenderCommand.h" +#include "OloEngine/Renderer/RHI/RHITypes.h" #include "OloEngine/Renderer/RendererAPI.h" #include "OloEngine/Renderer/ShaderBindingLayout.h" #include @@ -44,7 +45,7 @@ namespace OloEngine // Clear any cached texture-slot binding that points at this OpenGL texture ID. // Must be called when a Texture2D is destroyed so that a future call to // BindTrackedTexture with a recycled GL ID is not incorrectly skipped. - static void InvalidateTextureBinding(u32 textureID); + static void InvalidateTextureBinding(RHI::ResourceHandle texture); // Tell the redundant-bind cache that `slot` was clobbered by a raw glBindTextureUnit // outside this dispatcher, so the next tracked bind for that slot actually happens. @@ -94,18 +95,19 @@ namespace OloEngine // atlas used by the PCSS blocker search (0 = none; bound only when // non-zero). The atlas replaced the old spot array + point cubemaps // (issue #435). - static void SetShadowTextureIDs(u32 csmTextureID, u32 atlasTextureID, - u32 csmRawTextureID = 0, u32 atlasRawTextureID = 0); + static void SetShadowTextures(RHI::ResourceHandle csmTexture, RHI::ResourceHandle atlasTexture, + RHI::ResourceHandle csmRawTexture = {}, + RHI::ResourceHandle atlasRawTexture = {}); // Snow accumulation depth texture — set per-frame - static void SetSnowDepthTextureID(u32 textureID); + static void SetSnowDepthTexture(RHI::ResourceHandle texture); // Cloud shadow transmittance map (R8, issue #633) — set per-frame // from CloudShadowMap by RenderPipeline::UploadExecutionState; bound // at TEX_CLOUD_SHADOW (62) during PBR mesh dispatch (0 = none; bound // only when non-zero, gated shader-side by the AtmosphereShadingUBO // enabled flag). - static void SetCloudShadowTextureID(u32 textureID); + static void SetCloudShadowTexture(RHI::ResourceHandle texture); // Getters for current frame state (used for sort key generation and per-bucket view state) static const glm::mat4& GetViewMatrix(); diff --git a/OloEngine/src/OloEngine/Renderer/Commands/RenderCommand.h b/OloEngine/src/OloEngine/Renderer/Commands/RenderCommand.h index ffa675397..a7dfc720a 100644 --- a/OloEngine/src/OloEngine/Renderer/Commands/RenderCommand.h +++ b/OloEngine/src/OloEngine/Renderer/Commands/RenderCommand.h @@ -17,7 +17,7 @@ * * Design principles: * - Use AssetHandle (u64) instead of Ref for asset references - * - Use RendererID (u32) for GPU resource identifiers (VAO, textures, etc.) + * - Use RHI::ResourceHandle for GPU resource identities (VAO, textures, etc.) * - Use offset+count into FrameDataBuffer for variable-length data (bone matrices, transforms) * - Inline render state as POD flags instead of Ref * @@ -31,7 +31,13 @@ namespace OloEngine // Type aliases for POD command fields using AssetHandle = UUID; // u64 asset identifier - using RendererID = u32; // OpenGL resource ID + // `using RendererID = u32` lived here and is GONE (issue #691 step 3, + // slice 6). Every GPU-object field below is an RHI::ResourceHandle now: + // the command layer's redundant-bind cache keys on these values, and a + // driver name cannot key it safely — GL reissues names, so a deleted + // object and a newly created one could compare equal and the cache would + // skip a real bind. See CommandDispatch's InvalidateTextureSlot comment + // for the visual bug that actually shipped from exactly that. // Sentinel value for uninitialized render state index static constexpr u16 INVALID_RENDER_STATE_INDEX = UINT16_MAX; @@ -113,7 +119,7 @@ namespace OloEngine struct PODMaterialData { // Shader - RendererID shaderRendererID = 0; + RHI::ResourceHandle shaderRendererID{}; // Legacy material properties glm::vec3 ambient = glm::vec3(0.1f); @@ -121,8 +127,8 @@ namespace OloEngine glm::vec3 specular = glm::vec3(1.0f); f32 shininess = 32.0f; bool useTextureMaps = false; - RendererID diffuseMapID = 0; - RendererID specularMapID = 0; + RHI::ResourceHandle diffuseMapID{}; + RHI::ResourceHandle specularMapID{}; // PBR material properties bool enablePBR = false; @@ -139,15 +145,15 @@ namespace OloEngine f32 alphaCutoff = 0.5f; // PBR texture IDs (renderer IDs, 0 = none) - RendererID albedoMapID = 0; - RendererID metallicRoughnessMapID = 0; - RendererID normalMapID = 0; - RendererID aoMapID = 0; - RendererID emissiveMapID = 0; - RendererID environmentMapID = 0; - RendererID irradianceMapID = 0; - RendererID prefilterMapID = 0; - RendererID brdfLutMapID = 0; + RHI::ResourceHandle albedoMapID{}; + RHI::ResourceHandle metallicRoughnessMapID{}; + RHI::ResourceHandle normalMapID{}; + RHI::ResourceHandle aoMapID{}; + RHI::ResourceHandle emissiveMapID{}; + RHI::ResourceHandle environmentMapID{}; + RHI::ResourceHandle irradianceMapID{}; + RHI::ResourceHandle prefilterMapID{}; + RHI::ResourceHandle brdfLutMapID{}; // Field-wise equality (safe against struct padding, unlike memcmp) bool operator==(const PODMaterialData& o) const @@ -502,7 +508,7 @@ namespace OloEngine struct DrawIndexedCommand { CommandHeader header; - RendererID vertexArrayID; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID u32 indexCount; RHI::IndexType indexType; }; @@ -510,7 +516,7 @@ namespace OloEngine struct DrawIndexedInstancedCommand { CommandHeader header; - RendererID vertexArrayID; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID u32 indexCount; u32 instanceCount; RHI::IndexType indexType; @@ -519,7 +525,7 @@ namespace OloEngine struct DrawArraysCommand { CommandHeader header; - RendererID vertexArrayID; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID u32 vertexCount; RHI::PrimitiveTopology primitiveType; }; @@ -527,7 +533,7 @@ namespace OloEngine struct DrawLinesCommand { CommandHeader header; - RendererID vertexArrayID; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID u32 vertexCount; }; @@ -538,8 +544,8 @@ namespace OloEngine CommandHeader header; // Mesh data (POD identifiers) - AssetHandle meshHandle; // Mesh asset handle for resolution - RendererID vertexArrayID; // VAO renderer ID + AssetHandle meshHandle; // Mesh asset handle for resolution + RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID u32 indexCount; u32 baseIndex = 0; // Starting index offset in shared index buffer (for multi-submesh MeshSources) glm::mat4 transform; @@ -591,8 +597,8 @@ namespace OloEngine CommandHeader header; // Mesh data (POD identifiers) - AssetHandle meshHandle; // Mesh asset handle - RendererID vertexArrayID; // VAO renderer ID + AssetHandle meshHandle; // Mesh asset handle + RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID u32 indexCount; u32 baseIndex = 0; // Starting index offset in shared index buffer (for multi-submesh MeshSources) u32 instanceCount; @@ -648,13 +654,13 @@ namespace OloEngine struct DrawSkyboxCommand { CommandHeader header; - AssetHandle meshHandle; // Skybox mesh handle - RendererID vertexArrayID; // VAO renderer ID + AssetHandle meshHandle; // Skybox mesh handle + RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID u32 indexCount; glm::mat4 transform; // Usually identity matrix AssetHandle shaderHandle; // Skybox shader handle (for asset tracking) - RendererID shaderRendererID; // Shader program ID for glUseProgram - RendererID skyboxTextureID; // Cubemap texture renderer ID + RHI::ResourceHandle shaderRendererID{}; // Shader program ID for glUseProgram + RHI::ResourceHandle skyboxTextureID{}; // Cubemap texture renderer ID u16 renderStateIndex = INVALID_RENDER_STATE_INDEX; // Render state index }; @@ -665,8 +671,8 @@ namespace OloEngine { CommandHeader header; AssetHandle shaderHandle; // Grid shader handle (for asset tracking) - RendererID shaderRendererID; // Shader program ID for glUseProgram - RendererID quadVAOID; // Fullscreen quad VAO renderer ID + RHI::ResourceHandle shaderRendererID{}; // Shader program ID for glUseProgram + RHI::ResourceHandle quadVAOID{}; // Fullscreen quad VAO renderer ID f32 gridScale; // Grid spacing scale factor u16 renderStateIndex = INVALID_RENDER_STATE_INDEX; // Render state index }; @@ -678,10 +684,10 @@ namespace OloEngine { CommandHeader header; glm::mat4 transform; - RendererID textureID; // Texture renderer ID + RHI::ResourceHandle textureID{}; // Texture renderer ID AssetHandle shaderHandle; // Shader asset handle (for asset tracking) - RendererID shaderRendererID; // Shader program ID for glUseProgram - RendererID quadVAID; // Quad vertex array renderer ID + RHI::ResourceHandle shaderRendererID{}; // Shader program ID for glUseProgram + RHI::ResourceHandle quadVAID{}; // Quad vertex array renderer ID u16 renderStateIndex = INVALID_RENDER_STATE_INDEX; // Render state index }; @@ -694,20 +700,20 @@ namespace OloEngine CommandHeader header; // Mesh data - RendererID vertexArrayID = 0; + RHI::ResourceHandle vertexArrayID{}; u32 indexCount = 0; u32 patchVertexCount = 3; // Tessellation patch vertex count // Shader - RendererID shaderRendererID = 0; + RHI::ResourceHandle shaderRendererID{}; // Terrain textures - RendererID heightmapTextureID = 0; - RendererID splatmapTextureID = 0; - RendererID splatmap1TextureID = 0; - RendererID albedoArrayTextureID = 0; - RendererID normalArrayTextureID = 0; - RendererID armArrayTextureID = 0; + RHI::ResourceHandle heightmapTextureID{}; + RHI::ResourceHandle splatmapTextureID{}; + RHI::ResourceHandle splatmap1TextureID{}; + RHI::ResourceHandle albedoArrayTextureID{}; + RHI::ResourceHandle normalArrayTextureID{}; + RHI::ResourceHandle armArrayTextureID{}; // Transform glm::mat4 transform = glm::mat4(1.0f); @@ -728,16 +734,16 @@ namespace OloEngine CommandHeader header; // Mesh data - RendererID vertexArrayID = 0; + RHI::ResourceHandle vertexArrayID{}; u32 indexCount = 0; // Shader - RendererID shaderRendererID = 0; + RHI::ResourceHandle shaderRendererID{}; // Textures for triplanar sampling - RendererID albedoArrayTextureID = 0; - RendererID normalArrayTextureID = 0; - RendererID armArrayTextureID = 0; + RHI::ResourceHandle albedoArrayTextureID{}; + RHI::ResourceHandle normalArrayTextureID{}; + RHI::ResourceHandle armArrayTextureID{}; // Transform glm::mat4 transform = glm::mat4(1.0f); @@ -755,11 +761,11 @@ namespace OloEngine CommandHeader header; // Mesh data (decal projection cube) - RendererID vertexArrayID = 0; + RHI::ResourceHandle vertexArrayID{}; u32 indexCount = 0; // Shader - RendererID shaderRendererID = 0; + RHI::ResourceHandle shaderRendererID{}; // Decal transform glm::mat4 decalTransform = glm::mat4(1.0f); // Scaled transform for geometry @@ -769,9 +775,9 @@ namespace OloEngine // Decal appearance glm::vec4 decalColor = glm::vec4(1.0f); glm::vec4 decalParams = glm::vec4(0.0f); // x = fadeDistance, y = normalAngleThreshold, z/w = unused - RendererID albedoTextureID = 0; - RendererID normalTextureID = 0; // Bound at ShaderBindingLayout::TEX_USER_1 for Normal-mode decals (see CommandDispatch::DrawDecal) - RendererID rmaTextureID = 0; // Bound at ShaderBindingLayout::TEX_USER_2 for RMA-mode decals (R=roughness, G=metal, B=AO) + RHI::ResourceHandle albedoTextureID{}; + RHI::ResourceHandle normalTextureID{}; // Bound at ShaderBindingLayout::TEX_USER_1 for Normal-mode decals (see CommandDispatch::DrawDecal) + RHI::ResourceHandle rmaTextureID{}; // Bound at ShaderBindingLayout::TEX_USER_2 for RMA-mode decals (R=roughness, G=metal, B=AO) // Inserting fields between members above is safe: every Renderer3D::DrawDecal // call site assigns members by name (`cmd->normalTextureID = …`) rather than // positional brace initialization, and the same convention applies to @@ -806,7 +812,7 @@ namespace OloEngine // commands composite via the WB-OIT layout without resubmission. // DecalRenderPass populates this on the command itself (not a // global) so the queue stays stateless and replay-safe. - u32 oitProgramOverride = 0; + RHI::ResourceHandle oitProgramOverride{}; // Render state index (into FrameDataBuffer::RenderStateTable) u16 renderStateIndex = INVALID_RENDER_STATE_INDEX; @@ -821,12 +827,12 @@ namespace OloEngine CommandHeader header; // Mesh data (instanced quad) - RendererID vertexArrayID = 0; + RHI::ResourceHandle vertexArrayID{}; u32 indexCount = 0; u32 instanceCount = 0; // Shader - RendererID shaderRendererID = 0; + RHI::ResourceHandle shaderRendererID{}; // Model transform (parent terrain entity) glm::mat4 modelTransform = glm::mat4(1.0f); @@ -845,11 +851,11 @@ namespace OloEngine // Albedo texture (0 = no texture). On the impostor path this is the // octahedral albedo atlas (rgb + coverage). - RendererID albedoTextureID = 0; + RHI::ResourceHandle albedoTextureID{}; // Octahedral impostor atlas (issue #433): normal+depth atlas + params. // impostorEnabled == 0 for the flat-billboard path (fields ignored). - RendererID impostorNormalDepthTextureID = 0; + RHI::ResourceHandle impostorNormalDepthTextureID{}; f32 impostorEnabled = 0.0f; f32 impostorFramesPerAxis = 8.0f; f32 impostorHemi = 1.0f; @@ -873,11 +879,11 @@ namespace OloEngine CommandHeader header; // Mesh data - RendererID vertexArrayID = 0; + RHI::ResourceHandle vertexArrayID{}; u32 indexCount = 0; // Shader - RendererID shaderRendererID = 0; + RHI::ResourceHandle shaderRendererID{}; // Transform glm::mat4 modelTransform = glm::mat4(1.0f); @@ -903,13 +909,13 @@ namespace OloEngine glm::vec4 fftParams = glm::vec4(0.0f); // useFFT (0/1), 1/patchSize, heightScale, horizontalScale // Normal map / noise texture IDs - RendererID normalMap0ID = 0; - RendererID normalMap1ID = 0; - RendererID noiseTextureID = 0; - RendererID foamTextureID = 0; + RHI::ResourceHandle normalMap0ID{}; + RHI::ResourceHandle normalMap1ID{}; + RHI::ResourceHandle noiseTextureID{}; + RHI::ResourceHandle foamTextureID{}; // FFT ocean cascade textures (WATER_FUTURE_IMPROVEMENTS.md §1) - RendererID fftDisplacementID = 0; // rgb = (dx, height, dz), a = foam - RendererID fftDerivativesID = 0; // rgb = normal, a = jacobian + RHI::ResourceHandle fftDisplacementID{}; // rgb = (dx, height, dz), a = foam + RHI::ResourceHandle fftDerivativesID{}; // rgb = normal, a = jacobian // Feature toggles bool refractionEnabled = true; diff --git a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp index c38b9be1e..821f5ab92 100644 --- a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp +++ b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp @@ -688,7 +688,7 @@ namespace OloEngine for (const auto& caster : m_Casters) { - if (caster.vaoID == 0 || caster.indexCount == 0) + if (!caster.vaoID.IsValid() || caster.indexCount == 0) { continue; } @@ -697,7 +697,10 @@ namespace OloEngine continue; } - RenderCommand::BindTexture(0, caster.albedoTextureID != 0 ? caster.albedoTextureID : m_WhiteTexture); + if (caster.albedoTextureID.IsValid()) + RenderCommand::BindTexture(0, caster.albedoTextureID); + else + RenderCommand::BindTexture(0, m_WhiteTexture); DDGIPassDataUBO data{}; data.Model = MakeModelRelative(caster.transform, m_RenderOrigin); @@ -896,10 +899,12 @@ namespace OloEngine // samplerCube slot (TEX_ENVIRONMENT) — the black fallback keeps the // declared samplerCube valid when no scene environment exists, and the // slot normally carries this exact texture for the lit passes anyway. - const u32 envID = Renderer3D::GetGlobalEnvironmentMapID() != 0 - ? Renderer3D::GetGlobalEnvironmentMapID() - : m_BlackCubemap; - RenderCommand::BindTexture(ShaderBindingLayout::TEX_ENVIRONMENT, envID); + // Same split as the caster albedo above: the black fallback cubemap is + // still a pass-owned native texture. + if (const RHI::ResourceHandle envMap = Renderer3D::GetGlobalEnvironmentMapHandle(); envMap.IsValid()) + RenderCommand::BindTexture(ShaderBindingLayout::TEX_ENVIRONMENT, envMap); + else + RenderCommand::BindTexture(ShaderBindingLayout::TEX_ENVIRONMENT, m_BlackCubemap); // CSM + shadow atlas at the binding units include/PBRCommon.glsl's // evaluators expect (8 / 13 comparison, 33 / 34 raw for PCSS) — same diff --git a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.h b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.h index bc73c2707..3e20e634d 100644 --- a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.h +++ b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.h @@ -38,13 +38,13 @@ namespace OloEngine // plus the minimal material data the capture mini-G-buffer needs. struct DDGIMeshCaster { - RendererID vaoID = 0; + RHI::ResourceHandle vaoID{}; u32 indexCount = 0; u32 baseIndex = 0; glm::mat4 transform{ 1.0f }; // ABSOLUTE world BoundingBox worldBounds = NoBounds; - glm::vec4 baseColor{ 1.0f }; // material base color factor - RendererID albedoTextureID = 0; // 0 = white + glm::vec4 baseColor{ 1.0f }; // material base color factor + RHI::ResourceHandle albedoTextureID{}; // 0 = white bool twoSided = false; }; diff --git a/OloEngine/src/OloEngine/Renderer/Debug/CommandPacketDebugger.cpp b/OloEngine/src/OloEngine/Renderer/Debug/CommandPacketDebugger.cpp index b8927f189..c292fca1b 100644 --- a/OloEngine/src/OloEngine/Renderer/Debug/CommandPacketDebugger.cpp +++ b/OloEngine/src/OloEngine/Renderer/Debug/CommandPacketDebugger.cpp @@ -16,6 +16,15 @@ namespace OloEngine { + namespace + { + // Identities print as #Index:Generation via RHITypes' fmt formatter. + [[nodiscard]] std::string FormatHandle(const RHI::ResourceHandle handle) + { + return fmt::format("{}", handle); + } + } // namespace + namespace { constexpr std::string_view LiveGeometryNodeName = "ScenePass"; @@ -565,7 +574,9 @@ namespace OloEngine ImGui::Text("Entity ID: %d", cmd.entityID); const PODMaterialData* matPtr = frame ? frame->GetSnapshotMaterialData(cmd.materialDataIndex) : nullptr; - ImGui::Text("Shader: %u (handle: %llu)", matPtr ? matPtr->shaderRendererID : 0u, static_cast(cmd.shaderHandle)); + ImGui::Text("Shader: %s (handle: %llu)", + FormatHandle(matPtr ? matPtr->shaderRendererID : RHI::NullResource).c_str(), + static_cast(cmd.shaderHandle)); ImGui::Text("Material Data Index: %u", cmd.materialDataIndex); ImGui::Separator(); @@ -617,7 +628,9 @@ namespace OloEngine ImGui::Text("Transform Buffer: offset=%u, count=%u", cmd.transformBufferOffset, cmd.transformCount); const PODMaterialData* matPtr = frame ? frame->GetSnapshotMaterialData(cmd.materialDataIndex) : nullptr; - ImGui::Text("Shader: %u (handle: %llu)", matPtr ? matPtr->shaderRendererID : 0u, static_cast(cmd.shaderHandle)); + ImGui::Text("Shader: %s (handle: %llu)", + FormatHandle(matPtr ? matPtr->shaderRendererID : RHI::NullResource).c_str(), + static_cast(cmd.shaderHandle)); ImGui::Text("Material Data Index: %u", cmd.materialDataIndex); } @@ -1458,16 +1471,16 @@ namespace OloEngine const PODRenderState* state = frame->GetSnapshotRenderState(meshCmd->renderStateIndex); const PODMaterialData* mat = frame->GetSnapshotMaterialData(meshCmd->materialDataIndex); file << "### Draw #" << drawIdx++ << ": " << cmd.GetCommandTypeString() << "\n\n"; - file << "- Shader: " << (mat ? mat->shaderRendererID : 0u) << " (handle: " << static_cast(meshCmd->shaderHandle) << ")\n"; - file << "- VAO: " << meshCmd->vertexArrayID << ", Index Count: " << meshCmd->indexCount << "\n"; + file << "- Shader: " << FormatHandle(mat ? mat->shaderRendererID : RHI::NullResource) << " (handle: " << static_cast(meshCmd->shaderHandle) << ")\n"; + file << "- VAO: " << FormatHandle(meshCmd->vertexArrayID) << ", Index Count: " << meshCmd->indexCount << "\n"; file << "- Entity ID: " << meshCmd->entityID << "\n"; file << "- Material Data Index: " << meshCmd->materialDataIndex << "\n"; if (mat && mat->enablePBR) { file << "- PBR Material: baseColor=(" << mat->baseColorFactor.r << "," << mat->baseColorFactor.g << "," << mat->baseColorFactor.b << ")" << " metallic=" << mat->metallicFactor << " roughness=" << mat->roughnessFactor << "\n"; - file << "- Textures: albedo=" << mat->albedoMapID << " metallicRough=" << mat->metallicRoughnessMapID - << " normal=" << mat->normalMapID << " ao=" << mat->aoMapID << " emissive=" << mat->emissiveMapID << "\n"; + file << "- Textures: albedo=" << FormatHandle(mat->albedoMapID) << " metallicRough=" << FormatHandle(mat->metallicRoughnessMapID) + << " normal=" << FormatHandle(mat->normalMapID) << " ao=" << FormatHandle(mat->aoMapID) << " emissive=" << FormatHandle(mat->emissiveMapID) << "\n"; } file << "- Depth: write=" << (state ? (state->depthWriteMask ? "yes" : "no") : "N/A") << " test=" << (state ? (state->depthTestEnabled ? "yes" : "no") : "N/A") << "\n"; @@ -1484,9 +1497,9 @@ namespace OloEngine const PODMaterialData* instMat = frame->GetSnapshotMaterialData(instCmd->materialDataIndex); file << "### Draw #" << drawIdx++ << ": " << cmd.GetCommandTypeString() << "\n\n"; file << "- Instances: " << instCmd->instanceCount << "\n"; - file << "- Shader: " << (instMat ? instMat->shaderRendererID : 0u) << "\n"; + file << "- Shader: " << FormatHandle(instMat ? instMat->shaderRendererID : RHI::NullResource) << "\n"; file << "- Material Data Index: " << instCmd->materialDataIndex << "\n"; - file << "- VAO: " << instCmd->vertexArrayID << ", Index Count: " << instCmd->indexCount << "\n\n"; + file << "- VAO: " << FormatHandle(instCmd->vertexArrayID) << ", Index Count: " << instCmd->indexCount << "\n\n"; } } else diff --git a/OloEngine/src/OloEngine/Renderer/Instancing/GPUFrustumCuller.h b/OloEngine/src/OloEngine/Renderer/Instancing/GPUFrustumCuller.h index aa5b01652..ea40042f5 100644 --- a/OloEngine/src/OloEngine/Renderer/Instancing/GPUFrustumCuller.h +++ b/OloEngine/src/OloEngine/Renderer/Instancing/GPUFrustumCuller.h @@ -30,7 +30,7 @@ namespace OloEngine // 3. The same atomic increment populates `instanceCount` in a 5-uint // `DrawElementsIndirectCommand` SSBO bound at // SSBO_INSTANCE_DRAW_INDIRECT = 17. - // 4. `RendererAPI::DrawElementsIndirectRaw` reads the indirect command + // 4. `RendererAPI::DrawBoundElementsIndirect` reads the indirect command // (no CPU round trip) and draws exactly the surviving instances. // // **Multiple GPU-cull submissions per frame**: each call returns a fresh @@ -47,7 +47,7 @@ namespace OloEngine { public: // Result returned by `Cull()` — the dispatcher binds these and calls - // `DrawElementsIndirectRaw(vaoID, IndirectBufferID)`. + // `DrawBoundElementsIndirect(IndirectBufferID)`. struct CullResult { Ref OutputBuffer; // bind at SSBO_INSTANCE_DATA = 15 before the draw diff --git a/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTField.cpp b/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTField.cpp index 6a6729f34..cf9a1c087 100644 --- a/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTField.cpp +++ b/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTField.cpp @@ -179,6 +179,16 @@ namespace OloEngine::Ocean return m_DerivativesTex ? m_DerivativesTex->GetRendererID() : 0u; } + RHI::ResourceHandle OceanFFTField::GetDisplacementTextureHandle() const + { + return m_DisplacementTex ? m_DisplacementTex->GetRHIHandle() : RHI::NullResource; + } + + RHI::ResourceHandle OceanFFTField::GetDerivativesTextureHandle() const + { + return m_DerivativesTex ? m_DerivativesTex->GetRHIHandle() : RHI::NullResource; + } + glm::vec2 OceanFFTField::SampleHorizontalBilinear(glm::vec2 worldXZ) const { if (!m_Field.IsValid() || m_Params.m_PatchSize <= 0.0f) diff --git a/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTField.h b/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTField.h index 67dd71941..53defb23f 100644 --- a/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTField.h +++ b/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTField.h @@ -1,6 +1,7 @@ #pragma once #include "OloEngine/Core/Base.h" +#include "OloEngine/Renderer/RHI/RHITypes.h" #include "OloEngine/Core/Ref.h" #include "OloEngine/Math/Math.h" #include "OloEngine/Renderer/Ocean/OceanFFTGpu.h" @@ -65,6 +66,10 @@ namespace OloEngine::Ocean [[nodiscard]] u32 GetDisplacementTextureID() const; [[nodiscard]] u32 GetDerivativesTextureID() const; + // Identity forms — the water draw command's fields migrated in + // issue #691 step 3 slice 6. The raw ids stay for the debug/tools paths. + [[nodiscard]] RHI::ResourceHandle GetDisplacementTextureHandle() const; + [[nodiscard]] RHI::ResourceHandle GetDerivativesTextureHandle() const; [[nodiscard]] f32 GetPatchSize() const noexcept { return m_Params.m_PatchSize; diff --git a/OloEngine/src/OloEngine/Renderer/Passes/DecalRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/DecalRenderPass.cpp index 6312968c4..895ac4e8d 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/DecalRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/DecalRenderPass.cpp @@ -220,13 +220,13 @@ namespace OloEngine // DrawDecalCommand packet. Keeping the override on the command // (instead of a global on CommandDispatch) preserves the // stateless, replay-safe contract of the bucket. - const u32 decalOITProgramID = m_OITShader->GetRendererID(); + const RHI::ResourceHandle decalOITProgram = m_OITShader->GetRHIHandle(); for (CommandPacket* packet : m_CommandBucket.GetPackets()) { if (!packet || packet->GetCommandType() != CommandType::DrawDecal) continue; if (auto* cmd = packet->GetCommandData()) - cmd->oitProgramOverride = decalOITProgramID; + cmd->oitProgramOverride = decalOITProgram; } // Bind scene depth (for decal projection) — the OIT variant needs diff --git a/OloEngine/src/OloEngine/Renderer/Passes/DeferredLightingPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/DeferredLightingPass.cpp index 671d66758..b88818ed1 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/DeferredLightingPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/DeferredLightingPass.cpp @@ -230,7 +230,9 @@ namespace OloEngine // sample whenever this flag is on. When off, the shader falls back // to the global IBL cubemap. DeferredControlsData controls{}; - const bool iblAvailable = Renderer3D::GetGlobalIrradianceMapID() != 0 && Renderer3D::GetGlobalPrefilterMapID() != 0 && Renderer3D::GetGlobalBRDFLutMapID() != 0; + const bool iblAvailable = Renderer3D::GetGlobalIrradianceMapHandle().IsValid() && + Renderer3D::GetGlobalPrefilterMapHandle().IsValid() && + Renderer3D::GetGlobalBRDFLutMapHandle().IsValid(); controls.Controls.x = iblAvailable ? 1.0f : 0.0f; controls.Controls.y = Renderer3D::GetRendererSettings().Deferred.EnableLightProbes ? 1.0f : 0.0f; // Runtime IBL strength multiplier: plumb the global scalar set via diff --git a/OloEngine/src/OloEngine/Renderer/Passes/FluidCompositePass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/FluidCompositePass.cpp index 86117014a..2177242de 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/FluidCompositePass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/FluidCompositePass.cpp @@ -141,7 +141,7 @@ namespace OloEngine // Upload the appearance parameters of this frame's fluid. Counts.z // carries the environment-map-present flag for the reflection branch. - const u32 environmentMapID = Renderer3D::GetGlobalEnvironmentMapID(); + const RHI::ResourceHandle environmentMap = Renderer3D::GetGlobalEnvironmentMapHandle(); { const FluidRenderData& appearance = m_IntermediatesPass->GetLastAppearance(); @@ -159,7 +159,7 @@ namespace OloEngine 1.0f / static_cast(fbWidth), 1.0f / static_cast(fbHeight)); ubo.Counts = glm::uvec4(appearance.ParticleUpperBound, static_cast(appearance.EntityID), - environmentMapID != 0 ? 1u : 0u, 0u); + environmentMap.IsValid() ? 1u : 0u, 0u); m_FluidRenderUBO->SetData(&ubo, sizeof(ubo)); m_FluidRenderUBO->Bind(); } @@ -170,8 +170,8 @@ namespace OloEngine context.BindTexture(ShaderBindingLayout::TEX_FLUID_THICKNESS, fluidThicknessID); context.BindTexture(ShaderBindingLayout::TEX_WATER_REFRACTION, refractionTexID); context.BindTexture(ShaderBindingLayout::TEX_WATER_DEPTH, sceneDepthID); - if (environmentMapID != 0) - context.BindTexture(ShaderBindingLayout::TEX_ENVIRONMENT, environmentMapID); + if (environmentMap.IsValid()) + context.BindTexture(ShaderBindingLayout::TEX_ENVIRONMENT, environmentMap); // The shader discards non-fluid pixels — no depth test, no blending, // no depth writes. @@ -196,7 +196,7 @@ namespace OloEngine context.BindTexture(ShaderBindingLayout::TEX_FLUID_THICKNESS, 0); context.BindTexture(ShaderBindingLayout::TEX_WATER_REFRACTION, 0); context.BindTexture(ShaderBindingLayout::TEX_WATER_DEPTH, 0); - if (environmentMapID != 0) + if (environmentMap.IsValid()) context.BindTexture(ShaderBindingLayout::TEX_ENVIRONMENT, 0); m_SceneFramebuffer->Unbind(); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/PlanarReflectionRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/PlanarReflectionRenderPass.cpp index ae655eb3d..6738415a8 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/PlanarReflectionRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/PlanarReflectionRenderPass.cpp @@ -107,7 +107,7 @@ namespace OloEngine const auto publishDisabled = [&]() { - Renderer3D::SetPlanarReflectionTextureID(0); + Renderer3D::SetPlanarReflectionTextureID(RHI::NullResource); if (m_ReflectionUBO) { m_ReflectionUBO->SetData(&ubo, UBOData::GetSize()); @@ -210,7 +210,7 @@ namespace OloEngine CommandDispatch::UploadCameraUBO(); CommandDispatch::InvalidateRenderStateCache(); - Renderer3D::SetPlanarReflectionTextureID(m_ReflectionFB->GetColorAttachmentRendererID(0)); + Renderer3D::SetPlanarReflectionTextureID(m_ReflectionFB->GetColorAttachmentHandle(0)); if (m_ReflectionUBO) { m_ReflectionUBO->SetData(&ubo, UBOData::GetSize()); @@ -239,6 +239,6 @@ namespace OloEngine { // Drop the texture publish so a stale reflection can't be sampled after a // graph reset / asset reload. - Renderer3D::SetPlanarReflectionTextureID(0); + Renderer3D::SetPlanarReflectionTextureID(RHI::NullResource); } } // namespace OloEngine diff --git a/OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.cpp index 16a9d9a51..659971f1f 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.cpp @@ -322,7 +322,7 @@ namespace OloEngine // instances[gl_InstanceIndex].Transform from the SSBO. struct ShadowMeshBatch { - RendererID drawVao; + RHI::ResourceHandle drawVao; u32 indexCount; u32 baseIndex; bool twoSided; // rendered with culling disabled instead of front-cull (issue #650) @@ -336,7 +336,7 @@ namespace OloEngine if (cullFrustum && ShouldCull(caster.WorldBounds, *cullFrustum)) continue; - RendererID const drawVao = (caster.shadowVaoID != 0) ? caster.shadowVaoID : caster.vaoID; + RHI::ResourceHandle const drawVao = caster.shadowVaoID.IsValid() ? caster.shadowVaoID : caster.vaoID; const glm::mat4 relTransform = MakeModelRelative(caster.transform, renderOrigin); InstanceData inst; inst.Transform = relTransform; @@ -413,7 +413,7 @@ namespace OloEngine // so per-instance picking isn't meaningful here. profiler.RecordInstancedDraw( /*meshHandle=*/0, - batch.drawVao, + batch.drawVao.Index, batch.indexCount, static_cast(batch.instances.size()), /*entityIDs=*/nullptr, @@ -482,7 +482,7 @@ namespace OloEngine { uploadShadowModelUBO(caster.transform); - if (caster.heightmapTextureID != 0) + if (caster.heightmapTextureID.IsValid()) { RenderCommand::BindTexture(ShaderBindingLayout::TEX_TERRAIN_HEIGHTMAP, caster.heightmapTextureID); } @@ -575,26 +575,26 @@ namespace OloEngine } // Shadow caster submission methods - void ShadowRenderPass::AddMeshCaster(RendererID vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, - RendererID shadowVaoID, const BoundingBox& worldBounds, bool twoSided) + void ShadowRenderPass::AddMeshCaster(RHI::ResourceHandle vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, + RHI::ResourceHandle shadowVaoID, const BoundingBox& worldBounds, bool twoSided) { m_MeshCasters.push_back({ vaoID, indexCount, baseIndex, transform, shadowVaoID, worldBounds, twoSided }); } - void ShadowRenderPass::AddSkinnedCaster(RendererID vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, + void ShadowRenderPass::AddSkinnedCaster(RHI::ResourceHandle vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, u32 boneBufferOffset, u32 boneCount, const BoundingBox& worldBounds) { m_SkinnedCasters.push_back({ vaoID, indexCount, baseIndex, transform, boneBufferOffset, boneCount, worldBounds }); } - void ShadowRenderPass::AddTerrainCaster(RendererID vaoID, u32 indexCount, u32 patchVertexCount, - const glm::mat4& transform, RendererID heightmapTextureID, + void ShadowRenderPass::AddTerrainCaster(RHI::ResourceHandle vaoID, u32 indexCount, u32 patchVertexCount, + const glm::mat4& transform, RHI::ResourceHandle heightmapTextureID, const ShaderBindingLayout::TerrainUBO& terrainUBO) { m_TerrainCasters.push_back({ vaoID, indexCount, patchVertexCount, transform, heightmapTextureID, terrainUBO }); } - void ShadowRenderPass::AddVoxelCaster(RendererID vaoID, u32 indexCount, const glm::mat4& transform) + void ShadowRenderPass::AddVoxelCaster(RHI::ResourceHandle vaoID, u32 indexCount, const glm::mat4& transform) { m_VoxelCasters.push_back({ vaoID, indexCount, transform }); } diff --git a/OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.h b/OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.h index 20173d2b9..984ed4ac4 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.h +++ b/OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.h @@ -30,11 +30,11 @@ namespace OloEngine struct ShadowMeshCaster { - RendererID vaoID = 0; + RHI::ResourceHandle vaoID{}; u32 indexCount = 0; u32 baseIndex = 0; // Offset (in u32 entries) into the IBO — non-zero for submeshes sharing a combined IBO glm::mat4 transform = glm::mat4(1.0f); - RendererID shadowVaoID = 0; // Position-merged shadow IB; 0 = use vaoID + RHI::ResourceHandle shadowVaoID{}; // Position-merged shadow IB; 0 = use vaoID BoundingBox WorldBounds = NoBounds; // World-space AABB; NoBounds = always include // Material is MaterialFlag::TwoSided — rendered into the shadow map with culling DISABLED // instead of the default front-face cull, so single-sided planar geometry (a quad, a @@ -44,7 +44,7 @@ namespace OloEngine struct ShadowSkinnedCaster { - RendererID vaoID = 0; + RHI::ResourceHandle vaoID{}; u32 indexCount = 0; u32 baseIndex = 0; // Same role as in ShadowMeshCaster glm::mat4 transform = glm::mat4(1.0f); @@ -55,17 +55,17 @@ namespace OloEngine struct ShadowTerrainCaster { - RendererID vaoID = 0; + RHI::ResourceHandle vaoID{}; u32 indexCount = 0; u32 patchVertexCount = 3; glm::mat4 transform = glm::mat4(1.0f); - RendererID heightmapTextureID = 0; + RHI::ResourceHandle heightmapTextureID{}; ShaderBindingLayout::TerrainUBO terrainUBO{}; }; struct ShadowVoxelCaster { - RendererID vaoID = 0; + RHI::ResourceHandle vaoID{}; u32 indexCount = 0; glm::mat4 transform = glm::mat4(1.0f); }; @@ -110,15 +110,15 @@ namespace OloEngine // Pass worldBounds (world-space AABB) when available; it enables per-cascade // frustum culling in Execute() so empty cascades skip all GPU work. // Leave as NoBounds when no tight bounds are available (foliage, terrain, etc.). - void AddMeshCaster(RendererID vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, - RendererID shadowVaoID = 0, const BoundingBox& worldBounds = NoBounds, + void AddMeshCaster(RHI::ResourceHandle vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, + RHI::ResourceHandle shadowVaoID = {}, const BoundingBox& worldBounds = NoBounds, bool twoSided = false); - void AddSkinnedCaster(RendererID vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, + void AddSkinnedCaster(RHI::ResourceHandle vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, u32 boneBufferOffset, u32 boneCount, const BoundingBox& worldBounds = NoBounds); - void AddTerrainCaster(RendererID vaoID, u32 indexCount, u32 patchVertexCount, - const glm::mat4& transform, RendererID heightmapTextureID, + void AddTerrainCaster(RHI::ResourceHandle vaoID, u32 indexCount, u32 patchVertexCount, + const glm::mat4& transform, RHI::ResourceHandle heightmapTextureID, const ShaderBindingLayout::TerrainUBO& terrainUBO); - void AddVoxelCaster(RendererID vaoID, u32 indexCount, const glm::mat4& transform); + void AddVoxelCaster(RHI::ResourceHandle vaoID, u32 indexCount, const glm::mat4& transform); void AddFoliageCaster(FoliageRenderer* renderer, const Ref& depthShader, f32 time); private: diff --git a/OloEngine/src/OloEngine/Renderer/Passes/ToneMapRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/ToneMapRenderPass.cpp index fb103defa..985485424 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/ToneMapRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/ToneMapRenderPass.cpp @@ -300,8 +300,8 @@ namespace OloEngine // Per-pixel water-surface depth (nearest wavy surface) captured by the // water pass — lets the underwater fog find the real water boundary per // pixel instead of assuming a flat plane. 0 when no water rendered. - const u32 waterDepthTextureID = Renderer3D::GetWaterSurfaceDepthTextureID(); - context.BindTexture(ShaderBindingLayout::TEX_UNDERWATER_WATER_DEPTH, waterDepthTextureID); + const RHI::ResourceHandle waterDepthTexture = Renderer3D::GetWaterSurfaceDepthTextureID(); + context.BindTexture(ShaderBindingLayout::TEX_UNDERWATER_WATER_DEPTH, waterDepthTexture); m_Shader->SetInt("u_WaterSurfaceDepth", ShaderBindingLayout::TEX_UNDERWATER_WATER_DEPTH); const auto va = MeshPrimitives::GetFullscreenTriangle(); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/WaterRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/WaterRenderPass.cpp index ed28e0292..b1770f69a 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/WaterRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/WaterRenderPass.cpp @@ -99,7 +99,7 @@ namespace OloEngine // underwater fog never samples a stale texture if this pass early-exits // (no scene FB, no water commands, zero-size, failed texture resolve) // before the capture runs. The successful capture path re-publishes it. - Renderer3D::SetWaterSurfaceDepthTextureID(0); + Renderer3D::SetWaterSurfaceDepthTextureID(RHI::NullResource); // Resolve the setup-selected scene framebuffer instead of replaying // a blackboard lookup ladder at execute time. @@ -215,13 +215,13 @@ namespace OloEngine CommandDispatch::SetWaterDepthCaptureActive(false); CommandDispatch::InvalidateRenderStateCache(); m_WaterDepthFB->Unbind(); - Renderer3D::SetWaterSurfaceDepthTextureID(m_WaterDepthFB->GetDepthAttachmentRendererID()); + Renderer3D::SetWaterSurfaceDepthTextureID(m_WaterDepthFB->GetDepthAttachmentHandle()); // Rebind the scene target for the colour pass below. m_SceneFramebuffer->Bind(); } else { - Renderer3D::SetWaterSurfaceDepthTextureID(0); + Renderer3D::SetWaterSurfaceDepthTextureID(RHI::NullResource); } m_CommandBucket.Execute(rendererAPI); diff --git a/OloEngine/src/OloEngine/Renderer/RenderCommand.h b/OloEngine/src/OloEngine/Renderer/RenderCommand.h index 4a1d10f6a..0ee8f28eb 100644 --- a/OloEngine/src/OloEngine/Renderer/RenderCommand.h +++ b/OloEngine/src/OloEngine/Renderer/RenderCommand.h @@ -290,9 +290,30 @@ namespace OloEngine // Raw-VAO variant used by the GPU frustum-cull path that only has a // RendererID (no Ref on hand inside the dispatcher). - static void DrawElementsIndirectRaw(u32 vaoID, u32 indirectBufferID) + static void DrawBoundElementsIndirect(u32 indirectBufferID) { - s_RendererAPI->DrawElementsIndirectRaw(vaoID, indirectBufferID); + s_RendererAPI->DrawBoundElementsIndirect(indirectBufferID); + } + + static void DrawIndexedPatchesRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 patchVertices) + { + s_RendererAPI->DrawIndexedPatchesRaw(vertexArray, indexCount, patchVertices); + } + + static void DrawIndexedInstancedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 baseIndex, + u32 instanceCount) + { + s_RendererAPI->DrawIndexedInstancedRaw(vertexArray, indexCount, baseIndex, instanceCount); + } + + static void DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount) + { + s_RendererAPI->DrawIndexedRaw(vertexArray, indexCount); + } + + static void DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 baseIndex) + { + s_RendererAPI->DrawIndexedRaw(vertexArray, indexCount, baseIndex); } // Multi-draw indirect with a GPU-sourced draw count (core GL 4.6, issue #629). @@ -397,6 +418,12 @@ namespace OloEngine return s_RendererAPI->CreateDepthArrayCompareOffView(srcTextureID, numLayers); } + [[nodiscard]] static RHI::ResourceHandle CreateDepthArrayCompareOffViewHandle(RHI::ResourceHandle srcTexture, + u32 numLayers) + { + return s_RendererAPI->CreateDepthArrayCompareOffViewHandle(srcTexture, numLayers); + } + static void SetTextureFilter(RHI::ResourceHandle texture, RHI::Filter minFilter, RHI::Filter magFilter) { s_RendererAPI->SetTextureFilter(texture, minFilter, magFilter); diff --git a/OloEngine/src/OloEngine/Renderer/RenderPipeline.cpp b/OloEngine/src/OloEngine/Renderer/RenderPipeline.cpp index 13968dcfe..7c32fc89d 100644 --- a/OloEngine/src/OloEngine/Renderer/RenderPipeline.cpp +++ b/OloEngine/src/OloEngine/Renderer/RenderPipeline.cpp @@ -204,8 +204,8 @@ namespace OloEngine // unconditionally (ToneMap's underwater-fog water-depth slot) would // bind a texture name whose owning framebuffer died in an earlier graph // resize/rebuild — the #505 stale-texture GL_INVALID_OPERATION. - data.WaterSurfaceDepthTextureID = 0; - data.PlanarReflectionTextureID = 0; + data.WaterSurfaceDepthTextureID = {}; + data.PlanarReflectionTextureID = {}; // GPU frustum-cull pool reset — slot cursor recycles from 0 each // frame. Buffers stay allocated (lifetime = engine, not frame) so @@ -424,11 +424,11 @@ namespace OloEngine CommandDispatch::ResetState(); // Set shadow texture IDs AFTER ResetState() so they aren't zeroed out. - CommandDispatch::SetShadowTextureIDs( - data.Shadow.GetCSMRendererID(), - data.Shadow.GetAtlasRendererID(), - data.Shadow.GetCSMRawRendererID(), - data.Shadow.GetAtlasRawRendererID()); + CommandDispatch::SetShadowTextures( + data.Shadow.GetCSMHandle(), + data.Shadow.GetAtlasHandle(), + data.Shadow.GetCSMRawHandle(), + data.Shadow.GetAtlasRawHandle()); // Initialize parallel scene context with immutable frame data. data.ParallelContext.ViewMatrix = data.ViewMatrix; @@ -1191,7 +1191,7 @@ namespace OloEngine { SnowAccumulationSystem::Update(data.SnowAccumulation, data.ViewPos, Timestep(dt)); SnowAccumulationSystem::BindSnowDepthTexture(); - CommandDispatch::SetSnowDepthTextureID(SnowAccumulationSystem::GetSnowDepthTextureID()); + CommandDispatch::SetSnowDepthTexture(SnowAccumulationSystem::GetSnowDepthTextureHandle()); } // Update snow ejecta particle simulation @@ -1287,7 +1287,7 @@ namespace OloEngine // TEX_CLOUD_SHADOW (62); CommandDispatch::ResetState() // zeroed it in PrepareFrame (same lifecycle as the snow // depth id above). - CommandDispatch::SetCloudShadowTextureID(CloudShadowMap::GetTextureID()); + CommandDispatch::SetCloudShadowTexture(CloudShadowMap::GetTextureHandle()); } } else @@ -1442,14 +1442,20 @@ namespace OloEngine // blackboard imports them by raw GL ID so a change must invalidate. HashU32(h, data.Shadow.GetResolution()); HashU32(h, data.Shadow.GetAtlasResolution()); - HashU32(h, data.Shadow.GetCSMRendererID()); - HashU32(h, data.Shadow.GetAtlasRendererID()); + // By IDENTITY, not driver name — the same defect the DDGI atlases had + // (issue #691 step 3). ShadowMap::SetSettings calls Shutdown() BEFORE + // Init() on a resolution change, so the old textures are freed first + // and GL may reissue their names to the replacements; a raw-id hash + // then sees no change and the graph keeps an import describing the OLD + // resolution. A generation cannot be reissued. + HashU64(h, RHI::HashKey(data.Shadow.GetCSMHandle())); + HashU64(h, RHI::HashKey(data.Shadow.GetAtlasHandle())); // The comparison-OFF raw-depth views (issue #607) are declared as graph // resources only when their ids are non-zero — a declaration-PRESENCE // gate, which by the #530 rule must be hashed or PopulateBlackboard // never re-runs and the resource never appears. - HashU32(h, data.Shadow.GetCSMRawRendererID()); - HashU32(h, data.Shadow.GetAtlasRawRendererID()); + HashU64(h, RHI::HashKey(data.Shadow.GetCSMRawHandle())); + HashU64(h, RHI::HashKey(data.Shadow.GetAtlasRawHandle())); // IBL renderer IDs — same rule as the shadow IDs above, and for the same // reason: PopulateBlackboard imports them by raw GL ID. @@ -1462,10 +1468,10 @@ namespace OloEngine // name", thousands of times. It masqueraded as intermittent because GL often // recycles the freed texture names, in which case the stale ID happens to be // valid again and nothing looks wrong. - HashU32(h, data.GlobalIrradianceMapID); - HashU32(h, data.GlobalPrefilterMapID); - HashU32(h, data.GlobalBRDFLutMapID); - HashU32(h, data.GlobalEnvironmentMapID); + HashU64(h, RHI::HashKey(data.GlobalIrradianceMapID)); + HashU64(h, RHI::HashKey(data.GlobalPrefilterMapID)); + HashU64(h, RHI::HashKey(data.GlobalBRDFLutMapID)); + HashU64(h, RHI::HashKey(data.GlobalEnvironmentMapID)); // Post-process technique selection + per-effect toggles HashU32(h, static_cast(std::to_underlying(data.PostProcess.ActiveAOTechnique))); @@ -2968,22 +2974,22 @@ namespace OloEngine // ------------------------------------------------------------------ // IBL resources // ------------------------------------------------------------------ - if (data.GlobalIrradianceMapID != 0) + if (data.GlobalIrradianceMapNativeID != 0) { board.IBL.IrradianceMap = graph.ImportTexture( - ResourceNames::IrradianceMap, data.GlobalIrradianceMapID, + ResourceNames::IrradianceMap, data.GlobalIrradianceMapNativeID, RGResourceDesc::FromHandleKind(RGResourceHandle::Kind::TextureCube, ResourceNames::IrradianceMap)); } - if (data.GlobalPrefilterMapID != 0) + if (data.GlobalPrefilterMapNativeID != 0) { board.IBL.PrefilterMap = graph.ImportTexture( - ResourceNames::PrefilterMap, data.GlobalPrefilterMapID, + ResourceNames::PrefilterMap, data.GlobalPrefilterMapNativeID, RGResourceDesc::FromHandleKind(RGResourceHandle::Kind::TextureCube, ResourceNames::PrefilterMap)); } - if (data.GlobalBRDFLutMapID != 0) + if (data.GlobalBRDFLutMapNativeID != 0) { board.IBL.BrdfLut = graph.ImportTexture( - ResourceNames::BrdfLut, data.GlobalBRDFLutMapID, + ResourceNames::BrdfLut, data.GlobalBRDFLutMapNativeID, RGResourceDesc::FromHandleKind(RGResourceHandle::Kind::Texture2D, ResourceNames::BrdfLut)); } } diff --git a/OloEngine/src/OloEngine/Renderer/Renderer3D.h b/OloEngine/src/OloEngine/Renderer/Renderer3D.h index ecbc9200d..7509bebd8 100644 --- a/OloEngine/src/OloEngine/Renderer/Renderer3D.h +++ b/OloEngine/src/OloEngine/Renderer/Renderer3D.h @@ -240,7 +240,7 @@ namespace OloEngine // is the single source of truth for "what the GPU was actually given" — // olo_material_get (MCP, issue #607) reports it verbatim rather than // re-deriving the resolution and risking a confidently wrong answer. - static auto CreatePODMaterialDataForMaterial(const Material& material, RendererID shaderRendererID) -> PODMaterialData; + static auto CreatePODMaterialDataForMaterial(const Material& material, RHI::ResourceHandle shaderRendererID) -> PODMaterialData; // Animated drawing commands static CommandPacket* DrawAnimatedMesh(const Ref& mesh, const glm::mat4& modelMatrix, const Material& material, const std::vector& boneMatrices, bool isStatic = false, i32 entityID = -1); // Same as DrawAnimatedMesh but also carries the previous-frame bone matrices used by the @@ -274,18 +274,18 @@ namespace OloEngine // Terrain/Voxel rendering (returns command packets for sorted execution) static CommandPacket* DrawTerrainPatch( - RendererID vaoID, u32 indexCount, u32 patchVertexCount, + RHI::ResourceHandle vaoID, u32 indexCount, u32 patchVertexCount, const Ref& shader, - RendererID heightmapID, RendererID splatmapID, RendererID splatmap1ID, - RendererID albedoArrayID, RendererID normalArrayID, RendererID armArrayID, + RHI::ResourceHandle heightmapID, RHI::ResourceHandle splatmapID, RHI::ResourceHandle splatmap1ID, + RHI::ResourceHandle albedoArrayID, RHI::ResourceHandle normalArrayID, RHI::ResourceHandle armArrayID, const glm::mat4& transform, const ShaderBindingLayout::TerrainUBO& terrainUBO, i32 entityID = -1); static CommandPacket* DrawVoxelMesh( - RendererID vaoID, u32 indexCount, + RHI::ResourceHandle vaoID, u32 indexCount, const Ref& shader, - RendererID albedoArrayID, RendererID normalArrayID, RendererID armArrayID, + RHI::ResourceHandle albedoArrayID, RHI::ResourceHandle normalArrayID, RHI::ResourceHandle armArrayID, const glm::mat4& transform, i32 entityID = -1); @@ -610,45 +610,71 @@ namespace OloEngine // Set global IBL textures from the scene's EnvironmentMap. // These are used as fallbacks when individual materials don't have IBL configured. - static void SetGlobalIBL(RendererID irradianceMapID, RendererID prefilterMapID, - RendererID brdfLutMapID, RendererID environmentMapID, + // Takes BOTH currencies for the three graph-imported maps, from the one + // call site that has the Ref in hand — deriving one from the + // other later is impossible in Renderer/ (native -> handle is not + // recoverable, and handle -> native may only happen in Platform/). + static void SetGlobalIBL(RHI::ResourceHandle irradianceMap, RHI::ResourceHandle prefilterMap, + RHI::ResourceHandle brdfLutMap, RHI::ResourceHandle environmentMap, + u32 irradianceNativeID, u32 prefilterNativeID, u32 brdfLutNativeID, f32 iblIntensity = 1.0f); static void ClearGlobalIBL(); - [[nodiscard]] static RendererID GetGlobalIrradianceMapID() + // Identity forms — what the command layer's bind cache consumes. + [[nodiscard]] static RHI::ResourceHandle GetGlobalIrradianceMapHandle() { return s_Data.GlobalIrradianceMapID; } - [[nodiscard]] static RendererID GetGlobalPrefilterMapID() + [[nodiscard]] static RHI::ResourceHandle GetGlobalPrefilterMapHandle() { return s_Data.GlobalPrefilterMapID; } - [[nodiscard]] static RendererID GetGlobalBRDFLutMapID() + [[nodiscard]] static RHI::ResourceHandle GetGlobalBRDFLutMapHandle() { return s_Data.GlobalBRDFLutMapID; } - [[nodiscard]] static RendererID GetGlobalEnvironmentMapID() + [[nodiscard]] static RHI::ResourceHandle GetGlobalEnvironmentMapHandle() { return s_Data.GlobalEnvironmentMapID; } + + // Native forms — what RenderPipeline's graph IMPORT consumes, and only + // that. These cannot migrate with the rest: DeferredLightingPass reads + // the imported IBL resources back through context.ResolveTexture, which + // answers 0 for anything imported by handle (see + // docs/agent-rules/rhi-abstraction-boundary.md). Moving the import + // without moving that reader would silently drop IBL from the deferred + // path — lit scene, no ambient, no error. + [[nodiscard]] static u32 GetGlobalIrradianceMapNativeID() + { + return s_Data.GlobalIrradianceMapNativeID; + } + [[nodiscard]] static u32 GetGlobalPrefilterMapNativeID() + { + return s_Data.GlobalPrefilterMapNativeID; + } + [[nodiscard]] static u32 GetGlobalBRDFLutMapNativeID() + { + return s_Data.GlobalBRDFLutMapNativeID; + } // Nearest wavy water-surface depth captured by WaterRenderPass this frame // (0 when no water rendered). Consumed by the underwater-fog stage in the // ToneMap pass to find the per-pixel water boundary. See §7.2. - static void SetWaterSurfaceDepthTextureID(RendererID id) + static void SetWaterSurfaceDepthTextureID(RHI::ResourceHandle id) { s_Data.WaterSurfaceDepthTextureID = id; } - [[nodiscard]] static RendererID GetWaterSurfaceDepthTextureID() + [[nodiscard]] static RHI::ResourceHandle GetWaterSurfaceDepthTextureID() { return s_Data.WaterSurfaceDepthTextureID; } // Planar-reflection colour texture published by PlanarReflectionRenderPass // each frame (0 when reflection is disabled / unavailable). Sampled by // WaterRenderPass at TEX_WATER_PLANAR_REFLECTION. - static void SetPlanarReflectionTextureID(RendererID id) + static void SetPlanarReflectionTextureID(RHI::ResourceHandle id) { s_Data.PlanarReflectionTextureID = id; } - [[nodiscard]] static RendererID GetPlanarReflectionTextureID() + [[nodiscard]] static RHI::ResourceHandle GetPlanarReflectionTextureID() { return s_Data.PlanarReflectionTextureID; } @@ -881,18 +907,18 @@ namespace OloEngine return s_Data.Shadow; } - static void AddMeshShadowCaster(RendererID vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, - RendererID shadowVaoID = 0, const BoundingBox& worldBounds = NoBounds, + static void AddMeshShadowCaster(RHI::ResourceHandle vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, + RHI::ResourceHandle shadowVaoID = {}, const BoundingBox& worldBounds = NoBounds, bool twoSided = false); - static void AddSkinnedShadowCaster(RendererID vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, + static void AddSkinnedShadowCaster(RHI::ResourceHandle vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, u32 boneBufferOffset, u32 boneCount, const BoundingBox& worldBounds = NoBounds); - static void AddTerrainShadowCaster(RendererID vaoID, u32 indexCount, u32 patchVertexCount, - const glm::mat4& transform, RendererID heightmapTextureID, + static void AddTerrainShadowCaster(RHI::ResourceHandle vaoID, u32 indexCount, u32 patchVertexCount, + const glm::mat4& transform, RHI::ResourceHandle heightmapTextureID, const ShaderBindingLayout::TerrainUBO& terrainUBO); - static void AddVoxelShadowCaster(RendererID vaoID, u32 indexCount, const glm::mat4& transform); + static void AddVoxelShadowCaster(RHI::ResourceHandle vaoID, u32 indexCount, const glm::mat4& transform); static void AddFoliageShadowCaster(FoliageRenderer* renderer, const Ref& depthShader, f32 time); @@ -1010,38 +1036,44 @@ namespace OloEngine // ID snapshot (taken per prepass activation) so the per-draw resolve is // a handful of integer compares. IDs are 0 while shaders are unloaded, // which disables the swap safely. + // Identities, not driver names (issue #691 step 3, slice 6): these are + // compared against PODMaterialData::shaderRendererID to decide whether a + // material's program may be swapped for the depth-only one, and that + // field is an identity now. Comparing programs by GL name across a + // shader HOT-RELOAD is also unsound — a relinked program can be handed + // the name a different program just freed. struct DepthPrepassShaderIDs { // Standard mesh programs eligible for the swap - u32 PBRStatic = 0; - u32 PBRSkinned = 0; - u32 GBufferStatic = 0; - u32 GBufferSkinned = 0; + RHI::ResourceHandle PBRStatic{}; + RHI::ResourceHandle PBRSkinned{}; + RHI::ResourceHandle GBufferStatic{}; + RHI::ResourceHandle GBufferSkinned{}; // Replacement depth-only programs (DepthPrepass*.glsl) - u32 DepthStatic = 0; - u32 DepthSkinned = 0; - u32 DepthMaskStatic = 0; - u32 DepthMaskSkinned = 0; + RHI::ResourceHandle DepthStatic{}; + RHI::ResourceHandle DepthSkinned{}; + RHI::ResourceHandle DepthMaskStatic{}; + RHI::ResourceHandle DepthMaskSkinned{}; }; static DepthPrepassShaderIDs GetDepthPrepassShaderIDs() { DepthPrepassShaderIDs ids; if (s_Data.PBRMultiLightShader) - ids.PBRStatic = s_Data.PBRMultiLightShader->GetRendererID(); + ids.PBRStatic = s_Data.PBRMultiLightShader->GetRHIHandle(); if (s_Data.PBRMultiLightSkinnedShader) - ids.PBRSkinned = s_Data.PBRMultiLightSkinnedShader->GetRendererID(); + ids.PBRSkinned = s_Data.PBRMultiLightSkinnedShader->GetRHIHandle(); if (s_Data.PBRGBufferShader) - ids.GBufferStatic = s_Data.PBRGBufferShader->GetRendererID(); + ids.GBufferStatic = s_Data.PBRGBufferShader->GetRHIHandle(); if (s_Data.PBRGBufferSkinnedShader) - ids.GBufferSkinned = s_Data.PBRGBufferSkinnedShader->GetRendererID(); + ids.GBufferSkinned = s_Data.PBRGBufferSkinnedShader->GetRHIHandle(); if (s_Data.DepthPrepassShader) - ids.DepthStatic = s_Data.DepthPrepassShader->GetRendererID(); + ids.DepthStatic = s_Data.DepthPrepassShader->GetRHIHandle(); if (s_Data.DepthPrepassSkinnedShader) - ids.DepthSkinned = s_Data.DepthPrepassSkinnedShader->GetRendererID(); + ids.DepthSkinned = s_Data.DepthPrepassSkinnedShader->GetRHIHandle(); if (s_Data.DepthPrepassMaskShader) - ids.DepthMaskStatic = s_Data.DepthPrepassMaskShader->GetRendererID(); + ids.DepthMaskStatic = s_Data.DepthPrepassMaskShader->GetRHIHandle(); if (s_Data.DepthPrepassMaskSkinnedShader) - ids.DepthMaskSkinned = s_Data.DepthPrepassMaskSkinnedShader->GetRendererID(); + ids.DepthMaskSkinned = s_Data.DepthPrepassMaskSkinnedShader->GetRHIHandle(); return ids; } @@ -1146,7 +1178,7 @@ namespace OloEngine const glm::mat4& inverseDecalTransform, const glm::vec4& decalColor, const glm::vec4& decalParams, - RendererID albedoTextureID, + RHI::ResourceHandle albedoTextureID, i32 entityID = -1); // Extended decal rendering — mode picks the G-Buffer channel (0=Albedo, @@ -1161,9 +1193,9 @@ namespace OloEngine const glm::mat4& inverseDecalTransform, const glm::vec4& decalColor, const glm::vec4& decalParams, - RendererID albedoTextureID, - RendererID normalTextureID, - RendererID rmaTextureID, + RHI::ResourceHandle albedoTextureID, + RHI::ResourceHandle normalTextureID, + RHI::ResourceHandle rmaTextureID, DrawDecalCommand::DecalMode mode, bool transparent, i32 entityID = -1); @@ -1174,8 +1206,8 @@ namespace OloEngine struct FoliageImpostorParams { bool Enabled = false; - RendererID AlbedoAtlasID = 0; // rgb + coverage - RendererID NormalDepthAtlasID = 0; // obj normal + card depth + RHI::ResourceHandle AlbedoAtlasID{}; // rgb + coverage + RHI::ResourceHandle NormalDepthAtlasID{}; // obj normal + card depth u32 FramesPerAxis = 8; bool Hemi = true; f32 StartDistance = 40.0f; @@ -1186,8 +1218,8 @@ namespace OloEngine // Foliage rendering (submits DrawFoliageLayerCommand to FoliageRenderPass bucket) static CommandPacket* DrawFoliageLayer( - RendererID vertexArrayID, u32 indexCount, u32 instanceCount, - RendererID albedoTextureID, + RHI::ResourceHandle vertexArrayID, u32 indexCount, u32 instanceCount, + RHI::ResourceHandle albedoTextureID, const glm::mat4& modelTransform, f32 time, f32 prevTime, @@ -1224,12 +1256,12 @@ namespace OloEngine // FFT ocean (WATER_FUTURE_IMPROVEMENTS.md §1): x = useFFT (0/1), // y = 1/patchSize, z = heightScale, w = horizontalScale. glm::vec4 fftParams = glm::vec4(0.0f); - RendererID normalMap0ID = 0; - RendererID normalMap1ID = 0; - RendererID noiseTextureID = 0; - RendererID foamTextureID = 0; - RendererID fftDisplacementID = 0; // rgb = (dx,h,dz), a = foam - RendererID fftDerivativesID = 0; // rgb = normal, a = jacobian + RHI::ResourceHandle normalMap0ID{}; + RHI::ResourceHandle normalMap1ID{}; + RHI::ResourceHandle noiseTextureID{}; + RHI::ResourceHandle foamTextureID{}; + RHI::ResourceHandle fftDisplacementID{}; // rgb = (dx,h,dz), a = foam + RHI::ResourceHandle fftDerivativesID{}; // rgb = normal, a = jacobian bool refractionEnabled = true; bool ssrEnabled = true; // When true the water plane draws double-sided so it stays visible @@ -1241,7 +1273,7 @@ namespace OloEngine // Water rendering (submits DrawWaterCommand to WaterRenderPass bucket) static CommandPacket* DrawWaterSurface( - RendererID vertexArrayID, u32 indexCount, + RHI::ResourceHandle vertexArrayID, u32 indexCount, const glm::mat4& modelTransform, f32 time, f32 prevTime, @@ -1449,7 +1481,8 @@ namespace OloEngine // G-Buffer slots. static bool IsDeferredCapableShader(const Ref& shader); static auto GetRenderStreamNode(RenderStreamType stream) -> CommandBufferRenderPass*; - static auto ValidateDrawMeshRendererIDs(const char* context, u32 vaoID, u32 shaderID) -> bool; + static auto ValidateDrawMeshResources(const char* context, RHI::ResourceHandle vertexArray, + RHI::ResourceHandle shader) -> bool; // Shared Deferred-vs-forward-overlay shader routing decision for the // instanced submission paths (DrawMeshInstanced's CPU-cull path and @@ -1775,19 +1808,25 @@ namespace OloEngine glm::vec2 PrevJitterUV = glm::vec2(0.0f); // Global IBL fallback (from scene's EnvironmentMap) - RendererID GlobalIrradianceMapID = 0; - RendererID GlobalPrefilterMapID = 0; - RendererID GlobalBRDFLutMapID = 0; - RendererID GlobalEnvironmentMapID = 0; + RHI::ResourceHandle GlobalIrradianceMapID{}; + RHI::ResourceHandle GlobalPrefilterMapID{}; + RHI::ResourceHandle GlobalBRDFLutMapID{}; + RHI::ResourceHandle GlobalEnvironmentMapID{}; + // Native siblings of the three above that RenderPipeline imports + // into the render graph. Set from the same SetGlobalIBL call so the + // two spellings cannot describe different textures. + u32 GlobalIrradianceMapNativeID = 0; + u32 GlobalPrefilterMapNativeID = 0; + u32 GlobalBRDFLutMapNativeID = 0; f32 GlobalIBLIntensity = 1.0f; // Nearest water-surface depth texture for underwater fog (§7.2); // published by WaterRenderPass, consumed by ToneMap. 0 = no water. - RendererID WaterSurfaceDepthTextureID = 0; + RHI::ResourceHandle WaterSurfaceDepthTextureID{}; // Planar-reflection colour texture published by // PlanarReflectionRenderPass, sampled by WaterRenderPass. 0 = none. - RendererID PlanarReflectionTextureID = 0; + RHI::ResourceHandle PlanarReflectionTextureID{}; // Per-frame planar-reflection request from Scene.cpp (dominant water // surface), forwarded to PlanarReflectionRenderPass at EndScene. diff --git a/OloEngine/src/OloEngine/Renderer/Renderer3DMeshSubmission.cpp b/OloEngine/src/OloEngine/Renderer/Renderer3DMeshSubmission.cpp index 91a9f0a1f..2d7b95469 100644 --- a/OloEngine/src/OloEngine/Renderer/Renderer3DMeshSubmission.cpp +++ b/OloEngine/src/OloEngine/Renderer/Renderer3DMeshSubmission.cpp @@ -53,15 +53,19 @@ namespace OloEngine } } // namespace - auto Renderer3D::ValidateDrawMeshRendererIDs(const char* context, const u32 vaoID, const u32 shaderID) -> bool + auto Renderer3D::ValidateDrawMeshResources(const char* context, const RHI::ResourceHandle vertexArray, + const RHI::ResourceHandle shader) -> bool { - if (vaoID != 0 && shaderID != 0) + if (vertexArray.IsValid() && shader.IsValid()) return true; - if (static std::atomic s_InvalidRendererIDWarnCount{ 0 }; s_InvalidRendererIDWarnCount.fetch_add(1, std::memory_order_relaxed) < 1) + if (static std::atomic s_InvalidResourceWarnCount{ 0 }; s_InvalidResourceWarnCount.fetch_add(1, std::memory_order_relaxed) < 1) { - OLO_CORE_WARN("{}: Dropping draw with invalid renderer IDs (VAO={}, Shader={})", - context, vaoID, shaderID); + // The handles format as #Index:Generation, which is more useful here + // than a driver name was: a tells you the producer never + // minted, a stale one tells you it was retired underneath the draw. + OLO_CORE_WARN("{}: Dropping draw with invalid resources (VAO={}, Shader={})", + context, vertexArray, shader); } return false; @@ -157,7 +161,7 @@ namespace OloEngine const Material& resolved = ResolveSubmeshMaterial(overrideMaterial, meshSource.get(), entry.SubmeshIndex, defaultMaterial); const Material* material = &resolved; - PODMaterialData const materialData = CreatePODMaterialDataForMaterial(*material, 0); + PODMaterialData const materialData = CreatePODMaterialDataForMaterial(*material, RHI::NullResource); submission.MaterialDataIndices.push_back(FrameDataBufferManager::Get().AllocateMaterialData(materialData)); // Anything that is not fully opaque needs the cutout/blend test, which only the @@ -173,7 +177,7 @@ namespace OloEngine registry.Submit(submission); } - auto Renderer3D::CreatePODMaterialDataForMaterial(const Material& material, RendererID shaderRendererID) -> PODMaterialData + auto Renderer3D::CreatePODMaterialDataForMaterial(const Material& material, RHI::ResourceHandle shaderRendererID) -> PODMaterialData { PODMaterialData data{}; data.shaderRendererID = shaderRendererID; @@ -184,8 +188,8 @@ namespace OloEngine data.specular = material.GetSpecular(); data.shininess = material.GetShininess(); data.useTextureMaps = material.IsUsingTextureMaps(); - data.diffuseMapID = material.GetDiffuseMap() ? material.GetDiffuseMap()->GetRendererID() : 0; - data.specularMapID = material.GetSpecularMap() ? material.GetSpecularMap()->GetRendererID() : 0; + data.diffuseMapID = material.GetDiffuseMap() ? material.GetDiffuseMap()->GetRHIHandle() : RHI::NullResource; + data.specularMapID = material.GetSpecularMap() ? material.GetSpecularMap()->GetRHIHandle() : RHI::NullResource; // PBR material properties. data.enablePBR = (material.GetType() == MaterialType::PBR); @@ -200,24 +204,24 @@ namespace OloEngine data.alphaCutoff = material.GetAlphaCutoff(); // PBR texture renderer IDs. - data.albedoMapID = material.GetAlbedoMap() ? material.GetAlbedoMap()->GetRendererID() : 0; - data.metallicRoughnessMapID = material.GetMetallicRoughnessMap() ? material.GetMetallicRoughnessMap()->GetRendererID() : 0; - data.normalMapID = material.GetNormalMap() ? material.GetNormalMap()->GetRendererID() : 0; - data.aoMapID = material.GetAOMap() ? material.GetAOMap()->GetRendererID() : 0; - data.emissiveMapID = material.GetEmissiveMap() ? material.GetEmissiveMap()->GetRendererID() : 0; - data.environmentMapID = material.GetEnvironmentMap() ? material.GetEnvironmentMap()->GetRendererID() : 0; - data.irradianceMapID = material.GetIrradianceMap() ? material.GetIrradianceMap()->GetRendererID() : 0; - data.prefilterMapID = material.GetPrefilterMap() ? material.GetPrefilterMap()->GetRendererID() : 0; - data.brdfLutMapID = material.GetBRDFLutMap() ? material.GetBRDFLutMap()->GetRendererID() : 0; + data.albedoMapID = material.GetAlbedoMap() ? material.GetAlbedoMap()->GetRHIHandle() : RHI::NullResource; + data.metallicRoughnessMapID = material.GetMetallicRoughnessMap() ? material.GetMetallicRoughnessMap()->GetRHIHandle() : RHI::NullResource; + data.normalMapID = material.GetNormalMap() ? material.GetNormalMap()->GetRHIHandle() : RHI::NullResource; + data.aoMapID = material.GetAOMap() ? material.GetAOMap()->GetRHIHandle() : RHI::NullResource; + data.emissiveMapID = material.GetEmissiveMap() ? material.GetEmissiveMap()->GetRHIHandle() : RHI::NullResource; + data.environmentMapID = material.GetEnvironmentMap() ? material.GetEnvironmentMap()->GetRHIHandle() : RHI::NullResource; + data.irradianceMapID = material.GetIrradianceMap() ? material.GetIrradianceMap()->GetRHIHandle() : RHI::NullResource; + data.prefilterMapID = material.GetPrefilterMap() ? material.GetPrefilterMap()->GetRHIHandle() : RHI::NullResource; + data.brdfLutMapID = material.GetBRDFLutMap() ? material.GetBRDFLutMap()->GetRHIHandle() : RHI::NullResource; // Fall back to global IBL when the material has no IBL configured. - if (data.enablePBR && data.irradianceMapID == 0 && Renderer3D::GetGlobalIrradianceMapID() != 0) + if (data.enablePBR && !data.irradianceMapID.IsValid() && Renderer3D::GetGlobalIrradianceMapHandle().IsValid()) { - data.irradianceMapID = Renderer3D::GetGlobalIrradianceMapID(); - data.prefilterMapID = Renderer3D::GetGlobalPrefilterMapID(); - data.brdfLutMapID = Renderer3D::GetGlobalBRDFLutMapID(); - if (data.environmentMapID == 0) - data.environmentMapID = Renderer3D::GetGlobalEnvironmentMapID(); + data.irradianceMapID = Renderer3D::GetGlobalIrradianceMapHandle(); + data.prefilterMapID = Renderer3D::GetGlobalPrefilterMapHandle(); + data.brdfLutMapID = Renderer3D::GetGlobalBRDFLutMapHandle(); + if (!data.environmentMapID.IsValid()) + data.environmentMapID = Renderer3D::GetGlobalEnvironmentMapHandle(); data.enableIBL = true; data.iblIntensity = Renderer3D::GetGlobalIBLIntensity(); } @@ -364,9 +368,9 @@ namespace OloEngine return nullptr; } - const u32 vertexArrayID = meshToUse->GetVertexArray()->GetRendererID(); - const u32 shaderRendererID = shaderToUse->GetRendererID(); - if (!ValidateDrawMeshRendererIDs("Renderer3D::DrawMesh", vertexArrayID, shaderRendererID)) + const RHI::ResourceHandle vertexArrayID = meshToUse->GetVertexArray()->GetRHIHandle(); + const RHI::ResourceHandle shaderRendererID = shaderToUse->GetRHIHandle(); + if (!ValidateDrawMeshResources("Renderer3D::DrawMesh", vertexArrayID, shaderRendererID)) return nullptr; // Create POD command using asset handles and renderer IDs. @@ -424,7 +428,7 @@ namespace OloEngine // Set sort key for optimal command sorting. PacketMetadata metadata = packet->GetMetadata(); - u32 shaderID = shaderRendererID & 0xFFFF; // 16-bit shader ID. + u32 shaderID = shaderRendererID.Index & 0xFFFF; // 16-bit shader ID. u32 materialID = ComputeMaterialID(material); u32 depth = ComputeDepthForSortKey(modelMatrix); if (material.GetFlag(MaterialFlag::Blend)) @@ -634,9 +638,9 @@ namespace OloEngine // Validate before allocating a packet so an invalid draw doesn't // consume packet-arena storage it will never submit (matches // DrawMesh's ordering). - const u32 vertexArrayID = mesh->GetVertexArray()->GetRendererID(); - const u32 shaderRendererID = shaderToUse->GetRendererID(); - if (!ValidateDrawMeshRendererIDs("Renderer3D::DrawMeshInstanced", vertexArrayID, shaderRendererID)) + const RHI::ResourceHandle vertexArrayID = mesh->GetVertexArray()->GetRHIHandle(); + const RHI::ResourceHandle shaderRendererID = shaderToUse->GetRHIHandle(); + if (!ValidateDrawMeshResources("Renderer3D::DrawMeshInstanced", vertexArrayID, shaderRendererID)) return nullptr; // Create POD command. @@ -675,7 +679,7 @@ namespace OloEngine // Set sort key for instanced mesh commands (use first transform for depth). PacketMetadata metadata = packet->GetMetadata(); - u32 shaderID = shaderRendererID & 0xFFFF; + u32 shaderID = shaderRendererID.Index & 0xFFFF; u32 materialID = ComputeMaterialID(material); u32 depth = activeTransforms->empty() ? 0 : ComputeDepthForSortKey((*activeTransforms)[0]); if (material.GetFlag(MaterialFlag::Blend)) @@ -861,9 +865,9 @@ namespace OloEngine OLO_CORE_ERROR("Renderer3D::SubmitGPUCulledInstanced: No shader available!"); return nullptr; } - const u32 vertexArrayID = mesh->GetVertexArray()->GetRendererID(); - const u32 shaderRendererID = shaderToUse->GetRendererID(); - if (!ValidateDrawMeshRendererIDs("Renderer3D::SubmitGPUCulledInstanced", vertexArrayID, shaderRendererID)) + const RHI::ResourceHandle vertexArrayID = mesh->GetVertexArray()->GetRHIHandle(); + const RHI::ResourceHandle shaderRendererID = shaderToUse->GetRHIHandle(); + if (!ValidateDrawMeshResources("Renderer3D::SubmitGPUCulledInstanced", vertexArrayID, shaderRendererID)) return nullptr; // Material / render state allocated once; both phase-1 and phase-2 @@ -872,7 +876,7 @@ namespace OloEngine CreatePODMaterialDataForMaterial(material, shaderRendererID)); const u32 renderStateIndex = FrameDataBufferManager::Get().AllocateRenderState(CreatePODRenderStateForMaterial(material)); - const u32 shaderID = shaderRendererID & 0xFFFF; + const u32 shaderID = shaderRendererID.Index & 0xFFFF; const u32 materialID = ComputeMaterialID(material); const u32 sortDepth = transforms.empty() ? 0 : ComputeDepthForSortKey(transforms[0]); @@ -1165,9 +1169,9 @@ namespace OloEngine auto* cmd = packet->GetCommandData(); cmd->header.type = CommandType::DrawMesh; - const u32 vertexArrayID = vertexArray->GetRendererID(); - const u32 shaderRendererID = shaderToUse->GetRendererID(); - if (!ValidateDrawMeshRendererIDs("Renderer3D::DrawAnimatedMesh", vertexArrayID, shaderRendererID)) + const RHI::ResourceHandle vertexArrayID = vertexArray->GetRHIHandle(); + const RHI::ResourceHandle shaderRendererID = shaderToUse->GetRHIHandle(); + if (!ValidateDrawMeshResources("Renderer3D::DrawAnimatedMesh", vertexArrayID, shaderRendererID)) return nullptr; // Store asset handles and renderer IDs (POD). @@ -1211,7 +1215,7 @@ namespace OloEngine // Set sort key for animated mesh commands. PacketMetadata metadata = packet->GetMetadata(); - u32 shaderID = shaderRendererID & 0xFFFF; + u32 shaderID = shaderRendererID.Index & 0xFFFF; u32 materialID = ComputeMaterialID(material); u32 depth = ComputeDepthForSortKey(modelMatrix); if (material.GetFlag(MaterialFlag::Blend)) @@ -1596,9 +1600,9 @@ namespace OloEngine !IsDeferredCapableShader(shaderToUse) && s_Data.Pipeline->RenderStreamPasses.ForwardOverlay; - const u32 vertexArrayID = meshToUse->GetVertexArray()->GetRendererID(); - const u32 shaderRendererID = shaderToUse->GetRendererID(); - if (!ValidateDrawMeshRendererIDs("Renderer3D::DrawMeshParallel", vertexArrayID, shaderRendererID)) + const RHI::ResourceHandle vertexArrayID = meshToUse->GetVertexArray()->GetRHIHandle(); + const RHI::ResourceHandle shaderRendererID = shaderToUse->GetRHIHandle(); + if (!ValidateDrawMeshResources("Renderer3D::DrawMeshParallel", vertexArrayID, shaderRendererID)) return nullptr; // Create POD command using worker's allocator. @@ -1647,7 +1651,7 @@ namespace OloEngine // Set sort key using parallel context view matrix for depth. PacketMetadata metadata = packet->GetMetadata(); - const u32 shaderID = shaderRendererID & 0xFFFF; + const u32 shaderID = shaderRendererID.Index & 0xFFFF; const u32 materialID = ComputeMaterialID(material); const u32 depthKey = ComputeDepthForSortKeyWithView(modelMatrix, ctx.SceneContext->ViewMatrix); @@ -1813,9 +1817,9 @@ namespace OloEngine auto* cmd = packet->GetCommandData(); cmd->header.type = CommandType::DrawMesh; - const u32 vertexArrayID = mesh->GetVertexArray()->GetRendererID(); - const u32 shaderRendererID = shaderToUse->GetRendererID(); - if (!ValidateDrawMeshRendererIDs("Renderer3D::DrawAnimatedMeshParallel", vertexArrayID, shaderRendererID)) + const RHI::ResourceHandle vertexArrayID = mesh->GetVertexArray()->GetRHIHandle(); + const RHI::ResourceHandle shaderRendererID = shaderToUse->GetRHIHandle(); + if (!ValidateDrawMeshResources("Renderer3D::DrawAnimatedMeshParallel", vertexArrayID, shaderRendererID)) return nullptr; cmd->meshHandle = mesh->GetHandle(); @@ -1849,7 +1853,7 @@ namespace OloEngine // Set sort key. PacketMetadata metadata = packet->GetMetadata(); - const u32 shaderID = shaderRendererID & 0xFFFF; + const u32 shaderID = shaderRendererID.Index & 0xFFFF; const u32 materialID = ComputeMaterialID(material); const u32 depthKey = ComputeDepthForSortKeyWithView(modelMatrix, ctx.SceneContext->ViewMatrix); diff --git a/OloEngine/src/OloEngine/Renderer/Renderer3DSpecializedDraws.cpp b/OloEngine/src/OloEngine/Renderer/Renderer3DSpecializedDraws.cpp index b114d0d0f..4fabd6441 100644 --- a/OloEngine/src/OloEngine/Renderer/Renderer3DSpecializedDraws.cpp +++ b/OloEngine/src/OloEngine/Renderer/Renderer3DSpecializedDraws.cpp @@ -19,12 +19,12 @@ namespace OloEngine const glm::mat4& inverseDecalTransform, const glm::vec4& decalColor, const glm::vec4& decalParams, - RendererID albedoTextureID, + RHI::ResourceHandle albedoTextureID, i32 entityID) { // Delegate to the extended variant with Albedo mode + zero extra textures. return DrawDecal(decalTransform, inverseDecalTransform, decalColor, decalParams, - albedoTextureID, /*normal*/ 0u, /*rma*/ 0u, + albedoTextureID, /*normal*/ RHI::NullResource, /*rma*/ RHI::NullResource, DrawDecalCommand::DecalMode::Albedo, /*transparent*/ false, entityID); } @@ -34,9 +34,9 @@ namespace OloEngine const glm::mat4& inverseDecalTransform, const glm::vec4& decalColor, const glm::vec4& decalParams, - RendererID albedoTextureID, - RendererID normalTextureID, - RendererID rmaTextureID, + RHI::ResourceHandle albedoTextureID, + RHI::ResourceHandle normalTextureID, + RHI::ResourceHandle rmaTextureID, DrawDecalCommand::DecalMode mode, bool transparent, i32 entityID) @@ -109,9 +109,9 @@ namespace OloEngine } } - cmd->vertexArrayID = va->GetRendererID(); + cmd->vertexArrayID = va->GetRHIHandle(); cmd->indexCount = s_Data.DecalCubeMesh->GetIndexCount(); - cmd->shaderRendererID = decalShader->GetRendererID(); + cmd->shaderRendererID = decalShader->GetRHIHandle(); // Camera-relative (issue #429): the decal cube's model matrix goes up // through UploadModelInstance, which shifts it by the render origin, so // the rendered box is in render-relative space (correct screen position @@ -169,8 +169,8 @@ namespace OloEngine } CommandPacket* Renderer3D::DrawFoliageLayer( - RendererID vertexArrayID, u32 indexCount, u32 instanceCount, - RendererID albedoTextureID, + RHI::ResourceHandle vertexArrayID, u32 indexCount, u32 instanceCount, + RHI::ResourceHandle albedoTextureID, const glm::mat4& modelTransform, f32 time, f32 prevTime, @@ -197,7 +197,7 @@ namespace OloEngine // Octahedral impostor path (issue #433): always routes through the // forward FoliagePass with the impostor shader — the card does its own // relighting, so it composites into SceneColor after (deferred) lighting. - const bool useImpostor = impostor.Enabled && impostor.AlbedoAtlasID != 0 && s_Data.FoliageImpostorShader; + const bool useImpostor = impostor.Enabled && impostor.AlbedoAtlasID.IsValid() && s_Data.FoliageImpostorShader; // Deferred: route through ScenePass (the G-Buffer FB) with the // G-Buffer variant shader so foliage participates in the deferred @@ -233,7 +233,7 @@ namespace OloEngine cmd->vertexArrayID = vertexArrayID; cmd->indexCount = indexCount; cmd->instanceCount = instanceCount; - cmd->shaderRendererID = activeShader->GetRendererID(); + cmd->shaderRendererID = activeShader->GetRHIHandle(); cmd->modelTransform = modelTransform; cmd->normalMatrix = glm::transpose(glm::inverse(modelTransform)); cmd->time = time; @@ -290,7 +290,7 @@ namespace OloEngine } CommandPacket* Renderer3D::DrawWaterSurface( - RendererID vertexArrayID, u32 indexCount, + RHI::ResourceHandle vertexArrayID, u32 indexCount, const glm::mat4& modelTransform, f32 time, f32 prevTime, @@ -332,7 +332,7 @@ namespace OloEngine cmd->vertexArrayID = vertexArrayID; cmd->indexCount = indexCount; - cmd->shaderRendererID = s_Data.WaterShader->GetRendererID(); + cmd->shaderRendererID = s_Data.WaterShader->GetRHIHandle(); cmd->modelTransform = modelTransform; cmd->normalMatrix = glm::transpose(glm::inverse(modelTransform)); diff --git a/OloEngine/src/OloEngine/Renderer/Renderer3DState.cpp b/OloEngine/src/OloEngine/Renderer/Renderer3DState.cpp index 16268733f..93f6fb167 100644 --- a/OloEngine/src/OloEngine/Renderer/Renderer3DState.cpp +++ b/OloEngine/src/OloEngine/Renderer/Renderer3DState.cpp @@ -22,8 +22,8 @@ namespace OloEngine return s_Data.Pipeline->FrameCorePasses.Shadow != nullptr; } - void Renderer3D::AddMeshShadowCaster(RendererID vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, - RendererID shadowVaoID, const BoundingBox& worldBounds, bool twoSided) + void Renderer3D::AddMeshShadowCaster(RHI::ResourceHandle vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, + RHI::ResourceHandle shadowVaoID, const BoundingBox& worldBounds, bool twoSided) { if (auto shadowPass = s_Data.Pipeline->FrameCorePasses.Shadow; shadowPass) { @@ -31,7 +31,7 @@ namespace OloEngine } } - void Renderer3D::AddSkinnedShadowCaster(RendererID vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, + void Renderer3D::AddSkinnedShadowCaster(RHI::ResourceHandle vaoID, u32 indexCount, u32 baseIndex, const glm::mat4& transform, u32 boneBufferOffset, u32 boneCount, const BoundingBox& worldBounds) { if (auto shadowPass = s_Data.Pipeline->FrameCorePasses.Shadow; shadowPass) @@ -40,8 +40,8 @@ namespace OloEngine } } - void Renderer3D::AddTerrainShadowCaster(RendererID vaoID, u32 indexCount, u32 patchVertexCount, - const glm::mat4& transform, RendererID heightmapTextureID, + void Renderer3D::AddTerrainShadowCaster(RHI::ResourceHandle vaoID, u32 indexCount, u32 patchVertexCount, + const glm::mat4& transform, RHI::ResourceHandle heightmapTextureID, const ShaderBindingLayout::TerrainUBO& terrainUBO) { if (auto shadowPass = s_Data.Pipeline->FrameCorePasses.Shadow; shadowPass) @@ -50,7 +50,7 @@ namespace OloEngine } } - void Renderer3D::AddVoxelShadowCaster(RendererID vaoID, u32 indexCount, const glm::mat4& transform) + void Renderer3D::AddVoxelShadowCaster(RHI::ResourceHandle vaoID, u32 indexCount, const glm::mat4& transform) { if (auto shadowPass = s_Data.Pipeline->FrameCorePasses.Shadow; shadowPass) { @@ -174,23 +174,32 @@ namespace OloEngine // causing the shader to early-out. The SSBO remains bound from init (zeroed). } - void Renderer3D::SetGlobalIBL(RendererID irradianceMapID, RendererID prefilterMapID, - RendererID brdfLutMapID, RendererID environmentMapID, + void Renderer3D::SetGlobalIBL(RHI::ResourceHandle irradianceMap, RHI::ResourceHandle prefilterMap, + RHI::ResourceHandle brdfLutMap, RHI::ResourceHandle environmentMap, + u32 irradianceNativeID, u32 prefilterNativeID, u32 brdfLutNativeID, f32 iblIntensity) { - s_Data.GlobalIrradianceMapID = irradianceMapID; - s_Data.GlobalPrefilterMapID = prefilterMapID; - s_Data.GlobalBRDFLutMapID = brdfLutMapID; - s_Data.GlobalEnvironmentMapID = environmentMapID; + s_Data.GlobalIrradianceMapID = irradianceMap; + s_Data.GlobalPrefilterMapID = prefilterMap; + s_Data.GlobalBRDFLutMapID = brdfLutMap; + s_Data.GlobalEnvironmentMapID = environmentMap; + s_Data.GlobalIrradianceMapNativeID = irradianceNativeID; + s_Data.GlobalPrefilterMapNativeID = prefilterNativeID; + s_Data.GlobalBRDFLutMapNativeID = brdfLutNativeID; s_Data.GlobalIBLIntensity = iblIntensity; } void Renderer3D::ClearGlobalIBL() { - s_Data.GlobalIrradianceMapID = 0; - s_Data.GlobalPrefilterMapID = 0; - s_Data.GlobalBRDFLutMapID = 0; - s_Data.GlobalEnvironmentMapID = 0; + s_Data.GlobalIrradianceMapID = {}; + s_Data.GlobalPrefilterMapID = {}; + s_Data.GlobalBRDFLutMapID = {}; + s_Data.GlobalEnvironmentMapID = {}; + // Both currencies clear together — leaving the native trio set would let + // the graph import a texture the bind path considers gone. + s_Data.GlobalIrradianceMapNativeID = 0; + s_Data.GlobalPrefilterMapNativeID = 0; + s_Data.GlobalBRDFLutMapNativeID = 0; s_Data.GlobalIBLIntensity = 1.0f; } diff --git a/OloEngine/src/OloEngine/Renderer/Renderer3DUtilityDraws.cpp b/OloEngine/src/OloEngine/Renderer/Renderer3DUtilityDraws.cpp index 537ad63bc..71980a73a 100644 --- a/OloEngine/src/OloEngine/Renderer/Renderer3DUtilityDraws.cpp +++ b/OloEngine/src/OloEngine/Renderer/Renderer3DUtilityDraws.cpp @@ -55,10 +55,10 @@ namespace OloEngine auto* cmd = packet->GetCommandData(); cmd->header.type = CommandType::DrawQuad; cmd->transform = glm::mat4(modelMatrix); - cmd->textureID = texture->GetRendererID(); + cmd->textureID = texture->GetRHIHandle(); cmd->shaderHandle = s_Data.QuadShader->GetHandle(); - cmd->shaderRendererID = s_Data.QuadShader->GetRendererID(); - cmd->quadVAID = s_Data.QuadMesh->GetVertexArray()->GetRendererID(); + cmd->shaderRendererID = s_Data.QuadShader->GetRHIHandle(); + cmd->quadVAID = s_Data.QuadMesh->GetVertexArray()->GetRHIHandle(); cmd->renderStateIndex = FrameDataBufferManager::Get().AllocateRenderState(CreateDefaultPODRenderState()); packet->SetCommandType(cmd->header.type); @@ -104,9 +104,9 @@ namespace OloEngine auto* cmd = packet->GetCommandData(); cmd->header.type = CommandType::DrawMesh; - const u32 vertexArrayID = s_Data.CubeMesh->GetVertexArray()->GetRendererID(); - const u32 shaderRendererID = activeShader ? activeShader->GetRendererID() : 0u; - if (!ValidateDrawMeshRendererIDs("Renderer3D::DrawLightCube", vertexArrayID, shaderRendererID)) + const RHI::ResourceHandle vertexArrayID = s_Data.CubeMesh->GetVertexArray()->GetRHIHandle(); + const RHI::ResourceHandle shaderRendererID = activeShader ? activeShader->GetRHIHandle() : RHI::NullResource; + if (!ValidateDrawMeshResources("Renderer3D::DrawLightCube", vertexArrayID, shaderRendererID)) return nullptr; // Store asset handles and renderer IDs (POD) @@ -140,7 +140,7 @@ namespace OloEngine // Set sort key for light cube PacketMetadata metadata = packet->GetMetadata(); - u32 shaderID = shaderRendererID & 0xFFFF; + u32 shaderID = shaderRendererID.Index & 0xFFFF; u32 depth = ComputeDepthForSortKey(modelMatrix); metadata.m_SortKey = DrawKey::CreateOpaque(0, ViewLayerType::ThreeD, shaderID, 0, depth); packet->SetMetadata(metadata); @@ -201,12 +201,12 @@ namespace OloEngine // Store asset handles and renderer IDs (POD) cmd->meshHandle = s_Data.SkyboxMesh->GetHandle(); - cmd->vertexArrayID = s_Data.SkyboxMesh->GetVertexArray()->GetRendererID(); + cmd->vertexArrayID = s_Data.SkyboxMesh->GetVertexArray()->GetRHIHandle(); cmd->indexCount = s_Data.SkyboxMesh->GetIndexCount(); cmd->transform = glm::mat4(1.0f); // Identity matrix for skybox cmd->shaderHandle = activeShader->GetHandle(); - cmd->shaderRendererID = activeShader->GetRendererID(); - cmd->skyboxTextureID = skyboxTexture->GetRendererID(); + cmd->shaderRendererID = activeShader->GetRHIHandle(); + cmd->skyboxTextureID = skyboxTexture->GetRHIHandle(); // Skybox-specific POD render state { @@ -570,8 +570,8 @@ namespace OloEngine // Store renderer IDs (POD) cmd->shaderHandle = activeShader->GetHandle(); - cmd->shaderRendererID = activeShader->GetRendererID(); - cmd->quadVAOID = s_Data.FullscreenQuadVAO->GetRendererID(); + cmd->shaderRendererID = activeShader->GetRHIHandle(); + cmd->quadVAOID = s_Data.FullscreenQuadVAO->GetRHIHandle(); cmd->gridScale = gridScale; // Grid-specific render state. The G-Buffer variant writes gl_FragDepth @@ -623,10 +623,10 @@ namespace OloEngine } CommandPacket* Renderer3D::DrawTerrainPatch( - RendererID vaoID, u32 indexCount, u32 patchVertexCount, + RHI::ResourceHandle vaoID, u32 indexCount, u32 patchVertexCount, const Ref& shader, - RendererID heightmapID, RendererID splatmapID, RendererID splatmap1ID, - RendererID albedoArrayID, RendererID normalArrayID, RendererID armArrayID, + RHI::ResourceHandle heightmapID, RHI::ResourceHandle splatmapID, RHI::ResourceHandle splatmap1ID, + RHI::ResourceHandle albedoArrayID, RHI::ResourceHandle normalArrayID, RHI::ResourceHandle armArrayID, const glm::mat4& transform, const ShaderBindingLayout::TerrainUBO& terrainUBO, i32 entityID) @@ -639,7 +639,7 @@ namespace OloEngine return nullptr; } - if (vaoID == 0 || !shader) + if (!vaoID.IsValid() || !shader) { return nullptr; } @@ -674,7 +674,7 @@ namespace OloEngine cmd->vertexArrayID = vaoID; cmd->indexCount = indexCount; cmd->patchVertexCount = patchVertexCount; - cmd->shaderRendererID = activeShader->GetRendererID(); + cmd->shaderRendererID = activeShader->GetRHIHandle(); cmd->heightmapTextureID = heightmapID; cmd->splatmapTextureID = splatmapID; cmd->splatmap1TextureID = splatmap1ID; @@ -713,9 +713,9 @@ namespace OloEngine } CommandPacket* Renderer3D::DrawVoxelMesh( - RendererID vaoID, u32 indexCount, + RHI::ResourceHandle vaoID, u32 indexCount, const Ref& shader, - RendererID albedoArrayID, RendererID normalArrayID, RendererID armArrayID, + RHI::ResourceHandle albedoArrayID, RHI::ResourceHandle normalArrayID, RHI::ResourceHandle armArrayID, const glm::mat4& transform, i32 entityID) { @@ -727,7 +727,7 @@ namespace OloEngine return nullptr; } - if (vaoID == 0 || !shader) + if (!vaoID.IsValid() || !shader) { return nullptr; } @@ -750,7 +750,7 @@ namespace OloEngine cmd->vertexArrayID = vaoID; cmd->indexCount = indexCount; - cmd->shaderRendererID = activeShader->GetRendererID(); + cmd->shaderRendererID = activeShader->GetRHIHandle(); cmd->albedoArrayTextureID = albedoArrayID; cmd->normalArrayTextureID = normalArrayID; cmd->armArrayTextureID = armArrayID; diff --git a/OloEngine/src/OloEngine/Renderer/RendererAPI.h b/OloEngine/src/OloEngine/Renderer/RendererAPI.h index eb75269ff..8cd8c165b 100644 --- a/OloEngine/src/OloEngine/Renderer/RendererAPI.h +++ b/OloEngine/src/OloEngine/Renderer/RendererAPI.h @@ -67,9 +67,16 @@ namespace OloEngine // Raw VAO ID overloads for POD shadow casters (no Ref available) virtual void DrawIndexedRaw(u32 vaoID, u32 indexCount) = 0; virtual void DrawIndexedRaw(u32 vaoID, u32 indexCount, u32 baseIndex) = 0; + // Identity forms — vertex arrays migrated in issue #691 step 3 slice 6. + virtual void DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount) = 0; + virtual void DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 baseIndex) = 0; // Instanced raw variant for batched shadow casters that share VAO + submesh range. virtual void DrawIndexedInstancedRaw(u32 vaoID, u32 indexCount, u32 baseIndex, u32 instanceCount) = 0; + virtual void DrawIndexedInstancedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 baseIndex, + u32 instanceCount) = 0; virtual void DrawIndexedPatchesRaw(u32 vaoID, u32 indexCount, u32 patchVertices) = 0; + virtual void DrawIndexedPatchesRaw(RHI::ResourceHandle vertexArray, u32 indexCount, + u32 patchVertices) = 0; virtual void SetLineWidth(f32 width) = 0; @@ -109,7 +116,12 @@ namespace OloEngine virtual void DrawArraysIndirect(const Ref& vertexArray, u32 indirectBufferID) = 0; // Raw-VAO variant used by the GPU-frustum-cull path which only has a // RendererID (the dispatcher's BindVAOIfNeeded() cache populates it). - virtual void DrawElementsIndirectRaw(u32 vaoID, u32 indirectBufferID) = 0; + // Draws from the ALREADY-BOUND vertex array (issue #691 step 3, slice 6). + // Replaces the DrawElementsIndirectRaw(vaoID, ...) pair: its only caller + // had just run BindVAOIfNeeded, so re-binding inside the draw was both + // redundant and a bind behind the redundant-bind cache's back. Mirrors + // the existing DrawBound* family, whose comment gives the same reason. + virtual void DrawBoundElementsIndirect(u32 indirectBufferID) = 0; // Multi-draw indirect with a GPU-sourced draw count (core GL 4.6, issue #629): // reads DrawElementsIndirectCommand records from indirectBufferID starting at // indirectOffsetBytes and the u32 draw count from parameterBufferID at @@ -199,6 +211,12 @@ namespace OloEngine // provide). Source must be DEPTH_COMPONENT32F immutable storage. Returns 0 // if the platform lacks texture-view support. virtual u32 CreateDepthArrayCompareOffView(u32 srcTextureID, u32 numLayers) = 0; + // Handle form (issue #691 step 3, slice 6 — the command-layer bind + // cache). The view is a DISTINCT GPU object from the array it aliases, + // so it gets its own identity: ShadowMap holds both, and binding the + // wrong one is a silent PCSS bug rather than a loud one. + [[nodiscard]] virtual RHI::ResourceHandle CreateDepthArrayCompareOffViewHandle(RHI::ResourceHandle srcTexture, + u32 numLayers) = 0; // Replaces SetTextureParameter(id, GLenum pname, GLint value). `pname` // was an open-ended GL enum space, and mirroring it with an // RHI::TextureParameterName would have re-exported GL under a new name. @@ -449,6 +467,7 @@ namespace OloEngine // must fold that into a UBO and delete this. Recorded deliberately in // ADR 0011 amendment (9) rather than left to surprise Phase 7 bring-up. virtual void SetProgramUniformFloat(u32 programID, std::string_view name, f32 value) = 0; + virtual void SetProgramUniformFloat(RHI::ResourceHandle program, std::string_view name, f32 value) = 0; // GPU capability queries diff --git a/OloEngine/src/OloEngine/Renderer/Shadow/ShadowMap.cpp b/OloEngine/src/OloEngine/Renderer/Shadow/ShadowMap.cpp index f48e32168..39b018798 100644 --- a/OloEngine/src/OloEngine/Renderer/Shadow/ShadowMap.cpp +++ b/OloEngine/src/OloEngine/Renderer/Shadow/ShadowMap.cpp @@ -1,6 +1,7 @@ #include "OloEnginePCH.h" #include "OloEngine/Renderer/Shadow/ShadowMap.h" #include "OloEngine/Renderer/CameraRelative.h" +#include "OloEngine/Renderer/Debug/RenderGraphResourceIdentity.h" #include "OloEngine/Renderer/RenderCommand.h" #include "OloEngine/Renderer/Texture2DArray.h" #include "OloEngine/Renderer/UniformBuffer.h" @@ -43,10 +44,18 @@ namespace OloEngine // used by the PCSS blocker search (the hardware comparison sampler // can't read raw occluder depth). These alias the same immutable // storage, so the sampler2DArrayShadow bindings are unaffected. - m_CSMRawViewID = RenderCommand::CreateDepthArrayCompareOffView( - m_CSMTextureArray->GetRendererID(), MAX_CSM_CASCADES); - m_AtlasRawViewID = RenderCommand::CreateDepthArrayCompareOffView( - m_AtlasTexture->GetRendererID(), 1); + // + // Created through the HANDLE form so each view carries an identity of + // its own (issue #691 step 3): the bind cache keys on it, while + // RenderPipeline still declares the graph resource by raw id. The two + // spellings name the same object — the native id is read back out of + // the registry rather than minted separately, so they cannot drift. + m_CSMRawViewHandle = RenderCommand::CreateDepthArrayCompareOffViewHandle( + m_CSMTextureArray->GetRHIHandle(), MAX_CSM_CASCADES); + m_AtlasRawViewHandle = RenderCommand::CreateDepthArrayCompareOffViewHandle( + m_AtlasTexture->GetRHIHandle(), 1); + m_CSMRawViewID = Debug::NativeTextureIdForDiagnostics(m_CSMRawViewHandle); + m_AtlasRawViewID = Debug::NativeTextureIdForDiagnostics(m_AtlasRawViewHandle); // Create shadow UBO at binding 6 m_ShadowUBO = UniformBuffer::Create( @@ -85,16 +94,22 @@ namespace OloEngine { OLO_PROFILE_FUNCTION(); - if (m_CSMRawViewID != 0) + // Delete through the HANDLE form: it destroys the GL object AND + // retires the registry entry. Deleting by raw id would leave the slot + // live, so a stale handle would go on resolving to a name the driver + // may reissue to the view Init() creates moments later. + if (m_CSMRawViewHandle.IsValid()) { - RenderCommand::DeleteTexture(m_CSMRawViewID); - m_CSMRawViewID = 0; + RenderCommand::DeleteTexture(m_CSMRawViewHandle); + m_CSMRawViewHandle = {}; } - if (m_AtlasRawViewID != 0) + m_CSMRawViewID = 0; + if (m_AtlasRawViewHandle.IsValid()) { - RenderCommand::DeleteTexture(m_AtlasRawViewID); - m_AtlasRawViewID = 0; + RenderCommand::DeleteTexture(m_AtlasRawViewHandle); + m_AtlasRawViewHandle = {}; } + m_AtlasRawViewID = 0; m_CSMTextureArray.Reset(); m_AtlasTexture.Reset(); @@ -383,6 +398,16 @@ namespace OloEngine return m_AtlasTexture ? m_AtlasTexture->GetRendererID() : 0; } + RHI::ResourceHandle ShadowMap::GetCSMHandle() const + { + return m_CSMTextureArray ? m_CSMTextureArray->GetRHIHandle() : RHI::NullResource; + } + + RHI::ResourceHandle ShadowMap::GetAtlasHandle() const + { + return m_AtlasTexture ? m_AtlasTexture->GetRHIHandle() : RHI::NullResource; + } + // ------------------------------------------------------------------ // Placeholder shadow textures // ------------------------------------------------------------------ @@ -393,6 +418,7 @@ namespace OloEngine { Ref g_PlaceholderShadowArray; u32 g_PlaceholderShadowArrayRaw = 0u; // compare-OFF view of the array above + RHI::ResourceHandle g_PlaceholderShadowArrayRawHandle{}; Ref CreatePlaceholderShadowArray() { @@ -413,20 +439,46 @@ namespace OloEngine return g_PlaceholderShadowArray ? g_PlaceholderShadowArray->GetRendererID() : 0u; } + RHI::ResourceHandle ShadowMap::GetCSMPlaceholderHandle() + { + if (!g_PlaceholderShadowArray) + g_PlaceholderShadowArray = CreatePlaceholderShadowArray(); + return g_PlaceholderShadowArray ? g_PlaceholderShadowArray->GetRHIHandle() : RHI::NullResource; + } + + RHI::ResourceHandle ShadowMap::GetAtlasPlaceholderHandle() + { + // The atlas uses the same sampler2DArrayShadow type as CSM — share. + return GetCSMPlaceholderHandle(); + } + u32 ShadowMap::GetAtlasPlaceholderRendererID() { // The atlas uses the same sampler2DArrayShadow type as CSM — share. return GetCSMPlaceholderRendererID(); } - u32 ShadowMap::GetCSMRawPlaceholderRendererID() + RHI::ResourceHandle ShadowMap::GetCSMRawPlaceholderHandle() { - if (g_PlaceholderShadowArrayRaw == 0u) + if (!g_PlaceholderShadowArrayRawHandle.IsValid()) { - const u32 src = GetCSMPlaceholderRendererID(); // ensures the array exists - if (src != 0u) - g_PlaceholderShadowArrayRaw = RenderCommand::CreateDepthArrayCompareOffView(src, 1u); + // GetCSMPlaceholderHandle() ensures the source array exists. + if (const RHI::ResourceHandle src = GetCSMPlaceholderHandle(); src.IsValid()) + { + g_PlaceholderShadowArrayRawHandle = + RenderCommand::CreateDepthArrayCompareOffViewHandle(src, 1u); + g_PlaceholderShadowArrayRaw = + Debug::NativeTextureIdForDiagnostics(g_PlaceholderShadowArrayRawHandle); + } } + return g_PlaceholderShadowArrayRawHandle; + } + + u32 ShadowMap::GetCSMRawPlaceholderRendererID() + { + // Kept as the native spelling for the graph-declaration path; the view + // itself is created once, by the handle form above. + [[maybe_unused]] const RHI::ResourceHandle handle = GetCSMRawPlaceholderHandle(); return g_PlaceholderShadowArrayRaw; } @@ -436,13 +488,23 @@ namespace OloEngine return GetCSMRawPlaceholderRendererID(); } + RHI::ResourceHandle ShadowMap::GetAtlasRawPlaceholderHandle() + { + // Same plain sampler2DArray placeholder as CSM raw — share. + return GetCSMRawPlaceholderHandle(); + } + void ShadowMap::ShutdownPlaceholders() { - if (g_PlaceholderShadowArrayRaw != 0u) + // By handle, so the registry entry is retired with the GL object — + // deleting by raw id would leave a live slot pointing at a name the + // driver is free to reissue. + if (g_PlaceholderShadowArrayRawHandle.IsValid()) { - RenderCommand::DeleteTexture(g_PlaceholderShadowArrayRaw); - g_PlaceholderShadowArrayRaw = 0u; + RenderCommand::DeleteTexture(g_PlaceholderShadowArrayRawHandle); + g_PlaceholderShadowArrayRawHandle = {}; } + g_PlaceholderShadowArrayRaw = 0u; g_PlaceholderShadowArray.Reset(); } diff --git a/OloEngine/src/OloEngine/Renderer/Shadow/ShadowMap.h b/OloEngine/src/OloEngine/Renderer/Shadow/ShadowMap.h index d4b8e45c2..357dcfbe5 100644 --- a/OloEngine/src/OloEngine/Renderer/Shadow/ShadowMap.h +++ b/OloEngine/src/OloEngine/Renderer/Shadow/ShadowMap.h @@ -1,6 +1,7 @@ #pragma once #include "OloEngine/Core/Base.h" +#include "OloEngine/Renderer/RHI/RHITypes.h" #include "OloEngine/Core/Ref.h" #include "OloEngine/Renderer/ShaderBindingLayout.h" #include "OloEngine/Renderer/ShaderConstants.h" @@ -183,6 +184,25 @@ namespace OloEngine [[nodiscard]] u32 GetCSMRendererID() const; [[nodiscard]] u32 GetAtlasRendererID() const; + // Identity siblings (issue #691 step 3, slice 6). BOTH currencies are + // kept on purpose and the split is by CONSUMER, not by preference: + // + // * the bind path (CommandDispatch's redundant-bind cache) takes + // handles — that cache is keyed on them now, which is what makes a + // stale entry unable to collide with a recycled GL name; + // * the graph path (RenderPipeline's DeclareTransientTexture / + // blackboard import) still takes raw ids, because importing by + // handle leaves RenderGraph::ResolveTexture answering 0 and that is + // what the MCP capture endpoints read. See + // docs/agent-rules/rhi-abstraction-boundary.md. + // + // The pipeline FINGERPRINT reads the handles, not the ids: Shutdown() + // deletes these textures before Init() recreates them on a resolution + // change, so GL may reissue the same names and a raw-id hash would not + // see the recreate at all — the same defect the DDGI atlases had. + [[nodiscard]] RHI::ResourceHandle GetCSMHandle() const; + [[nodiscard]] RHI::ResourceHandle GetAtlasHandle() const; + // Render-graph resource names of the two raw-depth views below (issue // #607). They are pass-owned raw GL texture views, so until they are // declared under a stable name both olo_render_list_targets and @@ -206,6 +226,17 @@ namespace OloEngine { return m_AtlasRawViewID; } + // A texture VIEW is a distinct GPU object from the array it aliases, so + // it carries its own identity rather than borrowing the array's — see + // CreateDepthArrayCompareOffViewHandle. + [[nodiscard]] RHI::ResourceHandle GetCSMRawHandle() const + { + return m_CSMRawViewHandle; + } + [[nodiscard]] RHI::ResourceHandle GetAtlasRawHandle() const + { + return m_AtlasRawViewHandle; + } // Placeholder shadow textures for when no real shadow map is available // this frame. Some drivers validate the bound texture target at draw @@ -217,10 +248,14 @@ namespace OloEngine // sampler2DArrayShadow target). [[nodiscard]] static u32 GetCSMPlaceholderRendererID(); [[nodiscard]] static u32 GetAtlasPlaceholderRendererID(); + [[nodiscard]] static RHI::ResourceHandle GetCSMPlaceholderHandle(); + [[nodiscard]] static RHI::ResourceHandle GetAtlasPlaceholderHandle(); // Comparison-OFF raw-depth placeholders (plain sampler2DArray) for the // PCSS raw-view slots when no real shadow map is bound this frame. [[nodiscard]] static u32 GetCSMRawPlaceholderRendererID(); [[nodiscard]] static u32 GetAtlasRawPlaceholderRendererID(); + [[nodiscard]] static RHI::ResourceHandle GetCSMRawPlaceholderHandle(); + [[nodiscard]] static RHI::ResourceHandle GetAtlasRawPlaceholderHandle(); // Release placeholder textures. Called at renderer shutdown. static void ShutdownPlaceholders(); @@ -319,6 +354,10 @@ namespace OloEngine // PCSS blocker search). Owned GL texture-view objects; deleted in Shutdown(). u32 m_CSMRawViewID = 0; u32 m_AtlasRawViewID = 0; + // Identities for the two views above, minted by the facade's handle + // form and retired by DeleteTexture(handle) (which unregisters too). + RHI::ResourceHandle m_CSMRawViewHandle{}; + RHI::ResourceHandle m_AtlasRawViewHandle{}; // World-space atlas entry state (UBO carries the camera-relative copies) std::array m_AtlasEntryWorldMatrices{}; diff --git a/OloEngine/src/OloEngine/Scene/Scene.cpp b/OloEngine/src/OloEngine/Scene/Scene.cpp index a8df9949e..920a9ade8 100644 --- a/OloEngine/src/OloEngine/Scene/Scene.cpp +++ b/OloEngine/src/OloEngine/Scene/Scene.cpp @@ -6086,10 +6086,13 @@ namespace OloEngine { auto& envMap = sky.m_EnvironmentMap; Renderer3D::SetGlobalIBL( - envMap->GetIrradianceMap() ? envMap->GetIrradianceMap()->GetRendererID() : 0, - envMap->GetPrefilterMap() ? envMap->GetPrefilterMap()->GetRendererID() : 0, - envMap->GetBRDFLutMap() ? envMap->GetBRDFLutMap()->GetRendererID() : 0, - envMap->GetEnvironmentMap() ? envMap->GetEnvironmentMap()->GetRendererID() : 0, + envMap->GetIrradianceMap() ? envMap->GetIrradianceMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetPrefilterMap() ? envMap->GetPrefilterMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetBRDFLutMap() ? envMap->GetBRDFLutMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetEnvironmentMap() ? envMap->GetEnvironmentMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetIrradianceMap() ? envMap->GetIrradianceMap()->GetRendererID() : 0u, + envMap->GetPrefilterMap() ? envMap->GetPrefilterMap()->GetRendererID() : 0u, + envMap->GetBRDFLutMap() ? envMap->GetBRDFLutMap()->GetRendererID() : 0u, sky.m_IBLIntensity); } return; // Only one Star Nest sky drives the scene @@ -6249,10 +6252,13 @@ namespace OloEngine { auto& envMap = sky.m_EnvironmentMap; Renderer3D::SetGlobalIBL( - envMap->GetIrradianceMap() ? envMap->GetIrradianceMap()->GetRendererID() : 0, - envMap->GetPrefilterMap() ? envMap->GetPrefilterMap()->GetRendererID() : 0, - envMap->GetBRDFLutMap() ? envMap->GetBRDFLutMap()->GetRendererID() : 0, - envMap->GetEnvironmentMap() ? envMap->GetEnvironmentMap()->GetRendererID() : 0, + envMap->GetIrradianceMap() ? envMap->GetIrradianceMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetPrefilterMap() ? envMap->GetPrefilterMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetBRDFLutMap() ? envMap->GetBRDFLutMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetEnvironmentMap() ? envMap->GetEnvironmentMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetIrradianceMap() ? envMap->GetIrradianceMap()->GetRendererID() : 0u, + envMap->GetPrefilterMap() ? envMap->GetPrefilterMap()->GetRendererID() : 0u, + envMap->GetBRDFLutMap() ? envMap->GetBRDFLutMap()->GetRendererID() : 0u, sky.m_IBLIntensity); } return; // Only one procedural sky drives the scene @@ -6322,10 +6328,13 @@ namespace OloEngine { auto& envMap = envMapComp.m_EnvironmentMap; Renderer3D::SetGlobalIBL( - envMap->GetIrradianceMap() ? envMap->GetIrradianceMap()->GetRendererID() : 0, - envMap->GetPrefilterMap() ? envMap->GetPrefilterMap()->GetRendererID() : 0, - envMap->GetBRDFLutMap() ? envMap->GetBRDFLutMap()->GetRendererID() : 0, - envMap->GetEnvironmentMap() ? envMap->GetEnvironmentMap()->GetRendererID() : 0, + envMap->GetIrradianceMap() ? envMap->GetIrradianceMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetPrefilterMap() ? envMap->GetPrefilterMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetBRDFLutMap() ? envMap->GetBRDFLutMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetEnvironmentMap() ? envMap->GetEnvironmentMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetIrradianceMap() ? envMap->GetIrradianceMap()->GetRendererID() : 0u, + envMap->GetPrefilterMap() ? envMap->GetPrefilterMap()->GetRendererID() : 0u, + envMap->GetBRDFLutMap() ? envMap->GetBRDFLutMap()->GetRendererID() : 0u, envMapComp.m_IBLIntensity); } else @@ -6371,25 +6380,28 @@ namespace OloEngine auto const* bestProbe = probePtrs[static_cast(winner)]; auto const& envMap = bestProbe->m_BakedEnvironment; Renderer3D::SetGlobalIBL( - envMap->GetIrradianceMap() ? envMap->GetIrradianceMap()->GetRendererID() : 0, - envMap->GetPrefilterMap() ? envMap->GetPrefilterMap()->GetRendererID() : 0, - envMap->GetBRDFLutMap() ? envMap->GetBRDFLutMap()->GetRendererID() : 0, - envMap->GetEnvironmentMap() ? envMap->GetEnvironmentMap()->GetRendererID() : 0, + envMap->GetIrradianceMap() ? envMap->GetIrradianceMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetPrefilterMap() ? envMap->GetPrefilterMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetBRDFLutMap() ? envMap->GetBRDFLutMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetEnvironmentMap() ? envMap->GetEnvironmentMap()->GetRHIHandle() : RHI::NullResource, + envMap->GetIrradianceMap() ? envMap->GetIrradianceMap()->GetRendererID() : 0u, + envMap->GetPrefilterMap() ? envMap->GetPrefilterMap()->GetRendererID() : 0u, + envMap->GetBRDFLutMap() ? envMap->GetBRDFLutMap()->GetRendererID() : 0u, bestProbe->m_Intensity); } - // Helper: obtain the shadow VAO RendererID from a Mesh (returns 0 if unavailable). - [[nodiscard]] static RendererID GetShadowVaoID(const Ref& mesh) + // Helper: obtain the shadow VAO RHI::ResourceHandle from a Mesh (returns 0 if unavailable). + [[nodiscard]] static RHI::ResourceHandle GetShadowVaoID(const Ref& mesh) { if (!mesh) { - return 0; + return {}; } if (auto const& ms = mesh->GetMeshSource(); ms && ms->HasShadowVertexArray()) { - return ms->GetShadowVertexArray()->GetRendererID(); + return ms->GetShadowVertexArray()->GetRHIHandle(); } - return 0; + return {}; } // Submit every submesh of a MeshSource through the CLASSIC (non-virtualized) mesh path: @@ -6421,13 +6433,13 @@ namespace OloEngine } DDGIMeshCaster caster; - caster.vaoID = va->GetRendererID(); + caster.vaoID = va->GetRHIHandle(); caster.indexCount = mesh->GetIndexCount(); caster.baseIndex = mesh->GetBaseIndex(); caster.transform = worldTransform; caster.worldBounds = mesh->GetTransformedBoundingBox(worldTransform); caster.baseColor = material.GetBaseColorFactor(); - caster.albedoTextureID = material.GetAlbedoMap() ? material.GetAlbedoMap()->GetRendererID() : 0; + caster.albedoTextureID = material.GetAlbedoMap() ? material.GetAlbedoMap()->GetRHIHandle() : RHI::NullResource; caster.twoSided = material.GetFlag(MaterialFlag::TwoSided); Renderer3D::AddDDGICaster(caster); } @@ -6465,7 +6477,7 @@ namespace OloEngine { if (auto va = submesh->GetVertexArray(); va) { - Renderer3D::AddMeshShadowCaster(va->GetRendererID(), submesh->GetIndexCount(), + Renderer3D::AddMeshShadowCaster(va->GetRHIHandle(), submesh->GetIndexCount(), submesh->GetBaseIndex(), worldTransform, GetShadowVaoID(submesh), submesh->GetTransformedBoundingBox(worldTransform), @@ -7374,21 +7386,21 @@ namespace OloEngine bool hasMaterial = terrain.m_Material && terrain.m_Material->IsBuilt(); // Extract texture IDs for command packets - RendererID splatmapID = 0, splatmap1ID = 0; - RendererID albedoArrayID = 0, normalArrayID = 0, armArrayID = 0; + RHI::ResourceHandle splatmapID = {}, splatmap1ID = {}; + RHI::ResourceHandle albedoArrayID = {}, normalArrayID = {}, armArrayID = {}; if (hasMaterial) { auto& mat = terrain.m_Material; if (auto s0 = mat->GetSplatmap(0)) - splatmapID = s0->GetRendererID(); + splatmapID = s0->GetRHIHandle(); if (auto s1 = mat->GetSplatmap(1)) - splatmap1ID = s1->GetRendererID(); + splatmap1ID = s1->GetRHIHandle(); if (mat->GetAlbedoArray()) - albedoArrayID = mat->GetAlbedoArray()->GetRendererID(); + albedoArrayID = mat->GetAlbedoArray()->GetRHIHandle(); if (mat->GetNormalArray()) - normalArrayID = mat->GetNormalArray()->GetRendererID(); + normalArrayID = mat->GetNormalArray()->GetRHIHandle(); if (mat->GetARMArray()) - armArrayID = mat->GetARMArray()->GetRendererID(); + armArrayID = mat->GetARMArray()->GetRHIHandle(); } i32 entityID = static_cast(std::to_underlying(entity)); @@ -7458,28 +7470,28 @@ namespace OloEngine const TerrainLocalCullInputs tileCull = MakeTerrainLocalCullInputs(tileModel, cameraPosition, viewProjection); - RendererID heightmapID = 0; + RHI::ResourceHandle heightmapID{}; if (terrainData && terrainData->GetGPUHeightmap()) { - heightmapID = terrainData->GetGPUHeightmap()->GetRendererID(); + heightmapID = terrainData->GetGPUHeightmap()->GetRHIHandle(); } // Per-tile material overrides entity material texture IDs - RendererID tileSplatmapID = splatmapID, tileSplatmap1ID = splatmap1ID; - RendererID tileAlbedoArrayID = albedoArrayID, tileNormalArrayID = normalArrayID, tileArmArrayID = armArrayID; + RHI::ResourceHandle tileSplatmapID = splatmapID, tileSplatmap1ID = splatmap1ID; + RHI::ResourceHandle tileAlbedoArrayID = albedoArrayID, tileNormalArrayID = normalArrayID, tileArmArrayID = armArrayID; bool tileHasMaterial = tileMaterial && tileMaterial->IsBuilt(); if (tileHasMaterial && tileMaterial != terrain.m_Material.get()) { if (auto s0 = tileMaterial->GetSplatmap(0)) - tileSplatmapID = s0->GetRendererID(); + tileSplatmapID = s0->GetRHIHandle(); if (auto s1 = tileMaterial->GetSplatmap(1)) - tileSplatmap1ID = s1->GetRendererID(); + tileSplatmap1ID = s1->GetRHIHandle(); if (tileMaterial->GetAlbedoArray()) - tileAlbedoArrayID = tileMaterial->GetAlbedoArray()->GetRendererID(); + tileAlbedoArrayID = tileMaterial->GetAlbedoArray()->GetRHIHandle(); if (tileMaterial->GetNormalArray()) - tileNormalArrayID = tileMaterial->GetNormalArray()->GetRendererID(); + tileNormalArrayID = tileMaterial->GetNormalArray()->GetRHIHandle(); if (tileMaterial->GetARMArray()) - tileArmArrayID = tileMaterial->GetARMArray()->GetRendererID(); + tileArmArrayID = tileMaterial->GetARMArray()->GetRHIHandle(); } // Build base terrain UBO (tess factors filled per-chunk) @@ -7529,7 +7541,7 @@ namespace OloEngine terrainUBOData.TessFactors2.w = 1.0f; auto* packet = Renderer3D::DrawTerrainPatch( - va->GetRendererID(), rc.Chunk->GetIndexCount(), 3, + va->GetRHIHandle(), rc.Chunk->GetIndexCount(), 3, terrainShader, heightmapID, tileSplatmapID, tileSplatmap1ID, tileAlbedoArrayID, tileNormalArrayID, tileArmArrayID, @@ -7541,7 +7553,7 @@ namespace OloEngine if (hasActiveShadows) { Renderer3D::AddTerrainShadowCaster( - va->GetRendererID(), rc.Chunk->GetIndexCount(), 3, + va->GetRHIHandle(), rc.Chunk->GetIndexCount(), 3, tileModel, heightmapID, terrainUBOData); } } @@ -7561,7 +7573,7 @@ namespace OloEngine } auto* packet = Renderer3D::DrawTerrainPatch( - va->GetRendererID(), chunk->GetIndexCount(), 3, + va->GetRHIHandle(), chunk->GetIndexCount(), 3, terrainShader, heightmapID, tileSplatmapID, tileSplatmap1ID, tileAlbedoArrayID, tileNormalArrayID, tileArmArrayID, @@ -7572,7 +7584,7 @@ namespace OloEngine if (hasActiveShadows) { Renderer3D::AddTerrainShadowCaster( - va->GetRendererID(), chunk->GetIndexCount(), 3, + va->GetRHIHandle(), chunk->GetIndexCount(), 3, tileModel, heightmapID, terrainUBOData); } } @@ -7619,7 +7631,7 @@ namespace OloEngine if (mesh.VAO && mesh.IndexCount > 0) { auto* packet = Renderer3D::DrawVoxelMesh( - mesh.VAO->GetRendererID(), mesh.IndexCount, + mesh.VAO->GetRHIHandle(), mesh.IndexCount, voxelShader, albedoArrayID, normalArrayID, armArrayID, transform.GetTransform(), entityID); @@ -7629,7 +7641,7 @@ namespace OloEngine if (hasActiveShadows) { Renderer3D::AddVoxelShadowCaster( - mesh.VAO->GetRendererID(), mesh.IndexCount, + mesh.VAO->GetRHIHandle(), mesh.IndexCount, transform.GetTransform()); } } @@ -7960,7 +7972,7 @@ namespace OloEngine { if (auto tex = AssetManager::GetAsset(water.m_NormalMap0)) { - if (auto id = tex->GetRendererID(); id != 0) + if (auto id = tex->GetRHIHandle(); id.IsValid()) waterParams.normalMap0ID = id; } } @@ -7968,7 +7980,7 @@ namespace OloEngine { if (auto tex = AssetManager::GetAsset(water.m_NormalMap1)) { - if (auto id = tex->GetRendererID(); id != 0) + if (auto id = tex->GetRHIHandle(); id.IsValid()) waterParams.normalMap1ID = id; } } @@ -7976,7 +7988,7 @@ namespace OloEngine { if (auto tex = AssetManager::GetAsset(water.m_NoiseTexture)) { - if (auto id = tex->GetRendererID(); id != 0) + if (auto id = tex->GetRHIHandle(); id.IsValid()) waterParams.noiseTextureID = id; } } @@ -7984,7 +7996,7 @@ namespace OloEngine { if (auto tex = AssetManager::GetAsset(water.m_FoamTexture)) { - if (auto id = tex->GetRendererID(); id != 0) + if (auto id = tex->GetRHIHandle(); id.IsValid()) waterParams.foamTextureID = id; } } @@ -8024,9 +8036,9 @@ namespace OloEngine water.m_OceanField->Update(sp, animationTime, /*uploadToGpu=*/true, /*useGpuCompute=*/water.m_FFTUseGpuCompute); - const u32 dispID = water.m_OceanField->GetDisplacementTextureID(); - const u32 derivID = water.m_OceanField->GetDerivativesTextureID(); - if (dispID != 0 && derivID != 0) + const RHI::ResourceHandle dispID = water.m_OceanField->GetDisplacementTextureHandle(); + const RHI::ResourceHandle derivID = water.m_OceanField->GetDerivativesTextureHandle(); + if (dispID.IsValid() && derivID.IsValid()) { waterParams.fftDisplacementID = dispID; waterParams.fftDerivativesID = derivID; @@ -8077,7 +8089,7 @@ namespace OloEngine bounds.Max = glm::vec3(halfX, waveH, halfZ); auto* packet = Renderer3D::DrawWaterSurface( - va->GetRendererID(), submesh.m_IndexCount, + va->GetRHIHandle(), submesh.m_IndexCount, modelMat, animationTime, prevAnimationTime, @@ -8312,7 +8324,7 @@ namespace OloEngine // Resolve albedo texture ID (fallback to white if none assigned). // Emissive-mode decals reuse the primary slot for the emissive // texture (DecalShader samples the same TEX_USER_0 binding). - RendererID albedoTextureID = 0; + RHI::ResourceHandle albedoTextureID{}; if (decal.m_Mode == DecalMode::Emissive) { // Emissive-mode decals reuse the primary slot for the @@ -8323,29 +8335,29 @@ namespace OloEngine // colour into the emissive G-Buffer channel, painting // unintended self-illumination onto the surface). if (decal.m_EmissiveTexture) - albedoTextureID = decal.m_EmissiveTexture->GetRendererID(); + albedoTextureID = decal.m_EmissiveTexture->GetRHIHandle(); } else if (decal.m_AlbedoTexture) { - albedoTextureID = decal.m_AlbedoTexture->GetRendererID(); + albedoTextureID = decal.m_AlbedoTexture->GetRHIHandle(); } else { auto whiteTexture = Renderer3D::GetWhiteTexture(); if (whiteTexture) { - albedoTextureID = whiteTexture->GetRendererID(); + albedoTextureID = whiteTexture->GetRHIHandle(); } } // Optional normal / RMA textures. Only meaningful in the matching mode; // otherwise pass 0 and the dispatcher will skip the bind. - RendererID normalTextureID = (decal.m_Mode == DecalMode::Normal && decal.m_NormalTexture) - ? decal.m_NormalTexture->GetRendererID() - : 0u; - RendererID rmaTextureID = (decal.m_Mode == DecalMode::RMA && decal.m_RMATexture) - ? decal.m_RMATexture->GetRendererID() - : 0u; + RHI::ResourceHandle normalTextureID = (decal.m_Mode == DecalMode::Normal && decal.m_NormalTexture) + ? decal.m_NormalTexture->GetRHIHandle() + : RHI::NullResource; + RHI::ResourceHandle rmaTextureID = (decal.m_Mode == DecalMode::RMA && decal.m_RMATexture) + ? decal.m_RMATexture->GetRHIHandle() + : RHI::NullResource; glm::vec4 decalParams = glm::vec4( decal.m_FadeDistance, decal.m_NormalAngleThreshold, 0.0f, 0.0f); @@ -8736,11 +8748,11 @@ namespace OloEngine { if (auto va = submesh->GetVertexArray()) { - const u32 shadowVao = GetShadowVaoID(submesh); + const RHI::ResourceHandle shadowVao = GetShadowVaoID(submesh); for (sizet k = 0; k < totalCount; ++k) { Renderer3D::AddMeshShadowCaster( - va->GetRendererID(), submesh->GetIndexCount(), + va->GetRHIHandle(), submesh->GetIndexCount(), submesh->GetBaseIndex(), instData[k].Transform, shadowVao, submesh->GetTransformedBoundingBox(instData[k].Transform), material.GetFlag(MaterialFlag::TwoSided)); @@ -8814,7 +8826,7 @@ namespace OloEngine if (va) { Renderer3D::AddMeshShadowCaster( - va->GetRendererID(), submesh.m_Mesh->GetIndexCount(), submesh.m_Mesh->GetBaseIndex(), + va->GetRHIHandle(), submesh.m_Mesh->GetIndexCount(), submesh.m_Mesh->GetBaseIndex(), worldTransform, GetShadowVaoID(submesh.m_Mesh), submesh.m_Mesh->GetTransformedBoundingBox(worldTransform), material.GetFlag(MaterialFlag::TwoSided)); @@ -8880,7 +8892,7 @@ namespace OloEngine continue; Renderer3D::AddMeshShadowCaster( - va->GetRendererID(), submesh->GetIndexCount(), submesh->GetBaseIndex(), + va->GetRHIHandle(), submesh->GetIndexCount(), submesh->GetBaseIndex(), modelTransform, GetShadowVaoID(submesh), submesh->GetTransformedBoundingBox(modelTransform), shadowMaterial.GetFlag(MaterialFlag::TwoSided)); @@ -8944,7 +8956,7 @@ namespace OloEngine if (cmd) { Renderer3D::AddSkinnedShadowCaster( - va->GetRendererID(), submesh->GetIndexCount(), submesh->GetBaseIndex(), + va->GetRHIHandle(), submesh->GetIndexCount(), submesh->GetBaseIndex(), worldTransform, cmd->boneBufferOffset, cmd->boneCount, submesh->GetTransformedBoundingBox(worldTransform)); @@ -9017,7 +9029,7 @@ namespace OloEngine if (va) { Renderer3D::AddMeshShadowCaster( - va->GetRendererID(), tileComp.TileMesh->GetIndexCount(), tileComp.TileMesh->GetBaseIndex(), + va->GetRHIHandle(), tileComp.TileMesh->GetIndexCount(), tileComp.TileMesh->GetBaseIndex(), tileTransform, GetShadowVaoID(tileComp.TileMesh), tileComp.TileMesh->GetTransformedBoundingBox(tileTransform), material.GetFlag(MaterialFlag::TwoSided)); diff --git a/OloEngine/src/OloEngine/Snow/SnowAccumulationSystem.cpp b/OloEngine/src/OloEngine/Snow/SnowAccumulationSystem.cpp index f505477bc..57f4bae0b 100644 --- a/OloEngine/src/OloEngine/Snow/SnowAccumulationSystem.cpp +++ b/OloEngine/src/OloEngine/Snow/SnowAccumulationSystem.cpp @@ -321,6 +321,17 @@ namespace OloEngine return 0; } + RHI::ResourceHandle SnowAccumulationSystem::GetSnowDepthTextureHandle() + { + OLO_PROFILE_FUNCTION(); + + if (s_Data.m_Initialized && s_Data.m_SnowDepthTexture) + { + return s_Data.m_SnowDepthTexture->GetRHIHandle(); + } + return RHI::NullResource; + } + void SnowAccumulationSystem::Reset() { OLO_PROFILE_FUNCTION(); diff --git a/OloEngine/src/OloEngine/Snow/SnowAccumulationSystem.h b/OloEngine/src/OloEngine/Snow/SnowAccumulationSystem.h index ebdd555ed..3ab753caa 100644 --- a/OloEngine/src/OloEngine/Snow/SnowAccumulationSystem.h +++ b/OloEngine/src/OloEngine/Snow/SnowAccumulationSystem.h @@ -1,6 +1,7 @@ #pragma once #include "OloEngine/Core/Base.h" +#include "OloEngine/Renderer/RHI/RHITypes.h" #include "OloEngine/Core/Ref.h" #include "OloEngine/Core/Timestep.h" #include "OloEngine/Renderer/PostProcessSettings.h" @@ -87,6 +88,9 @@ namespace OloEngine /// @return OpenGL texture ID of the snow depth map (for debug overlay). [[nodiscard]] static u32 GetSnowDepthTextureID(); + // Identity form, for the command layer's redundant-bind cache + // (issue #691 step 3). The raw id stays for the graph/debug paths. + [[nodiscard]] static RHI::ResourceHandle GetSnowDepthTextureHandle(); /// Mark the snow depth buffer for clearing; the actual zeroing /// occurs during the next Update() pass (via Snow_Clear dispatch). diff --git a/OloEngine/src/OloEngine/Terrain/Foliage/FoliageRenderer.cpp b/OloEngine/src/OloEngine/Terrain/Foliage/FoliageRenderer.cpp index a5702a3f9..647b50dc7 100644 --- a/OloEngine/src/OloEngine/Terrain/Foliage/FoliageRenderer.cpp +++ b/OloEngine/src/OloEngine/Terrain/Foliage/FoliageRenderer.cpp @@ -490,10 +490,10 @@ namespace OloEngine } FoliageLayerDrawInfo info; - info.VertexArrayID = layer.VAO->GetRendererID(); + info.VertexArrayID = layer.VAO->GetRHIHandle(); info.IndexCount = layer.IndexCount; info.InstanceCount = layer.InstanceCount; - info.AlbedoTextureID = layer.AlbedoTexture ? layer.AlbedoTexture->GetRendererID() : 0; + info.AlbedoTextureID = layer.AlbedoTexture ? layer.AlbedoTexture->GetRHIHandle() : RHI::NullResource; info.ViewDistance = layer.ViewDistance; info.FadeStartDistance = layer.FadeStartDistance; info.WindStrength = layer.WindStrength; @@ -506,8 +506,8 @@ namespace OloEngine if (layer.UseImpostor && layer.Impostor.IsValid()) { info.UseImpostor = true; - info.ImpostorAlbedoAtlasID = layer.Impostor.Albedo->GetRendererID(); - info.ImpostorNormalDepthAtlasID = layer.Impostor.NormalDepth->GetRendererID(); + info.ImpostorAlbedoAtlasID = layer.Impostor.Albedo->GetRHIHandle(); + info.ImpostorNormalDepthAtlasID = layer.Impostor.NormalDepth->GetRHIHandle(); info.ImpostorFramesPerAxis = layer.Impostor.FramesPerAxis; info.ImpostorHemi = layer.Impostor.Hemi; info.ImpostorStartDistance = layer.ImpostorStartDistance; diff --git a/OloEngine/src/OloEngine/Terrain/Foliage/FoliageRenderer.h b/OloEngine/src/OloEngine/Terrain/Foliage/FoliageRenderer.h index d63bcbe74..ee8e427a5 100644 --- a/OloEngine/src/OloEngine/Terrain/Foliage/FoliageRenderer.h +++ b/OloEngine/src/OloEngine/Terrain/Foliage/FoliageRenderer.h @@ -1,6 +1,7 @@ #pragma once #include "OloEngine/Core/Base.h" +#include "OloEngine/Renderer/RHI/RHITypes.h" #include "OloEngine/Core/Ref.h" #include "OloEngine/Renderer/BoundingVolume.h" #include "OloEngine/Renderer/Impostor/ImpostorBaker.h" @@ -25,10 +26,10 @@ namespace OloEngine // Uses u32 for GL resource IDs to avoid pulling in RenderCommand.h. struct FoliageLayerDrawInfo { - u32 VertexArrayID = 0; + RHI::ResourceHandle VertexArrayID{}; u32 IndexCount = 0; u32 InstanceCount = 0; - u32 AlbedoTextureID = 0; + RHI::ResourceHandle AlbedoTextureID{}; f32 ViewDistance = 100.0f; f32 FadeStartDistance = 80.0f; f32 WindStrength = 0.3f; @@ -41,8 +42,8 @@ namespace OloEngine // route this layer through the impostor card shader instead of the flat // billboard; zero/false leaves the existing billboard path untouched. bool UseImpostor = false; - u32 ImpostorAlbedoAtlasID = 0; - u32 ImpostorNormalDepthAtlasID = 0; + RHI::ResourceHandle ImpostorAlbedoAtlasID{}; + RHI::ResourceHandle ImpostorNormalDepthAtlasID{}; u32 ImpostorFramesPerAxis = 8; bool ImpostorHemi = true; f32 ImpostorStartDistance = 40.0f; diff --git a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp index e0e52816f..57a1bf1ba 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp +++ b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp @@ -668,14 +668,15 @@ namespace OloEngine RendererProfiler::GetInstance().IncrementCounter(RendererProfiler::MetricType::DrawCalls, 1); } - void OpenGLRendererAPI::DrawElementsIndirectRaw(u32 vaoID, u32 indirectBufferID) + void OpenGLRendererAPI::DrawBoundElementsIndirect(u32 indirectBufferID) { OLO_PROFILE_FUNCTION(); - if (vaoID == 0 || indirectBufferID == 0) + if (indirectBufferID == 0) return; - glBindVertexArray(vaoID); + // No glBindVertexArray: the caller's BindVAOIfNeeded already bound it, + // and binding here would defeat that cache (see the DrawBound* family). glBindBuffer(GL_DRAW_INDIRECT_BUFFER, indirectBufferID); glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr); glBindBuffer(GL_DRAW_INDIRECT_BUFFER, 0); @@ -2039,6 +2040,49 @@ namespace OloEngine destFormat, destSizeBytes, dest); } + void OpenGLRendererAPI::DrawIndexedPatchesRaw(RHI::ResourceHandle vertexArray, u32 indexCount, + u32 patchVertices) + { + DrawIndexedPatchesRaw(Utils::ResolveNativeAs(vertexArray, RHI::ResourceKind::VertexArray), indexCount, + patchVertices); + } + + void OpenGLRendererAPI::DrawIndexedInstancedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, + u32 baseIndex, u32 instanceCount) + { + DrawIndexedInstancedRaw(Utils::ResolveNativeAs(vertexArray, RHI::ResourceKind::VertexArray), indexCount, + baseIndex, instanceCount); + } + + void OpenGLRendererAPI::DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount) + { + DrawIndexedRaw(Utils::ResolveNativeAs(vertexArray, RHI::ResourceKind::VertexArray), indexCount); + } + + void OpenGLRendererAPI::DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 baseIndex) + { + DrawIndexedRaw(Utils::ResolveNativeAs(vertexArray, RHI::ResourceKind::VertexArray), indexCount, baseIndex); + } + + void OpenGLRendererAPI::SetProgramUniformFloat(RHI::ResourceHandle program, std::string_view name, f32 value) + { + SetProgramUniformFloat(Utils::ResolveNativeAs(program, RHI::ResourceKind::ShaderProgram), name, value); + } + + RHI::ResourceHandle OpenGLRendererAPI::CreateDepthArrayCompareOffViewHandle(RHI::ResourceHandle srcTexture, + u32 numLayers) + { + const GLuint nativeView = CreateDepthArrayCompareOffView( + Utils::ResolveNativeAs(srcTexture, RHI::ResourceKind::Texture), numLayers); + if (nativeView == 0u) + return RHI::NullResource; + + // The view is registered as a Texture in its own right, NOT as an alias + // of the source: it is a separate GL name with its own sampler state and + // its own lifetime (ShadowMap deletes it independently of the array). + return RHI::ResourceRegistry::Get().Register(RHI::ResourceKind::Texture, nativeView, RHI::Backend::OpenGL); + } + bool OpenGLRendererAPI::ReadTextureSubImage(RHI::ResourceHandle texture, u32 mipLevel, i32 x, i32 y, i32 z, u32 width, u32 height, u32 depth, diff --git a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h index ce4e6696c..c128cd8ff 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h +++ b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h @@ -26,8 +26,13 @@ namespace OloEngine void DrawIndexedRaw(u32 vaoID, u32 indexCount) override; void DrawIndexedRaw(u32 vaoID, u32 indexCount, u32 baseIndex) override; + void DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount) override; + void DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 baseIndex) override; void DrawIndexedInstancedRaw(u32 vaoID, u32 indexCount, u32 baseIndex, u32 instanceCount) override; + void DrawIndexedInstancedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 baseIndex, + u32 instanceCount) override; void DrawIndexedPatchesRaw(u32 vaoID, u32 indexCount, u32 patchVertices) override; + void DrawIndexedPatchesRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 patchVertices) override; void SetLineWidth(f32 width) override; @@ -63,7 +68,7 @@ namespace OloEngine void DrawElementsIndirect(const Ref& vertexArray, u32 indirectBufferID) override; void DrawArraysIndirect(const Ref& vertexArray, u32 indirectBufferID) override; - void DrawElementsIndirectRaw(u32 vaoID, u32 indirectBufferID) override; + void DrawBoundElementsIndirect(u32 indirectBufferID) override; void MultiDrawElementsIndirectCountRaw(u32 vaoID, u32 indirectBufferID, u32 indirectOffsetBytes, u32 parameterBufferID, u32 parameterOffsetBytes, u32 maxDrawCount, u32 strideBytes) override; @@ -99,6 +104,8 @@ namespace OloEngine u32 CreateTexture2D(u32 width, u32 height, RHI::Format internalFormat) override; u32 CreateTextureCubemap(u32 width, u32 height, RHI::Format internalFormat) override; u32 CreateDepthArrayCompareOffView(u32 srcTextureID, u32 numLayers) override; + [[nodiscard]] RHI::ResourceHandle CreateDepthArrayCompareOffViewHandle(RHI::ResourceHandle srcTexture, + u32 numLayers) override; void SetTextureFilter(u32 textureID, RHI::Filter minFilter, RHI::Filter magFilter) override; void SetTextureFilter(RHI::ResourceHandle texture, RHI::Filter minFilter, RHI::Filter magFilter) override; void SetTextureWrap(u32 textureID, RHI::AddressMode wrap) override; @@ -228,6 +235,7 @@ namespace OloEngine [[nodiscard("Store this!")]] u32 GetMaxColorTextureSamples() const override; [[nodiscard("Store this!")]] u32 GetMaxDepthTextureSamples() const override; void SetProgramUniformFloat(u32 programID, std::string_view name, f32 value) override; + void SetProgramUniformFloat(RHI::ResourceHandle program, std::string_view name, f32 value) override; [[nodiscard("Store this!")]] bool IsDeviceAvailable() const override; [[nodiscard("Store this!")]] u32 GetMaxUniformBlockSize() const override; diff --git a/OloEngine/src/Platform/OpenGL/OpenGLTexture.cpp b/OloEngine/src/Platform/OpenGL/OpenGLTexture.cpp index e7cf2b17c..ddd3bce6f 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLTexture.cpp +++ b/OloEngine/src/Platform/OpenGL/OpenGLTexture.cpp @@ -574,7 +574,7 @@ namespace OloEngine // Drop any cached "this slot already has this texture bound" entries so a // future bind with a recycled GL ID isn't skipped against stale tracking. - CommandDispatch::InvalidateTextureBinding(m_RendererID); + CommandDispatch::InvalidateTextureBinding(m_RHIHandle.Get()); u32 id = m_RendererID; FrameResourceManager::Get().SubmitForDeletion([id]() @@ -610,7 +610,7 @@ namespace OloEngine // Dealloc old OLO_TRACK_DEALLOC(this); GPUResourceInspector::GetInstance().UnregisterResource(m_RendererID); - CommandDispatch::InvalidateTextureBinding(m_RendererID); + CommandDispatch::InvalidateTextureBinding(m_RHIHandle.Get()); u32 oldId = m_RendererID; FrameResourceManager::Get().SubmitForDeletion([oldId]() @@ -955,7 +955,7 @@ namespace OloEngine { OLO_TRACK_DEALLOC(this); GPUResourceInspector::GetInstance().UnregisterResource(m_RendererID); - CommandDispatch::InvalidateTextureBinding(m_RendererID); + CommandDispatch::InvalidateTextureBinding(m_RHIHandle.Get()); u32 oldId = m_RendererID; FrameResourceManager::Get().SubmitForDeletion([oldId]() { glDeleteTextures(1, &oldId); }); diff --git a/OloEngine/src/Platform/OpenGL/OpenGLTextureCubemap.cpp b/OloEngine/src/Platform/OpenGL/OpenGLTextureCubemap.cpp index 0691c0030..67942440b 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLTextureCubemap.cpp +++ b/OloEngine/src/Platform/OpenGL/OpenGLTextureCubemap.cpp @@ -223,7 +223,7 @@ namespace OloEngine // See OpenGLTexture2D::~OpenGLTexture2D — same skip-bind hazard applies // when the recycled ID is later supplied for a cubemap slot. - CommandDispatch::InvalidateTextureBinding(m_RendererID); + CommandDispatch::InvalidateTextureBinding(m_RHIHandle.Get()); u32 id = m_RendererID; FrameResourceManager::Get().SubmitForDeletion([id]() diff --git a/OloEngine/tests/Rendering/CommandBucketTest.cpp b/OloEngine/tests/Rendering/CommandBucketTest.cpp index 13f88b572..4e926aa43 100644 --- a/OloEngine/tests/Rendering/CommandBucketTest.cpp +++ b/OloEngine/tests/Rendering/CommandBucketTest.cpp @@ -374,7 +374,7 @@ TEST_F(CommandBucketBatchTest, BatchConvertsMeshToInstanced) { auto cmd = MakeSyntheticDrawMeshCommand(1, 1, static_cast(i) * 0.1f, static_cast(i)); // Make meshes identical (same VAO, material, shader) - cmd.vertexArrayID = 100; + cmd.vertexArrayID = TestHandle(100u); cmd.indexCount = 36; PacketMetadata meta; meta.m_SortKey = MakeSyntheticOpaqueKey(0, ViewLayerType::ThreeD, 1, 1, i * 10); @@ -402,14 +402,14 @@ TEST_F(CommandBucketBatchTest, BatchRejectsDifferentRenderStateIndex) // Submit two DrawMesh commands with same mesh+material but different render state auto cmd1 = MakeSyntheticDrawMeshCommand(1, 1, 0.1f, 1); - cmd1.vertexArrayID = 100; + cmd1.vertexArrayID = TestHandle(100u); cmd1.renderStateIndex = 0; PacketMetadata meta1; meta1.m_SortKey = MakeSyntheticOpaqueKey(0, ViewLayerType::ThreeD, 1, 1, 10); bucket.Submit(cmd1, meta1, m_Allocator.get()); auto cmd2 = MakeSyntheticDrawMeshCommand(1, 1, 0.2f, 2); - cmd2.vertexArrayID = 100; + cmd2.vertexArrayID = TestHandle(100u); cmd2.renderStateIndex = 1; PacketMetadata meta2; meta2.m_SortKey = MakeSyntheticOpaqueKey(0, ViewLayerType::ThreeD, 1, 1, 20); @@ -437,7 +437,7 @@ TEST_F(CommandBucketBatchTest, BatchAcceptsSameRenderStateIndex) for (u32 i = 0; i < 3; ++i) { auto cmd = MakeSyntheticDrawMeshCommand(1, 1, static_cast(i) * 0.1f, static_cast(i)); - cmd.vertexArrayID = 100; + cmd.vertexArrayID = TestHandle(100u); cmd.renderStateIndex = 5; PacketMetadata meta; meta.m_SortKey = MakeSyntheticOpaqueKey(0, ViewLayerType::ThreeD, 1, 1, i * 10); @@ -530,7 +530,7 @@ TEST_F(CommandBucketBatchTest, BatchRejectsDifferentMaterialDataIndex) // Submit two commands with same mesh+renderState but different materialDataIndex auto cmd1 = MakeSyntheticDrawMeshCommand(1, 1, 0.1f, 1); - cmd1.vertexArrayID = 100; + cmd1.vertexArrayID = TestHandle(100u); cmd1.renderStateIndex = 0; cmd1.materialDataIndex = 0; // material A PacketMetadata meta1; @@ -538,7 +538,7 @@ TEST_F(CommandBucketBatchTest, BatchRejectsDifferentMaterialDataIndex) bucket.Submit(cmd1, meta1, m_Allocator.get()); auto cmd2 = MakeSyntheticDrawMeshCommand(1, 1, 0.2f, 2); - cmd2.vertexArrayID = 100; + cmd2.vertexArrayID = TestHandle(100u); cmd2.renderStateIndex = 0; cmd2.materialDataIndex = 1; // material B PacketMetadata meta2; @@ -569,7 +569,7 @@ TEST_F(CommandBucketBatchTest, AnimatedMeshesAreNotBatched) for (u32 i = 0; i < 2; ++i) { auto cmd = MakeSyntheticDrawMeshCommand(1, 1, static_cast(i) * 0.1f, static_cast(i)); - cmd.vertexArrayID = 100; + cmd.vertexArrayID = TestHandle(100u); cmd.renderStateIndex = 0; cmd.isAnimatedMesh = true; cmd.boneBufferOffset = 42; @@ -613,7 +613,7 @@ TEST_F(CommandBucketBatchTest, HashTableGroupsNonAdjacentCommands) bool isGroupA = (i % 2 == 0); auto cmd = MakeSyntheticDrawMeshCommand(1, 1, static_cast(i) * 0.1f, static_cast(i)); cmd.meshHandle = UUID(isGroupA ? 100 : 200); - cmd.vertexArrayID = isGroupA ? 10u : 20u; + cmd.vertexArrayID = TestHandle(isGroupA ? 10u : 20u); cmd.renderStateIndex = 0; cmd.materialDataIndex = 0; PacketMetadata meta; @@ -657,7 +657,7 @@ TEST_F(CommandBucketBatchTest, SingleCommandGroupsRemainDrawMesh) { auto cmd = MakeSyntheticDrawMeshCommand(1, 1, static_cast(i) * 0.1f, static_cast(i)); cmd.meshHandle = UUID(100 + i); // Different mesh each time - cmd.vertexArrayID = 10 + i; + cmd.vertexArrayID = TestHandle(10u + i); cmd.renderStateIndex = 0; cmd.materialDataIndex = 0; PacketMetadata meta; @@ -693,7 +693,7 @@ TEST_F(CommandBucketBatchTest, BatchRespectsMaxMeshInstances) for (u32 i = 0; i < 5; ++i) { auto cmd = MakeSyntheticDrawMeshCommand(1, 1, static_cast(i) * 0.1f, static_cast(i)); - cmd.vertexArrayID = 100; + cmd.vertexArrayID = TestHandle(100u); cmd.renderStateIndex = 0; cmd.materialDataIndex = 0; PacketMetadata meta; @@ -737,7 +737,7 @@ TEST_F(CommandBucketBatchTest, BatchedTransformsAreContiguous) for (u32 i = 0; i < kCount; ++i) { auto cmd = MakeSyntheticDrawMeshCommand(1, 1, static_cast(i) * 0.1f, static_cast(i)); - cmd.vertexArrayID = 100; + cmd.vertexArrayID = TestHandle(100u); cmd.renderStateIndex = 0; cmd.materialDataIndex = 0; cmd.transform = glm::translate(glm::mat4(1.0f), glm::vec3(static_cast(i), 0.0f, 0.0f)); @@ -793,7 +793,7 @@ TEST_F(CommandBucketBatchTest, TenThousandInstancesCollapseToSinglePacket) for (u32 i = 0; i < kCount; ++i) { auto cmd = MakeSyntheticDrawMeshCommand(1, 1, 0.0f, static_cast(i)); - cmd.vertexArrayID = 100; + cmd.vertexArrayID = TestHandle(100u); cmd.renderStateIndex = 0; cmd.materialDataIndex = 0; cmd.transform = glm::translate(glm::mat4(1.0f), glm::vec3(static_cast(i) * 0.01f, 0.0f, 0.0f)); @@ -840,7 +840,7 @@ TEST_F(CommandBucketBatchTest, BatchedEntityIDAndPrevTransformSurviveCollapse) for (u32 i = 0; i < kCount; ++i) { auto cmd = MakeSyntheticDrawMeshCommand(1, 1, 0.0f, static_cast(100 + i)); - cmd.vertexArrayID = 100; + cmd.vertexArrayID = TestHandle(100u); cmd.renderStateIndex = 0; cmd.materialDataIndex = 0; cmd.transform = glm::translate(glm::mat4(1.0f), glm::vec3(static_cast(i), 0.0f, 0.0f)); @@ -897,7 +897,7 @@ TEST_F(CommandBucketBatchTest, IdentityColorAndCustomSkipParallelStreamAllocatio for (u32 i = 0; i < kCount; ++i) { auto cmd = MakeSyntheticDrawMeshCommand(1, 1, 0.0f, static_cast(i)); - cmd.vertexArrayID = 100; + cmd.vertexArrayID = TestHandle(100u); cmd.renderStateIndex = 0; cmd.materialDataIndex = 0; // Default color (1,1,1,1) and default custom (0.0) on every source. @@ -941,7 +941,7 @@ TEST_F(CommandBucketBatchTest, SameLODBatchesAndDifferentLODsStaySeparate) { auto cmd = MakeSyntheticDrawMeshCommand(1, 1, 0.0f, static_cast(nextEntity)); cmd.meshHandle = UUID(kLOD0Handle); // LOD-resolved mesh handle differs by LOD level. - cmd.vertexArrayID = kLOD0Handle; + cmd.vertexArrayID = TestHandle(kLOD0Handle); cmd.renderStateIndex = 0; cmd.materialDataIndex = 0; PacketMetadata meta; @@ -953,7 +953,7 @@ TEST_F(CommandBucketBatchTest, SameLODBatchesAndDifferentLODsStaySeparate) { auto cmd = MakeSyntheticDrawMeshCommand(1, 1, 0.0f, static_cast(nextEntity)); cmd.meshHandle = UUID(kLOD1Handle); - cmd.vertexArrayID = kLOD1Handle; + cmd.vertexArrayID = TestHandle(kLOD1Handle); cmd.renderStateIndex = 0; cmd.materialDataIndex = 0; PacketMetadata meta; @@ -1006,7 +1006,7 @@ TEST_F(CommandBucketBatchTest, NonDefaultColorAndCustomSurviveCollapse) for (u32 i = 0; i < kCount; ++i) { auto cmd = MakeSyntheticDrawMeshCommand(1, 1, 0.0f, static_cast(i)); - cmd.vertexArrayID = 100; + cmd.vertexArrayID = TestHandle(100u); cmd.renderStateIndex = 0; cmd.materialDataIndex = 0; cmd.color = glm::vec4(0.1f * static_cast(i), 0.5f, 0.25f, 1.0f); @@ -1066,8 +1066,8 @@ TEST_F(CommandBucketBatchTest, HandleZeroMeshesWithDifferentGeometryNeverMerge) // Two "cars" sharing VAO 10 and one "ship" with VAO 20 — all handle 0, // all the same material/render state (the shared-palette scenario). - constexpr u32 kCarVAO = 10; - constexpr u32 kShipVAO = 20; + constexpr auto kCarVAO = TestHandle(10u); + constexpr auto kShipVAO = TestHandle(20u); for (u32 i = 0; i < 3; ++i) { auto cmd = MakeSyntheticDrawMeshCommand(1, 1, 0.0f, static_cast(i)); @@ -1076,7 +1076,7 @@ TEST_F(CommandBucketBatchTest, HandleZeroMeshesWithDifferentGeometryNeverMerge) cmd.renderStateIndex = 0; cmd.materialDataIndex = 0; PacketMetadata meta; - meta.m_SortKey = MakeSyntheticOpaqueKey(0, ViewLayerType::ThreeD, cmd.vertexArrayID, 1, i); + meta.m_SortKey = MakeSyntheticOpaqueKey(0, ViewLayerType::ThreeD, cmd.vertexArrayID.Index, 1, i); bucket.Submit(cmd, meta, m_Allocator.get()); } @@ -1142,7 +1142,7 @@ TEST_F(CommandBucketBatchTest, SubmeshIndexRangesStaySeparateAndSurviveMerging) config.EnableBatching = true; CommandBucket bucket(config); - constexpr u32 kSharedVAO = 30; + constexpr auto kSharedVAO = TestHandle(30u); constexpr u32 kBaseA = 0; constexpr u32 kBaseB = 72; // Two instances of submesh B (merge candidates) + one of submesh A. @@ -1156,7 +1156,7 @@ TEST_F(CommandBucketBatchTest, SubmeshIndexRangesStaySeparateAndSurviveMerging) cmd.renderStateIndex = 0; cmd.materialDataIndex = 0; PacketMetadata meta; - meta.m_SortKey = MakeSyntheticOpaqueKey(0, ViewLayerType::ThreeD, kSharedVAO, 1, i); + meta.m_SortKey = MakeSyntheticOpaqueKey(0, ViewLayerType::ThreeD, kSharedVAO.Index, 1, i); bucket.Submit(cmd, meta, m_Allocator.get()); } diff --git a/OloEngine/tests/Rendering/FrameDataBufferTest.cpp b/OloEngine/tests/Rendering/FrameDataBufferTest.cpp index ae18c1a7c..06e4b8874 100644 --- a/OloEngine/tests/Rendering/FrameDataBufferTest.cpp +++ b/OloEngine/tests/Rendering/FrameDataBufferTest.cpp @@ -399,7 +399,7 @@ TEST(FrameDataBuffer, MaterialDataTableAllocateReturnsValidIndex) buffer.Reset(); PODMaterialData mat{}; - mat.shaderRendererID = 1; + mat.shaderRendererID = TestHandle(1u); u16 index = buffer.AllocateMaterialData(mat); EXPECT_NE(index, INVALID_MATERIAL_DATA_INDEX); EXPECT_EQ(index, 0u); @@ -412,7 +412,7 @@ TEST(FrameDataBuffer, MaterialDataTableDeduplicatesIdenticalData) buffer.Reset(); PODMaterialData mat{}; - mat.shaderRendererID = 42; + mat.shaderRendererID = TestHandle(42u); mat.ambient = glm::vec3(0.1f); u16 first = buffer.AllocateMaterialData(mat); u16 second = buffer.AllocateMaterialData(mat); @@ -427,12 +427,12 @@ TEST(FrameDataBuffer, MaterialDataTableDifferentDataGetDifferentIndices) buffer.Reset(); PODMaterialData matA{}; - matA.shaderRendererID = 1; + matA.shaderRendererID = TestHandle(1u); matA.enablePBR = false; matA.ambient = glm::vec3(0.1f); PODMaterialData matB{}; - matB.shaderRendererID = 2; + matB.shaderRendererID = TestHandle(2u); matB.enablePBR = true; matB.baseColorFactor = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); @@ -449,13 +449,13 @@ TEST(FrameDataBuffer, MaterialDataTableRoundTrip) buffer.Reset(); PODMaterialData mat{}; - mat.shaderRendererID = 99; + mat.shaderRendererID = TestHandle(99u); mat.enablePBR = true; mat.baseColorFactor = glm::vec4(0.5f, 0.6f, 0.7f, 1.0f); mat.metallicFactor = 0.3f; mat.roughnessFactor = 0.8f; - mat.albedoMapID = 100; - mat.normalMapID = 200; + mat.albedoMapID = TestHandle(100u); + mat.normalMapID = TestHandle(200u); u16 index = buffer.AllocateMaterialData(mat); const PODMaterialData& retrieved = buffer.GetMaterialData(index); @@ -470,7 +470,7 @@ TEST(FrameDataBuffer, MaterialDataTableResetsEachFrame) buffer.Reset(); PODMaterialData mat{}; - mat.shaderRendererID = 1; + mat.shaderRendererID = TestHandle(1u); buffer.AllocateMaterialData(mat); EXPECT_EQ(buffer.GetMaterialDataCount(), 1u); @@ -493,7 +493,7 @@ TEST(FrameDataBuffer, MaterialDataTableMultipleUniqueEntries) for (u16 i = 0; i < 10; ++i) { PODMaterialData mat{}; - mat.shaderRendererID = i + 1; + mat.shaderRendererID = TestHandle(static_cast(i) + 1u); mat.metallicFactor = static_cast(i) * 0.1f; u16 index = buffer.AllocateMaterialData(mat); EXPECT_EQ(index, i) << "Material " << i << " should get sequential index"; @@ -504,7 +504,7 @@ TEST(FrameDataBuffer, MaterialDataTableMultipleUniqueEntries) for (u16 i = 0; i < 10; ++i) { PODMaterialData mat{}; - mat.shaderRendererID = i + 1; + mat.shaderRendererID = TestHandle(static_cast(i) + 1u); mat.metallicFactor = static_cast(i) * 0.1f; u16 index = buffer.AllocateMaterialData(mat); EXPECT_EQ(index, i) << "Re-allocated material " << i << " should match original index"; @@ -522,7 +522,7 @@ TEST(FrameDataBuffer, PBRMaterialAllTextureFieldsRoundTrip) buffer.Reset(); PODMaterialData mat{}; - mat.shaderRendererID = 50; + mat.shaderRendererID = TestHandle(50u); mat.enablePBR = true; mat.baseColorFactor = glm::vec4(0.8f, 0.2f, 0.3f, 1.0f); mat.emissiveFactor = glm::vec4(0.1f, 0.2f, 0.3f, 0.0f); @@ -532,35 +532,35 @@ TEST(FrameDataBuffer, PBRMaterialAllTextureFieldsRoundTrip) mat.occlusionStrength = 0.8f; mat.enableIBL = true; // All 9 PBR texture IDs - mat.albedoMapID = 101; - mat.metallicRoughnessMapID = 102; - mat.normalMapID = 103; - mat.aoMapID = 104; - mat.emissiveMapID = 105; - mat.environmentMapID = 106; - mat.irradianceMapID = 107; - mat.prefilterMapID = 108; - mat.brdfLutMapID = 109; + mat.albedoMapID = TestHandle(101u); + mat.metallicRoughnessMapID = TestHandle(102u); + mat.normalMapID = TestHandle(103u); + mat.aoMapID = TestHandle(104u); + mat.emissiveMapID = TestHandle(105u); + mat.environmentMapID = TestHandle(106u); + mat.irradianceMapID = TestHandle(107u); + mat.prefilterMapID = TestHandle(108u); + mat.brdfLutMapID = TestHandle(109u); u16 index = buffer.AllocateMaterialData(mat); const PODMaterialData& ret = buffer.GetMaterialData(index); - EXPECT_EQ(ret.shaderRendererID, 50u); + EXPECT_EQ(ret.shaderRendererID, TestHandle(50u)); EXPECT_TRUE(ret.enablePBR); EXPECT_FLOAT_EQ(ret.metallicFactor, 0.9f); EXPECT_FLOAT_EQ(ret.roughnessFactor, 0.4f); EXPECT_FLOAT_EQ(ret.normalScale, 1.5f); EXPECT_FLOAT_EQ(ret.occlusionStrength, 0.8f); EXPECT_TRUE(ret.enableIBL); - EXPECT_EQ(ret.albedoMapID, 101u); - EXPECT_EQ(ret.metallicRoughnessMapID, 102u); - EXPECT_EQ(ret.normalMapID, 103u); - EXPECT_EQ(ret.aoMapID, 104u); - EXPECT_EQ(ret.emissiveMapID, 105u); - EXPECT_EQ(ret.environmentMapID, 106u); - EXPECT_EQ(ret.irradianceMapID, 107u); - EXPECT_EQ(ret.prefilterMapID, 108u); - EXPECT_EQ(ret.brdfLutMapID, 109u); + EXPECT_EQ(ret.albedoMapID, TestHandle(101u)); + EXPECT_EQ(ret.metallicRoughnessMapID, TestHandle(102u)); + EXPECT_EQ(ret.normalMapID, TestHandle(103u)); + EXPECT_EQ(ret.aoMapID, TestHandle(104u)); + EXPECT_EQ(ret.emissiveMapID, TestHandle(105u)); + EXPECT_EQ(ret.environmentMapID, TestHandle(106u)); + EXPECT_EQ(ret.irradianceMapID, TestHandle(107u)); + EXPECT_EQ(ret.prefilterMapID, TestHandle(108u)); + EXPECT_EQ(ret.brdfLutMapID, TestHandle(109u)); // Byte-for-byte identity EXPECT_EQ(std::memcmp(&mat, &ret, sizeof(PODMaterialData)), 0) @@ -573,16 +573,16 @@ TEST(FrameDataBuffer, PBRAndLegacyMaterialsDedupIndependently) buffer.Reset(); PODMaterialData pbrMat{}; - pbrMat.shaderRendererID = 1; + pbrMat.shaderRendererID = TestHandle(1u); pbrMat.enablePBR = true; pbrMat.baseColorFactor = glm::vec4(1.0f); - pbrMat.albedoMapID = 10; + pbrMat.albedoMapID = TestHandle(10u); PODMaterialData legacyMat{}; - legacyMat.shaderRendererID = 2; + legacyMat.shaderRendererID = TestHandle(2u); legacyMat.enablePBR = false; legacyMat.ambient = glm::vec3(0.5f); - legacyMat.diffuseMapID = 20; + legacyMat.diffuseMapID = TestHandle(20u); u16 pbrIdx = buffer.AllocateMaterialData(pbrMat); u16 legacyIdx = buffer.AllocateMaterialData(legacyMat); diff --git a/OloEngine/tests/Rendering/MockRendererAPI.h b/OloEngine/tests/Rendering/MockRendererAPI.h index 8485eef47..09a6a57ea 100644 --- a/OloEngine/tests/Rendering/MockRendererAPI.h +++ b/OloEngine/tests/Rendering/MockRendererAPI.h @@ -317,9 +317,9 @@ namespace OloEngine::Testing Record("DrawArraysIndirect"); ++m_DrawCallCount; } - void DrawElementsIndirectRaw(u32 /*vaoID*/, u32 /*bufID*/) override + void DrawBoundElementsIndirect(u32 /*bufID*/) override { - Record("DrawElementsIndirectRaw"); + Record("DrawBoundElementsIndirect"); ++m_DrawCallCount; } void MultiDrawElementsIndirectCountRaw(u32 /*vaoID*/, u32 /*bufID*/, u32 /*indirectOffset*/, u32 /*paramBufID*/, @@ -515,6 +515,36 @@ namespace OloEngine::Testing { return ReadTextureImage(Native(texture), mipLevel, destFormat, destSizeBytes, dest); } + void DrawIndexedPatchesRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 patchVertices) override + { + DrawIndexedPatchesRaw(Native(vertexArray), indexCount, patchVertices); + } + void DrawIndexedInstancedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 baseIndex, + u32 instanceCount) override + { + DrawIndexedInstancedRaw(Native(vertexArray), indexCount, baseIndex, instanceCount); + } + void DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount) override + { + DrawIndexedRaw(Native(vertexArray), indexCount); + } + void DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 baseIndex) override + { + DrawIndexedRaw(Native(vertexArray), indexCount, baseIndex); + } + void SetProgramUniformFloat(RHI::ResourceHandle program, std::string_view name, f32 value) override + { + SetProgramUniformFloat(Native(program), name, value); + } + [[nodiscard]] RHI::ResourceHandle CreateDepthArrayCompareOffViewHandle(RHI::ResourceHandle srcTexture, + u32 numLayers) override + { + const u32 nativeView = CreateDepthArrayCompareOffView(Native(srcTexture), numLayers); + if (nativeView == 0u) + return RHI::NullResource; + return RHI::ResourceRegistry::Get().Register(RHI::ResourceKind::Texture, nativeView, + RHI::Backend::OpenGL); + } private: [[nodiscard]] static u32 Native(RHI::ResourceHandle handle) noexcept diff --git a/OloEngine/tests/Rendering/PODCommandTest.cpp b/OloEngine/tests/Rendering/PODCommandTest.cpp index 56b3943ba..5acbade9a 100644 --- a/OloEngine/tests/Rendering/PODCommandTest.cpp +++ b/OloEngine/tests/Rendering/PODCommandTest.cpp @@ -74,38 +74,38 @@ TEST(PODCommand, AllCommandsTrivialCopy) { DrawSkyboxCommand cmd{}; cmd.header.type = CommandType::DrawSkybox; - cmd.vertexArrayID = 1; + cmd.vertexArrayID = TestHandle(1u); cmd.indexCount = 36; cmd.transform = glm::mat4(1.0f); - cmd.shaderRendererID = 5; - cmd.skyboxTextureID = 10; + cmd.shaderRendererID = TestHandle(5u); + cmd.skyboxTextureID = TestHandle(10u); AssertTriviallyCopyableByMemcpy(cmd, "DrawSkyboxCommand"); } { DrawTerrainPatchCommand cmd{}; cmd.header.type = CommandType::DrawTerrainPatch; - cmd.vertexArrayID = 1; + cmd.vertexArrayID = TestHandle(1u); cmd.indexCount = 1024; AssertTriviallyCopyableByMemcpy(cmd, "DrawTerrainPatchCommand"); } { DrawVoxelMeshCommand cmd{}; cmd.header.type = CommandType::DrawVoxelMesh; - cmd.vertexArrayID = 1; + cmd.vertexArrayID = TestHandle(1u); cmd.indexCount = 500; AssertTriviallyCopyableByMemcpy(cmd, "DrawVoxelMeshCommand"); } { DrawDecalCommand cmd{}; cmd.header.type = CommandType::DrawDecal; - cmd.vertexArrayID = 1; + cmd.vertexArrayID = TestHandle(1u); cmd.indexCount = 36; AssertTriviallyCopyableByMemcpy(cmd, "DrawDecalCommand"); } { DrawFoliageLayerCommand cmd{}; cmd.header.type = CommandType::DrawFoliageLayer; - cmd.vertexArrayID = 1; + cmd.vertexArrayID = TestHandle(1u); cmd.instanceCount = 1000; AssertTriviallyCopyableByMemcpy(cmd, "DrawFoliageLayerCommand"); } @@ -169,7 +169,7 @@ TEST(PODCommand, DrawMeshFieldRoundTrip) std::memcpy(©, &original, sizeof(DrawMeshCommand)); EXPECT_EQ(copy.header.type, CommandType::DrawMesh); - EXPECT_EQ(copy.vertexArrayID, 1u); + EXPECT_EQ(copy.vertexArrayID, TestHandle(1u)); EXPECT_EQ(copy.indexCount, 36u); EXPECT_EQ(copy.entityID, 777); EXPECT_EQ(copy.materialDataIndex, static_cast(99)); diff --git a/OloEngine/tests/Rendering/PropertyTests/WaterStaleTexturePublicationTest.cpp b/OloEngine/tests/Rendering/PropertyTests/WaterStaleTexturePublicationTest.cpp index b6815e248..b0424ac4c 100644 --- a/OloEngine/tests/Rendering/PropertyTests/WaterStaleTexturePublicationTest.cpp +++ b/OloEngine/tests/Rendering/PropertyTests/WaterStaleTexturePublicationTest.cpp @@ -103,24 +103,26 @@ namespace OloEngine::Tests // Both must be observed non-zero or the reset assertions below would // be vacuous. RunEditorFrames(camera, 2); - EXPECT_NE(Renderer3D::GetWaterSurfaceDepthTextureID(), 0u) + EXPECT_TRUE(Renderer3D::GetWaterSurfaceDepthTextureID().IsValid()) << "A frame that renders water must publish the water-surface depth " "texture (otherwise this test exercises nothing)."; - EXPECT_NE(Renderer3D::GetPlanarReflectionTextureID(), 0u) + EXPECT_TRUE(Renderer3D::GetPlanarReflectionTextureID().IsValid()) << "A frame that renders reflective water must publish the planar-" "reflection texture (otherwise this test exercises nothing)."; // 2. No-water frame: the graph culls the water pass, so nothing // re-publishes. The per-frame reset in PrepareFrame must have cleared - // the previous frame's id — a stale non-zero value here is exactly - // the #505 lifetime bug. + // the previous frame's publication — a stale VALID handle here is + // exactly the #505 lifetime bug. Post-#691 the handle also cannot + // silently name a DIFFERENT texture that inherited the GL name, + // which is the failure the raw-id version could not distinguish. GetScene().DestroyEntity(m_Water); RunEditorFrames(camera, 1); - EXPECT_EQ(Renderer3D::GetWaterSurfaceDepthTextureID(), 0u) + EXPECT_FALSE(Renderer3D::GetWaterSurfaceDepthTextureID().IsValid()) << "The water-surface depth publication survived a frame whose graph " "culled the water pass — a later consumer would bind a texture " "name it no longer owns (issue #505)."; - EXPECT_EQ(Renderer3D::GetPlanarReflectionTextureID(), 0u) + EXPECT_FALSE(Renderer3D::GetPlanarReflectionTextureID().IsValid()) << "The planar-reflection publication has the same per-frame " "contract as the water-surface depth (issue #505)."; diff --git a/OloEngine/tests/Rendering/RenderGraphFingerprintTest.cpp b/OloEngine/tests/Rendering/RenderGraphFingerprintTest.cpp index 6ac220bb6..2800443c7 100644 --- a/OloEngine/tests/Rendering/RenderGraphFingerprintTest.cpp +++ b/OloEngine/tests/Rendering/RenderGraphFingerprintTest.cpp @@ -1,6 +1,8 @@ #include "OloEnginePCH.h" #include +#include "RenderingTestUtils.h" + #include "OloEngine/Renderer/Renderer3DInternal.h" #include "OloEngine/Renderer/PostProcessSettings.h" #include "OloEngine/Renderer/RenderingPath.h" @@ -52,14 +54,19 @@ namespace OloEngine::Tests return data.Pipeline->ComputeBlackboardFingerprint(data); } - // The raw GL texture IDs the blackboard IMPORTS (as opposed to declares). They are + // The IDENTITIES the blackboard imports (as opposed to declares). They are // not settings, so they need their own hook. + // + // Handles, not raw GL ids, since issue #691 step 3 slice 6 — and the + // fingerprint hashes them for a reason this test now covers by + // construction: ShadowMap/DDGI free their textures BEFORE recreating, so + // GL may reissue the same name and a raw-id hash would see no change. struct ImportedIBL { - u32 Irradiance = 0; - u32 Prefilter = 0; - u32 BRDFLut = 0; - u32 Environment = 0; + RHI::ResourceHandle Irradiance{}; + RHI::ResourceHandle Prefilter{}; + RHI::ResourceHandle BRDFLut{}; + RHI::ResourceHandle Environment{}; }; [[nodiscard]] static u64 FingerprintWithIBL(const ImportedIBL& ibl) @@ -220,31 +227,31 @@ namespace OloEngine::Tests TEST(RenderGraphFingerprint, ChangingAnImportedIBLTextureIdChangesFingerprint) { using Access = RenderPipelineFingerprintAccess; - const Access::ImportedIBL base{ .Irradiance = 26, .Prefilter = 45, .BRDFLut = 46, .Environment = 12 }; + const Access::ImportedIBL base{ .Irradiance = TestHandle(26u), .Prefilter = TestHandle(45u), .BRDFLut = TestHandle(46u), .Environment = TestHandle(12u) }; const u64 baseFp = Access::FingerprintWithIBL(base); // Each ID must independently invalidate: a scene switch can change any subset. { Access::ImportedIBL changed = base; - changed.Irradiance = 99; + changed.Irradiance = TestHandle(99u); EXPECT_NE(Access::FingerprintWithIBL(changed), baseFp) << "GlobalIrradianceMapID is imported by raw GL ID — a change must repopulate the blackboard."; } { Access::ImportedIBL changed = base; - changed.Prefilter = 99; + changed.Prefilter = TestHandle(99u); EXPECT_NE(Access::FingerprintWithIBL(changed), baseFp) << "GlobalPrefilterMapID is imported by raw GL ID — a change must repopulate the blackboard."; } { Access::ImportedIBL changed = base; - changed.BRDFLut = 99; + changed.BRDFLut = TestHandle(99u); EXPECT_NE(Access::FingerprintWithIBL(changed), baseFp) << "GlobalBRDFLutMapID is imported by raw GL ID — a change must repopulate the blackboard."; } { Access::ImportedIBL changed = base; - changed.Environment = 99; + changed.Environment = TestHandle(99u); EXPECT_NE(Access::FingerprintWithIBL(changed), baseFp) << "GlobalEnvironmentMapID is imported by raw GL ID — a change must repopulate the blackboard."; } diff --git a/OloEngine/tests/Rendering/RenderingTestUtils.h b/OloEngine/tests/Rendering/RenderingTestUtils.h index 97e76a1b7..f89eae65e 100644 --- a/OloEngine/tests/Rendering/RenderingTestUtils.h +++ b/OloEngine/tests/Rendering/RenderingTestUtils.h @@ -20,6 +20,24 @@ using namespace OloEngine; // NOLINT(google-build-using-namespace) — test utility header +// ============================================================================= +// Synthetic identities for POD-command tests (issue #691 step 3, slice 6) +// ============================================================================= +// +// These tests exercise SORTING, BATCHING and packet layout — they never resolve +// a handle to a device object, so a registry-backed handle would add teardown +// for no benefit. +// +// Generation is 1, never 0, and that matters: a Generation-0 handle is inert by +// design (IsValid() is false and it resolves to nothing), so a test built on one +// would silently exercise the "no resource" path instead of the path it means to +// — the draw would be dropped by the validity guard and the assertion would pass +// for the wrong reason. +[[nodiscard]] constexpr RHI::ResourceHandle TestHandle(u32 index, u32 generation = 1u) noexcept +{ + return RHI::ResourceHandle{ index, generation }; +} + // ============================================================================= // Validation Helpers // ============================================================================= @@ -112,7 +130,7 @@ inline DrawMeshCommand MakeSyntheticDrawMeshCommand(u32 shaderID = 1, cmd.header.type = CommandType::DrawMesh; cmd.header.dispatchFn = nullptr; // Tests don't dispatch cmd.meshHandle = UUID(0); // Deterministic handle for batching tests - cmd.vertexArrayID = 1; + cmd.vertexArrayID = TestHandle(1u); cmd.indexCount = 36; cmd.transform = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, -depth)); cmd.entityID = entityID; diff --git a/OloEngine/tests/Rendering/WaterRenderingTest.cpp b/OloEngine/tests/Rendering/WaterRenderingTest.cpp index 26687dca0..4e2715d13 100644 --- a/OloEngine/tests/Rendering/WaterRenderingTest.cpp +++ b/OloEngine/tests/Rendering/WaterRenderingTest.cpp @@ -130,9 +130,9 @@ TEST(WaterRendering, DrawWaterCommandTrivialCopy) DrawWaterCommand cmd{}; cmd.header.type = CommandType::DrawWater; - cmd.vertexArrayID = 42; + cmd.vertexArrayID = TestHandle(42u); cmd.indexCount = 1024; - cmd.shaderRendererID = 7; + cmd.shaderRendererID = TestHandle(7u); cmd.modelTransform = glm::mat4(1.0f); cmd.normalMatrix = glm::mat4(1.0f); cmd.waveParams = glm::vec4(1.0f, 2.0f, 0.5f, 3.0f); @@ -143,9 +143,9 @@ TEST(WaterRendering, DrawWaterCommandTrivialCopy) std::memcpy(©, &cmd, sizeof(DrawWaterCommand)); EXPECT_EQ(copy.header.type, CommandType::DrawWater); - EXPECT_EQ(copy.vertexArrayID, 42u); + EXPECT_EQ(copy.vertexArrayID, TestHandle(42u)); EXPECT_EQ(copy.indexCount, 1024u); - EXPECT_EQ(copy.shaderRendererID, 7u); + EXPECT_EQ(copy.shaderRendererID, TestHandle(7u)); EXPECT_EQ(copy.entityID, 999); EXPECT_FLOAT_EQ(copy.waveParams.x, 1.0f); EXPECT_FLOAT_EQ(copy.waterColor.g, 0.4f); @@ -177,10 +177,19 @@ TEST(WaterRendering, DrawWaterCommandZeroInitNoNaN) ValidateVec4(cmd.sssColor, "sssColor"); ValidateVec4(cmd.ssrParams, "ssrParams"); ValidateVec4(cmd.tessParams, "tessParams"); - EXPECT_EQ(cmd.normalMap0ID, 0u); - EXPECT_EQ(cmd.normalMap1ID, 0u); - EXPECT_EQ(cmd.noiseTextureID, 0u); - EXPECT_EQ(cmd.foamTextureID, 0u); + // A value-initialised command must leave every texture slot NAMING NOTHING. + // The assertion is against RHI::NullResource, not TestHandle(0u): after + // issue #691 step 3 slice 6 these are identities, and index 0 is a perfectly + // ordinary live slot — asserting equality with it would let a command that + // wrongly points at whatever occupies registry slot 0 pass this test. + EXPECT_FALSE(cmd.normalMap0ID.IsValid()); + EXPECT_FALSE(cmd.normalMap1ID.IsValid()); + EXPECT_FALSE(cmd.noiseTextureID.IsValid()); + EXPECT_FALSE(cmd.foamTextureID.IsValid()); + EXPECT_EQ(cmd.normalMap0ID, RHI::NullResource); + EXPECT_EQ(cmd.normalMap1ID, RHI::NullResource); + EXPECT_EQ(cmd.noiseTextureID, RHI::NullResource); + EXPECT_EQ(cmd.foamTextureID, RHI::NullResource); } // ============================================================================= diff --git a/OloEngine/tests/Rendering/rhi_boundary_baseline.json b/OloEngine/tests/Rendering/rhi_boundary_baseline.json index c0d3de225..f8b2228c7 100644 --- a/OloEngine/tests/Rendering/rhi_boundary_baseline.json +++ b/OloEngine/tests/Rendering/rhi_boundary_baseline.json @@ -250,7 +250,50 @@ " scanner does not walk, so debug_escape_hatch stays honestly 0 rather than", " being waived). CONSEQUENCE FOR LATER SLICES: a resource's import may only", " move to ImportTextureHandle after the diagnostics can read one — which is", - " why DDGI's importAtlas is still native here." + " why DDGI's importAtlas is still native here.", + "", + "PHASE 2 STEP 3, SLICE 6 (2026-08-01): the command-layer bind cache — the", + "CURRENCY itself, not one producer family. sweep_renderer_id 653 -> 355 (six", + "times slice 5's move); facade_native_id_params 68 -> 67.", + "", + "`using RendererID = u32` is DELETED. Every GPU-object field on the POD", + "command structs, the redundant-bind cache (BoundTextures / BoundUBOs /", + "CurrentBoundShader / CurrentBoundVAO / the six per-frame shadow fields),", + "DepthPrepassShaderIDs, InstanceGroupKey and the draw sort keys are", + "RHI::ResourceHandle now.", + "", + "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, merging unrelated draws into one batch —", + " one mesh rendered with another's geometry. Two live handles cannot", + " collide.", + " 2. DepthPrepassShaderIDs compared PROGRAMS by GL name to decide whether a", + " material's shader could be swapped for the depth-only one. A relinked", + " program can inherit a freed name across a hot reload.", + " 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 would see no change — the", + " same defect the DDGI atlases had in slice 5.", + "", + "THE DOC CLAIM THIS SLICE FALSIFIED. rhi-abstraction-boundary.md said keying", + "the cache on identities makes the Invalidate* calls 'a pure optimisation'.", + "That is WRONG and 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 call InvalidateTextureBinding. The doc is", + "corrected in this commit.", + "", + "WHEN A MIGRATION SEEMS TO REQUIRE RAISING A RATCHET, THE CALL SITE IS USUALLY", + "WRONG. DrawElementsIndirectRaw(vaoID, bufferID) needed a handle form, and the", + "obvious mixed (handle, u32 bufferID) overload would have pushed", + "facade_native_id_params 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 instead of +2." ], "measured_on_commit": "11d7cea7", "measured_on_date": "2026-07-30", @@ -262,7 +305,7 @@ "sweep_glad_includes": 0, "tools_gl_calls": 236, "debug_escape_hatch": 0, - "sweep_renderer_id": 653, - "facade_native_id_params": 68, + "sweep_renderer_id": 355, + "facade_native_id_params": 67, "backend_resolve_hatch": 0 } diff --git a/docs/agent-rules/rhi-abstraction-boundary.md b/docs/agent-rules/rhi-abstraction-boundary.md index 0008d189b..7f985a062 100644 --- a/docs/agent-rules/rhi-abstraction-boundary.md +++ b/docs/agent-rules/rhi-abstraction-boundary.md @@ -461,6 +461,32 @@ render pass is exactly the "tests green, screen wrong" class `CLAUDE.md`'s rendering rule exists for. **When reviewing a scripted currency sweep, read the conditionals first** — the type changes are compiler-checked, the guards are not. +### A GLOBAL rename is always wrong here, and slice 6 proved it five times + +The defining property of a dual-currency migration is that **some call sites must +not move**. A regex cannot see which, so every broad tool overreaches. Slice 6 +hit this five separate times; recording the shapes because four of the five were +caught only by luck of the destination type differing: + +| The tool | What it did | How it surfaced | +| --- | --- | --- | +| `grep … \| head -30` to survey `RendererID` uses | Read a TRUNCATED list as complete, missed `Renderer3D.h`'s 38 | 4551-error parse cascade | +| `s/(=\s*)0(\s*[;,])/\1{}\2/` | Matched the `=` of `!=`, making `x != 0;` into `x != {};` | Failed to parse — but the same rewrite on `x != 0 &&` would have parsed | +| `s/->GetRendererID()/->GetRHIHandle()/` over `Scene.cpp` | Swept up the IBL trio that must stay native for the graph import | Error count went UP, 67 → 75 | +| the same, second pass | Swept up the cloudscape weather map and three fluid SSBO ids (later slices) | Compiler, because the destinations were still `u32` | +| `s/field = /field = TestHandle()/` in tests | Wrapped a literal `0` that meant "absent", producing a VALID handle naming slot 0 | **The test suite** — `DrawWaterCommandZeroInitNoNaN`. Nothing else would have. | + +The last row is the dangerous one and the reason to write this down: it is the +only one the compiler could not see, and had the default handle happened to be +`{0,1}` instead of `{0xFFFFFFFF,0}` it would have passed while silently +weakening a zero-init assertion. **In a test, a literal `0` on a migrated field +is the ABSENT sentinel (`RHI::NullResource` / `!IsValid()`), never a synthetic +id** — `TestHandle(0)` is a live handle naming registry slot 0. + +The tool that *did* work: drive edits from the compiler's own `(file, line)` +output and only rewrite lines it rejected. That cannot touch a site the compiler +accepted, which is exactly the set that must not move. + ### Identity is the C++ object, not the native name — and that fixes something Anchoring the registry entry to the resource *object* (with `UpdateNative` for @@ -511,20 +537,23 @@ script derives the repo root from its own location — the first version pinned absolute worktree path and reported a confident `TOTAL 0 across 0 files` everywhere else, so **if it prints 0, check that before believing it.** -Counts are *after* slice 5 (was 1196 across 118 files; now **1150 across 110**): +Counts are *after slice 6*. The raw total went 1196 across 118 files → 1150 +(slice 5) → **848 across 104** (slice 6): -| Where | Count | Note | -| --- | ---: | --- | -| `Platform/OpenGL/` | 412 | **Exempt.** The backend may name GL ids. | -| `Renderer/Commands/` (`RenderCommand.h` 72, `CommandDispatch.cpp` 67) | 141 | The bind-cache unit below. Untouched. | -| `Renderer3DMeshSubmission.cpp` | 71 | Same dataflow as above. Untouched. | -| `Scene/Scene.cpp` | 66 | Untouched (item 3). | -| `Renderer3D.h` | 62 | Untouched (item 3). | -| `Get{Color,Depth}AttachmentRendererID` call sites | 72 → **31** | Slice 5. The remainder is the deferred table below plus the native accessors kept on purpose. | +| Where | Then | Now | Note | +| --- | ---: | ---: | --- | +| `Platform/OpenGL/` | 412 | ~410 | **Exempt.** The backend may name GL ids. | +| `Renderer/Commands/` | 141 | **53** | Slice 6. What survives is `CommandDispatch.cpp`'s remaining native spellings. | +| `Renderer3DMeshSubmission.cpp` | 71 | **~0** | Slice 6, via the POD structs. | +| `Scene/Scene.cpp` | 66 | **~0** | Pulled in by slice 6 (it fills the PODs). | +| `Renderer3D.h` | 62 | **~0** | Pulled in by the alias deletion — item 3's header, landed early. | +| `Get{Color,Depth}AttachmentRendererID` call sites | 72 | **29** | Slice 5 + the two Renderer3D setters slice 6 unblocked. | -By spelling: `m_RendererID` 439 (mostly backend-internal, exempt), -`GetRendererID` 325 → **319** (**the real target** — consumers), -`shaderRendererID` 110, attachment getters 71 → **31**. +By spelling: `m_RendererID` 435 (backend-internal, exempt), `GetRendererID` +325 → **212** (**the real target**), `shaderRendererID` 110 → **111** (unchanged +— it is a FIELD NAME on the migrated structs, and renaming the field is +cosmetic churn better done with item 4's deletion pass), attachment getters +71 → **29**. Suggested order, each a buildable commit: @@ -542,7 +571,25 @@ Suggested order, each a buildable commit: `SetTextureFilter`/`SetTextureWrap`/`UploadTextureSubImage2D` discovery, one slice later: **you cannot enumerate a migration's facade needs by reading; migrate one real consumer per SINK FAMILY and let the compiler tell you.** -2. **The command-layer bind cache** — one indivisible unit, see below. +2. ~~**The command-layer bind cache**~~ — **DONE (slice 6)**, and it was + materially bigger than this line implies. Three corrections for whoever + scopes a comparable unit: + + * **The blast radius was 253 errors across 16 files, not "~180 concentrated + in `CommandDispatch.cpp`"** — that file was ~40% of it. The rest came from + deleting `using RendererID = u32`, which is a TYPE in headers included + everywhere: `Renderer3D.h` alone used it in 38 declarations and produced a + **4551-error parse cascade** on the first build. Item 3's `Renderer3D.h` + therefore lands with item 2 whether you planned it or not. + * **Seven resource chains not named in this worklist came with it**, because + each feeds the cache and `native -> handle` is unrecoverable: + `CloudShadowMap`, `SnowAccumulationSystem`, `OceanFFTField`, + `FoliageRenderer`, `DepthPrepassShaderIDs`, the global IBL maps, and + `ShadowMap`'s compare-off views. + * **Nine new facade virtuals were needed**, against a prediction of zero: + `CreateDepthArrayCompareOffViewHandle`, `SetProgramUniformFloat`, handle + forms of `DrawIndexedRaw` (×2), `DrawIndexedInstancedRaw`, + `DrawIndexedPatchesRaw`, and `DrawBoundElementsIndirect`. 3. `Scene.cpp` / `Renderer3D.h`, then the remaining passes (`ColorGrading` is a near-clone of the migrated SSAO; `Cloudscape` needs `CloudNoise` migrated first; `FluidIntermediates` recreates on resize and so is the first to need @@ -633,14 +680,32 @@ Deliberately **not** migrated, each for a stated reason rather than for size: | `RenderGraph`'s JSON topology dump | Reports native ids on purpose, for external tooling. | | `Renderer/Debug/`'s two | Phase 8 relocation. | -**Counters:** `sweep_renderer_id` 699 → **653**; `facade_native_id_params` -deliberately unchanged at 68, because this slice *adds* handle overloads and -deletes no `u32` form — that is worklist item 4's job, and the documented order. -The 46 is real but flatters the slice: the accessor name survives wherever a -native sibling is kept on purpose (DDGI's `GetIrradianceAtlasID` is still there -so `importAtlas` can call it), and 40 of the 46 are the attachment getters -themselves. Read §4's "do not use it as a progress meter" note before reading -either number as a completion fraction. +**Counters after slice 5:** `sweep_renderer_id` 699 → **653**; +`facade_native_id_params` unchanged at 68, because that slice *adds* handle +overloads and deletes no `u32` form — item 4's job, and the documented order. +The 46 flatters it: the accessor name survives wherever a native sibling is kept +on purpose (DDGI's `GetIrradianceAtlasID` is still there so `importAtlas` can +call it), and 40 of the 46 are the attachment getters themselves. + +**Counters after slice 6 (the bind cache):** `sweep_renderer_id` 653 → **355**, +`facade_native_id_params` 68 → **67**. The 298 is the honest measure of what +converting the CURRENCY (rather than one producer family) is worth — six times +slice 5's move. + +The `facade_native_id_params` fall is small but worth reading, because the naïve +version of this slice *raised* it. `DrawElementsIndirectRaw(vaoID, bufferID)` +needed a handle form, and the obvious answer was a mixed +`(RHI::ResourceHandle, u32 indirectBufferID)` overload — which adds a +`u32 ID` parameter and pushes a ratchet that may only fall to 69. The real +answer was that its single caller had *already* run `BindVAOIfNeeded`, so the +draw was re-binding the VAO behind the redundant-bind cache's back; replacing +both `u32` forms with `DrawBoundElementsIndirect(u32)` (matching the existing +`DrawBound*` family) removed a redundant bind AND netted −1. **When a migration +looks like it must raise a ratchet, that is usually the signal that the call +site's shape is wrong, not that the ratchet is.** + +Read §4's "do not use it as a progress meter" note before reading either number +as a completion fraction. ### Hashing a driver name into a cache fingerprint cannot see a destroy-then-recreate @@ -690,12 +755,39 @@ prevents.** The comments on `InvalidateTextureSlot` / `InvalidateTextureBinding` record it — `VirtualGeometryPass` binds the Hi-Z pyramid to unit 0 for its cull compute, unit 0 is also `u_AlbedoMap`, and "any material whose albedo ID matched the stale cache entry silently sampled the HZB depth texture as its albedo." -The current fix is manual invalidation that every future raw-GL binder must -remember to call. Keyed on identities the stale entry cannot collide, so the -invalidation calls stop being load-bearing correctness and become a pure -optimisation. That is a genuine behavioural win, not just a type change — say so -in the commit, and keep the invalidation calls (they still avoid redundant -binds) rather than deleting them as "no longer needed". +Keyed on identities that particular collision becomes unrepresentable: a deleted +texture's handle is retired, so it can never compare equal to a live one. + +### …but invalidation gets MORE load-bearing, not less — the one place handles are WEAKER + +**An earlier version of this section said the invalidation calls "stop being +load-bearing correctness and become a pure optimisation". That was wrong, and +wrong in the direction that ships bugs.** Recorded here because the reasoning is +seductive and the failure is silent. + +The recycled-name hazard does die. But there is a second hazard the identity +currency *creates*, because identity is deliberately stable where the driver +name is not: an **in-place reload** (`OpenGLTexture::InvalidateImpl`) deletes the +GL texture, creates new storage, and calls `ScopedResourceHandle::Sync`, which +**preserves** the handle — that is the whole point of §4's "identity is the C++ +object", and it is what makes caching a handle safe. So: + +- the cache holds handle `H` for slot N, GL name `old` is bound to unit N; +- reload: `old` is deleted, `new` created, `H` still names the object; +- `BindTrackedTexture(H, N)` sees `BoundTextures[N] == H`, concludes "already + bound", and **skips a bind that must happen** — leaving the unit pointing at a + deleted name. + +Under the old native-id keying this self-corrected, because the name changed and +the cache missed. Under handle keying it does not. So the rule inverts: +**every site that recreates a texture's storage MUST call +`InvalidateTextureBinding`** (it takes a handle now). Deleting those calls as +"no longer needed" — which the old paragraph invited — would produce a +tests-green / screen-wrong bug visible only after a hot reload. + +`InvalidateTextureSlot` is also still required, for an unrelated reason: a raw +binder bypasses the cache entirely, so the cache's claim about the slot is simply +untrue and no keying scheme can detect that from the inside. ### Delegating to the native path and stamping the identity on afterwards silently disables generation bumping From c6960d4d1b8eb5d1654cce583fc4cc6058785dfa Mon Sep 17 00:00:00 2001 From: Ole Bueker Date: Sat, 1 Aug 2026 19:47:12 +0200 Subject: [PATCH 5/6] =?UTF-8?q?refactor(rhi):=20finish=20item=203=20?= =?UTF-8?q?=E2=80=94=20the=20transient=20"blocker"=20was=20an=20invariant?= =?UTF-8?q?=20(#691)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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, 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) --- OloEditor/SandboxProject/AssetRegistry.oar | Bin 7204 -> 7204 bytes .../visual/WorldOriginRebase_far_before.png | Bin 295013 -> 293145 bytes OloEngine/src/OloEngine/Renderer/GBuffer.cpp | 16 ++++++ OloEngine/src/OloEngine/Renderer/GBuffer.h | 6 +++ .../Renderer/Passes/CloudscapeRenderPass.cpp | 21 +++++--- .../Renderer/Passes/CloudscapeRenderPass.h | 7 +-- .../Passes/GPUDrivenOcclusionPass.cpp | 25 +++++---- .../Renderer/Passes/SSAORenderPass.cpp | 34 +++++++----- .../Renderer/Passes/SceneRenderPass.cpp | 40 ++++++++------ .../src/OloEngine/Renderer/RenderGraph.cpp | 16 +++++- .../src/OloEngine/Renderer/RenderGraph.h | 36 +++++++++---- .../src/OloEngine/Renderer/RenderPipeline.cpp | 2 +- .../Rendering/rhi_boundary_baseline.json | 28 +++++++++- docs/agent-rules/rhi-abstraction-boundary.md | 49 ++++++++++++++---- 14 files changed, 205 insertions(+), 75 deletions(-) diff --git a/OloEditor/SandboxProject/AssetRegistry.oar b/OloEditor/SandboxProject/AssetRegistry.oar index 716a7da1a4751c8d47e1526ed05d34d3d8dbd7a0..fda67943b0706696faeb1048e017a8b3664d7eb4 100644 GIT binary patch delta 188 zcmZ2tvBZLtiGhKk1c)6cH}Yvs++@$_IXQtvbD~iwqZ3d>bMjP1uZbV47@ff!8z%9| z_Zf{DUBJAJKwdDD38O2Rr@|~g`2>>@qZ^nv3COc%)@AetnKqf9MQrj)W}C_JEDn=Z zS*#d+L86mb#U{^ZF=zAxD=7u?#8^!k{lUCXKwdYi?PNzb`^g+^mYW;c4hT+;k~ZFK MFCoP|xmS8R0Lkn&82|tP delta 193 zcmZ2tvBZLtiGhKk1c*H+FJRG}I46|RadIP{)JX0gesELMy@ zV4gpV*yQ^x=9A~MI7|*>HJvQR>M;2d(?=>T^@I_CfY diff --git a/OloEditor/assets/tests/visual/WorldOriginRebase_far_before.png b/OloEditor/assets/tests/visual/WorldOriginRebase_far_before.png index 95f3b4e0701d998aa306a0344d020fa133afe96b..f575f264839bb2ed7d9f2807e93c3d2cff767b66 100644 GIT binary patch delta 157055 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%0DRRU?3lg*ZnJQXf=%~ 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 z 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 P_~+uV-~PHSDf9mTGu^zB delta 158877 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�=xWUe(|{W6)D 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�rETrGq1Jo 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%0DVit_b0k8 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 diff --git a/OloEngine/src/OloEngine/Renderer/GBuffer.cpp b/OloEngine/src/OloEngine/Renderer/GBuffer.cpp index 1b1493762..367a70d71 100644 --- a/OloEngine/src/OloEngine/Renderer/GBuffer.cpp +++ b/OloEngine/src/OloEngine/Renderer/GBuffer.cpp @@ -194,6 +194,22 @@ namespace OloEngine return fb->GetDepthAttachmentRendererID(); } + RHI::ResourceHandle GBuffer::GetColorAttachmentHandle(AttachmentIndex index) const + { + const auto& fb = m_ResolvedFramebuffer ? m_ResolvedFramebuffer : m_Framebuffer; + if (!fb) + return RHI::NullResource; + return fb->GetColorAttachmentHandle(std::to_underlying(index)); + } + + RHI::ResourceHandle GBuffer::GetDepthAttachmentHandle() const + { + const auto& fb = m_ResolvedFramebuffer ? m_ResolvedFramebuffer : m_Framebuffer; + if (!fb) + return RHI::NullResource; + return fb->GetDepthAttachmentHandle(); + } + u32 GBuffer::GetMSColorAttachmentID(AttachmentIndex index) const { if (!m_Framebuffer) diff --git a/OloEngine/src/OloEngine/Renderer/GBuffer.h b/OloEngine/src/OloEngine/Renderer/GBuffer.h index 19f5a7f80..35496b642 100644 --- a/OloEngine/src/OloEngine/Renderer/GBuffer.h +++ b/OloEngine/src/OloEngine/Renderer/GBuffer.h @@ -1,6 +1,7 @@ #pragma once #include "OloEngine/Core/Base.h" +#include "OloEngine/Renderer/RHI/RHITypes.h" #include "OloEngine/Core/Ref.h" #include "OloEngine/Renderer/Framebuffer.h" @@ -100,6 +101,11 @@ namespace OloEngine // return resolved (single-sample) IDs when MSAA is active. [[nodiscard]] u32 GetColorAttachmentID(AttachmentIndex index) const; [[nodiscard]] u32 GetDepthAttachmentID() const; + // Identity forms (issue #691 step 3). The G-Buffer attachments are + // ordinary framebuffer attachments, so these just forward to the + // framebuffer's own handle accessors. + [[nodiscard]] RHI::ResourceHandle GetColorAttachmentHandle(AttachmentIndex index) const; + [[nodiscard]] RHI::ResourceHandle GetDepthAttachmentHandle() const; // Raw multisample attachment IDs. For sampleCount == 1 these are // identical to GetColorAttachmentID / GetDepthAttachmentID. For diff --git a/OloEngine/src/OloEngine/Renderer/Passes/CloudscapeRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/CloudscapeRenderPass.cpp index eac5b7da0..27be4d5c0 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/CloudscapeRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/CloudscapeRenderPass.cpp @@ -295,20 +295,25 @@ namespace OloEngine m_ResolveShader->Bind(); // This frame's raymarch at unit 0 (layout(binding = 0) in the shader). - const u32 cloudsRawColorID = cloudsRawFramebuffer->GetColorAttachmentRendererID(0); - context.BindTexture(0, cloudsRawColorID); + const RHI::ResourceHandle cloudsRawColor = cloudsRawFramebuffer->GetColorAttachmentHandle(0); + context.BindTexture(0, cloudsRawColor); // History at unit 1. Prefer the graph-imported handle (always the // live texture); fall back to the pipeline-supplied raw id, then to // the current frame when no valid history exists — with Misc.x // forced to 0 in that case (UploadAndBindUBO) the shader ignores it. - u32 historyTextureID = 0u; + // All three candidates are identities now (issue #691 step 3, slice 7): + // the graph import, the pipeline-owned history texture, and this + // frame's raymarch. The last native operand here was the transient + // resolve, which the planner now answers in both currencies. + RHI::ResourceHandle historyTexture{}; if (m_SelectedHistoryTexture.IsValid()) - historyTextureID = context.ResolveTexture(m_SelectedHistoryTexture); - if (historyTextureID == 0u) - historyTextureID = m_HistoryTextureID; - const u32 historyBindID = (m_HistoryValid && historyTextureID != 0u) ? historyTextureID : cloudsRawColorID; - context.BindTexture(1, historyBindID); + historyTexture = context.ResolveTextureHandle(m_SelectedHistoryTexture); + if (!historyTexture.IsValid()) + historyTexture = m_HistoryTexture; + const RHI::ResourceHandle historyBind = + (m_HistoryValid && historyTexture.IsValid()) ? historyTexture : cloudsRawColor; + context.BindTexture(1, historyBind); { const auto va = MeshPrimitives::GetFullscreenTriangle(); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/CloudscapeRenderPass.h b/OloEngine/src/OloEngine/Renderer/Passes/CloudscapeRenderPass.h index 5efa75e5f..6474dea5c 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/CloudscapeRenderPass.h +++ b/OloEngine/src/OloEngine/Renderer/Passes/CloudscapeRenderPass.h @@ -1,6 +1,7 @@ #pragma once #include "OloEngine/Core/Base.h" +#include "OloEngine/Renderer/RHI/RHITypes.h" #include "OloEngine/Renderer/RenderGraphNode.h" #include "OloEngine/Renderer/ResourceHandle.h" #include "OloEngine/Renderer/Shader.h" @@ -114,9 +115,9 @@ namespace OloEngine // target) + its validity for this frame. Must be set AFTER // PopulateBlackboard ran (EnsureHistoryStorage may recreate the // texture on resize) — UploadExecutionState is the call site. - void SetHistory(u32 historyTextureID, bool valid) noexcept + void SetHistory(RHI::ResourceHandle historyTexture, bool valid) noexcept { - m_HistoryTextureID = historyTextureID; + m_HistoryTexture = historyTexture; m_HistoryValid = valid; } @@ -149,7 +150,7 @@ namespace OloEngine u32 m_BaseNoiseTextureID = 0; u32 m_DetailNoiseTextureID = 0; u32 m_WeatherMapTextureID = 0; - u32 m_HistoryTextureID = 0; + RHI::ResourceHandle m_HistoryTexture{}; bool m_HistoryValid = false; RGFramebufferHandle m_SelectedCloudsRawFramebuffer{}; diff --git a/OloEngine/src/OloEngine/Renderer/Passes/GPUDrivenOcclusionPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/GPUDrivenOcclusionPass.cpp index 0a44ca6c6..1927b3329 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/GPUDrivenOcclusionPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/GPUDrivenOcclusionPass.cpp @@ -192,21 +192,26 @@ namespace OloEngine // occluders + phase-1 + phase-2 survivors; copy them over ScenePass's // earlier export. Texture-to-texture copies — no framebuffer needed. { - const u32 fbDepthID = m_SceneFramebuffer->GetDepthAttachmentRendererID(); - const u32 sceneDepthExportID = m_SelectedSceneDepth.IsValid() ? context.ResolveTexture(m_SelectedSceneDepth) : 0u; - if (sceneDepthExportID != 0u && fbDepthID != 0u && sceneDepthExportID != fbDepthID) + // Identities (issue #691 step 3, slice 7) -- same unblock as + // SceneRenderPass's exports: the destinations are graph transients. + const RHI::ResourceHandle fbDepth = m_SceneFramebuffer->GetDepthAttachmentHandle(); + const RHI::ResourceHandle sceneDepthExport = + m_SelectedSceneDepth.IsValid() ? context.ResolveTextureHandle(m_SelectedSceneDepth) : RHI::NullResource; + if (sceneDepthExport.IsValid() && fbDepth.IsValid() && sceneDepthExport != fbDepth) { - RenderCommand::CopyImageSubData(fbDepthID, RendererAPI::TextureTargetType::Texture2D, - sceneDepthExportID, RendererAPI::TextureTargetType::Texture2D, + RenderCommand::CopyImageSubData(fbDepth, RendererAPI::TextureTargetType::Texture2D, + sceneDepthExport, RendererAPI::TextureTargetType::Texture2D, sceneSpec.Width, sceneSpec.Height); } // RT2 is the octahedral view-normal attachment in the forward layout. - const u32 fbNormalsID = sceneColorAttachmentCount > 2 ? m_SceneFramebuffer->GetColorAttachmentRendererID(2) : 0u; - const u32 sceneNormalsExportID = m_SelectedSceneNormals.IsValid() ? context.ResolveTexture(m_SelectedSceneNormals) : 0u; - if (sceneNormalsExportID != 0u && fbNormalsID != 0u && sceneNormalsExportID != fbNormalsID) + const RHI::ResourceHandle fbNormals = + sceneColorAttachmentCount > 2 ? m_SceneFramebuffer->GetColorAttachmentHandle(2) : RHI::NullResource; + const RHI::ResourceHandle sceneNormalsExport = + m_SelectedSceneNormals.IsValid() ? context.ResolveTextureHandle(m_SelectedSceneNormals) : RHI::NullResource; + if (sceneNormalsExport.IsValid() && fbNormals.IsValid() && sceneNormalsExport != fbNormals) { - RenderCommand::CopyImageSubData(fbNormalsID, RendererAPI::TextureTargetType::Texture2D, - sceneNormalsExportID, RendererAPI::TextureTargetType::Texture2D, + RenderCommand::CopyImageSubData(fbNormals, RendererAPI::TextureTargetType::Texture2D, + sceneNormalsExport, RendererAPI::TextureTargetType::Texture2D, sceneSpec.Width, sceneSpec.Height); } } diff --git a/OloEngine/src/OloEngine/Renderer/Passes/SSAORenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/SSAORenderPass.cpp index 42932e555..7e15d203d 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/SSAORenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/SSAORenderPass.cpp @@ -150,16 +150,20 @@ namespace OloEngine // Phase F slice 37 — self-resolving SceneDepth and SceneNormals: look // up directly from the render graph blackboard so no per-frame // side-channel setter calls are needed from EndScene(). - u32 depthID = 0; - u32 normalsID = 0; - u32 aoOutputTexID = 0; + // Identities (issue #691 step 3, slice 7). These resolve now that the + // transient planner records a handle alongside the native id — before + // that, ResolveTextureHandle answered null for every pooled texture and + // this pass had to stay on driver names. + RHI::ResourceHandle depthTexture{}; + RHI::ResourceHandle normalsTexture{}; + RHI::ResourceHandle aoOutputTexture{}; if (m_SelectedSceneDepthTexture.IsValid()) - depthID = context.ResolveTexture(m_SelectedSceneDepthTexture); + depthTexture = context.ResolveTextureHandle(m_SelectedSceneDepthTexture); if (m_SelectedSceneNormalsTexture.IsValid()) - normalsID = context.ResolveTexture(m_SelectedSceneNormalsTexture); + normalsTexture = context.ResolveTextureHandle(m_SelectedSceneNormalsTexture); if (m_SelectedAOOutputTexture.IsValid()) - aoOutputTexID = context.ResolveTexture(m_SelectedAOOutputTexture); - if (depthID == 0 || normalsID == 0 || aoOutputTexID == 0) + aoOutputTexture = context.ResolveTextureHandle(m_SelectedAOOutputTexture); + if (!depthTexture.IsValid() || !normalsTexture.IsValid() || !aoOutputTexture.IsValid()) { return; } @@ -208,10 +212,10 @@ namespace OloEngine m_SSAOShader->Bind(); // Bind scene depth at TEX_POSTPROCESS_DEPTH (slot 19) - context.BindTexture(ShaderBindingLayout::TEX_POSTPROCESS_DEPTH, depthID); + context.BindTexture(ShaderBindingLayout::TEX_POSTPROCESS_DEPTH, depthTexture); // Bind scene view-space normals at TEX_SCENE_NORMALS (slot 22) - context.BindTexture(ShaderBindingLayout::TEX_SCENE_NORMALS, normalsID); + context.BindTexture(ShaderBindingLayout::TEX_SCENE_NORMALS, normalsTexture); // Bind noise texture at TEX_SSAO_NOISE (slot 21) context.BindTexture(ShaderBindingLayout::TEX_SSAO_NOISE, m_NoiseTexture); @@ -235,15 +239,19 @@ namespace OloEngine context.BindTexture(0, rawFB->GetColorAttachmentHandle(0)); // Bind scene depth at TEX_POSTPROCESS_DEPTH (slot 19) for bilateral edge detection - context.BindTexture(ShaderBindingLayout::TEX_POSTPROCESS_DEPTH, depthID); + context.BindTexture(ShaderBindingLayout::TEX_POSTPROCESS_DEPTH, depthTexture); DrawFullscreenTriangle(); blurFB->Unbind(); - if (const u32 blurredAOTextureID = blurFB->GetColorAttachmentRendererID(0); blurredAOTextureID != 0 && blurredAOTextureID != aoOutputTexID) + // Both operands are identities now, so the self-copy guard compares + // OBJECTS rather than driver names — a recycled name can no longer make + // two distinct textures look like the same one and skip a real copy. + if (const RHI::ResourceHandle blurredAO = blurFB->GetColorAttachmentHandle(0); + blurredAO.IsValid() && blurredAO != aoOutputTexture) { - RenderCommand::CopyImageSubData(blurredAOTextureID, RendererAPI::TextureTargetType::Texture2D, - aoOutputTexID, RendererAPI::TextureTargetType::Texture2D, + RenderCommand::CopyImageSubData(blurredAO, RendererAPI::TextureTargetType::Texture2D, + aoOutputTexture, RendererAPI::TextureTargetType::Texture2D, m_HalfWidth, m_HalfHeight); } diff --git a/OloEngine/src/OloEngine/Renderer/Passes/SceneRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/SceneRenderPass.cpp index 9c6c2b387..049f363a0 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/SceneRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/SceneRenderPass.cpp @@ -408,38 +408,44 @@ namespace OloEngine // scene pass still renders into the legacy scene/G-Buffer // attachments, but downstream consumers now sample the exported graph // textures instead of importing those attachments directly. - const auto copySceneExport = [this, &context](const RGTextureHandle handle, const u32 sourceTextureID) - { - if (!handle.IsValid() || sourceTextureID == 0u || + // Identities throughout (issue #691 step 3, slice 7): the export target + // is a graph TRANSIENT, which only began answering ResolveTextureHandle + // once the planner recorded a handle for pooled textures. The self-copy + // guard now compares OBJECTS -- under driver names a recycled name could + // make source and export look identical and skip a copy the frame needed. + const auto copySceneExport = [this, &context](const RGTextureHandle handle, + const RHI::ResourceHandle sourceTexture) + { + if (!handle.IsValid() || !sourceTexture.IsValid() || m_FramebufferSpec.Width == 0u || m_FramebufferSpec.Height == 0u) { return; } - const u32 exportedTextureID = context.ResolveTexture(handle); - if (exportedTextureID == 0u || exportedTextureID == sourceTextureID) + const RHI::ResourceHandle exportedTexture = context.ResolveTextureHandle(handle); + if (!exportedTexture.IsValid() || exportedTexture == sourceTexture) return; - RenderCommand::CopyImageSubData(sourceTextureID, RendererAPI::TextureTargetType::Texture2D, - exportedTextureID, RendererAPI::TextureTargetType::Texture2D, + RenderCommand::CopyImageSubData(sourceTexture, RendererAPI::TextureTargetType::Texture2D, + exportedTexture, RendererAPI::TextureTargetType::Texture2D, m_FramebufferSpec.Width, m_FramebufferSpec.Height); }; - const u32 sourceDepthID = deferredActive && m_GBuffer - ? m_GBuffer->GetDepthAttachmentID() - : m_Target->GetDepthAttachmentRendererID(); - copySceneExport(m_SelectedSceneDepthExport, sourceDepthID); + const RHI::ResourceHandle sourceDepth = deferredActive && m_GBuffer + ? m_GBuffer->GetDepthAttachmentHandle() + : m_Target->GetDepthAttachmentHandle(); + copySceneExport(m_SelectedSceneDepthExport, sourceDepth); if (!deferredActive) { - const u32 sourceNormalsID = m_Target->GetColorAttachmentRendererID(2); - copySceneExport(m_SelectedSceneNormalsExport, sourceNormalsID); + const RHI::ResourceHandle sourceNormals = m_Target->GetColorAttachmentHandle(2); + copySceneExport(m_SelectedSceneNormalsExport, sourceNormals); } - const u32 sourceVelocityID = deferredActive && m_GBuffer - ? m_GBuffer->GetColorAttachmentID(GBuffer::Velocity) - : m_Target->GetColorAttachmentRendererID(3); - copySceneExport(m_SelectedVelocityExport, sourceVelocityID); + const RHI::ResourceHandle sourceVelocity = deferredActive && m_GBuffer + ? m_GBuffer->GetColorAttachmentHandle(GBuffer::Velocity) + : m_Target->GetColorAttachmentHandle(3); + copySceneExport(m_SelectedVelocityExport, sourceVelocity); // Deferred debug visualisation: until DeferredLightingPass lands in // Phase 3, copy the selected G-Buffer channel into the forward scene diff --git a/OloEngine/src/OloEngine/Renderer/RenderGraph.cpp b/OloEngine/src/OloEngine/Renderer/RenderGraph.cpp index 573949e45..826b855c2 100644 --- a/OloEngine/src/OloEngine/Renderer/RenderGraph.cpp +++ b/OloEngine/src/OloEngine/Renderer/RenderGraph.cpp @@ -3455,7 +3455,21 @@ namespace OloEngine texHandleIt != m_TextureHandlesByName.end() && texHandleIt->second.Index < m_PhysicalTextures.size()) { - m_PhysicalTextures[texHandleIt->second.Index].TextureID = textureIt->second ? textureIt->second->GetRendererID() : 0; + // BOTH currencies, read off the one pooled Ref in one + // statement (issue #691 step 3, slice 7). See + // PhysicalTexture: the "exactly one is set" rule covers + // IMPORTS, where the importer only ever has one. The + // planner holds the object, so it has both and they + // cannot describe different textures. + // + // Setting Handle is what lets a consumer copy to/from a + // transient in the identity currency — without it, + // ResolveTextureHandle answers null for every transient + // and any pass whose other operand migrated is stuck. + auto& phys = m_PhysicalTextures[texHandleIt->second.Index]; + const auto& pooled = textureIt->second; + phys.TextureID = pooled ? pooled->GetRendererID() : 0u; + phys.Handle = pooled ? pooled->GetRHIHandle() : RHI::NullResource; } break; } diff --git a/OloEngine/src/OloEngine/Renderer/RenderGraph.h b/OloEngine/src/OloEngine/Renderer/RenderGraph.h index 6ee9363ee..31f5a0afc 100644 --- a/OloEngine/src/OloEngine/Renderer/RenderGraph.h +++ b/OloEngine/src/OloEngine/Renderer/RenderGraph.h @@ -1442,26 +1442,42 @@ namespace OloEngine // ------------------------------------------------------------------- struct PhysicalTexture { - // TextureID and Handle are ALTERNATIVES, not two views of one value, - // and exactly one is set per entry (issue #691 step 3, slice 5). + // For an IMPORTED entry, TextureID and Handle are ALTERNATIVES and + // exactly one is set (issue #691 step 3, slice 5). // // They cannot drift, because neither can be derived from the other: // `native -> handle` is unrecoverable (a driver name does not // identify a registry slot), and `handle -> native` is a resolution // that may only happen inside Platform// — RenderGraph - // lives in Renderer/, so it cannot perform it. An entry therefore - // carries whichever currency its importer had, and callers read - // through the matching accessor: ResolveTexture for the native form, - // ResolveTextureHandle for the identity form. + // lives in Renderer/, so it cannot perform it. An imported entry + // therefore carries whichever currency its importer had, and callers + // read through the matching accessor: ResolveTexture for the native + // form, ResolveTextureHandle for the identity form. // - // That is also why migration proceeds PER RESOURCE rather than per - // layer: a resource's creator -> import -> resolve -> bind chain - // moves together, inside one pass. The final slice deletes the - // native field once every chain has moved. + // A TRANSIENT entry sets BOTH, and that is not a loosening of the + // rule above but a case the rule never covered (slice 7). The + // exclusivity exists because an importer only ever HAS one currency. + // The transient planner is not an importer: it holds the pool's + // Ref itself, so it has both in hand and they provably + // name the same object — it reads them off one pointer, in one + // statement. Nothing is derived, so nothing can drift. + // + // This is what unblocks the copy sites whose OTHER operand is a + // transient (SSAO's blur output, SceneRenderPass's depth/normal/ + // velocity exports, GPUDrivenOcclusion's re-exports). Those were + // recorded as "blocked on the transient pool"; the pool was never + // the problem — this struct's contract was. + // + // Migration still proceeds PER RESOURCE for imports: a resource's + // creator -> import -> resolve -> bind chain moves together. The + // final slice deletes the native field once every chain has moved. u32 TextureID = 0; RHI::ResourceHandle Handle; bool IsHistory = false; + // "This entry can answer in the identity currency." True for a + // handle-import AND for a transient; false only for a native import + // whose chain has not migrated yet. [[nodiscard]] bool IsMigrated() const { return Handle.IsValid(); diff --git a/OloEngine/src/OloEngine/Renderer/RenderPipeline.cpp b/OloEngine/src/OloEngine/Renderer/RenderPipeline.cpp index 7c32fc89d..48cb4bb81 100644 --- a/OloEngine/src/OloEngine/Renderer/RenderPipeline.cpp +++ b/OloEngine/src/OloEngine/Renderer/RenderPipeline.cpp @@ -1266,7 +1266,7 @@ namespace OloEngine // have (re)created the texture this frame, so the id handed // to the pass must be read post-populate. PostProcessPasses.Cloudscape->SetHistory( - CloudsHistoryTexture ? CloudsHistoryTexture->GetRendererID() : 0u, + CloudsHistoryTexture ? CloudsHistoryTexture->GetRHIHandle() : RHI::NullResource, CloudsHistoryValid); PostProcessPasses.Cloudscape->UploadAndBindUBO(); diff --git a/OloEngine/tests/Rendering/rhi_boundary_baseline.json b/OloEngine/tests/Rendering/rhi_boundary_baseline.json index f8b2228c7..0cc61b0e1 100644 --- a/OloEngine/tests/Rendering/rhi_boundary_baseline.json +++ b/OloEngine/tests/Rendering/rhi_boundary_baseline.json @@ -293,7 +293,31 @@ "facade_native_id_params 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 instead of +2." + "bind, and netted -1 instead of +2.", + "", + "PHASE 2 STEP 3, SLICE 7 (2026-08-01): item 3's leftovers. 355 -> 347.", + "", + "All eight sites deferred by slice 5 as 'blocked on the transient pool' are", + "migrated. THE BLOCKER WAS NEVER REAL, and how it got recorded as fact is the", + "part worth keeping: TransientPool::AcquireTexture returns a Ref,", + "which has minted handles since slice 2. AcquiredInfo::RendererID is a", + "diagnostics field with no role in resolution. What actually stood in the way", + "was PhysicalTexture's documented invariant that TextureID and Handle are", + "'ALTERNATIVES, exactly one is set' -- correct for an IMPORT (an importer only", + "has one currency, and neither is derivable from the other), never true for a", + "TRANSIENT (the planner holds the pooled Ref, so it has both and reads them", + "off one pointer in one statement).", + "", + "So: 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. Not checking cost a slice of imagined work and put a false claim on the", + "issue tracker until it was corrected.", + "", + "Bonus: each of those sites guards its copy with `if (src != dst)`. Those now", + "compare OBJECTS, so a recycled driver name can no longer make a source and", + "its export look identical and skip a copy the frame needed -- the", + "InstanceGroupKey defect class, in four more places." ], "measured_on_commit": "11d7cea7", "measured_on_date": "2026-07-30", @@ -305,7 +329,7 @@ "sweep_glad_includes": 0, "tools_gl_calls": 236, "debug_escape_hatch": 0, - "sweep_renderer_id": 355, + "sweep_renderer_id": 347, "facade_native_id_params": 67, "backend_resolve_hatch": 0 } diff --git a/docs/agent-rules/rhi-abstraction-boundary.md b/docs/agent-rules/rhi-abstraction-boundary.md index 7f985a062..f55c32141 100644 --- a/docs/agent-rules/rhi-abstraction-boundary.md +++ b/docs/agent-rules/rhi-abstraction-boundary.md @@ -669,16 +669,45 @@ Migrated: all seven bakers (`ThumbnailCapture`, `LightProbeBaker`, `DDGIProbeUpdatePass`'s attachment reads including the `SetAtlasTextureParams` signature, and `RenderGraph`'s attachment clear + NaN-census readback. -Deliberately **not** migrated, each for a stated reason rather than for size: - -| Site | Why it cannot move yet | -| --- | --- | -| `SSAO` blur→AO copy, `SceneRenderPass`'s three exports, `GPUDrivenOcclusion`'s two | The *other* copy operand comes from `context.ResolveTexture` on a **transient**, and a graph transient has only a native id (the pool hands out `AcquiredInfo::RendererID`). Blocked on the transient-pool slice, not on facade surface. | -| `Cloudscape`'s raymarch source | Compared against a history id that is native (`m_HistoryTextureID` plus a `ResolveTexture`). The doc's own note — "`Cloudscape` needs `CloudNoise` migrated first" — is the same dependency. | -| `Renderer3DFrameExecution`'s HZB depth, `PlanarReflection`, `Water` | Feed `Renderer3D::` setters/`BuildCurrentOcclusionHZB`, i.e. worklist item 3. | -| `RenderGraph`'s three external-sink copies | The sink's `TextureID` is registered from outside the graph as a raw `u32`. | -| `RenderGraph`'s JSON topology dump | Reports native ids on purpose, for external tooling. | -| `Renderer/Debug/`'s two | Phase 8 relocation. | +Deferred at the time, each for a stated reason rather than for size — **and +five of the six were cleared by slices 6 and 7**, which is itself the lesson: + +| Site | Why it was deferred | Outcome | +| --- | --- | --- | +| `SSAO` blur→AO copy, `SceneRenderPass`'s three exports, `GPUDrivenOcclusion`'s two | "the other operand is a **transient**, and a transient has only a native id" | **WRONG — cleared in slice 7.** See below. | +| `Cloudscape`'s raymarch source + history | native history id + a transient resolve | **Cleared in slice 7** | +| `Renderer3DFrameExecution`'s HZB depth, `PlanarReflection`, `Water` | feed `Renderer3D::` setters | **Water + PlanarReflection cleared in slice 6** when those setters migrated; the HZB one remains | +| `RenderGraph`'s three external-sink copies | the sink's `TextureID` is registered from outside the graph as a raw `u32` | still open | +| `RenderGraph`'s JSON topology dump | reports native ids on purpose, for external tooling | stays native | +| `Renderer/Debug/`'s two | Phase 8 relocation | stays | + +### The "blocked on the transient pool" claim was wrong, and the shape of the error is worth keeping + +It was recorded as fact in the worklist AND posted to #691 before anyone +measured it. `TransientPool::AcquireTexture` returns a **`Ref`, which +has minted handles since slice 2**; `AcquiredInfo::RendererID` is a +diagnostics-report field with no role in resolution. The real constraint was one +line in the planner that simply never set `.Handle`, and behind it a **design +invariant, not a missing producer**: `PhysicalTexture` documented `TextureID` +and `Handle` as "ALTERNATIVES… exactly one is set per entry". + +That rule 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` itself, so it has both in hand and reads them +off one pointer in one statement. Nothing is derived, so nothing can drift. +Setting both is what 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*. A missing capability +is work. An invariant is a decision, and decisions can be revisited once you +know which case they were written for. Recording the blocker without checking +which kind it was cost this issue a whole slice of imagined work — and put a +false statement on the tracker. + +Bonus from doing it: every one of those sites guards its copy with +`if (src != dst)`. Those now compare OBJECTS, so a recycled driver name can no +longer make a source and its export look identical and skip a copy the frame +needed — the `InstanceGroupKey` defect class, in four more places. **Counters after slice 5:** `sweep_renderer_id` 699 → **653**; `facade_native_id_params` unchanged at 68, because that slice *adds* handle From e08913e7093c7d97398f359ff70cbd1bfed2c064 Mon Sep 17 00:00:00 2001 From: Ole Bueker Date: Sat, 1 Aug 2026 21:22:58 +0200 Subject: [PATCH 6/6] =?UTF-8?q?fix(rhi):=20address=20review=20findings=20o?= =?UTF-8?q?n=20#736=20=E2=80=94=20two=20were=20real=20defects=20(#691)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- OloEditor/src/MCP/McpToolsRender.cpp | 15 +++-- OloEngine/src/CMakeLists.txt | 5 +- .../src/OloEngine/Renderer/CloudShadowMap.h | 2 +- .../Renderer/Commands/RenderCommand.h | 31 ++++----- .../Renderer/DDGI/DDGIProbeUpdatePass.h | 2 +- .../Renderer/Debug/CommandPacketDebugger.cpp | 12 ++-- .../Renderer/Passes/ShadowRenderPass.h | 2 +- .../src/OloEngine/Renderer/RHI/RHIResources.h | 9 ++- .../Renderer/Renderer3DSpecializedDraws.cpp | 6 +- .../src/OloEngine/Renderer/SkyCubemapBake.cpp | 6 +- OloEngine/src/OloEngine/Renderer/Texture.h | 7 +- OloEngine/tests/Rendering/MockRendererAPI.h | 66 +++++++++++-------- .../Rendering/RenderGraphFingerprintTest.cpp | 8 +-- .../Rendering/rhi_boundary_baseline.json | 14 +++- docs/agent-rules/rhi-abstraction-boundary.md | 15 +++-- 15 files changed, 122 insertions(+), 78 deletions(-) diff --git a/OloEditor/src/MCP/McpToolsRender.cpp b/OloEditor/src/MCP/McpToolsRender.cpp index 2c0cdb39e..99f66c151 100644 --- a/OloEditor/src/MCP/McpToolsRender.cpp +++ b/OloEditor/src/MCP/McpToolsRender.cpp @@ -4132,12 +4132,17 @@ namespace OloEngine::MCP Json ResolvedMaterialJson(const Material& material, const PODMaterialData& data, u32 submeshIndex, std::string_view source) { + // NATIVE ids, matching this tool's published schema ("Bound GL + // texture id per slot ... 0 = none") and comparable with the ids + // olo_render_list_targets reports. The fields are identities since + // issue #691 step 3, so resolve rather than reformat — printing + // "#3:1" here would silently break every existing consumer. Json textures; - textures["albedo"] = fmt::format("{}", data.albedoMapID); - textures["metallicRoughness"] = fmt::format("{}", data.metallicRoughnessMapID); - textures["normal"] = fmt::format("{}", data.normalMapID); - textures["ao"] = fmt::format("{}", data.aoMapID); - textures["emissive"] = fmt::format("{}", data.emissiveMapID); + textures["albedo"] = Debug::NativeTextureIdForDiagnostics(data.albedoMapID); + textures["metallicRoughness"] = Debug::NativeTextureIdForDiagnostics(data.metallicRoughnessMapID); + textures["normal"] = Debug::NativeTextureIdForDiagnostics(data.normalMapID); + textures["ao"] = Debug::NativeTextureIdForDiagnostics(data.aoMapID); + textures["emissive"] = Debug::NativeTextureIdForDiagnostics(data.emissiveMapID); Json useMaps; useMaps["useAlbedoMap"] = data.albedoMapID.IsValid(); diff --git a/OloEngine/src/CMakeLists.txt b/OloEngine/src/CMakeLists.txt index b8e620f31..5bdfdaf39 100644 --- a/OloEngine/src/CMakeLists.txt +++ b/OloEngine/src/CMakeLists.txt @@ -595,8 +595,9 @@ # declaration-only: RHIResourceRegistry.{h,cpp} mints the generation-checked # RHI::ResourceHandle (defined in RHITypes.h) that the Platform/OpenGL # resource classes, RenderCommand and the render graph now carry. - # RHIResources.h is the exception — still forward-looking vocabulary that - # only RHIResourceRegistry.cpp includes so far. + # RHIResources.h is mostly still forward-looking vocabulary; its live part + # is GetNativeHandleForDebug, included by RHIResourceRegistry.cpp and by + # Renderer/Debug/RenderGraphResourceIdentity.cpp. # RHIBoundaryRatchetTest guards the no-backend-types boundary. "OloEngine/Renderer/RHI/RHITypes.h" "OloEngine/Renderer/RHI/RHIResources.h" diff --git a/OloEngine/src/OloEngine/Renderer/CloudShadowMap.h b/OloEngine/src/OloEngine/Renderer/CloudShadowMap.h index 096305bae..f5a559dd4 100644 --- a/OloEngine/src/OloEngine/Renderer/CloudShadowMap.h +++ b/OloEngine/src/OloEngine/Renderer/CloudShadowMap.h @@ -59,7 +59,7 @@ namespace OloEngine /// @return true after the first successful Update() dispatch. [[nodiscard]] static bool IsReady(); - /// @return GL renderer id of the R8 512² shadow map (0 when not ready). + /// @return Identity of the R8 512² shadow map; RHI::NullResource when not ready. [[nodiscard]] static RHI::ResourceHandle GetTextureHandle(); /// @return world-XZ center of the current map (texel-snapped). diff --git a/OloEngine/src/OloEngine/Renderer/Commands/RenderCommand.h b/OloEngine/src/OloEngine/Renderer/Commands/RenderCommand.h index a7dfc720a..29e8818c5 100644 --- a/OloEngine/src/OloEngine/Renderer/Commands/RenderCommand.h +++ b/OloEngine/src/OloEngine/Renderer/Commands/RenderCommand.h @@ -144,7 +144,8 @@ namespace OloEngine i32 alphaMode = 0; f32 alphaCutoff = 0.5f; - // PBR texture IDs (renderer IDs, 0 = none) + // PBR texture identities (an invalid handle means no map for that slot; + // test with .IsValid(), never against a literal 0) RHI::ResourceHandle albedoMapID{}; RHI::ResourceHandle metallicRoughnessMapID{}; RHI::ResourceHandle normalMapID{}; @@ -508,7 +509,7 @@ namespace OloEngine struct DrawIndexedCommand { CommandHeader header; - RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO identity (invalid = no VAO) u32 indexCount; RHI::IndexType indexType; }; @@ -516,7 +517,7 @@ namespace OloEngine struct DrawIndexedInstancedCommand { CommandHeader header; - RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO identity (invalid = no VAO) u32 indexCount; u32 instanceCount; RHI::IndexType indexType; @@ -525,7 +526,7 @@ namespace OloEngine struct DrawArraysCommand { CommandHeader header; - RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO identity (invalid = no VAO) u32 vertexCount; RHI::PrimitiveTopology primitiveType; }; @@ -533,7 +534,7 @@ namespace OloEngine struct DrawLinesCommand { CommandHeader header; - RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO identity (invalid = no VAO) u32 vertexCount; }; @@ -545,7 +546,7 @@ namespace OloEngine // Mesh data (POD identifiers) AssetHandle meshHandle; // Mesh asset handle for resolution - RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO identity (invalid = no VAO) u32 indexCount; u32 baseIndex = 0; // Starting index offset in shared index buffer (for multi-submesh MeshSources) glm::mat4 transform; @@ -598,7 +599,7 @@ namespace OloEngine // Mesh data (POD identifiers) AssetHandle meshHandle; // Mesh asset handle - RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO identity (invalid = no VAO) u32 indexCount; u32 baseIndex = 0; // Starting index offset in shared index buffer (for multi-submesh MeshSources) u32 instanceCount; @@ -655,12 +656,12 @@ namespace OloEngine { CommandHeader header; AssetHandle meshHandle; // Skybox mesh handle - RHI::ResourceHandle vertexArrayID{}; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO identity (invalid = no VAO) u32 indexCount; glm::mat4 transform; // Usually identity matrix AssetHandle shaderHandle; // Skybox shader handle (for asset tracking) - RHI::ResourceHandle shaderRendererID{}; // Shader program ID for glUseProgram - RHI::ResourceHandle skyboxTextureID{}; // Cubemap texture renderer ID + RHI::ResourceHandle shaderRendererID{}; // Shader program identity + RHI::ResourceHandle skyboxTextureID{}; // Cubemap texture identity u16 renderStateIndex = INVALID_RENDER_STATE_INDEX; // Render state index }; @@ -671,8 +672,8 @@ namespace OloEngine { CommandHeader header; AssetHandle shaderHandle; // Grid shader handle (for asset tracking) - RHI::ResourceHandle shaderRendererID{}; // Shader program ID for glUseProgram - RHI::ResourceHandle quadVAOID{}; // Fullscreen quad VAO renderer ID + RHI::ResourceHandle shaderRendererID{}; // Shader program identity + RHI::ResourceHandle quadVAOID{}; // Fullscreen quad VAO identity f32 gridScale; // Grid spacing scale factor u16 renderStateIndex = INVALID_RENDER_STATE_INDEX; // Render state index }; @@ -684,10 +685,10 @@ namespace OloEngine { CommandHeader header; glm::mat4 transform; - RHI::ResourceHandle textureID{}; // Texture renderer ID + RHI::ResourceHandle textureID{}; // Texture identity AssetHandle shaderHandle; // Shader asset handle (for asset tracking) - RHI::ResourceHandle shaderRendererID{}; // Shader program ID for glUseProgram - RHI::ResourceHandle quadVAID{}; // Quad vertex array renderer ID + RHI::ResourceHandle shaderRendererID{}; // Shader program identity + RHI::ResourceHandle quadVAID{}; // Quad vertex array identity u16 renderStateIndex = INVALID_RENDER_STATE_INDEX; // Render state index }; diff --git a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.h b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.h index 3e20e634d..ae73f7d66 100644 --- a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.h +++ b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.h @@ -44,7 +44,7 @@ namespace OloEngine glm::mat4 transform{ 1.0f }; // ABSOLUTE world BoundingBox worldBounds = NoBounds; glm::vec4 baseColor{ 1.0f }; // material base color factor - RHI::ResourceHandle albedoTextureID{}; // 0 = white + RHI::ResourceHandle albedoTextureID{}; // invalid = fall back to the white texture bool twoSided = false; }; diff --git a/OloEngine/src/OloEngine/Renderer/Debug/CommandPacketDebugger.cpp b/OloEngine/src/OloEngine/Renderer/Debug/CommandPacketDebugger.cpp index c292fca1b..5fd04b939 100644 --- a/OloEngine/src/OloEngine/Renderer/Debug/CommandPacketDebugger.cpp +++ b/OloEngine/src/OloEngine/Renderer/Debug/CommandPacketDebugger.cpp @@ -569,7 +569,7 @@ namespace OloEngine OLO_PROFILE_FUNCTION(); ImGui::Text("Mesh Handle: %llu", static_cast(cmd.meshHandle)); - ImGui::Text("VAO: %u", cmd.vertexArrayID); + ImGui::Text("VAO: %s", FormatHandle(cmd.vertexArrayID).c_str()); ImGui::Text("Index Count: %u", cmd.indexCount); ImGui::Text("Entity ID: %d", cmd.entityID); @@ -595,8 +595,12 @@ namespace OloEngine ImGui::Text(" Normal Scale: %.2f", matPtr->normalScale); ImGui::Text(" Occlusion: %.2f", matPtr->occlusionStrength); ImGui::Text(" IBL: %s (intensity=%.2f)", matPtr->enableIBL ? "Yes" : "No", matPtr->iblIntensity); - ImGui::Text(" Textures: albedo=%u, metallicRough=%u, normal=%u, ao=%u, emissive=%u", - matPtr->albedoMapID, matPtr->metallicRoughnessMapID, matPtr->normalMapID, matPtr->aoMapID, matPtr->emissiveMapID); + ImGui::Text(" Textures: albedo=%s, metallicRough=%s, normal=%s, ao=%s, emissive=%s", + FormatHandle(matPtr->albedoMapID).c_str(), + FormatHandle(matPtr->metallicRoughnessMapID).c_str(), + FormatHandle(matPtr->normalMapID).c_str(), + FormatHandle(matPtr->aoMapID).c_str(), + FormatHandle(matPtr->emissiveMapID).c_str()); } else if (matPtr) { @@ -622,7 +626,7 @@ namespace OloEngine void CommandPacketDebugger::RenderDrawMeshInstancedDetail(const DrawMeshInstancedCommand& cmd, const CapturedFrameData* frame) const { ImGui::Text("Mesh Handle: %llu", static_cast(cmd.meshHandle)); - ImGui::Text("VAO: %u", cmd.vertexArrayID); + ImGui::Text("VAO: %s", FormatHandle(cmd.vertexArrayID).c_str()); ImGui::Text("Index Count: %u", cmd.indexCount); ImGui::Text("Instance Count: %u", cmd.instanceCount); ImGui::Text("Transform Buffer: offset=%u, count=%u", cmd.transformBufferOffset, cmd.transformCount); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.h b/OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.h index 984ed4ac4..8477f715d 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.h +++ b/OloEngine/src/OloEngine/Renderer/Passes/ShadowRenderPass.h @@ -34,7 +34,7 @@ namespace OloEngine u32 indexCount = 0; u32 baseIndex = 0; // Offset (in u32 entries) into the IBO — non-zero for submeshes sharing a combined IBO glm::mat4 transform = glm::mat4(1.0f); - RHI::ResourceHandle shadowVaoID{}; // Position-merged shadow IB; 0 = use vaoID + RHI::ResourceHandle shadowVaoID{}; // Position-merged shadow IB; invalid = use vaoID BoundingBox WorldBounds = NoBounds; // World-space AABB; NoBounds = always include // Material is MaterialFlag::TwoSided — rendered into the shadow map with culling DISABLED // instead of the default front-face cull, so single-sided planar geometry (a quad, a diff --git a/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h b/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h index fe224ff96..b636c9fb8 100644 --- a/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h +++ b/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h @@ -6,9 +6,12 @@ // // Issue #691 Phase 1, ADR 0011 (docs/adr/0011-rhi-neutral-resource-and-binding-model.md). // -// **Not yet load-bearing: only RHIResourceRegistry.cpp includes these so far.** -// (Phase 2 step 3 minted RHI::ResourceHandle, but that lives in RHITypes.h — the -// resource *descriptions* below are still waiting on their consumers.) Same two +// **Partly live.** GetNativeHandleForDebug at the bottom is in use — by +// RHIResourceRegistry.cpp, which defines it, and by +// Renderer/Debug/RenderGraphResourceIdentity.cpp, which is the sanctioned +// caller for the introspection tools. The resource *descriptions* below are +// still forward-looking (Phase 2 step 3 minted RHI::ResourceHandle, but that +// lives in RHITypes.h). Same two // rules as RHITypes.h: no backend headers, no backend types, and these are engine // enums that a backend converts explicitly. // diff --git a/OloEngine/src/OloEngine/Renderer/Renderer3DSpecializedDraws.cpp b/OloEngine/src/OloEngine/Renderer/Renderer3DSpecializedDraws.cpp index 4fabd6441..220cf417b 100644 --- a/OloEngine/src/OloEngine/Renderer/Renderer3DSpecializedDraws.cpp +++ b/OloEngine/src/OloEngine/Renderer/Renderer3DSpecializedDraws.cpp @@ -157,7 +157,7 @@ namespace OloEngine // rendered after opaque geometry; in Deferred they write into the // G-Buffer pre-lighting so they are opaque from the sorter's POV. PacketMetadata metadata = packet->GetMetadata(); - const u32 shaderID = decalShader->GetRendererID() & 0xFFFF; + const u32 shaderID = cmd->shaderRendererID.Index & 0xFFFF; const u32 depth = ComputeDepthForSortKey(decalTransform); metadata.m_SortKey = deferredPath ? DrawKey::CreateOpaque(0, ViewLayerType::ThreeD, shaderID, 0, depth) @@ -280,7 +280,7 @@ namespace OloEngine // Sort key: opaque, sorted by shader then depth (front-to-back). PacketMetadata metadata = packet->GetMetadata(); - const u32 shaderID = activeShader->GetRendererID() & 0xFFFF; + const u32 shaderID = cmd->shaderRendererID.Index & 0xFFFF; const u32 depth = ComputeDepthForSortKey(modelTransform); metadata.m_SortKey = DrawKey::CreateOpaque(0, ViewLayerType::ThreeD, shaderID, 0, depth); metadata.m_IsStatic = false; @@ -400,7 +400,7 @@ namespace OloEngine // Sort key: translucent, sorted back-to-front for correct blending. PacketMetadata metadata = packet->GetMetadata(); - const u32 shaderID = s_Data.WaterShader->GetRendererID() & 0xFFFF; + const u32 shaderID = cmd->shaderRendererID.Index & 0xFFFF; const u32 depth = ComputeDepthForSortKey(modelTransform); metadata.m_SortKey = DrawKey::CreateTransparent(0, ViewLayerType::ThreeD, shaderID, 0, depth); metadata.m_IsStatic = false; diff --git a/OloEngine/src/OloEngine/Renderer/SkyCubemapBake.cpp b/OloEngine/src/OloEngine/Renderer/SkyCubemapBake.cpp index 01d413164..1b9233bb4 100644 --- a/OloEngine/src/OloEngine/Renderer/SkyCubemapBake.cpp +++ b/OloEngine/src/OloEngine/Renderer/SkyCubemapBake.cpp @@ -87,6 +87,11 @@ namespace OloEngine::SkyBake return false; } + // Hoisted out of the face loop: the framebuffer is not recreated between + // faces, so its attachment identity is constant for the whole bake + // (matches ReflectionProbeBaker::CaptureSceneCubemap). + const RHI::ResourceHandle fbColor = framebuffer->GetColorAttachmentHandle(0); + for (u32 i = 0; i < 6; ++i) { OLO_PROFILE_SCOPE("SkyBake::Face"); @@ -112,7 +117,6 @@ namespace OloEngine::SkyBake vao->Bind(); RenderCommand::DrawIndexed(vao); - const RHI::ResourceHandle fbColor = framebuffer->GetColorAttachmentHandle(0); RenderCommand::CopyImageSubDataFull( fbColor, RendererAPI::TextureTargetType::Texture2D, 0, 0, cubemap->GetRHIHandle(), RendererAPI::TextureTargetType::TextureCubeMap, 0, static_cast(i), diff --git a/OloEngine/src/OloEngine/Renderer/Texture.h b/OloEngine/src/OloEngine/Renderer/Texture.h index 00766779f..32331d909 100644 --- a/OloEngine/src/OloEngine/Renderer/Texture.h +++ b/OloEngine/src/OloEngine/Renderer/Texture.h @@ -110,9 +110,14 @@ namespace OloEngine */ virtual bool GetData(std::vector& outData, u32 mipLevel = 0) const = 0; + // Compares IDENTITIES, not driver names (issue #691 step 3). GL recycles + // object names, so a name comparison could report two genuinely different + // textures as equal once one had been destroyed — the defect the + // generation exists to make unrepresentable. A handle carries one, so + // two distinct objects can never compare equal here. bool operator==(const Texture& other) const { - return GetRendererID() == other.GetRendererID(); + return GetRHIHandle() == other.GetRHIHandle(); } // Asset interface diff --git a/OloEngine/tests/Rendering/MockRendererAPI.h b/OloEngine/tests/Rendering/MockRendererAPI.h index 09a6a57ea..54be9e419 100644 --- a/OloEngine/tests/Rendering/MockRendererAPI.h +++ b/OloEngine/tests/Rendering/MockRendererAPI.h @@ -377,32 +377,32 @@ namespace OloEngine::Testing // ---------------------------------------------------------------- void BindTexture(u32 slot, RHI::ResourceHandle texture) override { - BindTexture(slot, Native(texture)); + BindTexture(slot, Native(texture, RHI::ResourceKind::Texture)); } void BindImageTexture(u32 unit, RHI::ResourceHandle texture, u32 mipLevel, bool layered, u32 layer, RHI::Access access, RHI::Format format) override { - BindImageTexture(unit, Native(texture), mipLevel, layered, layer, access, format); + BindImageTexture(unit, Native(texture, RHI::ResourceKind::Texture), mipLevel, layered, layer, access, format); } void BindUniformBuffer(u32 bindingPoint, RHI::ResourceHandle buffer) override { - BindUniformBuffer(bindingPoint, Native(buffer)); + BindUniformBuffer(bindingPoint, Native(buffer, RHI::ResourceKind::Buffer)); } void BindStorageBuffer(u32 bindingPoint, RHI::ResourceHandle buffer) override { - BindStorageBuffer(bindingPoint, Native(buffer)); + BindStorageBuffer(bindingPoint, Native(buffer, RHI::ResourceKind::Buffer)); } void BindShaderProgram(RHI::ResourceHandle program) override { - BindShaderProgram(Native(program)); + BindShaderProgram(Native(program, RHI::ResourceKind::ShaderProgram)); } void BindVertexArrayRaw(RHI::ResourceHandle vertexArray) override { - BindVertexArrayRaw(Native(vertexArray)); + BindVertexArrayRaw(Native(vertexArray, RHI::ResourceKind::VertexArray)); } void BindFramebuffer(RHI::ResourceHandle framebuffer) override { - BindFramebuffer(Native(framebuffer)); + BindFramebuffer(Native(framebuffer, RHI::ResourceKind::Framebuffer)); } // Raw-creator siblings (slice 4). The mock plays a backend: it creates @@ -439,22 +439,22 @@ namespace OloEngine::Testing } void DeleteTexture(RHI::ResourceHandle texture) override { - DeleteTexture(Native(texture)); + DeleteTexture(Native(texture, RHI::ResourceKind::Texture)); RHI::ResourceRegistry::Get().Unregister(texture); } void DeleteFramebuffer(RHI::ResourceHandle framebuffer) override { - DeleteFramebuffer(Native(framebuffer)); + DeleteFramebuffer(Native(framebuffer, RHI::ResourceKind::Framebuffer)); RHI::ResourceRegistry::Get().Unregister(framebuffer); } void DeleteBuffer(RHI::ResourceHandle buffer) override { - DeleteBuffer(Native(buffer)); + DeleteBuffer(Native(buffer, RHI::ResourceKind::Buffer)); RHI::ResourceRegistry::Get().Unregister(buffer); } void DeleteVertexArray(RHI::ResourceHandle vertexArray) override { - DeleteVertexArray(Native(vertexArray)); + DeleteVertexArray(Native(vertexArray, RHI::ResourceKind::VertexArray)); RHI::ResourceRegistry::Get().Unregister(vertexArray); } @@ -469,16 +469,16 @@ namespace OloEngine::Testing // currencies has the registry to check instead. void SetTextureFilter(RHI::ResourceHandle texture, RHI::Filter minFilter, RHI::Filter magFilter) override { - SetTextureFilter(Native(texture), minFilter, magFilter); + SetTextureFilter(Native(texture, RHI::ResourceKind::Texture), minFilter, magFilter); } void SetTextureWrap(RHI::ResourceHandle texture, RHI::AddressMode wrap) override { - SetTextureWrap(Native(texture), wrap); + SetTextureWrap(Native(texture, RHI::ResourceKind::Texture), wrap); } void UploadTextureSubImage2D(RHI::ResourceHandle texture, u32 width, u32 height, RHI::Format sourceFormat, const void* data) override { - UploadTextureSubImage2D(Native(texture), width, height, sourceFormat, data); + UploadTextureSubImage2D(Native(texture, RHI::ResourceKind::Texture), width, height, sourceFormat, data); } // Copy / clear / upload-at-offset / readback handle forms (slice 5). @@ -487,7 +487,8 @@ namespace OloEngine::Testing RHI::ResourceHandle dst, TextureTargetType dstTarget, u32 width, u32 height) override { - CopyImageSubData(Native(src), srcTarget, Native(dst), dstTarget, width, height); + CopyImageSubData(Native(src, RHI::ResourceKind::Texture), srcTarget, + Native(dst, RHI::ResourceKind::Texture), dstTarget, width, height); } [[nodiscard("Store this!")]] bool ReadTextureSubImage(RHI::ResourceHandle texture, u32 mipLevel, i32 x, i32 y, i32 z, @@ -495,51 +496,51 @@ namespace OloEngine::Testing RHI::Format destFormat, sizet destSizeBytes, void* dest) override { - return ReadTextureSubImage(Native(texture), mipLevel, x, y, z, width, height, depth, + return ReadTextureSubImage(Native(texture, RHI::ResourceKind::Texture), mipLevel, x, y, z, width, height, depth, destFormat, destSizeBytes, dest); } void CopyImageSubDataFull(RHI::ResourceHandle src, TextureTargetType srcTarget, i32 srcLevel, i32 srcZ, RHI::ResourceHandle dst, TextureTargetType dstTarget, i32 dstLevel, i32 dstZ, u32 width, u32 height) override { - CopyImageSubDataFull(Native(src), srcTarget, srcLevel, srcZ, - Native(dst), dstTarget, dstLevel, dstZ, width, height); + CopyImageSubDataFull(Native(src, RHI::ResourceKind::Texture), srcTarget, srcLevel, srcZ, + Native(dst, RHI::ResourceKind::Texture), dstTarget, dstLevel, dstZ, width, height); } void ClearTextureFloat(RHI::ResourceHandle texture, u32 mipLevel, const glm::vec4& color) override { - ClearTextureFloat(Native(texture), mipLevel, color); + ClearTextureFloat(Native(texture, RHI::ResourceKind::Texture), mipLevel, color); } [[nodiscard("Store this!")]] bool ReadTextureImage(RHI::ResourceHandle texture, u32 mipLevel, RHI::Format destFormat, sizet destSizeBytes, void* dest) override { - return ReadTextureImage(Native(texture), mipLevel, destFormat, destSizeBytes, dest); + return ReadTextureImage(Native(texture, RHI::ResourceKind::Texture), mipLevel, destFormat, destSizeBytes, dest); } void DrawIndexedPatchesRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 patchVertices) override { - DrawIndexedPatchesRaw(Native(vertexArray), indexCount, patchVertices); + DrawIndexedPatchesRaw(Native(vertexArray, RHI::ResourceKind::VertexArray), indexCount, patchVertices); } void DrawIndexedInstancedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 baseIndex, u32 instanceCount) override { - DrawIndexedInstancedRaw(Native(vertexArray), indexCount, baseIndex, instanceCount); + DrawIndexedInstancedRaw(Native(vertexArray, RHI::ResourceKind::VertexArray), indexCount, baseIndex, instanceCount); } void DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount) override { - DrawIndexedRaw(Native(vertexArray), indexCount); + DrawIndexedRaw(Native(vertexArray, RHI::ResourceKind::VertexArray), indexCount); } void DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 baseIndex) override { - DrawIndexedRaw(Native(vertexArray), indexCount, baseIndex); + DrawIndexedRaw(Native(vertexArray, RHI::ResourceKind::VertexArray), indexCount, baseIndex); } void SetProgramUniformFloat(RHI::ResourceHandle program, std::string_view name, f32 value) override { - SetProgramUniformFloat(Native(program), name, value); + SetProgramUniformFloat(Native(program, RHI::ResourceKind::ShaderProgram), name, value); } [[nodiscard]] RHI::ResourceHandle CreateDepthArrayCompareOffViewHandle(RHI::ResourceHandle srcTexture, u32 numLayers) override { - const u32 nativeView = CreateDepthArrayCompareOffView(Native(srcTexture), numLayers); + const u32 nativeView = CreateDepthArrayCompareOffView(Native(srcTexture, RHI::ResourceKind::Texture), numLayers); if (nativeView == 0u) return RHI::NullResource; return RHI::ResourceRegistry::Get().Register(RHI::ResourceKind::Texture, nativeView, @@ -547,9 +548,16 @@ namespace OloEngine::Testing } private: - [[nodiscard]] static u32 Native(RHI::ResourceHandle handle) noexcept - { - return static_cast(RHI::ResourceRegistry::Get().ResolveNativeForBackend(handle)); + // Kind-checked, mirroring Platform/OpenGL's Utils::ResolveNativeAs. An + // untyped resolve would let a wrong-family handle (a buffer passed where + // a texture is wanted) succeed here and fail in the real backend, so a + // green mock test would say nothing about the shipping path. + [[nodiscard]] static u32 Native(RHI::ResourceHandle handle, RHI::ResourceKind expected) noexcept + { + auto& registry = RHI::ResourceRegistry::Get(); + if (handle.IsValid() && registry.KindOf(handle) != expected) + return 0u; + return static_cast(registry.ResolveNativeForBackend(handle)); } public: diff --git a/OloEngine/tests/Rendering/RenderGraphFingerprintTest.cpp b/OloEngine/tests/Rendering/RenderGraphFingerprintTest.cpp index 2800443c7..689cf933c 100644 --- a/OloEngine/tests/Rendering/RenderGraphFingerprintTest.cpp +++ b/OloEngine/tests/Rendering/RenderGraphFingerprintTest.cpp @@ -235,25 +235,25 @@ namespace OloEngine::Tests Access::ImportedIBL changed = base; changed.Irradiance = TestHandle(99u); EXPECT_NE(Access::FingerprintWithIBL(changed), baseFp) - << "GlobalIrradianceMapID is imported by raw GL ID — a change must repopulate the blackboard."; + << "GlobalIrradianceMapID is hashed by IDENTITY — a change must repopulate the blackboard."; } { Access::ImportedIBL changed = base; changed.Prefilter = TestHandle(99u); EXPECT_NE(Access::FingerprintWithIBL(changed), baseFp) - << "GlobalPrefilterMapID is imported by raw GL ID — a change must repopulate the blackboard."; + << "GlobalPrefilterMapID is hashed by IDENTITY — a change must repopulate the blackboard."; } { Access::ImportedIBL changed = base; changed.BRDFLut = TestHandle(99u); EXPECT_NE(Access::FingerprintWithIBL(changed), baseFp) - << "GlobalBRDFLutMapID is imported by raw GL ID — a change must repopulate the blackboard."; + << "GlobalBRDFLutMapID is hashed by IDENTITY — a change must repopulate the blackboard."; } { Access::ImportedIBL changed = base; changed.Environment = TestHandle(99u); EXPECT_NE(Access::FingerprintWithIBL(changed), baseFp) - << "GlobalEnvironmentMapID is imported by raw GL ID — a change must repopulate the blackboard."; + << "GlobalEnvironmentMapID is hashed by IDENTITY — a change must repopulate the blackboard."; } // And the teardown case: a scene with NO environment map clears the IDs to 0. That is diff --git a/OloEngine/tests/Rendering/rhi_boundary_baseline.json b/OloEngine/tests/Rendering/rhi_boundary_baseline.json index 0cc61b0e1..58a9e9ac3 100644 --- a/OloEngine/tests/Rendering/rhi_boundary_baseline.json +++ b/OloEngine/tests/Rendering/rhi_boundary_baseline.json @@ -317,7 +317,17 @@ "Bonus: each of those sites guards its copy with `if (src != dst)`. Those now", "compare OBJECTS, so a recycled driver name can no longer make a source and", "its export look identical and skip a copy the frame needed -- the", - "InstanceGroupKey defect class, in four more places." + "InstanceGroupKey defect class, in four more places.", + "", + "REVIEW PASS (2026-08-01): 347 -> 345, and the split is a live demonstration", + "of the UNDERCOUNT warning above. Two fixes landed: Texture::operator== moved", + "off GetRendererID (-2, the whole delta), and three sort keys in", + "Renderer3DSpecializedDraws.cpp moved from cmd->shader->GetRendererID() to", + "cmd->shaderRendererID.Index -- which nets ZERO, because the counter matches", + "`RendererID` in any spelling and the field is still named shaderRendererID.", + "The sort-key fix is the more consequential of the two (it stops sorting", + "identity-keyed draws by a driver name the command no longer carries), and the", + "proxy cannot see it at all. Do not read a flat counter as a flat slice." ], "measured_on_commit": "11d7cea7", "measured_on_date": "2026-07-30", @@ -329,7 +339,7 @@ "sweep_glad_includes": 0, "tools_gl_calls": 236, "debug_escape_hatch": 0, - "sweep_renderer_id": 347, + "sweep_renderer_id": 345, "facade_native_id_params": 67, "backend_resolve_hatch": 0 } diff --git a/docs/agent-rules/rhi-abstraction-boundary.md b/docs/agent-rules/rhi-abstraction-boundary.md index f55c32141..2747775d8 100644 --- a/docs/agent-rules/rhi-abstraction-boundary.md +++ b/docs/agent-rules/rhi-abstraction-boundary.md @@ -340,12 +340,15 @@ backend-private native handle reachable only through a deliberately conspicuous Two details worth keeping: -- **The generation is load-bearing.** GL recycles object names, so today two - genuinely different objects can compare equal through `Texture::operator==` - (which compares `GetRendererID()`) when one was deleted and another created. - `TransientPool`'s alias reporting — the tooling built in #607 specifically to - answer "did these two plan entries get the same object?" — depends on telling - those apart. +- **The generation is load-bearing.** GL recycles object names, so two genuinely + different objects *could* compare equal through `Texture::operator==` when one + was deleted and another created. `TransientPool`'s alias reporting — the + tooling built in #607 specifically to answer "did these two plan entries get + the same object?" — depends on telling those apart. Step 3 closed this: the + operator compares `GetRHIHandle()`, so the generation now makes the collision + unrepresentable rather than merely unlikely. Note the fix had to be made in + the *operator*; minting handles everywhere did not fix it on its own, because + a comparison keeps reading whatever currency it names. - **`HeapOffset` must stay layout-compatible with `u32`.** It gets written into a UBO and read by GLSL as an array index; it cannot be opaque.