diff --git a/OloEditor/SandboxProject/AssetRegistry.oar b/OloEditor/SandboxProject/AssetRegistry.oar index 716a7da1a..fda67943b 100644 Binary files a/OloEditor/SandboxProject/AssetRegistry.oar and b/OloEditor/SandboxProject/AssetRegistry.oar differ diff --git a/OloEditor/assets/tests/visual/WorldOriginRebase_far_before.png b/OloEditor/assets/tests/visual/WorldOriginRebase_far_before.png index 95f3b4e07..f575f2648 100644 Binary files a/OloEditor/assets/tests/visual/WorldOriginRebase_far_before.png and b/OloEditor/assets/tests/visual/WorldOriginRebase_far_before.png differ diff --git a/OloEditor/src/MCP/McpToolsRender.cpp b/OloEditor/src/MCP/McpToolsRender.cpp index 5605da308..99f66c151 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/Debug/RenderGraphResourceIdentity.h" #include "OloEngine/Renderer/TransientPool.h" #include "OloEngine/Renderer/Renderer2D.h" #include "OloEngine/Renderer/Renderer3D.h" @@ -414,7 +415,7 @@ namespace OloEngine::MCP // frame's (transients can re-alias next frame). if (resource.TextureHandle.IsValid()) { - info.GLTextureId = graph->ResolveTexture(resource.TextureHandle); + info.GLTextureId = Debug::NativeTextureIdForDiagnostics(*graph, resource.TextureHandle); info.ViewOfParentLayer = graph->GetTextureViewLayerIndex(resource.Name); } if (resource.FramebufferHandle.IsValid()) @@ -474,6 +475,16 @@ namespace OloEngine::MCP if (const u32 textureId = Renderer3D::ResolveFrameGraphTexture(name); textureId != 0) return textureId; + // 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) + { + return nativeId; + } + const Ref framebuffer = Renderer3D::ResolveFrameGraphFramebuffer(name); if (!framebuffer) return 0; @@ -3696,7 +3707,7 @@ namespace OloEngine::MCP RenderValidate::ResourceIdentity identity; identity.Name = resource.Name; if (resource.TextureHandle.IsValid()) - identity.GLTextureId = graph->ResolveTexture(resource.TextureHandle); + identity.GLTextureId = Debug::NativeTextureIdForDiagnostics(*graph, resource.TextureHandle); else if (resource.FramebufferHandle.IsValid()) identity.GLTextureId = ResolveTargetTexture(resource.Name); if (resource.BufferHandle.IsValid()) @@ -4121,19 +4132,24 @@ 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"] = data.albedoMapID; - textures["metallicRoughness"] = data.metallicRoughnessMapID; - textures["normal"] = data.normalMapID; - textures["ao"] = data.aoMapID; - textures["emissive"] = 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 != 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; @@ -4254,7 +4270,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/CMakeLists.txt b/OloEngine/src/CMakeLists.txt index 79c01da7c..5bdfdaf39 100644 --- a/OloEngine/src/CMakeLists.txt +++ b/OloEngine/src/CMakeLists.txt @@ -591,10 +591,14 @@ "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 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" "OloEngine/Renderer/RHI/RHIResourceRegistry.h" @@ -724,6 +728,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/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..f5a559dd4 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 @@ -58,8 +59,8 @@ 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). - [[nodiscard]] static u32 GetTextureID(); + /// @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). [[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..29e8818c5 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; @@ -138,16 +144,17 @@ namespace OloEngine i32 alphaMode = 0; 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; + // 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{}; + 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 +509,7 @@ namespace OloEngine struct DrawIndexedCommand { CommandHeader header; - RendererID vertexArrayID; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO identity (invalid = no VAO) u32 indexCount; RHI::IndexType indexType; }; @@ -510,7 +517,7 @@ namespace OloEngine struct DrawIndexedInstancedCommand { CommandHeader header; - RendererID vertexArrayID; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO identity (invalid = no VAO) u32 indexCount; u32 instanceCount; RHI::IndexType indexType; @@ -519,7 +526,7 @@ namespace OloEngine struct DrawArraysCommand { CommandHeader header; - RendererID vertexArrayID; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO identity (invalid = no VAO) u32 vertexCount; RHI::PrimitiveTopology primitiveType; }; @@ -527,7 +534,7 @@ namespace OloEngine struct DrawLinesCommand { CommandHeader header; - RendererID vertexArrayID; // VAO renderer ID + RHI::ResourceHandle vertexArrayID{}; // VAO identity (invalid = no VAO) u32 vertexCount; }; @@ -538,8 +545,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 identity (invalid = no VAO) u32 indexCount; u32 baseIndex = 0; // Starting index offset in shared index buffer (for multi-submesh MeshSources) glm::mat4 transform; @@ -591,8 +598,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 identity (invalid = no VAO) u32 indexCount; u32 baseIndex = 0; // Starting index offset in shared index buffer (for multi-submesh MeshSources) u32 instanceCount; @@ -648,13 +655,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 identity (invalid = no VAO) 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 identity + RHI::ResourceHandle skyboxTextureID{}; // Cubemap texture identity u16 renderStateIndex = INVALID_RENDER_STATE_INDEX; // Render state index }; @@ -665,8 +672,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 identity + RHI::ResourceHandle quadVAOID{}; // Fullscreen quad VAO identity f32 gridScale; // Grid spacing scale factor u16 renderStateIndex = INVALID_RENDER_STATE_INDEX; // Render state index }; @@ -678,10 +685,10 @@ namespace OloEngine { CommandHeader header; glm::mat4 transform; - RendererID textureID; // Texture renderer ID + RHI::ResourceHandle textureID{}; // Texture identity 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 identity + RHI::ResourceHandle quadVAID{}; // Quad vertex array identity u16 renderStateIndex = INVALID_RENDER_STATE_INDEX; // Render state index }; @@ -694,20 +701,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 +735,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 +762,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 +776,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 +813,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 +828,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 +852,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 +880,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 +910,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 e1ab88388..821f5ab92 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 @@ -661,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; } @@ -670,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); @@ -697,8 +727,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 +742,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 +842,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 +856,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,20 +889,22 @@ 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 // 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 @@ -939,9 +971,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..ae73f7d66 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{}; // invalid = fall back to the white texture bool twoSided = false; }; @@ -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/Debug/CommandPacketDebugger.cpp b/OloEngine/src/OloEngine/Renderer/Debug/CommandPacketDebugger.cpp index b8927f189..5fd04b939 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"; @@ -560,12 +569,14 @@ 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); 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(); @@ -584,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) { @@ -611,13 +626,15 @@ 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); 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 +1475,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 +1501,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/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/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/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/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/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/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/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..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(); @@ -344,7 +349,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/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/DecalRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/DecalRenderPass.cpp index 0878ee908..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 @@ -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/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/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/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/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/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/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/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/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..8477f715d 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; 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 @@ -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/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..b636c9fb8 100644 --- a/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h +++ b/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h @@ -6,9 +6,14 @@ // // 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. +// **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. // // 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..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). @@ -334,6 +355,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 +373,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) { @@ -377,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); @@ -710,6 +757,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 +789,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 +806,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..826b855c2 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; @@ -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 534bd371d..48cb4bb81 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 @@ -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(); @@ -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))); @@ -1565,16 +1571,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); @@ -2959,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 ce3954332..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; } @@ -814,6 +840,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 @@ -863,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); @@ -992,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; } @@ -1128,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, @@ -1143,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); @@ -1156,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; @@ -1168,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, @@ -1206,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 @@ -1223,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, @@ -1431,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 @@ -1757,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..220cf417b 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 @@ -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) @@ -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; @@ -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; @@ -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)); @@ -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/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 0ead5bf66..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 @@ -163,10 +175,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; @@ -185,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. @@ -351,6 +383,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 +402,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. @@ -421,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/Renderer/SkyCubemapBake.cpp b/OloEngine/src/OloEngine/Renderer/SkyCubemapBake.cpp index 36f1c927f..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,10 +117,9 @@ namespace OloEngine::SkyBake vao->Bind(); RenderCommand::DrawIndexed(vao); - const u32 fbColor = framebuffer->GetColorAttachmentRendererID(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/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/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/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 157c9a204..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); @@ -1992,4 +1993,103 @@ 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); + } + + 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, + 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..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; @@ -84,15 +89,23 @@ 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; 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; @@ -173,6 +186,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 +198,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; @@ -213,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 04d7bd78f..54be9e419 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*/, @@ -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,22 +469,95 @@ 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); } - private: - [[nodiscard]] static u32 Native(RHI::ResourceHandle handle) noexcept + // 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, 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, + u32 width, u32 height, u32 depth, + RHI::Format destFormat, + sizet destSizeBytes, void* dest) override + { + 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, 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, 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, RHI::ResourceKind::Texture), mipLevel, destFormat, destSizeBytes, dest); + } + void DrawIndexedPatchesRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 patchVertices) override + { + DrawIndexedPatchesRaw(Native(vertexArray, RHI::ResourceKind::VertexArray), indexCount, patchVertices); + } + void DrawIndexedInstancedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 baseIndex, + u32 instanceCount) override { - return static_cast(RHI::ResourceRegistry::Get().ResolveNativeForBackend(handle)); + DrawIndexedInstancedRaw(Native(vertexArray, RHI::ResourceKind::VertexArray), indexCount, baseIndex, instanceCount); + } + void DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount) override + { + DrawIndexedRaw(Native(vertexArray, RHI::ResourceKind::VertexArray), indexCount); + } + void DrawIndexedRaw(RHI::ResourceHandle vertexArray, u32 indexCount, u32 baseIndex) override + { + DrawIndexedRaw(Native(vertexArray, RHI::ResourceKind::VertexArray), indexCount, baseIndex); + } + void SetProgramUniformFloat(RHI::ResourceHandle program, std::string_view name, f32 value) override + { + SetProgramUniformFloat(Native(program, RHI::ResourceKind::ShaderProgram), name, value); + } + [[nodiscard]] RHI::ResourceHandle CreateDepthArrayCompareOffViewHandle(RHI::ResourceHandle srcTexture, + u32 numLayers) override + { + 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, + RHI::Backend::OpenGL); + } + + private: + // 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/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/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/RenderGraphFingerprintTest.cpp b/OloEngine/tests/Rendering/RenderGraphFingerprintTest.cpp index 6ac220bb6..689cf933c 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,33 +227,33 @@ 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."; + << "GlobalIrradianceMapID is hashed by IDENTITY — 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."; + << "GlobalPrefilterMapID is hashed by IDENTITY — 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."; + << "GlobalBRDFLutMapID is hashed by IDENTITY — 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."; + << "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/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/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 e2bac281e..58a9e9ac3 100644 --- a/OloEngine/tests/Rendering/rhi_boundary_baseline.json +++ b/OloEngine/tests/Rendering/rhi_boundary_baseline.json @@ -205,19 +205,141 @@ " 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.", + "", + "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.", + "", + "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.", + "", + "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", - "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, - "facade_native_id_params": 68, + "sweep_renderer_id": 345, + "facade_native_id_params": 67, "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..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. @@ -461,6 +464,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 @@ -505,30 +534,65 @@ 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): - -| 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). | - -By spelling: `m_RendererID` 439 (mostly backend-internal, exempt), -`GetRendererID` 325 (**the real target** — consumers), `shaderRendererID` 110, -attachment getters 72. +(`\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 6*. The raw total went 1196 across 118 files → 1150 +(slice 5) → **848 across 104** (slice 6): + +| 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` 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: -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. -2. **The command-layer bind cache** — one indivisible unit, see below. +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**~~ — **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 @@ -543,6 +607,156 @@ 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 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 +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. + +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 +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 + +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 @@ -573,12 +787,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