diff --git a/CLAUDE.md b/CLAUDE.md index 99fe132ab..157e8d5ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ Read before doing anything non-trivial; do not duplicate their content here: - [docs/agent-rules/gl-clear-program-revalidation.md](docs/agent-rules/gl-clear-program-revalidation.md) — NVIDIA JIT-recompiles the *bound* program's vertex shader at `glClear` against the newly bound FBO (debug id 131218); wrap any new clear site in `Utils::GLClearProgramGuard` (unbind + **restore** — a bare unbind blacked out every bind-once-draw-per-face bake loop), plus how to debug per-program driver warnings (program-id→shader-name labels, synchronous-callback stack capture). - [docs/agent-rules/cluster-lod-simplification.md](docs/agent-rules/cluster-lod-simplification.md) — `VirtualMeshBuilder` / meshoptimizer: a TERMINAL group's boundary lock must outlive the level that created it (the per-level lock pass stops seeing it once its clusters leave `pending`, and only the COARSE cuts crack); `meshopt_SimplifyPermissive` and the old position-weld cure the same #651 many-attribute-wedges stall but trade cook time against UV-seam fidelity; `meshopt_simplifyWithUpdate` is unusable while one vertex array serves every LOD level; `simplifySloppy` needs a manifold + border-edge guard; and how to A/B a builder change (the cook is NOT reachable from a plain editor run). - [docs/agent-rules/render-pipeline-caches.md](docs/agent-rules/render-pipeline-caches.md) — process-wide render caches (blackboard fingerprint, etc.) must invalidate on every topology reset, not just on a fingerprint change; hash `RenderGraph::GetTopologyGeneration()` into per-frame cache keys (issue #530). -- [docs/agent-rules/rhi-abstraction-boundary.md](docs/agent-rules/rhi-abstraction-boundary.md) — where the OpenGL boundary *actually* leaks (issue #691): a `glXxx(` grep is wrong three different ways (comments, log strings, and `glfw*` counted as GL — 549 real calls, not the 620/724 previously published); the **include** graph is the provable boundary and is far worse than the call count (40 files include all of GL while calling none of it, purely to name the `GLenum`s in `RendererAPI`'s own virtuals) — and because `RendererAPI.h` includes `glad/gl.h`, deleting a per-file include proves nothing until that header is clean; `Renderer/Debug/` is 43% of the calls and must *relocate*, not be exempted, or Phase 7 is unverifiable under the rendering-verification rule; `ResourceTransition` is neutral only by accident (its `RGWriteUsage → RGReadUsage` pair cannot express write→write, and silently defaults to `ShaderSample`); shader hot-reload invalidates 1 GL program but N `VkPipeline`s; backend selection is startup-time with an already-load-bearing ordering contract (`s_API` before `Window::Create`); and how to write a ratchet test that cannot silently pass. +- [docs/agent-rules/rhi-abstraction-boundary.md](docs/agent-rules/rhi-abstraction-boundary.md) — where the OpenGL boundary *actually* leaks (issue #691): a `glXxx(` grep is wrong three different ways (comments, log strings, and `glfw*` counted as GL — 549 real calls, not the 620/724 previously published); the **include** graph is the provable boundary and is far worse than the call count (40 files include all of GL while calling none of it, purely to name the `GLenum`s in `RendererAPI`'s own virtuals) — and because `RendererAPI.h` includes `glad/gl.h`, deleting a per-file include proves nothing until that header is clean; `Renderer/Debug/` is 43% of the calls and must *relocate*, not be exempted, or Phase 7 is unverifiable under the rendering-verification rule; `ResourceTransition` is neutral only by accident (its `RGWriteUsage → RGReadUsage` pair cannot express write→write, and silently defaults to `ShaderSample`); shader hot-reload invalidates 1 GL program but N `VkPipeline`s; backend selection is startup-time with an already-load-bearing ordering contract (`s_API` before `Window::Create`); and how to write a ratchet test that cannot silently pass. **Phase 2 step 2 (both counters now 0) added three more:** the facade was not just GL-typed but *incomplete* — 84 distinct entry points at the call sites, ~60% with no `RendererAPI` equivalent, so the sweep needed ~60 **new** virtuals (measure distinct *operations*, not call count, before scoping a sweep); a `Platform//` include leaks exactly as much as `glad/gl.h` and this scan cannot see it either (three passes pulled `OpenGLUtilities.h` in for `GLClearProgramGuard` — the fix was moving the guard **into** the backend clears, not deleting the include); and an earlier phase's declaration-only header must be read for the **vocabulary you are about to invent**, not just the types you consume (the sweep nearly shipped a second, worse spelling of `RHI::MemoryResidency`, caught only by a name collision that fires in the *test* build alone). - [docs/agent-rules/two-phase-occlusion-culling.md](docs/agent-rules/two-phase-occlusion-culling.md) — phase 1 must test the PREVIOUS frame's FINAL pyramid (a mid-frame one holds only the occluders drawn before it, which is why VG could never occlude VG); `BuildCurrentOcclusionHZB` overwrites the retained pyramid **in place**, so pass order decides who still sees previous-frame depth; phase 2 needs its own command/args region or the second MDI re-issues every phase-1 draw; and a 1.24%-of-pixels A/B diff that was software-raster depth-tie noise, not an over-cull — with the one-switch check that proved it (issue #682). - [docs/agent-rules/render-graph-transient-aliasing.md](docs/agent-rules/render-graph-transient-aliasing.md) — `WriteNewVersion` is a RENAME of the same physical resource (`m_VersionAliasTargets` resolution alias + planner lifetime folding, never an allocation); the stale-pool-read bug archetype (LIFO reuse hides it in steady state, plan rebuilds surface one-frame garbage, hit rate grows with pool age, per-pass toggle bisection can NOT find it) and the permanent hunt instruments `OLO_RG_POISON_TRANSIENTS` (per-resource hue poison — turns the stochastic artifact into a deterministic one-screenshot signal) / `OLO_RG_DISABLE_ALIASING`. - [docs/agent-rules/per-frame-scratch-reuse.md](docs/agent-rules/per-frame-scratch-reuse.md) — a per-tick hot-path local (`std::vector` scratch built inside `AnimationGraph::Update`/`AnimationStateMachine::Update`) should become persistent per-instance state instead, but only after checking three things: every writer fully overwrites the buffer (or `.clear()`s it first), the owning instance isn't shared across entities, and the call isn't running on an unsynchronized `.Parallelizable()` path (issue #445). diff --git a/OloEngine/src/OloEngine/Animation/MorphTargets/MorphTargetEvaluator.cpp b/OloEngine/src/OloEngine/Animation/MorphTargets/MorphTargetEvaluator.cpp index c38407115..bda447200 100644 --- a/OloEngine/src/OloEngine/Animation/MorphTargets/MorphTargetEvaluator.cpp +++ b/OloEngine/src/OloEngine/Animation/MorphTargets/MorphTargetEvaluator.cpp @@ -1,8 +1,9 @@ #include "OloEnginePCH.h" #include "MorphTargetEvaluator.h" #include "OloEngine/Core/Log.h" +#include "OloEngine/Renderer/MemoryBarrierFlags.h" +#include "OloEngine/Renderer/RenderCommand.h" -#include #include #include @@ -93,23 +94,23 @@ namespace OloEngine // binding 2: Weights (readonly SSBO) // binding 3: OutputVerts (writeonly SSBO) - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, baseVertexSSBO); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, morphDeltaSSBO); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, weightsSSBO); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, outputVertexSSBO); + RenderCommand::BindStorageBuffer(0, baseVertexSSBO); + RenderCommand::BindStorageBuffer(1, morphDeltaSSBO); + RenderCommand::BindStorageBuffer(2, weightsSSBO); + RenderCommand::BindStorageBuffer(3, outputVertexSSBO); // Dispatch compute shader with enough work groups to cover all vertices const u32 workGroupSize = 256; const u32 numGroups = (vertexCount + workGroupSize - 1) / workGroupSize; - glDispatchCompute(numGroups, 1, 1); + RenderCommand::DispatchCompute(numGroups, 1, 1); // Memory barrier to ensure compute shader writes are visible - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + RenderCommand::MemoryBarrier(MemoryBarrierFlags::ShaderStorage); // Unbind SSBOs - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, 0); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, 0); + RenderCommand::BindStorageBuffer(0, 0); + RenderCommand::BindStorageBuffer(1, 0); + RenderCommand::BindStorageBuffer(2, 0); + RenderCommand::BindStorageBuffer(3, 0); } } // namespace OloEngine diff --git a/OloEngine/src/OloEngine/Precipitation/PrecipitationSystem.cpp b/OloEngine/src/OloEngine/Precipitation/PrecipitationSystem.cpp index 9258f7efa..2117c1d2d 100644 --- a/OloEngine/src/OloEngine/Precipitation/PrecipitationSystem.cpp +++ b/OloEngine/src/OloEngine/Precipitation/PrecipitationSystem.cpp @@ -12,8 +12,6 @@ #include "OloEngine/Renderer/ShaderBindingLayout.h" #include "OloEngine/Snow/SnowAccumulationSystem.h" -#include - #include #include #include @@ -198,7 +196,7 @@ namespace OloEngine } // Create GPU timer queries for performance monitoring - glGenQueries(2, s_Data.m_TimerQueries); + RenderCommand::CreateQueries(RHI::QueryType::TimeElapsed, s_Data.m_TimerQueries); s_Data.m_CurrentIntensity = 0.0f; s_Data.m_TargetIntensity = 0.0f; @@ -221,7 +219,7 @@ namespace OloEngine if (s_Data.m_TimerQueries[0] != 0) { - glDeleteQueries(2, s_Data.m_TimerQueries); + RenderCommand::DeleteQueries(s_Data.m_TimerQueries); s_Data.m_TimerQueries[0] = 0; s_Data.m_TimerQueries[1] = 0; } @@ -307,7 +305,7 @@ namespace OloEngine // --- Begin GPU timer --- u32 queryIdx = s_Data.m_CurrentTimerQuery; - glBeginQuery(GL_TIME_ELAPSED, s_Data.m_TimerQueries[queryIdx]); + RenderCommand::BeginQuery(RHI::QueryType::TimeElapsed, s_Data.m_TimerQueries[queryIdx]); // 1. Intensity interpolation s_Data.m_LastBaseEmissionRate = settings.BaseEmissionRate; @@ -404,18 +402,15 @@ namespace OloEngine } // --- End GPU timer --- - glEndQuery(GL_TIME_ELAPSED); + RenderCommand::EndQuery(RHI::QueryType::TimeElapsed); // Read back previous frame's timer result (double-buffered to avoid stalls) u32 prevQueryIdx = 1 - queryIdx; if (s_Data.m_TimerQueryActive) { - GLint available = GL_FALSE; - glGetQueryObjectiv(s_Data.m_TimerQueries[prevQueryIdx], GL_QUERY_RESULT_AVAILABLE, &available); - if (available == GL_TRUE) + if (RenderCommand::IsQueryResultAvailable(s_Data.m_TimerQueries[prevQueryIdx])) { - GLuint64 elapsedNs = 0; - glGetQueryObjectui64v(s_Data.m_TimerQueries[prevQueryIdx], GL_QUERY_RESULT, &elapsedNs); + const u64 elapsedNs = RenderCommand::GetQueryResultU64(s_Data.m_TimerQueries[prevQueryIdx]); s_Data.m_LastFrameTimeMs = static_cast(elapsedNs) / 1000000.0f; } // If not available yet, keep the previous value — avoids CPU stall diff --git a/OloEngine/src/OloEngine/Renderer/Commands/CommandDispatch.cpp b/OloEngine/src/OloEngine/Renderer/Commands/CommandDispatch.cpp index 8330353cf..23042b2bc 100644 --- a/OloEngine/src/OloEngine/Renderer/Commands/CommandDispatch.cpp +++ b/OloEngine/src/OloEngine/Renderer/Commands/CommandDispatch.cpp @@ -23,7 +23,6 @@ #include "OloEngine/Renderer/Occlusion/OcclusionQueryPool.h" #include "OloEngine/Asset/AssetManager.h" -#include #include #include @@ -164,7 +163,7 @@ namespace OloEngine } // Conditionally bind a UBO only when the binding point has changed, - // avoiding redundant glBindBufferBase calls each draw. + // avoiding a redundant binding-point update each draw. static void BindUBOIfNeeded(u32 bindingPoint, u32 rendererID) { if (bindingPoint < CommandDispatchData::MAX_TRACKED_UBO_BINDINGS) @@ -173,15 +172,18 @@ namespace OloEngine return; s_Data.BoundUBOIDs[bindingPoint] = rendererID; } - glBindBufferBase(GL_UNIFORM_BUFFER, bindingPoint, rendererID); + RenderCommand::BindUniformBuffer(bindingPoint, rendererID); } // 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) { if (s_Data.CurrentBoundVAO != vaoID) { - glBindVertexArray(vaoID); + RenderCommand::BindVertexArrayRaw(vaoID); s_Data.CurrentBoundVAO = vaoID; } } @@ -404,12 +406,18 @@ namespace OloEngine // Skips entirely when materialDataIndex matches the last-used index. // Helper: Conditionally bind a texture only when the slot isn't already // bound to the same ID, updating tracking and stats. - static void BindTrackedTexture(RendererID textureID, u32 slot, GLenum target) + // + // The texture TARGET parameter is gone. It existed because this used the + // legacy glActiveTexture + glBindTexture(target, id) pair, which needs to be + // told which target of the unit to touch; the facade's BindTexture is the + // 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) { if (textureID != 0 && s_Data.BoundTextureIDs[slot] != textureID) { - glActiveTexture(GL_TEXTURE0 + slot); - glBindTexture(target, textureID); + RenderCommand::BindTexture(slot, textureID); s_Data.BoundTextureIDs[slot] = textureID; ++s_Data.Stats.TextureBinds; } @@ -419,22 +427,22 @@ namespace OloEngine // AO, emissive, environment cubemap, irradiance, prefilter, BRDF LUT). static void BindPBRTextures(const PODMaterialData& mat) { - BindTrackedTexture(mat.albedoMapID, ShaderBindingLayout::TEX_DIFFUSE, GL_TEXTURE_2D); - BindTrackedTexture(mat.metallicRoughnessMapID, ShaderBindingLayout::TEX_SPECULAR, GL_TEXTURE_2D); - BindTrackedTexture(mat.normalMapID, ShaderBindingLayout::TEX_NORMAL, GL_TEXTURE_2D); - BindTrackedTexture(mat.aoMapID, ShaderBindingLayout::TEX_AMBIENT, GL_TEXTURE_2D); - BindTrackedTexture(mat.emissiveMapID, ShaderBindingLayout::TEX_EMISSIVE, GL_TEXTURE_2D); - BindTrackedTexture(mat.environmentMapID, ShaderBindingLayout::TEX_ENVIRONMENT, GL_TEXTURE_CUBE_MAP); - BindTrackedTexture(mat.irradianceMapID, ShaderBindingLayout::TEX_USER_0, GL_TEXTURE_CUBE_MAP); - BindTrackedTexture(mat.prefilterMapID, ShaderBindingLayout::TEX_USER_1, GL_TEXTURE_CUBE_MAP); - BindTrackedTexture(mat.brdfLutMapID, ShaderBindingLayout::TEX_USER_2, GL_TEXTURE_2D); + BindTrackedTexture(mat.albedoMapID, ShaderBindingLayout::TEX_DIFFUSE); + BindTrackedTexture(mat.metallicRoughnessMapID, ShaderBindingLayout::TEX_SPECULAR); + BindTrackedTexture(mat.normalMapID, ShaderBindingLayout::TEX_NORMAL); + BindTrackedTexture(mat.aoMapID, ShaderBindingLayout::TEX_AMBIENT); + BindTrackedTexture(mat.emissiveMapID, ShaderBindingLayout::TEX_EMISSIVE); + BindTrackedTexture(mat.environmentMapID, ShaderBindingLayout::TEX_ENVIRONMENT); + BindTrackedTexture(mat.irradianceMapID, ShaderBindingLayout::TEX_USER_0); + BindTrackedTexture(mat.prefilterMapID, ShaderBindingLayout::TEX_USER_1); + BindTrackedTexture(mat.brdfLutMapID, ShaderBindingLayout::TEX_USER_2); } // Helper: Bind legacy material textures (diffuse, specular). static void BindLegacyTextures(const PODMaterialData& mat) { - BindTrackedTexture(mat.diffuseMapID, ShaderBindingLayout::TEX_DIFFUSE, GL_TEXTURE_2D); - BindTrackedTexture(mat.specularMapID, ShaderBindingLayout::TEX_SPECULAR, GL_TEXTURE_2D); + BindTrackedTexture(mat.diffuseMapID, ShaderBindingLayout::TEX_DIFFUSE); + BindTrackedTexture(mat.specularMapID, ShaderBindingLayout::TEX_SPECULAR); } static void UploadMaterialState(const PODMaterialData& mat, u16 materialDataIndex) @@ -545,7 +553,7 @@ namespace OloEngine return; if (s_Data.BoundTextureIDs[slot] != textureID) { - glBindTextureUnit(slot, textureID); + RenderCommand::BindTexture(slot, textureID); s_Data.BoundTextureIDs[slot] = textureID; ++s_Data.Stats.TextureBinds; } @@ -1235,47 +1243,7 @@ namespace OloEngine } } - // Local lowering for the two POD draw-command fields that are now RHI enums - // (Commands/RenderCommand.h). These three glDraw* sites are still raw GL — - // they are part of the Phase 2 step-2 call-site sweep, not step 1 — so the - // lowering lives here rather than pulling Platform/OpenGL/ into this file. - // Delete both helpers together with the raw calls when the sweep reaches - // this dispatcher. - static GLenum ToGLIndexType(RHI::IndexType type) - { - switch (type) - { - case RHI::IndexType::UInt16: - return GL_UNSIGNED_SHORT; - case RHI::IndexType::UInt32: - return GL_UNSIGNED_INT; - } - OLO_CORE_ERROR("CommandDispatch: unhandled RHI::IndexType {}", static_cast(type)); - return GL_UNSIGNED_INT; - } - - static GLenum ToGLPrimitive(RHI::PrimitiveTopology topology) - { - switch (topology) - { - case RHI::PrimitiveTopology::TriangleList: - return GL_TRIANGLES; - case RHI::PrimitiveTopology::TriangleStrip: - return GL_TRIANGLE_STRIP; - case RHI::PrimitiveTopology::LineList: - return GL_LINES; - case RHI::PrimitiveTopology::LineStrip: - return GL_LINE_STRIP; - case RHI::PrimitiveTopology::PointList: - return GL_POINTS; - case RHI::PrimitiveTopology::PatchList: - return GL_PATCHES; - } - OLO_CORE_ERROR("CommandDispatch: unhandled RHI::PrimitiveTopology {}", static_cast(topology)); - return GL_TRIANGLES; - } - - void CommandDispatch::DrawIndexed(const void* data, [[maybe_unused]] RendererAPI& api) + void CommandDispatch::DrawIndexed(const void* data, RendererAPI& api) { auto const* cmd = static_cast(data); @@ -1287,10 +1255,10 @@ namespace OloEngine // Bind VAO (cached) and draw BindVAOIfNeeded(cmd->vertexArrayID); - glDrawElements(GL_TRIANGLES, static_cast(cmd->indexCount), ToGLIndexType(cmd->indexType), nullptr); + api.DrawBoundIndexed(RHI::PrimitiveTopology::TriangleList, cmd->indexCount, cmd->indexType, 0); } - void CommandDispatch::DrawIndexedInstanced(const void* data, [[maybe_unused]] RendererAPI& api) + void CommandDispatch::DrawIndexedInstanced(const void* data, RendererAPI& api) { auto const* cmd = static_cast(data); @@ -1302,11 +1270,11 @@ namespace OloEngine // Bind VAO (cached) and draw instanced BindVAOIfNeeded(cmd->vertexArrayID); - glDrawElementsInstanced(GL_TRIANGLES, static_cast(cmd->indexCount), ToGLIndexType(cmd->indexType), - nullptr, static_cast(cmd->instanceCount)); + api.DrawBoundIndexedInstanced(RHI::PrimitiveTopology::TriangleList, cmd->indexCount, cmd->indexType, + 0, cmd->instanceCount); } - void CommandDispatch::DrawArrays(const void* data, [[maybe_unused]] RendererAPI& api) + void CommandDispatch::DrawArrays(const void* data, RendererAPI& api) { auto const* cmd = static_cast(data); @@ -1318,10 +1286,10 @@ namespace OloEngine // Bind VAO (cached) and draw arrays BindVAOIfNeeded(cmd->vertexArrayID); - glDrawArrays(ToGLPrimitive(cmd->primitiveType), 0, static_cast(cmd->vertexCount)); + api.DrawBoundArrays(cmd->primitiveType, 0, cmd->vertexCount); } - void CommandDispatch::DrawLines(const void* data, [[maybe_unused]] RendererAPI& api) + void CommandDispatch::DrawLines(const void* data, RendererAPI& api) { auto const* cmd = static_cast(data); @@ -1333,7 +1301,7 @@ namespace OloEngine // Bind VAO (cached) and draw lines BindVAOIfNeeded(cmd->vertexArrayID); - glDrawArrays(GL_LINES, 0, static_cast(cmd->vertexCount)); + api.DrawBoundArrays(RHI::PrimitiveTopology::LineList, 0, cmd->vertexCount); } void CommandDispatch::DrawMesh(const void* data, RendererAPI& api) @@ -1391,7 +1359,7 @@ namespace OloEngine } if (s_Data.CurrentBoundShaderID != shaderToBind) { - glUseProgram(shaderToBind); + api.BindShaderProgram(shaderToBind); s_Data.CurrentBoundShaderID = shaderToBind; ++s_Data.Stats.ShaderBinds; } @@ -1502,10 +1470,11 @@ namespace OloEngine } } - // Use baseIndex offset for multi-submesh MeshSources sharing a single IBO - const void* indexOffset = reinterpret_cast(static_cast(cmd->baseIndex) * sizeof(u32)); - - glDrawElements(GL_TRIANGLES, static_cast(cmd->indexCount), GL_UNSIGNED_INT, indexOffset); + // baseIndex offsets into a single IBO shared by a multi-submesh + // MeshSource. The index-count-to-byte-offset conversion now lives in + // the backend, which is the only layer that knows the index stride. + api.DrawBoundIndexed(RHI::PrimitiveTopology::TriangleList, cmd->indexCount, + RHI::IndexType::UInt32, cmd->baseIndex); ++s_Data.Stats.DrawCalls; if (startedConditionalRender) @@ -1569,7 +1538,7 @@ namespace OloEngine } if (s_Data.CurrentBoundShaderID != shaderToBind) { - glUseProgram(shaderToBind); + api.BindShaderProgram(shaderToBind); s_Data.CurrentBoundShaderID = shaderToBind; ++s_Data.Stats.ShaderBinds; } @@ -1599,8 +1568,7 @@ namespace OloEngine // Rebind slot 15 to the per-submission output buffer. The engine- // wide `s_Data.ModelInstanceBuffer` is unchanged so it can be // reused by subsequent CPU-path draws in the same frame. - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ShaderBindingLayout::SSBO_INSTANCE_DATA, - cmd->cullOutputInstanceBufferID); + api.BindStorageBuffer(ShaderBindingLayout::SSBO_INSTANCE_DATA, cmd->cullOutputInstanceBufferID); // Shadow/snow textures (per-frame, outside material diffing). // Depth-only prepass draws never sample shadows. @@ -1742,8 +1710,8 @@ namespace OloEngine // Bind VAO (cached) and draw instanced BindVAOIfNeeded(cmd->vertexArrayID); ++s_Data.Stats.DrawCalls; - const void* indexOffset = reinterpret_cast(static_cast(cmd->baseIndex) * sizeof(u32)); - glDrawElementsInstanced(GL_TRIANGLES, static_cast(cmd->indexCount), GL_UNSIGNED_INT, indexOffset, static_cast(instanceCount)); + api.DrawBoundIndexedInstanced(RHI::PrimitiveTopology::TriangleList, cmd->indexCount, + RHI::IndexType::UInt32, cmd->baseIndex, instanceCount); // RendererProfiler: surface the batching savings. One instanced draw // covers `instanceCount` entities; `InstancesBatched` reports the @@ -1805,7 +1773,7 @@ namespace OloEngine // Bind skybox shader using renderer ID directly if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) { - glUseProgram(cmd->shaderRendererID); + api.BindShaderProgram(cmd->shaderRendererID); s_Data.CurrentBoundShaderID = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } @@ -1819,15 +1787,14 @@ namespace OloEngine // Bind skybox cubemap texture using renderer ID directly if (s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_ENVIRONMENT] != cmd->skyboxTextureID) { - glActiveTexture(GL_TEXTURE0 + ShaderBindingLayout::TEX_ENVIRONMENT); - glBindTexture(GL_TEXTURE_CUBE_MAP, cmd->skyboxTextureID); + api.BindTexture(ShaderBindingLayout::TEX_ENVIRONMENT, cmd->skyboxTextureID); s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_ENVIRONMENT] = cmd->skyboxTextureID; ++s_Data.Stats.TextureBinds; } // Bind VAO (cached) and draw BindVAOIfNeeded(cmd->vertexArrayID); - glDrawElements(GL_TRIANGLES, static_cast(cmd->indexCount), GL_UNSIGNED_INT, nullptr); + api.DrawBoundIndexed(RHI::PrimitiveTopology::TriangleList, cmd->indexCount, RHI::IndexType::UInt32, 0); // Update statistics ++s_Data.Stats.DrawCalls; @@ -1858,7 +1825,7 @@ namespace OloEngine // Bind shader using renderer ID directly if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) { - glUseProgram(cmd->shaderRendererID); + api.BindShaderProgram(cmd->shaderRendererID); s_Data.CurrentBoundShaderID = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } @@ -1881,8 +1848,7 @@ namespace OloEngine // Bind texture using renderer ID directly if (s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_DIFFUSE] != cmd->textureID) { - glActiveTexture(GL_TEXTURE0 + ShaderBindingLayout::TEX_DIFFUSE); - glBindTexture(GL_TEXTURE_2D, cmd->textureID); + api.BindTexture(ShaderBindingLayout::TEX_DIFFUSE, cmd->textureID); s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_DIFFUSE] = cmd->textureID; ++s_Data.Stats.TextureBinds; } @@ -1890,7 +1856,7 @@ namespace OloEngine // Bind VAO (cached) and draw quad BindVAOIfNeeded(cmd->quadVAID); ++s_Data.Stats.DrawCalls; - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); + api.DrawBoundIndexed(RHI::PrimitiveTopology::TriangleList, 6, RHI::IndexType::UInt32, 0); } void CommandDispatch::DrawInfiniteGrid(const void* data, RendererAPI& api) @@ -1912,7 +1878,7 @@ namespace OloEngine // Bind grid shader using renderer ID directly if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) { - glUseProgram(cmd->shaderRendererID); + api.BindShaderProgram(cmd->shaderRendererID); s_Data.CurrentBoundShaderID = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } @@ -1924,15 +1890,18 @@ namespace OloEngine BindUBOIfNeeded(ShaderBindingLayout::UBO_CAMERA, s_Data.CameraUBO->GetRendererID()); } - // Set grid scale uniform if the shader supports it - if (GLint gridScaleLoc = glGetUniformLocation(cmd->shaderRendererID, "u_GridScale"); gridScaleLoc != -1) - { - glUniform1f(gridScaleLoc, cmd->gridScale); - } + // Set grid scale uniform if the shader supports it. + // + // This is the one facade call with no faithful Vulkan lowering — SPIR-V + // has push constants and UBO members, not a name-queryable default + // uniform block. Phase 6 folds u_GridScale into a UBO and deletes + // SetProgramUniformFloat (ADR 0011 amendment (9)); the debt is recorded + // rather than hidden so Phase 7 bring-up is not surprised by it. + api.SetProgramUniformFloat(cmd->shaderRendererID, "u_GridScale", cmd->gridScale); // Bind fullscreen quad VAO (cached) and draw BindVAOIfNeeded(cmd->quadVAOID); - glDrawArrays(GL_TRIANGLES, 0, 6); + api.DrawBoundArrays(RHI::PrimitiveTopology::TriangleList, 0, 6); ++s_Data.Stats.DrawCalls; } @@ -1955,7 +1924,7 @@ namespace OloEngine // Bind shader if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) { - glUseProgram(cmd->shaderRendererID); + api.BindShaderProgram(cmd->shaderRendererID); s_Data.CurrentBoundShaderID = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } @@ -1983,33 +1952,33 @@ namespace OloEngine if (auto terrainUBO = Renderer3D::GetTerrainUBO(); terrainUBO) { terrainUBO->SetData(&cmd->terrainUBOData, ShaderBindingLayout::TerrainUBO::GetSize()); - glBindBufferBase(GL_UNIFORM_BUFFER, ShaderBindingLayout::UBO_TERRAIN, terrainUBO->GetRendererID()); + api.BindUniformBuffer(ShaderBindingLayout::UBO_TERRAIN, terrainUBO->GetRendererID()); } // Bind terrain textures if (cmd->heightmapTextureID != 0) { - glBindTextureUnit(ShaderBindingLayout::TEX_TERRAIN_HEIGHTMAP, cmd->heightmapTextureID); + api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_HEIGHTMAP, cmd->heightmapTextureID); } if (cmd->splatmapTextureID != 0) { - glBindTextureUnit(ShaderBindingLayout::TEX_TERRAIN_SPLATMAP, cmd->splatmapTextureID); + api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_SPLATMAP, cmd->splatmapTextureID); } if (cmd->splatmap1TextureID != 0) { - glBindTextureUnit(ShaderBindingLayout::TEX_TERRAIN_SPLATMAP_1, cmd->splatmap1TextureID); + api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_SPLATMAP_1, cmd->splatmap1TextureID); } if (cmd->albedoArrayTextureID != 0) { - glBindTextureUnit(ShaderBindingLayout::TEX_TERRAIN_ALBEDO_ARRAY, cmd->albedoArrayTextureID); + api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_ALBEDO_ARRAY, cmd->albedoArrayTextureID); } if (cmd->normalArrayTextureID != 0) { - glBindTextureUnit(ShaderBindingLayout::TEX_TERRAIN_NORMAL_ARRAY, cmd->normalArrayTextureID); + api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_NORMAL_ARRAY, cmd->normalArrayTextureID); } if (cmd->armArrayTextureID != 0) { - glBindTextureUnit(ShaderBindingLayout::TEX_TERRAIN_ARM_ARRAY, cmd->armArrayTextureID); + api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_ARM_ARRAY, cmd->armArrayTextureID); } // Bind the full shadow contract the terrain shaders sample — CSM, spot, @@ -2022,8 +1991,8 @@ namespace OloEngine // Bind VAO (cached) and draw with GL_PATCHES BindVAOIfNeeded(cmd->vertexArrayID); - glPatchParameteri(GL_PATCH_VERTICES, static_cast(cmd->patchVertexCount)); - glDrawElements(GL_PATCHES, static_cast(cmd->indexCount), GL_UNSIGNED_INT, nullptr); + api.SetPatchVertexCount(cmd->patchVertexCount); + api.DrawBoundIndexed(RHI::PrimitiveTopology::PatchList, cmd->indexCount, RHI::IndexType::UInt32, 0); ++s_Data.Stats.DrawCalls; } @@ -2045,7 +2014,7 @@ namespace OloEngine // Bind shader if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) { - glUseProgram(cmd->shaderRendererID); + api.BindShaderProgram(cmd->shaderRendererID); s_Data.CurrentBoundShaderID = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } @@ -2072,15 +2041,15 @@ namespace OloEngine // Bind textures for triplanar sampling if (cmd->albedoArrayTextureID != 0) { - glBindTextureUnit(ShaderBindingLayout::TEX_TERRAIN_ALBEDO_ARRAY, cmd->albedoArrayTextureID); + api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_ALBEDO_ARRAY, cmd->albedoArrayTextureID); } if (cmd->normalArrayTextureID != 0) { - glBindTextureUnit(ShaderBindingLayout::TEX_TERRAIN_NORMAL_ARRAY, cmd->normalArrayTextureID); + api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_NORMAL_ARRAY, cmd->normalArrayTextureID); } if (cmd->armArrayTextureID != 0) { - glBindTextureUnit(ShaderBindingLayout::TEX_TERRAIN_ARM_ARRAY, cmd->armArrayTextureID); + api.BindTexture(ShaderBindingLayout::TEX_TERRAIN_ARM_ARRAY, cmd->armArrayTextureID); } // Bind the shadow contract Terrain_Voxel.glsl samples (CSM + spot + PCSS @@ -2090,7 +2059,7 @@ namespace OloEngine // Bind VAO (cached) and draw BindVAOIfNeeded(cmd->vertexArrayID); - glDrawElements(GL_TRIANGLES, static_cast(cmd->indexCount), GL_UNSIGNED_INT, nullptr); + api.DrawBoundIndexed(RHI::PrimitiveTopology::TriangleList, cmd->indexCount, RHI::IndexType::UInt32, 0); ++s_Data.Stats.DrawCalls; } @@ -2120,7 +2089,7 @@ namespace OloEngine : cmd->shaderRendererID; if (s_Data.CurrentBoundShaderID != decalProgramID) { - glUseProgram(decalProgramID); + api.BindShaderProgram(decalProgramID); s_Data.CurrentBoundShaderID = decalProgramID; ++s_Data.Stats.ShaderBinds; } @@ -2151,7 +2120,7 @@ namespace OloEngine decalData.DecalColor = cmd->decalColor; decalData.DecalParams = cmd->decalParams; decalUBO->SetData(&decalData, ShaderBindingLayout::DecalUBO::GetSize()); - glBindBufferBase(GL_UNIFORM_BUFFER, ShaderBindingLayout::UBO_DECAL, decalUBO->GetRendererID()); + api.BindUniformBuffer(ShaderBindingLayout::UBO_DECAL, decalUBO->GetRendererID()); } // Bind albedo texture (with redundancy check) @@ -2159,7 +2128,7 @@ namespace OloEngine { if (s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_USER_0] != cmd->albedoTextureID) { - glBindTextureUnit(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.Stats.TextureBinds; } @@ -2171,21 +2140,21 @@ namespace OloEngine if (cmd->normalTextureID != 0 && s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_USER_1] != cmd->normalTextureID) { - glBindTextureUnit(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.Stats.TextureBinds; } if (cmd->rmaTextureID != 0 && s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_USER_2] != cmd->rmaTextureID) { - glBindTextureUnit(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.Stats.TextureBinds; } // Bind VAO (cached) and draw decal cube BindVAOIfNeeded(cmd->vertexArrayID); - glDrawElements(GL_TRIANGLES, static_cast(cmd->indexCount), GL_UNSIGNED_INT, nullptr); + api.DrawBoundIndexed(RHI::PrimitiveTopology::TriangleList, cmd->indexCount, RHI::IndexType::UInt32, 0); ++s_Data.Stats.DrawCalls; } @@ -2208,7 +2177,7 @@ namespace OloEngine // Bind shader (cached) if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) { - glUseProgram(cmd->shaderRendererID); + api.BindShaderProgram(cmd->shaderRendererID); s_Data.CurrentBoundShaderID = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } @@ -2241,7 +2210,7 @@ namespace OloEngine foliageData.ImpostorParams0 = glm::vec4(cmd->impostorFramesPerAxis, cmd->impostorHemi, cmd->impostorStartDistance, cmd->impostorBand); foliageData.ImpostorParams1 = glm::vec4(cmd->impostorEnabled, cmd->impostorRadius, cmd->impostorParallaxScale, 0.0f); foliageUBO->SetData(&foliageData, ShaderBindingLayout::FoliageUBO::GetSize()); - glBindBufferBase(GL_UNIFORM_BUFFER, ShaderBindingLayout::UBO_FOLIAGE, foliageUBO->GetRendererID()); + api.BindUniformBuffer(ShaderBindingLayout::UBO_FOLIAGE, foliageUBO->GetRendererID()); } // Bind albedo texture (with redundancy check). On the impostor path this @@ -2250,7 +2219,7 @@ namespace OloEngine { if (s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_DIFFUSE] != cmd->albedoTextureID) { - glBindTextureUnit(ShaderBindingLayout::TEX_DIFFUSE, cmd->albedoTextureID); + api.BindTexture(ShaderBindingLayout::TEX_DIFFUSE, cmd->albedoTextureID); s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_DIFFUSE] = cmd->albedoTextureID; ++s_Data.Stats.TextureBinds; } @@ -2262,7 +2231,8 @@ namespace OloEngine // Bind VAO (cached) and draw instanced foliage BindVAOIfNeeded(cmd->vertexArrayID); - glDrawElementsInstanced(GL_TRIANGLES, static_cast(cmd->indexCount), GL_UNSIGNED_INT, nullptr, static_cast(cmd->instanceCount)); + api.DrawBoundIndexedInstanced(RHI::PrimitiveTopology::TriangleList, cmd->indexCount, + RHI::IndexType::UInt32, 0, cmd->instanceCount); ++s_Data.Stats.DrawCalls; } void CommandDispatch::DrawWater(const void* data, RendererAPI& api) @@ -2283,7 +2253,7 @@ namespace OloEngine // Bind shader (cached). if (s_Data.CurrentBoundShaderID != cmd->shaderRendererID) { - glUseProgram(cmd->shaderRendererID); + api.BindShaderProgram(cmd->shaderRendererID); s_Data.CurrentBoundShaderID = cmd->shaderRendererID; ++s_Data.Stats.ShaderBinds; } @@ -2328,17 +2298,17 @@ namespace OloEngine waterData.TessParams = cmd->tessParams; waterData.FFTParams = cmd->fftParams; waterUBO->SetData(&waterData, ShaderBindingLayout::WaterUBO::GetSize()); - glBindBufferBase(GL_UNIFORM_BUFFER, ShaderBindingLayout::UBO_WATER, waterUBO->GetRendererID()); + api.BindUniformBuffer(ShaderBindingLayout::UBO_WATER, waterUBO->GetRendererID()); } // Bind normal map and noise textures (tracked for redundancy elimination and stats) - BindTrackedTexture(cmd->normalMap0ID, ShaderBindingLayout::TEX_WATER_NORMAL_0, GL_TEXTURE_2D); - BindTrackedTexture(cmd->normalMap1ID, ShaderBindingLayout::TEX_WATER_NORMAL_1, GL_TEXTURE_2D); - BindTrackedTexture(cmd->noiseTextureID, ShaderBindingLayout::TEX_WATER_NOISE, GL_TEXTURE_2D); - BindTrackedTexture(cmd->foamTextureID, ShaderBindingLayout::TEX_WATER_FOAM, GL_TEXTURE_2D); + BindTrackedTexture(cmd->normalMap0ID, ShaderBindingLayout::TEX_WATER_NORMAL_0); + BindTrackedTexture(cmd->normalMap1ID, ShaderBindingLayout::TEX_WATER_NORMAL_1); + BindTrackedTexture(cmd->noiseTextureID, ShaderBindingLayout::TEX_WATER_NOISE); + BindTrackedTexture(cmd->foamTextureID, ShaderBindingLayout::TEX_WATER_FOAM); // FFT ocean cascade textures (sampled when u_FFTParams.x > 0.5) - BindTrackedTexture(cmd->fftDisplacementID, ShaderBindingLayout::TEX_WATER_FFT_DISPLACEMENT, GL_TEXTURE_2D); - BindTrackedTexture(cmd->fftDerivativesID, ShaderBindingLayout::TEX_WATER_FFT_DERIVATIVES, GL_TEXTURE_2D); + BindTrackedTexture(cmd->fftDisplacementID, ShaderBindingLayout::TEX_WATER_FFT_DISPLACEMENT); + BindTrackedTexture(cmd->fftDerivativesID, ShaderBindingLayout::TEX_WATER_FFT_DERIVATIVES); // Bind the global environment cubemap for water reflections (binding 9). // The water pass doesn't otherwise touch this slot, so set it explicitly @@ -2350,12 +2320,11 @@ namespace OloEngine // so clear it directly and update the tracking). if (const auto envMapID = Renderer3D::GetGlobalEnvironmentMapID(); envMapID != 0) { - BindTrackedTexture(envMapID, ShaderBindingLayout::TEX_ENVIRONMENT, GL_TEXTURE_CUBE_MAP); + BindTrackedTexture(envMapID, ShaderBindingLayout::TEX_ENVIRONMENT); } else if (s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_ENVIRONMENT] != 0) { - glActiveTexture(GL_TEXTURE0 + ShaderBindingLayout::TEX_ENVIRONMENT); - glBindTexture(GL_TEXTURE_CUBE_MAP, 0); + api.BindTexture(ShaderBindingLayout::TEX_ENVIRONMENT, 0); s_Data.BoundTextureIDs[ShaderBindingLayout::TEX_ENVIRONMENT] = 0; } @@ -2369,8 +2338,8 @@ namespace OloEngine // consumed by TCS to collapse tess factors toward 1.0 when disabled, // so we can keep a single, valid primitive mode at draw time. BindVAOIfNeeded(cmd->vertexArrayID); - glPatchParameteri(GL_PATCH_VERTICES, 3); - glDrawElements(GL_PATCHES, static_cast(cmd->indexCount), GL_UNSIGNED_INT, nullptr); + api.SetPatchVertexCount(3); + api.DrawBoundIndexed(RHI::PrimitiveTopology::PatchList, cmd->indexCount, RHI::IndexType::UInt32, 0); ++s_Data.Stats.DrawCalls; } } // namespace OloEngine diff --git a/OloEngine/src/OloEngine/Renderer/Commands/FrameResourceManager.cpp b/OloEngine/src/OloEngine/Renderer/Commands/FrameResourceManager.cpp index 8e99f73ca..59719b848 100644 --- a/OloEngine/src/OloEngine/Renderer/Commands/FrameResourceManager.cpp +++ b/OloEngine/src/OloEngine/Renderer/Commands/FrameResourceManager.cpp @@ -1,7 +1,7 @@ #include "OloEnginePCH.h" #include "FrameResourceManager.h" -#include +#include "OloEngine/Renderer/RenderCommand.h" #include @@ -268,21 +268,21 @@ namespace OloEngine } // ======================================================================== - // OpenGL Fence Implementation + // Fence implementation (routed through the RHI facade) // ======================================================================== u64 FrameResourceManager::CreateFence() const { OLO_PROFILE_FUNCTION(); - GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - if (!sync) + const u64 fence = RenderCommand::CreateFence(); + if (fence == 0) { - OLO_CORE_ERROR("FrameResourceManager::CreateFence: glFenceSync failed!"); + OLO_CORE_ERROR("FrameResourceManager::CreateFence: fence creation failed!"); return 0; } - return static_cast(reinterpret_cast(sync)); + return fence; } bool FrameResourceManager::WaitForFence(u64 fenceId) const @@ -292,21 +292,28 @@ namespace OloEngine if (fenceId == 0) return true; - GLsync sync = reinterpret_cast(static_cast(fenceId)); - - constexpr GLuint64 TIMEOUT_NS = 1000000000ULL; // 1 second - if (GLenum result = glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, TIMEOUT_NS); result == GL_TIMEOUT_EXPIRED) - { - OLO_CORE_WARN("FrameResourceManager::WaitForFence: Fence wait timed out!"); - return false; - } - else if (result == GL_WAIT_FAILED) + constexpr u64 TIMEOUT_NS = 1000000000ULL; // 1 second + switch (RenderCommand::ClientWaitFence(fenceId, TIMEOUT_NS)) { - OLO_CORE_ERROR("FrameResourceManager::WaitForFence: Fence wait failed!"); - return false; + case RHI::FenceStatus::AlreadySignaled: + case RHI::FenceStatus::ConditionSatisfied: + return true; + case RHI::FenceStatus::TimeoutExpired: + OLO_CORE_WARN("FrameResourceManager::WaitForFence: Fence wait timed out!"); + return false; + case RHI::FenceStatus::Failed: + OLO_CORE_ERROR("FrameResourceManager::WaitForFence: Fence wait failed!"); + return false; } - return true; + // Deliberately NO `default:` inside the switch — that would suppress the + // compiler's exhaustiveness warning, which is what actually catches a new + // RHI::FenceStatus member at build time. This fallthrough is the runtime + // backstop, and it FAILS CLOSED: a fence gates reuse of double-buffered + // GPU resources, so reporting success for a status we do not understand + // is the one answer that could hand a caller memory the GPU still owns. + OLO_CORE_ERROR("FrameResourceManager::WaitForFence: unrecognized fence status; treating as failure"); + return false; } bool FrameResourceManager::IsFenceSignaled(u64 fenceId) const @@ -314,13 +321,7 @@ namespace OloEngine if (fenceId == 0) return true; - GLsync sync = reinterpret_cast(static_cast(fenceId)); - - GLint signaled = GL_FALSE; - GLsizei length = 0; - glGetSynciv(sync, GL_SYNC_STATUS, sizeof(signaled), &length, &signaled); - - return signaled == GL_SIGNALED; + return RenderCommand::IsFenceSignaled(fenceId); } void FrameResourceManager::DeleteFence(u64 fenceId) const @@ -328,8 +329,7 @@ namespace OloEngine if (fenceId == 0) return; - GLsync sync = reinterpret_cast(static_cast(fenceId)); - glDeleteSync(sync); + RenderCommand::DestroyFence(fenceId); } } // namespace OloEngine diff --git a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp index 8f317fd7a..9c89bdd98 100644 --- a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp +++ b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp @@ -11,8 +11,6 @@ #include "OloEngine/Renderer/ShaderBindingLayout.h" #include "OloEngine/Renderer/Shadow/ShadowMap.h" -#include - #include #include #include @@ -160,7 +158,7 @@ namespace OloEngine // slots (sampler2D reads of .rg / .rgb / .w all see zero, and // state 0 == Uncaptured makes the sampler skip every probe). m_PlaceholderTexture = RenderCommand::CreateTexture2D(1, 1, RHI::Format::RGBA16Float); - glClearTexImage(m_PlaceholderTexture, 0, GL_RGBA, GL_FLOAT, nullptr); + RenderCommand::ClearTextureFloat(m_PlaceholderTexture, 0, glm::vec4(0.0f)); SetAtlasTextureParams(m_PlaceholderTexture, RHI::Filter::Nearest); } if (m_WhiteTexture == 0) @@ -175,7 +173,7 @@ namespace OloEngine // Environment fallback for the relight sky term when no global // IBL environment cubemap exists this frame. m_BlackCubemap = RenderCommand::CreateTextureCubemap(1, 1, RHI::Format::RGBA16Float); - glClearTexImage(m_BlackCubemap, 0, GL_RGBA, GL_FLOAT, nullptr); + RenderCommand::ClearTextureFloat(m_BlackCubemap, 0, glm::vec4(0.0f)); RenderCommand::SetTextureFilter(m_BlackCubemap, RHI::Filter::Linear, RHI::Filter::Linear); RenderCommand::SetTextureWrap(m_BlackCubemap, RHI::AddressMode::ClampToEdge); } @@ -473,7 +471,7 @@ namespace OloEngine // the relocation/classification step); cleared to zero == Uncaptured. m_ProbeDataTexture = RenderCommand::CreateTexture2D(static_cast(tileDims.x), static_cast(tileDims.y), RHI::Format::RGBA16Float); - glClearTexImage(m_ProbeDataTexture, 0, GL_RGBA, GL_FLOAT, nullptr); + RenderCommand::ClearTextureFloat(m_ProbeDataTexture, 0, glm::vec4(0.0f)); SetAtlasTextureParams(m_ProbeDataTexture, RHI::Filter::Nearest); // Reset the CPU scheduling mirror — a new grid invalidates every record. @@ -719,8 +717,20 @@ namespace OloEngine // Read the probe's hit-geo tile back (rg = octNormal, b = distance // [< 0 = sky], a = DDGI_HIT_* flag). RGBA16F -> GL converts to float. std::vector texels(static_cast(t) * static_cast(t)); - glGetTextureSubImage(geoTex, 0, tile.x * t, tile.y * t, 0, t, t, 1, GL_RGBA, GL_FLOAT, - static_cast(texels.size() * sizeof(glm::vec4)), texels.data()); + if (!RenderCommand::ReadTextureSubImage(geoTex, 0, tile.x * t, tile.y * t, 0, + static_cast(t), static_cast(t), 1, + RHI::Format::RGBA32Float, + texels.size() * sizeof(glm::vec4), texels.data())) + { + // Skip this probe's relocation/classification for this frame rather + // than aggregating `texels`, whose contents are unspecified after a + // failed read — classifying from undefined data can park a probe + // inside geometry, which then leaks through every later gather. + // The update is amortized, so the probe simply retries next frame. + OLO_CORE_WARN("DDGIProbeUpdatePass: probe hit-geo tile readback failed; skipping probe {} this frame", + probeIdx); + return; + } DDGI::ProbeHitAggregates agg; i32 backfaceCount = 0; @@ -792,7 +802,8 @@ namespace OloEngine const f32 texel[4] = { newOffset.x, newOffset.y, newOffset.z, static_cast(std::to_underlying(newState)) }; - glTextureSubImage2D(m_ProbeDataTexture, 0, tile.x, tile.y, 1, 1, GL_RGBA, GL_FLOAT, texel); + RenderCommand::UploadTextureSubImage2D(m_ProbeDataTexture, tile.x, tile.y, 1, 1, + RHI::Format::RGBA32Float, texel); } void DDGIProbeUpdatePass::BlendVisibility(const std::vector& capturedProbes) diff --git a/OloEngine/src/OloEngine/Renderer/GBuffer.cpp b/OloEngine/src/OloEngine/Renderer/GBuffer.cpp index 7eaaba69b..1b1493762 100644 --- a/OloEngine/src/OloEngine/Renderer/GBuffer.cpp +++ b/OloEngine/src/OloEngine/Renderer/GBuffer.cpp @@ -3,8 +3,7 @@ #include "OloEngine/Core/Log.h" #include "OloEngine/Debug/Instrumentor.h" - -#include +#include "OloEngine/Renderer/RenderCommand.h" #include @@ -26,7 +25,7 @@ namespace OloEngine FramebufferTextureSpecification{ FramebufferTextureFormat::RED_INTEGER }, // RT4 EntityID (picking) // Depth must match the scene framebuffer's depth format // (`FramebufferTextureFormat::Depth` = DEPTH24STENCIL8) so that - // `glBlitNamedFramebuffer(GL_DEPTH_BUFFER_BIT, …)` — the path used + // `RenderCommand::BlitFramebuffer(RHI::BlitAspect::Depth, …)` — the path used // by `DeferredLightingPass` to hand G-Buffer depth to downstream // passes and by `SceneRenderPass::ResolveToScene` in forward+ — // succeeds. A format mismatch here surfaces as a per-frame flood @@ -74,8 +73,7 @@ namespace OloEngine // incomplete and every subsequent blit/lighting pass silently no-ops. if (sampleCount > 1) { - GLint maxSamples = 1; - glGetIntegerv(GL_MAX_SAMPLES, &maxSamples); + const u32 maxSamples = std::max(RenderCommand::GetMaxFramebufferSamples(), 1u); const u32 deviceMax = static_cast(maxSamples); if (sampleCount > deviceMax) { @@ -153,39 +151,31 @@ namespace OloEngine const u32 srcFB = m_Framebuffer->GetRendererID(); const u32 dstFB = m_ResolvedFramebuffer->GetRendererID(); - const GLint w = static_cast(m_Width); - const GLint h = static_cast(m_Height); + const i32 w = static_cast(m_Width); + const i32 h = static_cast(m_Height); - // Resolve each colour attachment independently — glBlitNamedFramebuffer - // only reads/writes the currently-selected read-/draw-buffer so this - // is the safe pattern for MRT MSAA resolve. + // Resolve each colour attachment independently — a framebuffer blit + // only reads/writes the currently-selected read / draw attachment, so + // this is the safe pattern for an MRT MSAA resolve. for (u32 i = 0; i < std::to_underlying(Count); ++i) { - const GLenum attachment = GL_COLOR_ATTACHMENT0 + i; - glNamedFramebufferReadBuffer(srcFB, attachment); - glNamedFramebufferDrawBuffer(dstFB, attachment); - glBlitNamedFramebuffer(srcFB, dstFB, - 0, 0, w, h, - 0, 0, w, h, - GL_COLOR_BUFFER_BIT, GL_NEAREST); + RenderCommand::SetFramebufferReadAttachment(srcFB, i); + RenderCommand::SetFramebufferDrawAttachments(dstFB, std::array{ i }); + RenderCommand::BlitFramebuffer(srcFB, dstFB, + 0, 0, w, h, + 0, 0, w, h, + RHI::BlitAspect::Color, RHI::Filter::Nearest); } // Resolve depth (no sample filtering — GL_NEAREST is the only legal choice). - glBlitNamedFramebuffer(srcFB, dstFB, - 0, 0, w, h, - 0, 0, w, h, - GL_DEPTH_BUFFER_BIT, GL_NEAREST); + RenderCommand::BlitFramebuffer(srcFB, dstFB, + 0, 0, w, h, + 0, 0, w, h, + RHI::BlitAspect::Depth, RHI::Filter::Nearest); // Restore full draw-buffer set on the resolved FB so subsequent // passes that bind it for composition get all attachments. - const GLenum fullDrawBufs[] = { - GL_COLOR_ATTACHMENT0, - GL_COLOR_ATTACHMENT1, - GL_COLOR_ATTACHMENT2, - GL_COLOR_ATTACHMENT3, - GL_COLOR_ATTACHMENT4 - }; - glNamedFramebufferDrawBuffers(dstFB, static_cast(std::to_underlying(Count)), fullDrawBufs); + RenderCommand::RestoreAllFramebufferDrawAttachments(dstFB, std::to_underlying(Count)); } u32 GBuffer::GetColorAttachmentID(AttachmentIndex index) const @@ -227,14 +217,14 @@ namespace OloEngine const u32 srcFB = m_Framebuffer->GetRendererID(); const u32 dstFB = m_ResolvedFramebuffer->GetRendererID(); - const GLint w = static_cast(m_Width); - const GLint h = static_cast(m_Height); + const i32 w = static_cast(m_Width); + const i32 h = static_cast(m_Height); // Depth-only blit — skips colour resolves so per-sample colour data // stays intact for the MSAA deferred lighting shader to consume. - glBlitNamedFramebuffer(srcFB, dstFB, - 0, 0, w, h, - 0, 0, w, h, - GL_DEPTH_BUFFER_BIT, GL_NEAREST); + RenderCommand::BlitFramebuffer(srcFB, dstFB, + 0, 0, w, h, + 0, 0, w, h, + RHI::BlitAspect::Depth, RHI::Filter::Nearest); } } // namespace OloEngine diff --git a/OloEngine/src/OloEngine/Renderer/IBLPrecompute.cpp b/OloEngine/src/OloEngine/Renderer/IBLPrecompute.cpp index 4427db718..326b0fa17 100644 --- a/OloEngine/src/OloEngine/Renderer/IBLPrecompute.cpp +++ b/OloEngine/src/OloEngine/Renderer/IBLPrecompute.cpp @@ -14,8 +14,6 @@ #include #include #include -#include - #include #include @@ -364,7 +362,7 @@ namespace OloEngine std::forward(work)(); // Sync — flushes the GL command queue so the elapsed time covers // the actual rasterisation work, not just submission. - ::glFinish(); + RenderCommand::WaitForDeviceIdle(); const auto end = std::chrono::steady_clock::now(); return std::chrono::duration(end - start).count(); } @@ -829,7 +827,7 @@ namespace OloEngine // Sync so the elapsed-time measurement covers GPU work, not just // command submission — matches MeasureMillisecondsWithGPUSync above. - ::glFinish(); + RenderCommand::WaitForDeviceIdle(); const auto pathEnd = std::chrono::steady_clock::now(); const f64 elapsedMs = std::chrono::duration(pathEnd - pathStart).count(); OLO_CORE_INFO("SH-based irradiance map generation complete ({:.2f} ms, L2 SH, 9 coefficients)", elapsedMs); diff --git a/OloEngine/src/OloEngine/Renderer/Instancing/GPUFrustumCuller.cpp b/OloEngine/src/OloEngine/Renderer/Instancing/GPUFrustumCuller.cpp index 97cc19c82..7666e84fc 100644 --- a/OloEngine/src/OloEngine/Renderer/Instancing/GPUFrustumCuller.cpp +++ b/OloEngine/src/OloEngine/Renderer/Instancing/GPUFrustumCuller.cpp @@ -9,8 +9,6 @@ #include "OloEngine/Renderer/CameraRelative.h" #include "OloEngine/Renderer/Renderer3D.h" -#include - #include namespace OloEngine @@ -285,8 +283,8 @@ namespace OloEngine slot.InputBuffer->Bind(); // 16 — full input slot.OutputBuffer->Bind(); // 15 — phase-1 survivors slot.IndirectBuffer->Bind(); // 17 — phase-1 indirect - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, kRejectedBinding, slot.RejectedBuffer->GetRendererID()); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, kRejectedCountBinding, slot.RejectedCounter->GetRendererID()); + RenderCommand::BindStorageBuffer(kRejectedBinding, slot.RejectedBuffer->GetRendererID()); + RenderCommand::BindStorageBuffer(kRejectedCountBinding, slot.RejectedCounter->GetRendererID()); // Use the occlusion variant when a previous-frame HZB is available; else // (frame 0 / no HZB) fall back to the frustum-only shader — no rejects @@ -354,12 +352,12 @@ namespace OloEngine // Bind the reject buffer AS the input (16); phase-2 survivors (15) and // indirect (17) are this slot's phase-2 buffers; the reject counter (19) // bounds the dispatch in-shader. - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ShaderBindingLayout::SSBO_INSTANCE_CULL_INPUT, - result.RejectedBuffer->GetRendererID()); + RenderCommand::BindStorageBuffer(ShaderBindingLayout::SSBO_INSTANCE_CULL_INPUT, + result.RejectedBuffer->GetRendererID()); result.Phase2Output->Bind(); // 15 - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ShaderBindingLayout::SSBO_INSTANCE_DRAW_INDIRECT, - result.Phase2Indirect->GetRendererID()); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, kRejectedCountBinding, result.RejectedCounter->GetRendererID()); + RenderCommand::BindStorageBuffer(ShaderBindingLayout::SSBO_INSTANCE_DRAW_INDIRECT, + result.Phase2Indirect->GetRendererID()); + RenderCommand::BindStorageBuffer(kRejectedCountBinding, result.RejectedCounter->GetRendererID()); RenderCommand::BindTexture(0, currentHZB.HZBTextureID); // current-frame HZB m_OcclusionCullShader->Bind(); diff --git a/OloEngine/src/OloEngine/Renderer/LightCulling/TiledForwardPlus.cpp b/OloEngine/src/OloEngine/Renderer/LightCulling/TiledForwardPlus.cpp index 4dc40282e..17facdde6 100644 --- a/OloEngine/src/OloEngine/Renderer/LightCulling/TiledForwardPlus.cpp +++ b/OloEngine/src/OloEngine/Renderer/LightCulling/TiledForwardPlus.cpp @@ -2,10 +2,10 @@ #include "OloEngine/Renderer/LightCulling/TiledForwardPlus.h" #include "OloEngine/Renderer/CameraRelative.h" #include "OloEngine/Renderer/LightCulling/ClusteredLighting.h" +#include "OloEngine/Renderer/RenderCommand.h" #include "OloEngine/Renderer/Renderer3D.h" #include "OloEngine/Renderer/Shader.h" #include "OloEngine/Renderer/UniformBuffer.h" -#include #include namespace OloEngine @@ -216,17 +216,17 @@ namespace OloEngine debugShader->Bind(); // Enable alpha blending for the overlay - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDisable(GL_DEPTH_TEST); + RenderCommand::SetBlendState(true); + RenderCommand::SetBlendFunc(RHI::BlendFactor::SrcAlpha, RHI::BlendFactor::OneMinusSrcAlpha); + RenderCommand::SetDepthTest(false); - glBindVertexArray(fullscreenQuadVAO); - glDrawArrays(GL_TRIANGLES, 0, 6); - glBindVertexArray(0); + RenderCommand::BindVertexArrayRaw(fullscreenQuadVAO); + RenderCommand::DrawBoundArrays(RHI::PrimitiveTopology::TriangleList, 0, 6); + RenderCommand::BindVertexArrayRaw(0); // Restore state - glEnable(GL_DEPTH_TEST); - glDisable(GL_BLEND); + RenderCommand::SetDepthTest(true); + RenderCommand::SetBlendState(false); debugShader->Unbind(); } diff --git a/OloEngine/src/OloEngine/Renderer/LightProbeBaker.cpp b/OloEngine/src/OloEngine/Renderer/LightProbeBaker.cpp index 5cf9a1f7c..ef5ed9730 100644 --- a/OloEngine/src/OloEngine/Renderer/LightProbeBaker.cpp +++ b/OloEngine/src/OloEngine/Renderer/LightProbeBaker.cpp @@ -8,7 +8,6 @@ #include "OloEngine/Renderer/Renderer3D.h" #include "OloEngine/Debug/Instrumentor.h" -#include #include #include @@ -33,7 +32,7 @@ namespace OloEngine { 0.0f, -1.0f, 0.0f } }; - void LightProbeBaker::RenderCubemapAtPosition( + bool LightProbeBaker::RenderCubemapAtPosition( Ref& scene, const glm::vec3& position, u32 resolution, @@ -69,12 +68,22 @@ namespace OloEngine // Read back RGBA16F pixel data from the color attachment u32 const colorAttachmentID = fbo->GetColorAttachmentRendererID(0); - glGetTextureImage(colorAttachmentID, 0, GL_RGBA, GL_FLOAT, - static_cast(rgbaBuffer.size() * sizeof(f32)), - rgbaBuffer.data()); + const bool readOk = RenderCommand::ReadTextureImage( + colorAttachmentID, 0, RHI::Format::RGBA32Float, + rgbaBuffer.size() * sizeof(f32), rgbaBuffer.data()); fbo->Unbind(); + if (!readOk) + { + // Fail the whole bake rather than folding an unspecified buffer + // into the SH projection: these coefficients are PERSISTED into + // LightProbeVolumeAsset, so a rare readback failure would write + // bad lighting to disk that no later run would recompute. + OLO_CORE_ERROR("LightProbeBaker: cubemap face {} readback failed; abandoning this probe", face); + return false; + } + // Convert RGBA to RGB and store auto const faceOffset = static_cast(face) * resolution * resolution; for (size_t i = 0; i < static_cast(resolution) * resolution; ++i) @@ -85,6 +94,8 @@ namespace OloEngine rgbaBuffer[i * 4 + 2]); } } + + return true; } SHCoefficients LightProbeBaker::ProjectToSH( @@ -183,7 +194,16 @@ namespace OloEngine OLO_PROFILE_FUNCTION(); std::vector pixels; - RenderCubemapAtPosition(scene, position, cubemapResolution, pixels); + if (!RenderCubemapAtPosition(scene, position, cubemapResolution, pixels)) + { + // Readback failed — report the probe as invalid so the caller stores + // nothing rather than persisting SH derived from undefined pixels. + if (outValid) + { + *outValid = false; + } + return {}; + } SHCoefficients sh = ProjectToSH(pixels, cubemapResolution); diff --git a/OloEngine/src/OloEngine/Renderer/LightProbeBaker.h b/OloEngine/src/OloEngine/Renderer/LightProbeBaker.h index 52229168e..e11196628 100644 --- a/OloEngine/src/OloEngine/Renderer/LightProbeBaker.h +++ b/OloEngine/src/OloEngine/Renderer/LightProbeBaker.h @@ -47,7 +47,9 @@ namespace OloEngine private: // Render the scene into a cubemap FBO at the given position - static void RenderCubemapAtPosition( + // Returns false when the GPU readback of any face fails; `outPixels` is + // then not safe to project. Callers must not persist SH built from it. + [[nodiscard]] static bool RenderCubemapAtPosition( Ref& scene, const glm::vec3& position, u32 resolution, diff --git a/OloEngine/src/OloEngine/Renderer/Occlusion/OcclusionQueryPool.cpp b/OloEngine/src/OloEngine/Renderer/Occlusion/OcclusionQueryPool.cpp index 36eed0e91..0196f6d1f 100644 --- a/OloEngine/src/OloEngine/Renderer/Occlusion/OcclusionQueryPool.cpp +++ b/OloEngine/src/OloEngine/Renderer/Occlusion/OcclusionQueryPool.cpp @@ -1,8 +1,7 @@ #include "OloEnginePCH.h" #include "OcclusionQueryPool.h" #include "OloEngine/Core/Log.h" - -#include +#include "OloEngine/Renderer/RenderCommand.h" namespace OloEngine { @@ -29,7 +28,7 @@ namespace OloEngine for (u32 buf = 0; buf < 2; ++buf) { m_QueryObjects[buf].resize(maxQueries, 0); - glCreateQueries(GL_ANY_SAMPLES_PASSED, static_cast(maxQueries), m_QueryObjects[buf].data()); + RenderCommand::CreateQueries(RHI::QueryType::OcclusionAnySamples, m_QueryObjects[buf]); } m_Results.resize(maxQueries, true); // Default visible until proven otherwise @@ -54,7 +53,7 @@ namespace OloEngine { if (!m_QueryObjects[buf].empty()) { - glDeleteQueries(static_cast(m_QueryObjects[buf].size()), m_QueryObjects[buf].data()); + RenderCommand::DeleteQueries(m_QueryObjects[buf]); m_QueryObjects[buf].clear(); } } @@ -93,8 +92,7 @@ namespace OloEngine continue; } - GLint available = GL_FALSE; - glGetQueryObjectiv(m_QueryObjects[readBuffer][i], GL_QUERY_RESULT_AVAILABLE, &available); + const bool available = RenderCommand::IsQueryResultAvailable(m_QueryObjects[readBuffer][i]); if (!available) { // If result not yet available, assume visible to avoid popping @@ -102,8 +100,7 @@ namespace OloEngine continue; } - GLuint result = 0; - glGetQueryObjectuiv(m_QueryObjects[readBuffer][i], GL_QUERY_RESULT, &result); + const u32 result = RenderCommand::GetQueryResultU32(m_QueryObjects[readBuffer][i]); m_Results[i] = (result != 0); } hasResults = (m_ReadableQueryCount > 0); @@ -124,7 +121,8 @@ namespace OloEngine if (!m_Active || objectIndex >= m_MaxQueries) return; - glBeginQuery(GL_ANY_SAMPLES_PASSED, m_QueryObjects[m_WriteBuffer][objectIndex]); + RenderCommand::BeginQuery(RHI::QueryType::OcclusionAnySamples, + m_QueryObjects[m_WriteBuffer][objectIndex]); m_QueryIssued[m_WriteBuffer][objectIndex] = true; if (objectIndex >= m_WriteQueryCount) @@ -137,7 +135,7 @@ namespace OloEngine if (!m_Active) return; - glEndQuery(GL_ANY_SAMPLES_PASSED); + RenderCommand::EndQuery(RHI::QueryType::OcclusionAnySamples); } void OcclusionQueryPool::EndFrame() diff --git a/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTGpu.cpp b/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTGpu.cpp index d267997ac..3da5250cb 100644 --- a/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTGpu.cpp +++ b/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTGpu.cpp @@ -4,8 +4,6 @@ #include "OloEngine/Renderer/MemoryBarrierFlags.h" #include "OloEngine/Renderer/RenderCommand.h" -#include - #include #include @@ -270,15 +268,15 @@ namespace OloEngine::Ocean // Clear both arrays (the butterfly chain transforms all 4 layers; the // unused ones must not feed NaN/garbage through imageLoad). const glm::vec4 zero(0.0f); - glClearTexImage(m_PingPong[0]->GetRendererID(), 0, GL_RGBA, GL_FLOAT, &zero); - glClearTexImage(m_PingPong[1]->GetRendererID(), 0, GL_RGBA, GL_FLOAT, &zero); + RenderCommand::ClearTextureFloat(m_PingPong[0]->GetRendererID(), 0, zero); + RenderCommand::ClearTextureFloat(m_PingPong[1]->GetRendererID(), 0, zero); // Upload the input into layer 0 (rg = complex, ba unused). m_Scratch.assign(count, glm::vec4(0.0f)); for (sizet i = 0; i < count; ++i) m_Scratch[i] = glm::vec4(freq[i].real(), freq[i].imag(), 0.0f, 0.0f); - glTextureSubImage3D(m_PingPong[0]->GetRendererID(), 0, 0, 0, 0, static_cast(N), - static_cast(N), 1, GL_RGBA, GL_FLOAT, m_Scratch.data()); + RenderCommand::UploadTextureSubImage3D(m_PingPong[0]->GetRendererID(), 0, 0, 0, N, N, 1, + RHI::Format::RGBA32Float, m_Scratch.data()); RenderCommand::MemoryBarrier(MemoryBarrierFlags::TextureUpdate); const u32 finalIndex = RunButterflyPasses(0u); @@ -286,9 +284,16 @@ namespace OloEngine::Ocean // Read back layer 0 and apply the 1/N² normalisation the production // path defers to the assemble pass. std::vector readback(count); - glGetTextureSubImage(m_PingPong[finalIndex]->GetRendererID(), 0, 0, 0, 0, static_cast(N), - static_cast(N), 1, GL_RGBA, GL_FLOAT, - static_cast(count * sizeof(glm::vec4)), readback.data()); + if (!RenderCommand::ReadTextureSubImage(m_PingPong[finalIndex]->GetRendererID(), 0, 0, 0, 0, + N, N, 1, RHI::Format::RGBA32Float, + count * sizeof(glm::vec4), readback.data())) + { + // Return empty rather than normalizing an unspecified buffer — this + // is a verification utility, so silently handing back plausible-looking + // garbage would corrupt the very comparison it exists to make. + OLO_CORE_WARN("OceanFFTGpu: butterfly readback failed; returning no result"); + return {}; + } const f32 invN2 = 1.0f / (static_cast(N) * static_cast(N)); std::vector result(count); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/BloomRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/BloomRenderPass.cpp index 3eb5daa08..4b7cc436e 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/BloomRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/BloomRenderPass.cpp @@ -8,8 +8,6 @@ #include "OloEngine/Renderer/RenderPipelineBuilderInternal.h" #include "OloEngine/Renderer/ResourceHandle.h" -#include - #include namespace OloEngine diff --git a/OloEngine/src/OloEngine/Renderer/Passes/DecalRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/DecalRenderPass.cpp index 8938dd55f..0878ee908 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/DecalRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/DecalRenderPass.cpp @@ -11,7 +11,7 @@ #include "OloEngine/Renderer/Commands/CommandPacket.h" #include "OloEngine/Renderer/Commands/RenderCommand.h" -#include +#include namespace OloEngine { @@ -353,19 +353,19 @@ namespace OloEngine // Manual per-packet dispatch — each DrawDecalCommand::mode selects a // different drawbuffer + colorMask configuration so the decal only // writes into the intended G-Buffer channels. Arrays are sized to - // `GBuffer::Count` so RT4 (entity ID) stays at GL_NONE during decal + // `GBuffer::Count` so RT4 (entity ID) stays unwritten during decal // rendering — decals must not stamp their own pickability over the // underlying mesh's entity ID. - constexpr GLsizei kGBufferCount = static_cast(std::to_underlying(GBuffer::Count)); - const GLenum drawAlbedoOnly[kGBufferCount] = { GL_COLOR_ATTACHMENT0, GL_NONE, GL_NONE, GL_NONE, GL_NONE }; - const GLenum drawNormalOnly[kGBufferCount] = { GL_NONE, GL_COLOR_ATTACHMENT1, GL_NONE, GL_NONE, GL_NONE }; - const GLenum drawAlbedoAndNormal[kGBufferCount] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_NONE, GL_NONE, GL_NONE }; - const GLenum drawEmissiveOnly[kGBufferCount] = { GL_NONE, GL_NONE, GL_COLOR_ATTACHMENT2, GL_NONE, GL_NONE }; - const GLenum fullDrawBufs[kGBufferCount] = { - GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, - GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3, - GL_COLOR_ATTACHMENT4 - }; + // RHI::NoAttachment is the neutral spelling of "this draw slot writes + // nowhere". It exists precisely for these lists: it is not an attachment + // index, and both backends need it (GL_NONE / VK_ATTACHMENT_UNUSED). + constexpr sizet kGBufferCount = static_cast(std::to_underlying(GBuffer::Count)); + constexpr u32 kNone = RHI::NoAttachment; + const std::array drawAlbedoOnly = { 0, kNone, kNone, kNone, kNone }; + const std::array drawNormalOnly = { kNone, 1, kNone, kNone, kNone }; + const std::array drawAlbedoAndNormal = { 0, 1, kNone, kNone, kNone }; + const std::array drawEmissiveOnly = { kNone, kNone, 2, kNone, kNone }; + const std::array fullDrawBufs = { 0, 1, 2, 3, 4 }; using DecalMode = DrawDecalCommand::DecalMode; // Sentinel outside the valid enumerator range — forces the first @@ -403,47 +403,44 @@ namespace OloEngine // other modes overwrite (the previous value is preserved for // channels outside the colour mask). const bool wantAdditive = (packetMode == DecalMode::Emissive); - glBlendFunci(2, GL_ONE, GL_ONE); - if (wantAdditive) - glEnablei(GL_BLEND, 2); - else - glDisablei(GL_BLEND, 2); + RenderCommand::SetBlendFuncForAttachment(2, RHI::BlendFactor::One, RHI::BlendFactor::One); + RenderCommand::SetBlendStateForAttachment(2, wantAdditive); switch (packetMode) { case DecalMode::Normal: // RT1 only, xy writable, zw preserved - glNamedFramebufferDrawBuffers(gbufferID, kGBufferCount, drawNormalOnly); - glColorMaski(0, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); - glColorMaski(1, GL_TRUE, GL_TRUE, GL_FALSE, GL_FALSE); - glColorMaski(2, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); - glColorMaski(3, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + RenderCommand::SetFramebufferDrawAttachments(gbufferID, drawNormalOnly); + RenderCommand::SetColorMaskForAttachment(0, false, false, false, false); + RenderCommand::SetColorMaskForAttachment(1, true, true, false, false); + RenderCommand::SetColorMaskForAttachment(2, false, false, false, false); + RenderCommand::SetColorMaskForAttachment(3, false, false, false, false); break; case DecalMode::RMA: // RT0.a + RT1.zw writable - glNamedFramebufferDrawBuffers(gbufferID, kGBufferCount, drawAlbedoAndNormal); - glColorMaski(0, GL_FALSE, GL_FALSE, GL_FALSE, GL_TRUE); - glColorMaski(1, GL_FALSE, GL_FALSE, GL_TRUE, GL_TRUE); - glColorMaski(2, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); - glColorMaski(3, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + RenderCommand::SetFramebufferDrawAttachments(gbufferID, drawAlbedoAndNormal); + RenderCommand::SetColorMaskForAttachment(0, false, false, false, true); + RenderCommand::SetColorMaskForAttachment(1, false, false, true, true); + RenderCommand::SetColorMaskForAttachment(2, false, false, false, false); + RenderCommand::SetColorMaskForAttachment(3, false, false, false, false); break; case DecalMode::Emissive: // RT2.rgb writable, RT2.a (unlit flag) preserved - glNamedFramebufferDrawBuffers(gbufferID, kGBufferCount, drawEmissiveOnly); - glColorMaski(0, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); - glColorMaski(1, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); - glColorMaski(2, GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); - glColorMaski(3, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + RenderCommand::SetFramebufferDrawAttachments(gbufferID, drawEmissiveOnly); + RenderCommand::SetColorMaskForAttachment(0, false, false, false, false); + RenderCommand::SetColorMaskForAttachment(1, false, false, false, false); + RenderCommand::SetColorMaskForAttachment(2, true, true, true, false); + RenderCommand::SetColorMaskForAttachment(3, false, false, false, false); break; case DecalMode::Albedo: default: // RT0.rgb writable, RT0.a preserved - glNamedFramebufferDrawBuffers(gbufferID, kGBufferCount, drawAlbedoOnly); - glColorMaski(0, GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); - glColorMaski(1, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); - glColorMaski(2, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); - glColorMaski(3, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + RenderCommand::SetFramebufferDrawAttachments(gbufferID, drawAlbedoOnly); + RenderCommand::SetColorMaskForAttachment(0, true, true, true, false); + RenderCommand::SetColorMaskForAttachment(1, false, false, false, false); + RenderCommand::SetColorMaskForAttachment(2, false, false, false, false); + RenderCommand::SetColorMaskForAttachment(3, false, false, false, false); break; } currentMode = packetMode; - // The raw GL calls above bypass our cached render-state + // The per-attachment state above bypasses our cached render-state // tracking; invalidate so the next dispatched packet // re-applies its POD state instead of skipping as a no-op. CommandDispatch::InvalidateRenderStateCache(); @@ -459,20 +456,20 @@ namespace OloEngine // Restore full colour masks + draw buffers for subsequent passes. // Only the RGBA-colour attachments (RT0-RT3) need a colour-mask - // restore — RT4 is integer (R32I, entity ID) and `glColorMaski` - // on integer attachments is a no-op (per OpenGL spec the mask only - // applies to floating-point/normalised outputs). - for (GLuint rt = 0; rt < 4; ++rt) - glColorMaski(rt, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - glNamedFramebufferDrawBuffers(gbufferID, kGBufferCount, fullDrawBufs); + // restore — RT4 is integer (R32I, entity ID) and a per-attachment + // colour mask is a no-op there (the mask only applies to + // floating-point / normalised outputs). + for (u32 rt = 0; rt < 4; ++rt) + RenderCommand::SetColorMaskForAttachment(rt, true, true, true, true); + RenderCommand::SetFramebufferDrawAttachments(gbufferID, fullDrawBufs); // Restore RT2 blend state — emissive additive blending leaks into // the next pass otherwise (observed as SSAO / GTAO darkening the // emissive channel during composite). - glDisablei(GL_BLEND, 2); + RenderCommand::SetBlendStateForAttachment(2, false); - // The raw glColorMaski/glDisablei/glNamedFramebufferDrawBuffers calls - // above bypass the cached render-state tracking; invalidate so the + // The per-attachment mask / blend / draw-buffer calls above bypass the + // cached render-state tracking; invalidate so the // next pass's first packet reapplies its POD state instead of being // elided as a no-op against the now-stale cache snapshot. CommandDispatch::InvalidateRenderStateCache(); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/DeferredGPUOcclusionPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/DeferredGPUOcclusionPass.cpp index 7b4e3ed36..24bbc1988 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/DeferredGPUOcclusionPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/DeferredGPUOcclusionPass.cpp @@ -9,8 +9,6 @@ #include "OloEngine/Renderer/Commands/CommandDispatch.h" #include "OloEngine/Renderer/Commands/CommandPacket.h" -#include - #include namespace OloEngine @@ -127,11 +125,7 @@ namespace OloEngine targetFB->Bind(); if (colorAttachmentCount > 0) { - std::array drawBufs{}; - const u32 n = std::min(colorAttachmentCount, static_cast(drawBufs.size())); - for (u32 i = 0; i < n; ++i) - drawBufs[i] = GL_COLOR_ATTACHMENT0 + i; - glNamedFramebufferDrawBuffers(targetFBID, static_cast(n), drawBufs.data()); + RenderCommand::RestoreAllFramebufferDrawAttachments(targetFBID, colorAttachmentCount); } context.SetDepthTest(true); context.SetDepthMask(true); @@ -151,7 +145,7 @@ namespace OloEngine // pipeline; the Hi-Z build samples it as a texture. Order the // framebuffer-write → texture-fetch explicitly (the forward pass gets // the same guarantee from GPUDrivenOcclusionPass::Execute). - ::glTextureBarrier(); + RenderCommand::TextureBarrier(); const GPUFrustumCuller::HZBOcclusionInputs currentHZB = Renderer3D::BuildCurrentOcclusionHZB(depthTexID, m_GBuffer->GetWidth(), m_GBuffer->GetHeight()); @@ -179,16 +173,17 @@ namespace OloEngine const u32 width = m_GBuffer->GetWidth(); const u32 height = m_GBuffer->GetHeight(); - const auto copyExport = [&context, width, height](const RGTextureHandle handle, const u32 sourceTextureID, const GLenum textureTarget) + const auto copyExport = [&context, width, height](const RGTextureHandle handle, const u32 sourceTextureID, + const RendererAPI::TextureTargetType textureTarget) { if (!handle.IsValid() || sourceTextureID == 0u) return; const u32 exportedTextureID = context.ResolveTexture(handle); if (exportedTextureID == 0u || exportedTextureID == sourceTextureID) return; - ::glCopyImageSubData(sourceTextureID, textureTarget, 0, 0, 0, 0, - exportedTextureID, textureTarget, 0, 0, 0, 0, - static_cast(width), static_cast(height), 1); + RenderCommand::CopyImageSubData(sourceTextureID, textureTarget, + exportedTextureID, textureTarget, + width, height); }; const u32 albedoID = m_GBuffer->GetColorAttachmentID(GBuffer::Albedo); @@ -197,12 +192,12 @@ namespace OloEngine const u32 velocityID = m_GBuffer->GetColorAttachmentID(GBuffer::Velocity); const u32 gbufferDepthID = m_GBuffer->GetDepthAttachmentID(); - copyExport(m_SelectedSceneDepthExport, gbufferDepthID, GL_TEXTURE_2D); - copyExport(m_SelectedSceneNormalsExport, normalID, GL_TEXTURE_2D); - copyExport(m_SelectedVelocityExport, velocityID, GL_TEXTURE_2D); - copyExport(m_SelectedGBufferAlbedoExport, albedoID, GL_TEXTURE_2D); - copyExport(m_SelectedGBufferNormalExport, normalID, GL_TEXTURE_2D); - copyExport(m_SelectedGBufferEmissiveExport, emissiveID, GL_TEXTURE_2D); + copyExport(m_SelectedSceneDepthExport, gbufferDepthID, RendererAPI::TextureTargetType::Texture2D); + copyExport(m_SelectedSceneNormalsExport, normalID, RendererAPI::TextureTargetType::Texture2D); + copyExport(m_SelectedVelocityExport, velocityID, RendererAPI::TextureTargetType::Texture2D); + copyExport(m_SelectedGBufferAlbedoExport, albedoID, RendererAPI::TextureTargetType::Texture2D); + copyExport(m_SelectedGBufferNormalExport, normalID, RendererAPI::TextureTargetType::Texture2D); + copyExport(m_SelectedGBufferEmissiveExport, emissiveID, RendererAPI::TextureTargetType::Texture2D); // Only re-export the multisample attachments when phase-2 actually // rasterized into them (per-sample MSAA path). Non-per-sample mode @@ -212,11 +207,11 @@ namespace OloEngine // Resolve() above. if (perSampleMSAA) { - copyExport(m_SelectedGBufferAlbedoMSExport, m_GBuffer->GetMSColorAttachmentID(GBuffer::Albedo), GL_TEXTURE_2D_MULTISAMPLE); - copyExport(m_SelectedGBufferNormalMSExport, m_GBuffer->GetMSColorAttachmentID(GBuffer::Normal), GL_TEXTURE_2D_MULTISAMPLE); - copyExport(m_SelectedGBufferEmissiveMSExport, m_GBuffer->GetMSColorAttachmentID(GBuffer::Emissive), GL_TEXTURE_2D_MULTISAMPLE); - copyExport(m_SelectedVelocityMSExport, m_GBuffer->GetMSColorAttachmentID(GBuffer::Velocity), GL_TEXTURE_2D_MULTISAMPLE); - copyExport(m_SelectedSceneDepthMSExport, m_GBuffer->GetMSDepthAttachmentID(), GL_TEXTURE_2D_MULTISAMPLE); + copyExport(m_SelectedGBufferAlbedoMSExport, m_GBuffer->GetMSColorAttachmentID(GBuffer::Albedo), RendererAPI::TextureTargetType::Texture2DMultisample); + copyExport(m_SelectedGBufferNormalMSExport, m_GBuffer->GetMSColorAttachmentID(GBuffer::Normal), RendererAPI::TextureTargetType::Texture2DMultisample); + copyExport(m_SelectedGBufferEmissiveMSExport, m_GBuffer->GetMSColorAttachmentID(GBuffer::Emissive), RendererAPI::TextureTargetType::Texture2DMultisample); + copyExport(m_SelectedVelocityMSExport, m_GBuffer->GetMSColorAttachmentID(GBuffer::Velocity), RendererAPI::TextureTargetType::Texture2DMultisample); + copyExport(m_SelectedSceneDepthMSExport, m_GBuffer->GetMSDepthAttachmentID(), RendererAPI::TextureTargetType::Texture2DMultisample); } } @@ -226,8 +221,8 @@ namespace OloEngine context.SetBlendState(false); rendererAPI.SetCullFace(RHI::CullMode::Back); rendererAPI.SetPolygonMode(RHI::PolygonMode::Fill); - ::glBindVertexArray(0); - ::glUseProgram(0); + RenderCommand::BindVertexArrayRaw(0); + RenderCommand::BindShaderProgram(0); m_Phase2Packets.clear(); m_Phase2Culls.clear(); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/DeferredLightingPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/DeferredLightingPass.cpp index 584846cf0..671d66758 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/DeferredLightingPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/DeferredLightingPass.cpp @@ -14,12 +14,14 @@ #include "OloEngine/Renderer/Shadow/ShadowMap.h" #include "OloEngine/Renderer/VirtualGeometry/VirtualMeshRegistry.h" -#include - #include namespace OloEngine { + // Draw slot 0 -> colour attachment 0, nothing else. Hoisted to file + // scope so the several blit helpers below share one definition. + static constexpr std::array kAttachment0Only = { 0u }; + namespace { // Must match the `DeferredLightingControls` block layout in @@ -194,8 +196,7 @@ namespace OloEngine ++sceneColorAttachmentCount; } - const GLenum drawBuf = GL_COLOR_ATTACHMENT0; - glNamedFramebufferDrawBuffers(sceneFBID, 1, &drawBuf); + RenderCommand::SetFramebufferDrawAttachments(sceneFBID, kAttachment0Only); context.SetDepthTest(false); context.SetDepthMask(false); @@ -361,11 +362,7 @@ namespace OloEngine // GL_COLOR_ATTACHMENT3 entries. if (sceneColorAttachmentCount > 0) { - std::array fullDrawBufs{}; - const u32 n = std::min(sceneColorAttachmentCount, static_cast(fullDrawBufs.size())); - for (u32 i = 0; i < n; ++i) - fullDrawBufs[i] = GL_COLOR_ATTACHMENT0 + i; - glNamedFramebufferDrawBuffers(sceneFBID, static_cast(n), fullDrawBufs.data()); + RenderCommand::RestoreAllFramebufferDrawAttachments(sceneFBID, sceneColorAttachmentCount); } context.SetDepthTest(true); @@ -380,11 +377,11 @@ namespace OloEngine if (auto const& samplingFB = m_GBuffer->GetSamplingFramebuffer()) { const u32 samplingFBID = samplingFB->GetRendererID(); - glBlitNamedFramebuffer( + RenderCommand::BlitFramebuffer( samplingFBID, sceneFBID, - 0, 0, static_cast(w), static_cast(h), - 0, 0, static_cast(w), static_cast(h), - GL_DEPTH_BUFFER_BIT, GL_NEAREST); + 0, 0, static_cast(w), static_cast(h), + 0, 0, static_cast(w), static_cast(h), + RHI::BlitAspect::Depth, RHI::Filter::Nearest); // Copy the G-Buffer's per-pixel entity-ID attachment (RT4) into // the scene FB's entity-ID attachment (RT1). The forward path @@ -400,13 +397,13 @@ namespace OloEngine // require GL_NEAREST (per the GL 4.6 spec); MSAA → single- // sample resolution takes sample 0, which is correct for // discrete entity IDs. - glNamedFramebufferReadBuffer(samplingFBID, GL_COLOR_ATTACHMENT0 + static_cast(std::to_underlying(GBuffer::EntityID))); - glNamedFramebufferDrawBuffer(sceneFBID, GL_COLOR_ATTACHMENT1); - glBlitNamedFramebuffer( + RenderCommand::SetFramebufferReadAttachment(samplingFBID, static_cast(std::to_underlying(GBuffer::EntityID))); + RenderCommand::SetFramebufferDrawAttachments(sceneFBID, std::array{ 1u }); + RenderCommand::BlitFramebuffer( samplingFBID, sceneFBID, - 0, 0, static_cast(w), static_cast(h), - 0, 0, static_cast(w), static_cast(h), - GL_COLOR_BUFFER_BIT, GL_NEAREST); + 0, 0, static_cast(w), static_cast(h), + 0, 0, static_cast(w), static_cast(h), + RHI::BlitAspect::Color, RHI::Filter::Nearest); // Restore the scene FB's draw-buffer set so the following // ForwardOverlayPass write-RT0-RT2 path still sees its intended @@ -414,11 +411,7 @@ namespace OloEngine // attachments to be available on bind). if (sceneColorAttachmentCount > 0) { - std::array fullDrawBufs{}; - const u32 n = std::min(sceneColorAttachmentCount, static_cast(fullDrawBufs.size())); - for (u32 i = 0; i < n; ++i) - fullDrawBufs[i] = GL_COLOR_ATTACHMENT0 + i; - glNamedFramebufferDrawBuffers(sceneFBID, static_cast(n), fullDrawBufs.data()); + RenderCommand::RestoreAllFramebufferDrawAttachments(sceneFBID, sceneColorAttachmentCount); } } @@ -432,8 +425,8 @@ namespace OloEngine // so downstream passes see a clean slate. The GLStateGuard would // otherwise restore both via ApplyCore() — explicit clears here keep // the safety net pristine so it surfaces only genuine regressions. - ::glBindVertexArray(0); - ::glUseProgram(0); + RenderCommand::BindVertexArrayRaw(0); + RenderCommand::BindShaderProgram(0); } void DeferredLightingPass::BlitVirtualGeometryDebugOverlay() @@ -483,8 +476,7 @@ namespace OloEngine // Scene colour only (RT0). The scene FB also carries entity-id / normals attachments; // writing the overlay into those would corrupt mouse picking with cluster-hash colours. const u32 sceneFBID = m_SceneFramebuffer->GetRendererID(); - const GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 }; - glNamedFramebufferDrawBuffers(sceneFBID, 1, drawBufs); + RenderCommand::SetFramebufferDrawAttachments(sceneFBID, kAttachment0Only); RenderCommand::SetViewport(0, 0, m_SceneFramebuffer->GetSpecification().Width, m_SceneFramebuffer->GetSpecification().Height); @@ -515,15 +507,11 @@ namespace OloEngine } if (colorCount > 0) { - std::array fullDrawBufs{}; - const u32 n = std::min(colorCount, static_cast(fullDrawBufs.size())); - for (u32 i = 0; i < n; ++i) - fullDrawBufs[i] = GL_COLOR_ATTACHMENT0 + i; - glNamedFramebufferDrawBuffers(sceneFBID, static_cast(n), fullDrawBufs.data()); + RenderCommand::RestoreAllFramebufferDrawAttachments(sceneFBID, colorCount); } - ::glBindVertexArray(0); - ::glUseProgram(0); + RenderCommand::BindVertexArrayRaw(0); + RenderCommand::BindShaderProgram(0); } Ref DeferredLightingPass::GetTarget() const diff --git a/OloEngine/src/OloEngine/Renderer/Passes/DeferredOpaqueDecalPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/DeferredOpaqueDecalPass.cpp index 96a529cff..b8f9ec9d2 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/DeferredOpaqueDecalPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/DeferredOpaqueDecalPass.cpp @@ -3,10 +3,9 @@ #include "OloEngine/Renderer/Passes/DecalRenderPass.h" #include "OloEngine/Renderer/RGBuilder.h" +#include "OloEngine/Renderer/RenderCommand.h" #include "OloEngine/Renderer/ResourceHandle.h" -#include - namespace OloEngine { DeferredOpaqueDecalPass::DeferredOpaqueDecalPass() @@ -139,11 +138,9 @@ namespace OloEngine if (exportedTextureID == 0u || exportedTextureID == sourceTextureID) return; - glCopyImageSubData(sourceTextureID, GL_TEXTURE_2D, 0, 0, 0, 0, - exportedTextureID, GL_TEXTURE_2D, 0, 0, 0, 0, - static_cast(m_GBuffer->GetWidth()), - static_cast(m_GBuffer->GetHeight()), - 1); + RenderCommand::CopyImageSubData(sourceTextureID, RendererAPI::TextureTargetType::Texture2D, + exportedTextureID, RendererAPI::TextureTargetType::Texture2D, + m_GBuffer->GetWidth(), m_GBuffer->GetHeight()); }; const auto copyMultisampleGBufferExport = [this, &context](const RGTextureHandle handle, const u32 sourceTextureID) @@ -155,11 +152,9 @@ namespace OloEngine if (exportedTextureID == 0u || exportedTextureID == sourceTextureID) return; - glCopyImageSubData(sourceTextureID, GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, - exportedTextureID, GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, - static_cast(m_GBuffer->GetWidth()), - static_cast(m_GBuffer->GetHeight()), - 1); + RenderCommand::CopyImageSubData(sourceTextureID, RendererAPI::TextureTargetType::Texture2DMultisample, + exportedTextureID, RendererAPI::TextureTargetType::Texture2DMultisample, + m_GBuffer->GetWidth(), m_GBuffer->GetHeight()); }; const u32 albedoID = m_GBuffer->GetColorAttachmentID(GBuffer::Albedo); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/FluidCompositePass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/FluidCompositePass.cpp index c997f246a..86117014a 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/FluidCompositePass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/FluidCompositePass.cpp @@ -11,8 +11,6 @@ #include "OloEngine/Renderer/Renderer3D.h" #include "OloEngine/Renderer/ShaderBindingLayout.h" -#include - #include namespace OloEngine @@ -137,10 +135,9 @@ namespace OloEngine GLStateGuard guard("FluidCompositePass", GLStateGuard::Policy::Ignore); // Snapshot the pre-fluid scene colour for refraction sampling. - glCopyImageSubData( - sceneColorID, GL_TEXTURE_2D, 0, 0, 0, 0, - refractionTexID, GL_TEXTURE_2D, 0, 0, 0, 0, - static_cast(fbWidth), static_cast(fbHeight), 1); + RenderCommand::CopyImageSubData(sceneColorID, RendererAPI::TextureTargetType::Texture2D, + refractionTexID, RendererAPI::TextureTargetType::Texture2D, + fbWidth, fbHeight); // Upload the appearance parameters of this frame's fluid. Counts.z // carries the environment-map-present flag for the reflection branch. diff --git a/OloEngine/src/OloEngine/Renderer/Passes/FluidIntermediatesPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/FluidIntermediatesPass.cpp index a9a6179a6..a9bad6a18 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/FluidIntermediatesPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/FluidIntermediatesPass.cpp @@ -12,9 +12,8 @@ #include "OloEngine/Renderer/Renderer3D.h" #include "OloEngine/Renderer/ShaderBindingLayout.h" #include "OloEngine/Renderer/VertexBuffer.h" -#include "Platform/OpenGL/OpenGLUtilities.h" -#include +#include #include #include @@ -169,9 +168,8 @@ namespace OloEngine // The pass renders into raw pass-owned FBOs, so the viewport must be // set (and restored) by hand — engine Framebuffer::Bind() would // normally do this. - GLint previousViewport[4] = { 0, 0, 0, 0 }; - glGetIntegerv(GL_VIEWPORT, previousViewport); - glViewport(0, 0, static_cast(m_Width), static_cast(m_Height)); + const Viewport previousViewport = RenderCommand::GetViewport(); + RenderCommand::SetViewport(0, 0, m_Width, m_Height); // Scene depth for behind-geometry discard in both splat shaders // (water-identical slot/uniform name so IsKnownTextureBinding passes). @@ -179,21 +177,22 @@ namespace OloEngine auto bindDrawBuffers = [](const FluidRenderData& draw) { - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ShaderBindingLayout::SSBO_FLUID_POSITIONS, draw.PositionsSSBOId); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ShaderBindingLayout::SSBO_FLUID_VELOCITIES, draw.VelocitiesSSBOId); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ShaderBindingLayout::SSBO_FLUID_COUNTERS, draw.CountersSSBOId); + RenderCommand::BindStorageBuffer(ShaderBindingLayout::SSBO_FLUID_POSITIONS, draw.PositionsSSBOId); + RenderCommand::BindStorageBuffer(ShaderBindingLayout::SSBO_FLUID_VELOCITIES, draw.VelocitiesSSBOId); + RenderCommand::BindStorageBuffer(ShaderBindingLayout::SSBO_FLUID_COUNTERS, draw.CountersSSBOId); }; // --- 1. Depth splat: nearest sphere-impostor view depth into A ------ - glBindFramebuffer(GL_FRAMEBUFFER, m_DepthFBO); + RenderCommand::BindFramebuffer(m_DepthFBO); { - // Unbind any stale program for the clears — NVIDIA revalidates the - // bound program against the new FBO during clears (debug id 131218). - Utils::GLClearProgramGuard programGuard; - constexpr f32 kNoFluidSentinel[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; - glClearNamedFramebufferfv(m_DepthFBO, GL_COLOR, 0, kNoFluidSentinel); + // The clear-program guard that used to be constructed here now lives + // inside the backend clear (issue #691 Phase 2 step 2) — it is an + // OpenGL driver hazard, so it is backend knowledge, and keeping it + // here meant including a Platform/OpenGL header from a render pass. + constexpr glm::vec4 kNoFluidSentinel(0.0f); + RenderCommand::ClearFramebufferColorAttachment(m_DepthFBO, 0, kNoFluidSentinel); constexpr f32 kFarDepth = 1.0f; - glClearNamedFramebufferfv(m_DepthFBO, GL_DEPTH, 0, &kFarDepth); + RenderCommand::ClearFramebufferDepth(m_DepthFBO, kFarDepth); } RenderCommand::SetDepthTest(true); @@ -212,11 +211,10 @@ namespace OloEngine } // --- 2. Thickness: additive chord accumulation -------------------- - glBindFramebuffer(GL_FRAMEBUFFER, m_ThicknessFBO); + RenderCommand::BindFramebuffer(m_ThicknessFBO); { - Utils::GLClearProgramGuard programGuard; - constexpr f32 kZero[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; - glClearNamedFramebufferfv(m_ThicknessFBO, GL_COLOR, 0, kZero); + constexpr glm::vec4 kZero(0.0f); + RenderCommand::ClearFramebufferColorAttachment(m_ThicknessFBO, 0, kZero); } RenderCommand::SetDepthTest(false); @@ -233,7 +231,7 @@ namespace OloEngine RenderCommand::DrawIndexedInstanced(m_SplatVAO, 6, draw.ParticleUpperBound); } - glBindFramebuffer(GL_FRAMEBUFFER, 0); + RenderCommand::BindDefaultFramebuffer(); // --- 3. Bilateral smooth: A -> B -> A ------------------------------ // The last-uploaded FluidRenderUBO stays bound; with multiple fluids @@ -264,12 +262,12 @@ namespace OloEngine CommandDispatch::InvalidateRenderStateCache(); context.BindTexture(ShaderBindingLayout::TEX_WATER_DEPTH, 0); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ShaderBindingLayout::SSBO_FLUID_POSITIONS, 0); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ShaderBindingLayout::SSBO_FLUID_VELOCITIES, 0); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ShaderBindingLayout::SSBO_FLUID_COUNTERS, 0); + RenderCommand::BindStorageBuffer(ShaderBindingLayout::SSBO_FLUID_POSITIONS, 0); + RenderCommand::BindStorageBuffer(ShaderBindingLayout::SSBO_FLUID_VELOCITIES, 0); + RenderCommand::BindStorageBuffer(ShaderBindingLayout::SSBO_FLUID_COUNTERS, 0); - glViewport(previousViewport[0], previousViewport[1], - static_cast(previousViewport[2]), static_cast(previousViewport[3])); + RenderCommand::SetViewport(previousViewport.x, previousViewport.y, + previousViewport.width, previousViewport.height); m_LastAppearance = draws.front(); m_RanThisFrame = true; @@ -323,39 +321,35 @@ namespace OloEngine m_Width = width; m_Height = height; - const auto createTexture = [width, height](GLenum internalFormat, GLint filter) + const auto createTexture = [width, height](RHI::Format internalFormat, RHI::Filter filter) { - u32 id = 0; - glCreateTextures(GL_TEXTURE_2D, 1, &id); - glTextureStorage2D(id, 1, internalFormat, - static_cast(width), static_cast(height)); - glTextureParameteri(id, GL_TEXTURE_MIN_FILTER, filter); - glTextureParameteri(id, GL_TEXTURE_MAG_FILTER, filter); - glTextureParameteri(id, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTextureParameteri(id, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + const u32 id = RenderCommand::CreateTexture2D(width, height, internalFormat); + RenderCommand::SetTextureFilter(id, filter, filter); + RenderCommand::SetTextureWrap(id, RHI::AddressMode::ClampToEdge); return id; }; // NEAREST on the depth pair: the smooth compute and the composite's // normal reconstruction both want unfiltered texel values. LINEAR on // thickness: it's a smooth accumulation sampled once per pixel. - m_DepthTexA = createTexture(GL_R32F, GL_NEAREST); - m_DepthTexB = createTexture(GL_R32F, GL_NEAREST); - m_ThicknessTex = createTexture(GL_RG16F, GL_LINEAR); - m_SplatZTex = createTexture(GL_DEPTH_COMPONENT32F, GL_NEAREST); - - glCreateFramebuffers(1, &m_DepthFBO); - glNamedFramebufferTexture(m_DepthFBO, GL_COLOR_ATTACHMENT0, m_DepthTexA, 0); - glNamedFramebufferTexture(m_DepthFBO, GL_DEPTH_ATTACHMENT, m_SplatZTex, 0); - constexpr GLenum kColor0 = GL_COLOR_ATTACHMENT0; - glNamedFramebufferDrawBuffers(m_DepthFBO, 1, &kColor0); - - glCreateFramebuffers(1, &m_ThicknessFBO); - glNamedFramebufferTexture(m_ThicknessFBO, GL_COLOR_ATTACHMENT0, m_ThicknessTex, 0); - glNamedFramebufferDrawBuffers(m_ThicknessFBO, 1, &kColor0); - - if (glCheckNamedFramebufferStatus(m_DepthFBO, GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE || - glCheckNamedFramebufferStatus(m_ThicknessFBO, GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + m_DepthTexA = createTexture(RHI::Format::R32Float, RHI::Filter::Nearest); + m_DepthTexB = createTexture(RHI::Format::R32Float, RHI::Filter::Nearest); + m_ThicknessTex = createTexture(RHI::Format::RG16Float, RHI::Filter::Linear); + m_SplatZTex = createTexture(RHI::Format::D32Float, RHI::Filter::Nearest); + + static constexpr std::array kColor0 = { 0u }; + + m_DepthFBO = RenderCommand::CreateFramebuffer(); + RenderCommand::AttachFramebufferColorTexture(m_DepthFBO, 0, m_DepthTexA, 0); + RenderCommand::AttachFramebufferDepthTexture(m_DepthFBO, m_SplatZTex, 0); + RenderCommand::SetFramebufferDrawAttachments(m_DepthFBO, kColor0); + + m_ThicknessFBO = RenderCommand::CreateFramebuffer(); + RenderCommand::AttachFramebufferColorTexture(m_ThicknessFBO, 0, m_ThicknessTex, 0); + RenderCommand::SetFramebufferDrawAttachments(m_ThicknessFBO, kColor0); + + if (!RenderCommand::IsFramebufferComplete(m_DepthFBO) || + !RenderCommand::IsFramebufferComplete(m_ThicknessFBO)) { OLO_CORE_ERROR("FluidIntermediatesPass: fluid intermediate framebuffers incomplete ({}x{})", width, height); @@ -367,12 +361,12 @@ namespace OloEngine { if (m_DepthFBO != 0) { - glDeleteFramebuffers(1, &m_DepthFBO); + RenderCommand::DeleteFramebuffer(m_DepthFBO); m_DepthFBO = 0; } if (m_ThicknessFBO != 0) { - glDeleteFramebuffers(1, &m_ThicknessFBO); + RenderCommand::DeleteFramebuffer(m_ThicknessFBO); m_ThicknessFBO = 0; } @@ -380,7 +374,7 @@ namespace OloEngine { if (id != 0) { - glDeleteTextures(1, &id); + RenderCommand::DeleteTexture(id); id = 0; } }; diff --git a/OloEngine/src/OloEngine/Renderer/Passes/ForwardOverlayRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/ForwardOverlayRenderPass.cpp index e909cc474..72a3806cb 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/ForwardOverlayRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/ForwardOverlayRenderPass.cpp @@ -4,12 +4,11 @@ #include "OloEngine/Renderer/Debug/GLStateGuard.h" #include "OloEngine/Renderer/RGBuilder.h" #include "OloEngine/Renderer/RGCommandContext.h" +#include "OloEngine/Renderer/RenderCommand.h" #include "OloEngine/Renderer/Renderer.h" #include "OloEngine/Renderer/Renderer3D.h" #include "OloEngine/Renderer/Commands/CommandDispatch.h" -#include - #include namespace OloEngine @@ -116,11 +115,15 @@ namespace OloEngine // Bind attachments 0-2 (clamped to what the scene FB actually has) so // those side buffers are repopulated per-frame. RT3 (velocity) is // intentionally left off: overlay shaders don't track motion vectors. - std::array drawBufs = { - GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 + std::array drawBufs = { + 0u, 1u, 2u }; - if (const GLsizei overlayDrawBufCount = static_cast(std::min(sceneColorAttachmentCount, static_cast(drawBufs.size()))); overlayDrawBufCount > 0) - glNamedFramebufferDrawBuffers(sceneFBID, overlayDrawBufCount, drawBufs.data()); + if (const u32 overlayDrawBufCount = std::min(sceneColorAttachmentCount, static_cast(drawBufs.size())); + overlayDrawBufCount > 0) + { + RenderCommand::SetFramebufferDrawAttachments( + sceneFBID, std::span(drawBufs.data(), overlayDrawBufCount)); + } auto& rendererAPI = RenderCommand::GetRendererAPI(); context.SetDepthTest(true); @@ -147,11 +150,7 @@ namespace OloEngine // attachment count differs from the previous 4-entry hardcoded list. if (sceneColorAttachmentCount > 0) { - std::array fullDrawBufs{}; - const u32 n = std::min(sceneColorAttachmentCount, static_cast(fullDrawBufs.size())); - for (u32 i = 0; i < n; ++i) - fullDrawBufs[i] = GL_COLOR_ATTACHMENT0 + i; - glNamedFramebufferDrawBuffers(sceneFBID, static_cast(n), fullDrawBufs.data()); + RenderCommand::RestoreAllFramebufferDrawAttachments(sceneFBID, sceneColorAttachmentCount); } // Restores cull face + polygon mode too — skybox / debug commands inside @@ -164,15 +163,16 @@ namespace OloEngine // above disables blending but leaves the func sticky. Any downstream // pass that enables blending without setting its own func would inherit // the leak. - ::glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ZERO); + RenderCommand::SetBlendFuncSeparate(RHI::BlendFactor::One, RHI::BlendFactor::Zero, + RHI::BlendFactor::One, RHI::BlendFactor::Zero); m_SceneFramebuffer->Unbind(); // Unbind shader program + VAO so the GLStateGuard surfaces only // genuine regressions in downstream passes (the bucket's last // command leaves both bound). - ::glBindVertexArray(0); - ::glUseProgram(0); + RenderCommand::BindVertexArrayRaw(0); + RenderCommand::BindShaderProgram(0); ResetCommandBucket(); } diff --git a/OloEngine/src/OloEngine/Renderer/Passes/GPUDrivenOcclusionPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/GPUDrivenOcclusionPass.cpp index 67ec947f7..0a44ca6c6 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/GPUDrivenOcclusionPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/GPUDrivenOcclusionPass.cpp @@ -9,8 +9,6 @@ #include "OloEngine/Renderer/RenderCommand.h" #include "OloEngine/Renderer/Commands/CommandDispatch.h" -#include - #include namespace OloEngine @@ -134,11 +132,7 @@ namespace OloEngine m_SceneFramebuffer->Bind(); if (sceneColorAttachmentCount > 0) { - std::array drawBufs{}; - const u32 n = std::min(sceneColorAttachmentCount, static_cast(drawBufs.size())); - for (u32 i = 0; i < n; ++i) - drawBufs[i] = GL_COLOR_ATTACHMENT0 + i; - glNamedFramebufferDrawBuffers(sceneFBID, static_cast(n), drawBufs.data()); + RenderCommand::RestoreAllFramebufferDrawAttachments(sceneFBID, sceneColorAttachmentCount); } context.SetDepthTest(true); context.ResetOpaqueForwardDrawState(); @@ -172,7 +166,7 @@ namespace OloEngine // pipeline; the Hi-Z build samples it as a texture. Order the // framebuffer-write → texture-fetch (UE gets this from RDG; here it // is an explicit GL 4.5 texture barrier). - ::glTextureBarrier(); + RenderCommand::TextureBarrier(); const GPUFrustumCuller::HZBOcclusionInputs currentHZB = Renderer3D::BuildCurrentOcclusionHZB(depthTexID, sceneSpec.Width, sceneSpec.Height); @@ -202,27 +196,28 @@ namespace OloEngine const u32 sceneDepthExportID = m_SelectedSceneDepth.IsValid() ? context.ResolveTexture(m_SelectedSceneDepth) : 0u; if (sceneDepthExportID != 0u && fbDepthID != 0u && sceneDepthExportID != fbDepthID) { - ::glCopyImageSubData(fbDepthID, GL_TEXTURE_2D, 0, 0, 0, 0, - sceneDepthExportID, GL_TEXTURE_2D, 0, 0, 0, 0, - static_cast(sceneSpec.Width), static_cast(sceneSpec.Height), 1); + RenderCommand::CopyImageSubData(fbDepthID, RendererAPI::TextureTargetType::Texture2D, + sceneDepthExportID, 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) { - ::glCopyImageSubData(fbNormalsID, GL_TEXTURE_2D, 0, 0, 0, 0, - sceneNormalsExportID, GL_TEXTURE_2D, 0, 0, 0, 0, - static_cast(sceneSpec.Width), static_cast(sceneSpec.Height), 1); + RenderCommand::CopyImageSubData(fbNormalsID, RendererAPI::TextureTargetType::Texture2D, + sceneNormalsExportID, RendererAPI::TextureTargetType::Texture2D, + sceneSpec.Width, sceneSpec.Height); } } context.ResetOpaqueForwardDrawState(); - ::glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ZERO); + RenderCommand::SetBlendFuncSeparate(RHI::BlendFactor::One, RHI::BlendFactor::Zero, + RHI::BlendFactor::One, RHI::BlendFactor::Zero); m_SceneFramebuffer->Unbind(); - ::glBindVertexArray(0); - ::glUseProgram(0); + RenderCommand::BindVertexArrayRaw(0); + RenderCommand::BindShaderProgram(0); m_Phase2Packets.clear(); m_Phase2Culls.clear(); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/GTAORenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/GTAORenderPass.cpp index 5b5abc19a..af5c6888f 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/GTAORenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/GTAORenderPass.cpp @@ -5,8 +5,6 @@ #include "OloEngine/Renderer/RenderCommand.h" #include "OloEngine/Renderer/ShaderBindingLayout.h" -#include - namespace OloEngine { // Hilbert curve LUT: maps (x,y) in a 64×64 grid to a 1D index. @@ -332,10 +330,9 @@ namespace OloEngine MemoryBarrierFlags::TextureFetch | MemoryBarrierFlags::TextureUpdate); - glCopyImageSubData( - finalAOTextureID, GL_TEXTURE_2D, 0, 0, 0, 0, - aoOutputTexID, GL_TEXTURE_2D, 0, 0, 0, 0, - static_cast(m_Width), static_cast(m_Height), 1); + RenderCommand::CopyImageSubData(finalAOTextureID, RendererAPI::TextureTargetType::Texture2D, + aoOutputTexID, RendererAPI::TextureTargetType::Texture2D, + m_Width, m_Height); } } diff --git a/OloEngine/src/OloEngine/Renderer/Passes/OITPrepareRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/OITPrepareRenderPass.cpp index 3e1f37509..9349ce17f 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/OITPrepareRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/OITPrepareRenderPass.cpp @@ -4,8 +4,8 @@ #include "OloEngine/Renderer/Framebuffer.h" #include "OloEngine/Renderer/RGBuilder.h" #include "OloEngine/Renderer/RGCommandContext.h" +#include "OloEngine/Renderer/RenderCommand.h" -#include #include #include @@ -15,8 +15,8 @@ namespace OloEngine { namespace { - constexpr GLint kOITAccumAttachmentIndex = 0; - constexpr GLint kOITRevealageAttachmentIndex = 1; + constexpr u32 kOITAccumAttachmentIndex = 0; + constexpr u32 kOITRevealageAttachmentIndex = 1; [[nodiscard]] bool HasBlitCompatibleDepth(const Ref& framebuffer) { @@ -126,10 +126,10 @@ namespace OloEngine const auto oitFramebufferID = oitFramebuffer->GetRendererID(); const glm::vec4 accumClear(0.0f, 0.0f, 0.0f, 0.0f); - glClearNamedFramebufferfv(oitFramebufferID, GL_COLOR, kOITAccumAttachmentIndex, glm::value_ptr(accumClear)); + RenderCommand::ClearFramebufferColorAttachment(oitFramebufferID, kOITAccumAttachmentIndex, accumClear); const glm::vec4 revealageClear(1.0f, 0.0f, 0.0f, 0.0f); - glClearNamedFramebufferfv(oitFramebufferID, GL_COLOR, kOITRevealageAttachmentIndex, glm::value_ptr(revealageClear)); + RenderCommand::ClearFramebufferColorAttachment(oitFramebufferID, kOITRevealageAttachmentIndex, revealageClear); bool seededFromSceneDepth = false; if (sceneFramebuffer && sceneFramebuffer->GetRendererID() != 0 && @@ -138,18 +138,18 @@ namespace OloEngine const auto& sceneSpec = sceneFramebuffer->GetSpecification(); if (sceneSpec.Width == oitSpec.Width && sceneSpec.Height == oitSpec.Height) { - glBlitNamedFramebuffer(sceneFramebuffer->GetRendererID(), oitFramebufferID, - 0, 0, static_cast(oitSpec.Width), static_cast(oitSpec.Height), - 0, 0, static_cast(oitSpec.Width), static_cast(oitSpec.Height), - GL_DEPTH_BUFFER_BIT, GL_NEAREST); + RenderCommand::BlitFramebuffer(sceneFramebuffer->GetRendererID(), oitFramebufferID, + 0, 0, static_cast(oitSpec.Width), static_cast(oitSpec.Height), + 0, 0, static_cast(oitSpec.Width), static_cast(oitSpec.Height), + RHI::BlitAspect::Depth, RHI::Filter::Nearest); seededFromSceneDepth = true; } } if (!seededFromSceneDepth) { - const GLfloat depthClear = 1.0f; - glClearNamedFramebufferfv(oitFramebufferID, GL_DEPTH, 0, &depthClear); + constexpr f32 depthClear = 1.0f; + RenderCommand::ClearFramebufferDepth(oitFramebufferID, depthClear); } } } // namespace OloEngine diff --git a/OloEngine/src/OloEngine/Renderer/Passes/OITResolveRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/OITResolveRenderPass.cpp index 38a863035..fecdeaae6 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/OITResolveRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/OITResolveRenderPass.cpp @@ -9,8 +9,6 @@ #include "OloEngine/Renderer/RenderPipelineBuilderInternal.h" #include "OloEngine/Renderer/ShaderBindingLayout.h" -#include - #include #include #include diff --git a/OloEngine/src/OloEngine/Renderer/Passes/PlanarReflectionRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/PlanarReflectionRenderPass.cpp index f2b1003b6..ae655eb3d 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/PlanarReflectionRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/PlanarReflectionRenderPass.cpp @@ -11,8 +11,6 @@ #include "OloEngine/Renderer/Debug/GLStateGuard.h" #include "OloEngine/Renderer/ShaderBindingLayout.h" -#include - namespace OloEngine { PlanarReflectionRenderPass::PlanarReflectionRenderPass() @@ -187,7 +185,7 @@ namespace OloEngine // A reflection reverses handedness, so the geometry's front faces now wind // clockwise — declare CW the front winding for the replay so back-face // culling still removes the correct triangles. - ::glFrontFace(GL_CW); + RenderCommand::SetFrontFace(RHI::FrontFace::Clockwise); // Re-establish shared scene resources the scene pass left bound (camera // UBO binding, shadow maps, IBL) and replay the already-batched opaque @@ -195,7 +193,7 @@ namespace OloEngine CommandDispatch::BindSceneResources(); m_ScenePass->GetCommandBucket().Execute(rendererAPI); - ::glFrontFace(GL_CCW); + RenderCommand::SetFrontFace(RHI::FrontFace::CounterClockwise); m_ReflectionFB->Unbind(); // Put back everything the mirror replay reconfigured (depth test/func/mask, diff --git a/OloEngine/src/OloEngine/Renderer/Passes/SSAORenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/SSAORenderPass.cpp index 455fe953e..ab0683f62 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/SSAORenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/SSAORenderPass.cpp @@ -7,8 +7,6 @@ #include "OloEngine/Renderer/MeshPrimitives.h" #include "OloEngine/Renderer/ShaderBindingLayout.h" -#include - #include #include @@ -243,10 +241,9 @@ namespace OloEngine if (const u32 blurredAOTextureID = blurFB->GetColorAttachmentRendererID(0); blurredAOTextureID != 0 && blurredAOTextureID != aoOutputTexID) { - glCopyImageSubData( - blurredAOTextureID, GL_TEXTURE_2D, 0, 0, 0, 0, - aoOutputTexID, GL_TEXTURE_2D, 0, 0, 0, 0, - static_cast(m_HalfWidth), static_cast(m_HalfHeight), 1); + RenderCommand::CopyImageSubData(blurredAOTextureID, RendererAPI::TextureTargetType::Texture2D, + aoOutputTexID, RendererAPI::TextureTargetType::Texture2D, + m_HalfWidth, m_HalfHeight); } // Restore full-res viewport (will be set by next pass anyway, but be clean) diff --git a/OloEngine/src/OloEngine/Renderer/Passes/SceneRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/SceneRenderPass.cpp index 077c774d3..9c6c2b387 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/SceneRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/SceneRenderPass.cpp @@ -15,10 +15,12 @@ #include "OloEngine/Renderer/Passes/DecalRenderPass.h" #include "OloEngine/Renderer/ShaderBindingLayout.h" -#include - namespace OloEngine { + // Draw slot 0 -> colour attachment 0, nothing else. Hoisted to file + // scope so the several blit helpers below share one definition. + static constexpr std::array kAttachment0Only = { 0u }; + SceneRenderPass::SceneRenderPass() { SetName("SceneRenderPass"); @@ -418,11 +420,9 @@ namespace OloEngine if (exportedTextureID == 0u || exportedTextureID == sourceTextureID) return; - glCopyImageSubData(sourceTextureID, GL_TEXTURE_2D, 0, 0, 0, 0, - exportedTextureID, GL_TEXTURE_2D, 0, 0, 0, 0, - static_cast(m_FramebufferSpec.Width), - static_cast(m_FramebufferSpec.Height), - 1); + RenderCommand::CopyImageSubData(sourceTextureID, RendererAPI::TextureTargetType::Texture2D, + exportedTextureID, RendererAPI::TextureTargetType::Texture2D, + m_FramebufferSpec.Width, m_FramebufferSpec.Height); }; const u32 sourceDepthID = deferredActive && m_GBuffer @@ -586,11 +586,6 @@ namespace OloEngine if (!isDepth && att.TextureFormat != FramebufferTextureFormat::None) ++targetColorCount; } - std::array fullDrawBufs{}; - const u32 fullN = std::min(targetColorCount, static_cast(fullDrawBufs.size())); - for (u32 i = 0; i < fullN; ++i) - fullDrawBufs[i] = GL_COLOR_ATTACHMENT0 + i; - // Channel 3 (RMA) needs data from TWO attachments — RT0.a (metallic) // and RT1.zw (roughness, AO). glBlitFramebuffer cannot swizzle, so // use a dedicated fullscreen shader for this one channel. @@ -604,8 +599,7 @@ namespace OloEngine m_Target->Bind(); const u32 dstFB = m_Target->GetRendererID(); - const GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 }; - glNamedFramebufferDrawBuffers(dstFB, 1, drawBufs); + RenderCommand::SetFramebufferDrawAttachments(dstFB, kAttachment0Only); const u32 w = m_GBuffer->GetWidth(); const u32 h = m_GBuffer->GetHeight(); @@ -628,24 +622,24 @@ namespace OloEngine // downstream passes (post-process, UI) find the expected slots // (including RT3 velocity for TAA). Count is computed from the // FB spec above rather than hardcoded. - glNamedFramebufferDrawBuffers(dstFB, static_cast(fullN), fullDrawBufs.data()); + RenderCommand::RestoreAllFramebufferDrawAttachments(dstFB, targetColorCount); RenderCommand::SetDepthMask(true); RenderCommand::SetDepthTest(true); // Copy depth across so selection-outline / UI still depth-test. const u32 srcFB = m_GBuffer->GetSamplingFramebuffer()->GetRendererID(); - glBlitNamedFramebuffer( + RenderCommand::BlitFramebuffer( srcFB, dstFB, - 0, 0, static_cast(w), static_cast(h), - 0, 0, static_cast(w), static_cast(h), - GL_DEPTH_BUFFER_BIT, GL_NEAREST); + 0, 0, static_cast(w), static_cast(h), + 0, 0, static_cast(w), static_cast(h), + RHI::BlitAspect::Depth, RHI::Filter::Nearest); // Unbind the blit shader + VAO so the RAII guard sees us leave // shader/program/VAO state at zero, matching entry expectations // for downstream passes that rebind their own. - ::glUseProgram(0); - ::glBindVertexArray(0); + RenderCommand::BindShaderProgram(0); + RenderCommand::BindVertexArrayRaw(0); return; } @@ -675,37 +669,35 @@ namespace OloEngine const u32 h = m_GBuffer->GetHeight(); // Select source attachment on the read FB and destination attachment 0 - // on the draw FB. glBlitNamedFramebuffer requires both FBs to have - // the read/draw buffers pre-selected; do so via DSA. - const GLenum srcAttach = GL_COLOR_ATTACHMENT0 + attachmentIndex; - glNamedFramebufferReadBuffer(srcFB, srcAttach); - const GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 }; - glNamedFramebufferDrawBuffers(dstFB, 1, drawBufs); - - glBlitNamedFramebuffer( + // on the draw FB. A framebuffer blit requires both FBs to have their + // read / draw attachments pre-selected. + RenderCommand::SetFramebufferReadAttachment(srcFB, attachmentIndex); + RenderCommand::SetFramebufferDrawAttachments(dstFB, kAttachment0Only); + + RenderCommand::BlitFramebuffer( srcFB, dstFB, - 0, 0, static_cast(w), static_cast(h), - 0, 0, static_cast(w), static_cast(h), - GL_COLOR_BUFFER_BIT, GL_NEAREST); + 0, 0, static_cast(w), static_cast(h), + 0, 0, static_cast(w), static_cast(h), + RHI::BlitAspect::Color, RHI::Filter::Nearest); // Restore the draw FB's draw-buffer list using the count captured // from the target FB spec above — narrowing to fewer attachments // would drop later-shader outputs (e.g. PBR_MultiLight's motion // vector at layout(location=3)), breaking TAA/MotionBlur. - glNamedFramebufferDrawBuffers(dstFB, static_cast(fullN), fullDrawBufs.data()); + RenderCommand::RestoreAllFramebufferDrawAttachments(dstFB, targetColorCount); // Also copy depth so downstream passes (post-process, selection // outline, UI) have a coherent depth buffer. - glBlitNamedFramebuffer( + RenderCommand::BlitFramebuffer( srcFB, dstFB, - 0, 0, static_cast(w), static_cast(h), - 0, 0, static_cast(w), static_cast(h), - GL_DEPTH_BUFFER_BIT, GL_NEAREST); + 0, 0, static_cast(w), static_cast(h), + 0, 0, static_cast(w), static_cast(h), + RHI::BlitAspect::Depth, RHI::Filter::Nearest); // Reset the G-Buffer's read buffer to attachment 0 so any downstream // read on that FB picks up a deterministic default instead of the // last debug channel we selected. - glNamedFramebufferReadBuffer(srcFB, GL_COLOR_ATTACHMENT0); + RenderCommand::SetFramebufferReadAttachment(srcFB, 0); } void SceneRenderPass::BlitForwardVelocityDebug() @@ -743,28 +735,22 @@ namespace OloEngine if (!isDepth && att.TextureFormat != FramebufferTextureFormat::None) ++colorCount; } - std::array prevDrawBufs{}; - const u32 n = std::min(colorCount, static_cast(prevDrawBufs.size())); - for (u32 i = 0; i < n; ++i) - prevDrawBufs[i] = GL_COLOR_ATTACHMENT0 + i; - - glNamedFramebufferReadBuffer(fb, GL_COLOR_ATTACHMENT3); - const GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 }; - glNamedFramebufferDrawBuffers(fb, 1, drawBufs); + RenderCommand::SetFramebufferReadAttachment(fb, 3); + RenderCommand::SetFramebufferDrawAttachments(fb, kAttachment0Only); - glBlitNamedFramebuffer( + RenderCommand::BlitFramebuffer( fb, fb, - 0, 0, static_cast(w), static_cast(h), - 0, 0, static_cast(w), static_cast(h), - GL_COLOR_BUFFER_BIT, GL_NEAREST); + 0, 0, static_cast(w), static_cast(h), + 0, 0, static_cast(w), static_cast(h), + RHI::BlitAspect::Color, RHI::Filter::Nearest); // Restore the scene FB's full multi-attachment draw-buffer list for // downstream passes (post-process, UI composite); see comment above. - glNamedFramebufferDrawBuffers(fb, static_cast(n), prevDrawBufs.data()); + RenderCommand::RestoreAllFramebufferDrawAttachments(fb, colorCount); // Reset the read buffer selection so subsequent reads on this FB // see the default (attachment 0) rather than the velocity slot. - glNamedFramebufferReadBuffer(fb, GL_COLOR_ATTACHMENT0); + RenderCommand::SetFramebufferReadAttachment(fb, 0); } void SceneRenderPass::OnReset() diff --git a/OloEngine/src/OloEngine/Renderer/Passes/WaterRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/WaterRenderPass.cpp index f55a1d6d9..ed28e0292 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/WaterRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/WaterRenderPass.cpp @@ -9,9 +9,6 @@ #include "OloEngine/Renderer/Renderer.h" #include "OloEngine/Renderer/Renderer3D.h" #include "OloEngine/Renderer/ShaderBindingLayout.h" -#include "Platform/OpenGL/OpenGLUtilities.h" - -#include namespace OloEngine { @@ -163,10 +160,9 @@ namespace OloEngine return; } - glCopyImageSubData( - sceneColorID, GL_TEXTURE_2D, 0, 0, 0, 0, - refractionTexID, GL_TEXTURE_2D, 0, 0, 0, 0, - static_cast(fbWidth), static_cast(fbHeight), 1); + RenderCommand::CopyImageSubData(sceneColorID, RendererAPI::TextureTargetType::Texture2D, + refractionTexID, RendererAPI::TextureTargetType::Texture2D, + fbWidth, fbHeight); m_SceneFramebuffer->Bind(); @@ -208,14 +204,12 @@ namespace OloEngine if (m_WaterDepthFB) { m_WaterDepthFB->Bind(); - glDepthMask(GL_TRUE); - glClearDepth(1.0); - { - // Unbind any stale program for the clear — NVIDIA revalidates - // the bound program against the new FBO during glClear (id 131218). - Utils::GLClearProgramGuard programGuard; - glClear(GL_DEPTH_BUFFER_BIT); // far = "no water at this pixel" - } + RenderCommand::SetDepthMask(true); + RenderCommand::SetClearDepth(1.0f); + // ClearDepthOnly() carries the clear-program guard inside the + // backend (NVIDIA revalidates the bound program against the new FBO + // during a clear, debug id 131218). + RenderCommand::ClearDepthOnly(); // far = "no water at this pixel" CommandDispatch::SetWaterDepthCaptureActive(true); m_CommandBucket.Execute(rendererAPI); CommandDispatch::SetWaterDepthCaptureActive(false); diff --git a/OloEngine/src/OloEngine/Renderer/RGCommandContext.cpp b/OloEngine/src/OloEngine/Renderer/RGCommandContext.cpp index a8b41e535..286c70966 100644 --- a/OloEngine/src/OloEngine/Renderer/RGCommandContext.cpp +++ b/OloEngine/src/OloEngine/Renderer/RGCommandContext.cpp @@ -4,8 +4,6 @@ #include "OloEngine/Renderer/RenderCommand.h" #include "OloEngine/Renderer/RenderGraph.h" -#include - namespace OloEngine { void RGCommandContext::SetViewport(const u32 x, const u32 y, const u32 width, const u32 height) const @@ -115,21 +113,18 @@ namespace OloEngine void RGCommandContext::BeginAsyncBatch(const u32 batchIndex) const { // GL 4.6 runs a single command stream — no true async queue overlap. - // Insert a KHR_debug group label so the batch region is visible in - // RenderDoc / Nsight. The guard prevents crashes in headless / test - // contexts where glad has not been initialised. - if (GLAD_GL_KHR_debug) - { - const std::string label = "AsyncBatch[" + std::to_string(batchIndex) + "]"; - glPushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, batchIndex, - static_cast(label.size()), label.c_str()); - } + // Insert a debug group label so the batch region is visible in + // RenderDoc / Nsight. The backend no-ops when the capability is absent + // (or when no device is up), which is why the GLAD_GL_KHR_debug probe + // that used to guard this is gone — a loader-symbol test is not a + // portable way to ask "does this backend support debug markers". + const std::string label = "AsyncBatch[" + std::to_string(batchIndex) + "]"; + RenderCommand::PushDebugGroup(batchIndex, label); } void RGCommandContext::EndAsyncBatch([[maybe_unused]] const u32 batchIndex) const { - if (GLAD_GL_KHR_debug) - glPopDebugGroup(); + RenderCommand::PopDebugGroup(); } u32 RGCommandContext::ResolveTexture(const RGTextureHandle handle) const diff --git a/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h b/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h index 3d98fe8e0..2a11732d4 100644 --- a/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h +++ b/OloEngine/src/OloEngine/Renderer/RHI/RHIResources.h @@ -109,17 +109,9 @@ namespace OloEngine::RHI return (static_cast(value) & static_cast(flag)) != 0u; } - // Where the memory lives, expressed as intent rather than as a heap index. - // The GL backend maps these onto buffer-storage flags; a Vulkan backend maps - // them onto VMA usage hints. Naming them by intent is what keeps the choice - // reviewable — "this buffer is written once per frame by the CPU" is a fact - // about the engine, "VK_MEMORY_PROPERTY_HOST_COHERENT_BIT" is not. - enum class MemoryResidency : u8 - { - DeviceLocal = 0, ///< GPU-only; upload via a transfer - HostToDevice, ///< CPU writes each frame, GPU reads (per-frame UBOs) - DeviceToHost, ///< GPU writes, CPU reads back (readback, queries) - }; + // MemoryResidency MOVED to RHITypes.h in Phase 2 step 2. It turned out to be + // vocabulary rather than resource description: RendererAPI::AllocateBufferStorage + // needs it, and RendererAPI.h includes only RHITypes.h. See the note there. struct BufferDesc { diff --git a/OloEngine/src/OloEngine/Renderer/RHI/RHITypes.h b/OloEngine/src/OloEngine/Renderer/RHI/RHITypes.h index ce9246524..ddfa7e7bd 100644 --- a/OloEngine/src/OloEngine/Renderer/RHI/RHITypes.h +++ b/OloEngine/src/OloEngine/Renderer/RHI/RHITypes.h @@ -448,4 +448,81 @@ namespace OloEngine::RHI Compute, Transfer, }; + + // ------------------------------------------------------------------------- + // Added in Phase 2 step 2 (the call-site sweep) — ADR 0011 amendment (10). + // + // Step 1 converted the facade's existing vocabulary; step 2 discovered the + // facade was also INCOMPLETE. 84 distinct GL entry points appear at the 313 + // swept call sites and roughly 60% of them had no RendererAPI equivalent at + // all — whole categories (buffer lifecycle, named-framebuffer state, + // queries, fences) that every pass reached past the facade to perform. The + // enums below are the neutral vocabulary those ~60 new virtuals needed. + // ------------------------------------------------------------------------- + + // The two query kinds the engine actually issues: OcclusionQueryPool's + // visibility test and PrecipitationSystem's GPU timer. Deliberately NOT a + // mirror of GL's query-target space — that would re-export GL under a new + // spelling, the mistake amendment (3) called out for SetTextureParameter. + enum class QueryType : u8 + { + OcclusionAnySamples = 0, ///< GL_ANY_SAMPLES_PASSED / VK_QUERY_TYPE_OCCLUSION + TimeElapsed, ///< GL_TIME_ELAPSED / a VK_QUERY_TYPE_TIMESTAMP pair + }; + + // The four outcomes of a client-side fence wait. Mirrors glClientWaitSync's + // return set; a Vulkan backend folds VK_SUCCESS into ConditionSatisfied and + // VK_TIMEOUT into TimeoutExpired. AlreadySignaled is kept distinct from + // ConditionSatisfied because the caller uses it to skip a flush. + enum class FenceStatus : u8 + { + AlreadySignaled = 0, + ConditionSatisfied, + TimeoutExpired, + Failed, + }; + + // Which aspect(s) of a framebuffer a blit moves. Colour and depth are never + // combined at any call site in the engine (an MRT resolve must select one + // read/draw attachment pair at a time), so this is a plain enum rather than + // a flag set. + enum class BlitAspect : u8 + { + Color = 0, + Depth, + Stencil, + DepthStencil, + }; + + // Where a buffer's memory lives, expressed as intent rather than as a heap + // index. The GL backend maps these onto buffer-storage usage hints; a Vulkan + // backend maps them onto VMA usage hints. Naming them by intent is what keeps + // the choice reviewable — "this buffer is written once per frame by the CPU" + // is a fact about the engine, "VK_MEMORY_PROPERTY_HOST_COHERENT_BIT" is not. + // + // MOVED here from RHIResources.h in Phase 2 step 2. Phase 1 had already + // designed exactly this and put it next to BufferDesc, where nothing outside + // the (then declaration-only) resource header could reach it; the sweep + // started to reinvent it as a "BufferUsage" access-pattern enum and only the + // resulting NAME COLLISION with RHIResources.h's bind-flag BufferUsage + // surfaced the duplication. Recorded because the near-miss is the lesson: + // when a phase leaves a declaration-only header, later phases must read it + // for vocabulary they are about to invent, not just for the types they + // consume. RendererAPI.h includes only RHITypes.h, which is why it lives + // here now rather than being reachable only alongside BufferDesc. + enum class MemoryResidency : u8 + { + DeviceLocal = 0, ///< GPU-only; GPU writes and reads (compute output, copy target) + HostToDevice, ///< CPU writes each frame, GPU reads (per-frame UBOs, upload arenas) + DeviceToHost, ///< GPU writes, CPU reads back (readback staging, query results) + }; + + // "This draw slot writes nowhere" in a framebuffer draw-attachment list. + // + // Not expressible as an attachment index, and BOTH backends need it: + // GL spells it GL_NONE inside glNamedFramebufferDrawBuffers, Vulkan spells + // it VK_ATTACHMENT_UNUSED inside VkSubpassDescription::pColorAttachments. + // DecalRenderPass depends on it to steer one decal into exactly one + // G-Buffer attachment while leaving the others untouched. + inline constexpr u32 NoAttachment = std::numeric_limits::max(); } // namespace OloEngine::RHI diff --git a/OloEngine/src/OloEngine/Renderer/ReflectionProbeBaker.cpp b/OloEngine/src/OloEngine/Renderer/ReflectionProbeBaker.cpp index d595914e8..3b7cebc6a 100644 --- a/OloEngine/src/OloEngine/Renderer/ReflectionProbeBaker.cpp +++ b/OloEngine/src/OloEngine/Renderer/ReflectionProbeBaker.cpp @@ -6,13 +6,13 @@ #include "OloEngine/Renderer/Camera/Camera.h" #include "OloEngine/Renderer/EnvironmentMap.h" #include "OloEngine/Renderer/Framebuffer.h" +#include "OloEngine/Renderer/RenderCommand.h" #include "OloEngine/Renderer/Renderer3D.h" #include "OloEngine/Renderer/ResourceHandle.h" #include "OloEngine/Renderer/TextureCubemap.h" #include "OloEngine/Scene/Components.h" #include "OloEngine/Scene/Scene.h" -#include #include #include @@ -179,11 +179,13 @@ namespace OloEngine scene->RenderScene3D(captureCamera, transform); // Read back this face's lit HDR radiance from the graph's - // SceneColor RT0. glGetTextureImage reads the texture directly + // SceneColor RT0. The readback reads the texture directly // (no FBO-bound restriction) and lets the driver pick its path. - glGetTextureImage(colorAttachmentID, 0, GL_RGBA, GL_FLOAT, - static_cast(faceBytes), - pixelBuffer.data()); + if (!RenderCommand::ReadTextureImage(colorAttachmentID, 0, RHI::Format::RGBA32Float, + faceBytes, pixelBuffer.data())) + { + OLO_CORE_WARN("ReflectionProbeBaker: cubemap face readback failed"); + } // Upload into the cubemap face. SetFaceData triggers // glGenerateTextureMipmap on every call; redundant on faces diff --git a/OloEngine/src/OloEngine/Renderer/RenderCommand.h b/OloEngine/src/OloEngine/Renderer/RenderCommand.h index 01a578d59..2e3830b10 100644 --- a/OloEngine/src/OloEngine/Renderer/RenderCommand.h +++ b/OloEngine/src/OloEngine/Renderer/RenderCommand.h @@ -417,6 +417,353 @@ namespace OloEngine return s_RendererAPI->SupportsInt64ShaderAtomics(); } + // ===================================================================== + // Phase 2 step 2 additions (issue #691). One-line forwarders, same as + // everything above — see RendererAPI.h for the shape rationale and + // ADR 0011's "Amendments from Phase 2 step 2" for the design. + // ===================================================================== + + static void BindUniformBuffer(u32 bindingPoint, u32 bufferID) + { + s_RendererAPI->BindUniformBuffer(bindingPoint, bufferID); + } + + static void BindStorageBuffer(u32 bindingPoint, u32 bufferID) + { + s_RendererAPI->BindStorageBuffer(bindingPoint, bufferID); + } + + static void BindShaderProgram(u32 programID) + { + s_RendererAPI->BindShaderProgram(programID); + } + + static void BindVertexArrayRaw(u32 vaoID) + { + s_RendererAPI->BindVertexArrayRaw(vaoID); + } + + static void BindFramebuffer(u32 framebufferID) + { + s_RendererAPI->BindFramebuffer(framebufferID); + } + + static void DrawBoundIndexed(RHI::PrimitiveTopology topology, u32 indexCount, + RHI::IndexType indexType = RHI::IndexType::UInt32, u32 baseIndex = 0) + { + s_RendererAPI->DrawBoundIndexed(topology, indexCount, indexType, baseIndex); + } + + static void DrawBoundIndexedInstanced(RHI::PrimitiveTopology topology, u32 indexCount, + RHI::IndexType indexType, u32 baseIndex, u32 instanceCount) + { + s_RendererAPI->DrawBoundIndexedInstanced(topology, indexCount, indexType, baseIndex, instanceCount); + } + + static void DrawBoundArrays(RHI::PrimitiveTopology topology, u32 firstVertex, u32 vertexCount) + { + s_RendererAPI->DrawBoundArrays(topology, firstVertex, vertexCount); + } + + static void SetPatchVertexCount(u32 patchVertices) + { + s_RendererAPI->SetPatchVertexCount(patchVertices); + } + + static void SetFrontFace(RHI::FrontFace face) + { + s_RendererAPI->SetFrontFace(face); + } + + static void SetBlendFuncSeparate(RHI::BlendFactor srcRGB, RHI::BlendFactor dstRGB, + RHI::BlendFactor srcAlpha, RHI::BlendFactor dstAlpha) + { + s_RendererAPI->SetBlendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha); + } + + static void SetClearDepth(f32 depth) + { + s_RendererAPI->SetClearDepth(depth); + } + + // Named framebuffers + static u32 CreateFramebuffer() + { + return s_RendererAPI->CreateFramebuffer(); + } + + static void DeleteFramebuffer(u32 framebufferID) + { + s_RendererAPI->DeleteFramebuffer(framebufferID); + } + + static void AttachFramebufferColorTexture(u32 framebufferID, u32 attachmentIndex, + u32 textureID, u32 mipLevel = 0) + { + s_RendererAPI->AttachFramebufferColorTexture(framebufferID, attachmentIndex, textureID, mipLevel); + } + + static void AttachFramebufferDepthTexture(u32 framebufferID, u32 textureID, u32 mipLevel = 0) + { + s_RendererAPI->AttachFramebufferDepthTexture(framebufferID, textureID, mipLevel); + } + + [[nodiscard("Store this!")]] static bool IsFramebufferComplete(u32 framebufferID) + { + return s_RendererAPI->IsFramebufferComplete(framebufferID); + } + + static void SetFramebufferDrawAttachments(u32 framebufferID, std::span attachmentIndices) + { + s_RendererAPI->SetFramebufferDrawAttachments(framebufferID, attachmentIndices); + } + + static void RestoreAllFramebufferDrawAttachments(u32 framebufferID, u32 colorAttachmentCount) + { + s_RendererAPI->RestoreAllFramebufferDrawAttachments(framebufferID, colorAttachmentCount); + } + + static void SetFramebufferReadAttachment(u32 framebufferID, u32 attachmentIndex) + { + s_RendererAPI->SetFramebufferReadAttachment(framebufferID, attachmentIndex); + } + + static void ClearFramebufferColorAttachment(u32 framebufferID, u32 attachmentIndex, const glm::vec4& color) + { + s_RendererAPI->ClearFramebufferColorAttachment(framebufferID, attachmentIndex, color); + } + + static void ClearFramebufferDepth(u32 framebufferID, f32 depth) + { + s_RendererAPI->ClearFramebufferDepth(framebufferID, depth); + } + + static void BlitFramebuffer(u32 srcFramebufferID, u32 dstFramebufferID, + i32 srcX0, i32 srcY0, i32 srcX1, i32 srcY1, + i32 dstX0, i32 dstY0, i32 dstX1, i32 dstY1, + RHI::BlitAspect aspect, RHI::Filter filter = RHI::Filter::Nearest) + { + s_RendererAPI->BlitFramebuffer(srcFramebufferID, dstFramebufferID, + srcX0, srcY0, srcX1, srcY1, + dstX0, dstY0, dstX1, dstY1, aspect, filter); + } + + // Raw buffers + static u32 CreateBuffer() + { + return s_RendererAPI->CreateBuffer(); + } + + static void DeleteBuffer(u32 bufferID) + { + s_RendererAPI->DeleteBuffer(bufferID); + } + + static void AllocateBufferStorage(u32 bufferID, u64 sizeBytes, RHI::MemoryResidency residency) + { + s_RendererAPI->AllocateBufferStorage(bufferID, sizeBytes, residency); + } + + static void* AllocatePersistentUploadStorage(u32 bufferID, u64 sizeBytes) + { + return s_RendererAPI->AllocatePersistentUploadStorage(bufferID, sizeBytes); + } + + static void UnmapBuffer(u32 bufferID) + { + s_RendererAPI->UnmapBuffer(bufferID); + } + + static void UploadBufferSubData(u32 bufferID, u64 offsetBytes, u64 sizeBytes, const void* data) + { + s_RendererAPI->UploadBufferSubData(bufferID, offsetBytes, sizeBytes, data); + } + + static void ReadBufferSubData(u32 bufferID, u64 offsetBytes, u64 sizeBytes, void* dest) + { + s_RendererAPI->ReadBufferSubData(bufferID, offsetBytes, sizeBytes, dest); + } + + static void CopyBufferSubData(u32 srcBufferID, u32 dstBufferID, + u64 srcOffsetBytes, u64 dstOffsetBytes, u64 sizeBytes) + { + s_RendererAPI->CopyBufferSubData(srcBufferID, dstBufferID, srcOffsetBytes, dstOffsetBytes, sizeBytes); + } + + static void ClearBufferUInt(u32 bufferID, u32 value) + { + s_RendererAPI->ClearBufferUInt(bufferID, value); + } + + static void ClearBufferFloat(u32 bufferID, f32 value) + { + s_RendererAPI->ClearBufferFloat(bufferID, value); + } + + // Vertex arrays + static u32 CreateVertexArray() + { + return s_RendererAPI->CreateVertexArray(); + } + + static void SetVertexArrayIndexBuffer(u32 vaoID, u32 bufferID) + { + s_RendererAPI->SetVertexArrayIndexBuffer(vaoID, bufferID); + } + + static void DeleteVertexArray(u32 vaoID) + { + s_RendererAPI->DeleteVertexArray(vaoID); + } + + // Texture clear / upload / readback + static void ClearTextureFloat(u32 textureID, u32 mipLevel, const glm::vec4& color) + { + s_RendererAPI->ClearTextureFloat(textureID, mipLevel, color); + } + + static void ClearTextureUInt(u32 textureID, u32 mipLevel, u32 value) + { + s_RendererAPI->ClearTextureUInt(textureID, mipLevel, value); + } + + static void UploadTextureSubImage2D(u32 textureID, i32 xOffset, i32 yOffset, + u32 width, u32 height, + RHI::Format sourceFormat, const void* data) + { + s_RendererAPI->UploadTextureSubImage2D(textureID, xOffset, yOffset, width, height, sourceFormat, data); + } + + static void UploadTextureSubImage3D(u32 textureID, i32 xOffset, i32 yOffset, i32 zOffset, + u32 width, u32 height, u32 depth, + RHI::Format sourceFormat, const void* data) + { + s_RendererAPI->UploadTextureSubImage3D(textureID, xOffset, yOffset, zOffset, + width, height, depth, sourceFormat, data); + } + + [[nodiscard("Store this!")]] static bool ReadTextureImage(u32 textureID, u32 mipLevel, + RHI::Format destFormat, + sizet destSizeBytes, void* dest) + { + return s_RendererAPI->ReadTextureImage(textureID, 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, + RHI::Format destFormat, + sizet destSizeBytes, void* dest) + { + return s_RendererAPI->ReadTextureSubImage(textureID, 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); + } + + static void TextureBarrier() + { + s_RendererAPI->TextureBarrier(); + } + + // Queries + static void CreateQueries(RHI::QueryType type, std::span outQueryIDs) + { + s_RendererAPI->CreateQueries(type, outQueryIDs); + } + + static void DeleteQueries(std::span queryIDs) + { + s_RendererAPI->DeleteQueries(queryIDs); + } + + static void BeginQuery(RHI::QueryType type, u32 queryID) + { + s_RendererAPI->BeginQuery(type, queryID); + } + + static void EndQuery(RHI::QueryType type) + { + s_RendererAPI->EndQuery(type); + } + + [[nodiscard("Store this!")]] static bool IsQueryResultAvailable(u32 queryID) + { + return s_RendererAPI->IsQueryResultAvailable(queryID); + } + + [[nodiscard("Store this!")]] static u32 GetQueryResultU32(u32 queryID) + { + return s_RendererAPI->GetQueryResultU32(queryID); + } + + [[nodiscard("Store this!")]] static u64 GetQueryResultU64(u32 queryID) + { + return s_RendererAPI->GetQueryResultU64(queryID); + } + + // Fences + [[nodiscard("Store this!")]] static u64 CreateFence() + { + return s_RendererAPI->CreateFence(); + } + + [[nodiscard("Store this!")]] static RHI::FenceStatus ClientWaitFence(u64 fence, u64 timeoutNanoseconds) + { + return s_RendererAPI->ClientWaitFence(fence, timeoutNanoseconds); + } + + [[nodiscard("Store this!")]] static bool IsFenceSignaled(u64 fence) + { + return s_RendererAPI->IsFenceSignaled(fence); + } + + static void DestroyFence(u64 fence) + { + s_RendererAPI->DestroyFence(fence); + } + + // Debug markers + static void PushDebugGroup(u32 id, std::string_view label) + { + s_RendererAPI->PushDebugGroup(id, label); + } + + static void PopDebugGroup() + { + s_RendererAPI->PopDebugGroup(); + } + + static void WaitForDeviceIdle() + { + s_RendererAPI->WaitForDeviceIdle(); + } + + [[nodiscard("Store this!")]] static u32 GetMaxFramebufferSamples() + { + return s_RendererAPI->GetMaxFramebufferSamples(); + } + + [[nodiscard("Store this!")]] static u32 GetMaxColorTextureSamples() + { + return s_RendererAPI->GetMaxColorTextureSamples(); + } + + [[nodiscard("Store this!")]] static u32 GetMaxDepthTextureSamples() + { + return s_RendererAPI->GetMaxDepthTextureSamples(); + } + + // See RendererAPI.h: the one virtual with no faithful Vulkan lowering. + // Phase 6 deletes it by moving u_GridScale into a UBO. + static void SetProgramUniformFloat(u32 programID, std::string_view name, f32 value) + { + s_RendererAPI->SetProgramUniformFloat(programID, name, value); + } + static RendererAPI& GetRendererAPI() { return *s_RendererAPI; diff --git a/OloEngine/src/OloEngine/Renderer/RenderGraph.cpp b/OloEngine/src/OloEngine/Renderer/RenderGraph.cpp index 5e489529c..e82cfc294 100644 --- a/OloEngine/src/OloEngine/Renderer/RenderGraph.cpp +++ b/OloEngine/src/OloEngine/Renderer/RenderGraph.cpp @@ -2,6 +2,7 @@ #include "OloEngine/Renderer/RenderGraph.h" #include "OloEngine/Core/PerformanceProfiler.h" +#include "OloEngine/Renderer/RenderCommand.h" #include "OloEngine/Renderer/RenderGraphBarrierPlanner.h" #include "OloEngine/Renderer/RenderGraphHandleAllocator.h" #include "OloEngine/Renderer/RenderGraphHazardValidator.h" @@ -13,8 +14,6 @@ #include "OloEngine/Renderer/RGCommandContext.h" #include "OloEngine/Renderer/StorageBuffer.h" -#include - #include #include #include @@ -171,11 +170,13 @@ namespace OloEngine const sizet texelCount = static_cast(spec.Width) * spec.Height; static thread_local std::vector s_Scratch; s_Scratch.resize(texelCount * 4u); - glGetTextureSubImage(textureID, 0, 0, 0, 0, - static_cast(spec.Width), static_cast(spec.Height), 1, - GL_RGBA, GL_FLOAT, - static_cast(s_Scratch.size() * sizeof(f32)), - s_Scratch.data()); + if (!RenderCommand::ReadTextureSubImage(textureID, 0, 0, 0, 0, + spec.Width, spec.Height, 1, + RHI::Format::RGBA32Float, + s_Scratch.size() * sizeof(f32), s_Scratch.data())) + { + return; + } // NaN census first: a single NaN texel in the scene input snowballs // through bloom's 13-tap downsample/upsample chain into a ~300px @@ -307,7 +308,8 @@ namespace OloEngine return; const u32 mipLevels = std::max(spec.MipLevels, 1u); for (u32 level = 0; level < mipLevels; ++level) - glClearTexImage(texture->GetRendererID(), static_cast(level), GL_RGBA, GL_FLOAT, color.RGBA); + RenderCommand::ClearTextureFloat(texture->GetRendererID(), level, + glm::vec4(color.RGBA[0], color.RGBA[1], color.RGBA[2], color.RGBA[3])); } void PoisonBuffer(const Ref& buffer) @@ -320,7 +322,7 @@ namespace OloEngine // plausibly wrong. NaN would be even louder but risks GPU hangs // in indirect-dispatch consumers, so stay finite. constexpr f32 kPoisonValue = 1.0e9f; - glClearNamedBufferData(buffer->GetRendererID(), GL_R32F, GL_RED, GL_FLOAT, &kPoisonValue); + RenderCommand::ClearBufferFloat(buffer->GetRendererID(), kPoisonValue); } void PoisonFramebuffer(const Ref& framebuffer, const PoisonColor& color) @@ -339,7 +341,9 @@ namespace OloEngine case FramebufferTextureFormat::RGB32F: case FramebufferTextureFormat::RG16F: case FramebufferTextureFormat::RG32F: - glClearTexImage(framebuffer->GetColorAttachmentRendererID(colorIndex), 0, GL_RGBA, GL_FLOAT, color.RGBA); + RenderCommand::ClearTextureFloat( + framebuffer->GetColorAttachmentRendererID(colorIndex), 0, + glm::vec4(color.RGBA[0], color.RGBA[1], color.RGBA[2], color.RGBA[3])); ++colorIndex; break; case FramebufferTextureFormat::RED_INTEGER: @@ -2186,11 +2190,9 @@ namespace OloEngine if (sourceTextureID == 0) continue; - glCopyImageSubData(sourceTextureID, GL_TEXTURE_2D, 0, 0, 0, 0, - sink.TextureID, GL_TEXTURE_2D, 0, 0, 0, 0, - static_cast(sink.Width), - static_cast(sink.Height), - 1); + RenderCommand::CopyImageSubData(sourceTextureID, RendererAPI::TextureTargetType::Texture2D, + sink.TextureID, RendererAPI::TextureTargetType::Texture2D, + sink.Width, sink.Height); if (sink.ValidFlag) *sink.ValidFlag = true; } @@ -2232,11 +2234,9 @@ namespace OloEngine if (sourceTextureID == 0) continue; - glCopyImageSubData(sourceTextureID, GL_TEXTURE_2D, 0, 0, 0, 0, - sink.TextureID, GL_TEXTURE_2D, 0, 0, 0, 0, - static_cast(sink.Width), - static_cast(sink.Height), - 1); + RenderCommand::CopyImageSubData(sourceTextureID, RendererAPI::TextureTargetType::Texture2D, + sink.TextureID, RendererAPI::TextureTargetType::Texture2D, + sink.Width, sink.Height); if (sink.ValidFlag) *sink.ValidFlag = true; } diff --git a/OloEngine/src/OloEngine/Renderer/Renderer3DLifecycle.cpp b/OloEngine/src/OloEngine/Renderer/Renderer3DLifecycle.cpp index 190e43d60..2fb7bd76c 100644 --- a/OloEngine/src/OloEngine/Renderer/Renderer3DLifecycle.cpp +++ b/OloEngine/src/OloEngine/Renderer/Renderer3DLifecycle.cpp @@ -5,7 +5,6 @@ // Raw GL below is part of the issue #691 Phase 2 step-2 sweep backlog; the // include is direct rather than transitive through RendererAPI.h, which is // now GL-free. -#include #include "OloEngine/Renderer/Instancing/GPUFrustumCuller.h" #include "OloEngine/Renderer/Renderer3DDrawHelpers.h" #include "OloEngine/Renderer/ShaderBindingLayout.h" @@ -117,14 +116,10 @@ namespace OloEngine // supports. We take the min of colour-attachment and depth-texture // caps because the G-Buffer needs matching sample counts on both. { - GLint colorSamples = 0; - GLint depthSamples = 0; - glGetIntegerv(GL_MAX_COLOR_TEXTURE_SAMPLES, &colorSamples); - glGetIntegerv(GL_MAX_DEPTH_TEXTURE_SAMPLES, &depthSamples); - s_Data.MaxMSAASamplesColor = static_cast(std::max(colorSamples, 1)); - s_Data.MaxMSAASamplesDepth = static_cast(std::max(depthSamples, 1)); - OLO_CORE_INFO("Renderer3D: Driver MSAA caps — GL_MAX_COLOR_TEXTURE_SAMPLES={}, " - "GL_MAX_DEPTH_TEXTURE_SAMPLES={} (usable max = {})", + s_Data.MaxMSAASamplesColor = std::max(RenderCommand::GetMaxColorTextureSamples(), 1u); + s_Data.MaxMSAASamplesDepth = std::max(RenderCommand::GetMaxDepthTextureSamples(), 1u); + OLO_CORE_INFO("Renderer3D: Driver MSAA caps — max colour-texture samples={}, " + "max depth-texture samples={} (usable max = {})", s_Data.MaxMSAASamplesColor, s_Data.MaxMSAASamplesDepth, std::min(s_Data.MaxMSAASamplesColor, s_Data.MaxMSAASamplesDepth)); diff --git a/OloEngine/src/OloEngine/Renderer/RendererAPI.h b/OloEngine/src/OloEngine/Renderer/RendererAPI.h index 9cdecb004..4aabfa29a 100644 --- a/OloEngine/src/OloEngine/Renderer/RendererAPI.h +++ b/OloEngine/src/OloEngine/Renderer/RendererAPI.h @@ -6,6 +6,7 @@ #include #include +#include namespace OloEngine { @@ -36,7 +37,14 @@ namespace OloEngine enum class TextureTargetType : u8 { Texture2D = 0, - TextureCubeMap + TextureCubeMap, + // Added by the Phase 2 step-2 sweep (issue #691): the per-sample + // MSAA paths copy *multisample* G-Buffer attachments, and a + // multisample image cannot be copied as if it were a plain 2D one — + // glCopyImageSubData requires matching targets and Vulkan requires + // matching VkImageCreateInfo::samples. Without this member those + // call sites had to keep a raw GL_TEXTURE_2D_MULTISAMPLE. + Texture2DMultisample }; public: @@ -179,6 +187,191 @@ namespace OloEngine virtual void BeginConditionalRender(u32 queryID) = 0; virtual void EndConditionalRender() = 0; + // ===================================================================== + // Phase 2 step 2 additions (issue #691) — the operations the sweep + // found the facade had never abstracted at all. + // + // Step 1 converted the vocabulary of the 74 virtuals that already + // existed. This block is the other half of the finding: 84 distinct GL + // entry points appear across the 313 swept call sites and ~60% of them + // had NO facade equivalent, so passes reached past it. See ADR 0011's + // "Amendments from Phase 2 step 2" for the category table and the + // reasoning behind each shape. + // ===================================================================== + + // --- Buffer binding points ------------------------------------------- + // The single biggest gap (26 call sites). A 0 id unbinds the point. + virtual void BindUniformBuffer(u32 bindingPoint, u32 bufferID) = 0; + virtual void BindStorageBuffer(u32 bindingPoint, u32 bufferID) = 0; + + // --- Program / VAO / framebuffer binding ------------------------------ + // The POD command dispatcher holds a raw program id by design (it + // resolves materials to renderer IDs at build time and has no + // Ref on hand), so Shader::Bind() cannot serve it. 0 unbinds. + virtual void BindShaderProgram(u32 programID) = 0; + virtual void BindVertexArrayRaw(u32 vaoID) = 0; + // 0 selects the default framebuffer — same as BindDefaultFramebuffer(). + virtual void BindFramebuffer(u32 framebufferID) = 0; + + // --- Draws from already-bound geometry -------------------------------- + // Distinct from the DrawIndexedRaw(vaoID, ...) family above, which binds + // its own VAO: CommandDispatch keeps a redundant-bind cache, so a draw + // that re-binds would defeat it. This is also the NATIVE Vulkan shape + // (vkCmdBindIndexBuffer then vkCmdDrawIndexed) — the combined + // bind-and-draw form is the less portable of the two. Topology and index + // width are explicit rather than hard-coded to triangles / u32. + virtual void DrawBoundIndexed(RHI::PrimitiveTopology topology, u32 indexCount, + RHI::IndexType indexType, u32 baseIndex) = 0; + virtual void DrawBoundIndexedInstanced(RHI::PrimitiveTopology topology, u32 indexCount, + RHI::IndexType indexType, u32 baseIndex, + u32 instanceCount) = 0; + virtual void DrawBoundArrays(RHI::PrimitiveTopology topology, u32 firstVertex, u32 vertexCount) = 0; + // Split out rather than folded into a patch-draw variant: the + // tessellation call sites set the count once and then draw many times. + virtual void SetPatchVertexCount(u32 patchVertices) = 0; + + // --- Pipeline state the facade was missing ----------------------------- + virtual void SetFrontFace(RHI::FrontFace face) = 0; + virtual void SetBlendFuncSeparate(RHI::BlendFactor srcRGB, RHI::BlendFactor dstRGB, + RHI::BlendFactor srcAlpha, RHI::BlendFactor dstAlpha) = 0; + virtual void SetClearDepth(f32 depth) = 0; + + // --- Named framebuffers ------------------------------------------------ + // SetDrawBuffers/RestoreAllDrawBuffers above act on the CURRENTLY BOUND + // framebuffer; every swept call site names a specific one through DSA. + // + // `attachmentIndices[i]` is the attachment written by draw slot i, or + // RHI::NoAttachment for "slot i writes nowhere" — which is not an index + // and which both backends need (GL_NONE / VK_ATTACHMENT_UNUSED). + // DecalRenderPass depends on it to steer a decal into exactly one + // G-Buffer attachment. + virtual u32 CreateFramebuffer() = 0; + virtual void DeleteFramebuffer(u32 framebufferID) = 0; + virtual void AttachFramebufferColorTexture(u32 framebufferID, u32 attachmentIndex, + u32 textureID, u32 mipLevel) = 0; + virtual void AttachFramebufferDepthTexture(u32 framebufferID, u32 textureID, u32 mipLevel) = 0; + [[nodiscard("Store this!")]] virtual bool IsFramebufferComplete(u32 framebufferID) = 0; + virtual void SetFramebufferDrawAttachments(u32 framebufferID, std::span attachmentIndices) = 0; + // The identity list { 0, 1, ... count-1 } — "draw to every colour + // attachment this framebuffer has". Nine call sites were open-coding the + // same std::array + fill loop + span; that is the named-framebuffer + // counterpart of RestoreAllDrawBuffers(u32) above, which already existed + // for the BOUND framebuffer. Restoring a narrower list than the target + // actually has silently drops later shader outputs (PBR_MultiLight's + // motion vector at location 3, breaking TAA), which is exactly the kind + // of off-by-one an open-coded loop invites. + virtual void RestoreAllFramebufferDrawAttachments(u32 framebufferID, u32 colorAttachmentCount) = 0; + virtual void SetFramebufferReadAttachment(u32 framebufferID, u32 attachmentIndex) = 0; + virtual void ClearFramebufferColorAttachment(u32 framebufferID, u32 attachmentIndex, + const glm::vec4& color) = 0; + virtual void ClearFramebufferDepth(u32 framebufferID, f32 depth) = 0; + virtual void BlitFramebuffer(u32 srcFramebufferID, u32 dstFramebufferID, + i32 srcX0, i32 srcY0, i32 srcX1, i32 srcY1, + i32 dstX0, i32 dstY0, i32 dstX1, i32 dstY1, + RHI::BlitAspect aspect, RHI::Filter filter) = 0; + + // --- Raw buffer lifecycle ---------------------------------------------- + // UniformBuffer / StorageBuffer wrap *their own* buffers; VirtualMeshRegistry + // hand-rolls a vertex/index arena plus a persistent-mapped upload ring and + // needs the primitives directly. + virtual u32 CreateBuffer() = 0; + virtual void DeleteBuffer(u32 bufferID) = 0; + // Mutable storage — re-callable to resize. + virtual void AllocateBufferStorage(u32 bufferID, u64 sizeBytes, RHI::MemoryResidency residency) = 0; + // Immutable storage + a persistent, coherent WRITE mapping in one step: + // the only mapping mode the engine uses, so splitting it would invite a + // storage/mapping flag mismatch that GL only reports at map time. + // Returns the CPU pointer, or nullptr if the mapping failed. + virtual void* AllocatePersistentUploadStorage(u32 bufferID, u64 sizeBytes) = 0; + virtual void UnmapBuffer(u32 bufferID) = 0; + virtual void UploadBufferSubData(u32 bufferID, u64 offsetBytes, u64 sizeBytes, const void* data) = 0; + virtual void ReadBufferSubData(u32 bufferID, u64 offsetBytes, u64 sizeBytes, void* dest) = 0; + virtual void CopyBufferSubData(u32 srcBufferID, u32 dstBufferID, + u64 srcOffsetBytes, u64 dstOffsetBytes, u64 sizeBytes) = 0; + virtual void ClearBufferUInt(u32 bufferID, u32 value) = 0; + virtual void ClearBufferFloat(u32 bufferID, f32 value) = 0; + + // --- Vertex array lifecycle --------------------------------------------- + virtual u32 CreateVertexArray() = 0; + virtual void SetVertexArrayIndexBuffer(u32 vaoID, u32 bufferID) = 0; + virtual void DeleteVertexArray(u32 vaoID) = 0; + + // --- Texture clear / upload / readback ----------------------------------- + // Two clears rather than one type-punned value pointer, mirroring + // 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 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 + // format — see ADR 0011 amendment (4). + virtual void UploadTextureSubImage2D(u32 textureID, i32 xOffset, i32 yOffset, + u32 width, u32 height, + RHI::Format sourceFormat, const void* data) = 0; + virtual void UploadTextureSubImage3D(u32 textureID, i32 xOffset, i32 yOffset, i32 zOffset, + u32 width, u32 height, u32 depth, + RHI::Format sourceFormat, const void* data) = 0; + // Readbacks return success rather than leaving the caller to ask the + // backend for an error: GL's error model is a global sticky flag and + // Vulkan's is a per-call result, so exposing either would force the + // other backend to fake it. ThumbnailCapture's glGetError() disappears + // with no replacement (ADR 0011 amendment (7)). + [[nodiscard("Store this!")]] virtual bool ReadTextureImage(u32 textureID, 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; + 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. + virtual void TextureBarrier() = 0; + + // --- Queries -------------------------------------------------------------- + virtual void CreateQueries(RHI::QueryType type, std::span outQueryIDs) = 0; + virtual void DeleteQueries(std::span queryIDs) = 0; + virtual void BeginQuery(RHI::QueryType type, u32 queryID) = 0; + virtual void EndQuery(RHI::QueryType type) = 0; + [[nodiscard("Store this!")]] virtual bool IsQueryResultAvailable(u32 queryID) = 0; + [[nodiscard("Store this!")]] virtual u32 GetQueryResultU32(u32 queryID) = 0; + [[nodiscard("Store this!")]] virtual u64 GetQueryResultU64(u32 queryID) = 0; + + // --- Fences --------------------------------------------------------------- + // An opaque u64 rather than a handle type: GLsync is a pointer and + // VkFence a 64-bit handle, and FrameResourceManager stores one per + // in-flight frame. 0 means "no fence" / creation failed. + [[nodiscard("Store this!")]] virtual u64 CreateFence() = 0; + [[nodiscard("Store this!")]] virtual RHI::FenceStatus ClientWaitFence(u64 fence, u64 timeoutNanoseconds) = 0; + [[nodiscard("Store this!")]] virtual bool IsFenceSignaled(u64 fence) = 0; + virtual void DestroyFence(u64 fence) = 0; + + // --- Debug markers ---------------------------------------------------------- + virtual void PushDebugGroup(u32 id, std::string_view label) = 0; + virtual void PopDebugGroup() = 0; + + // --- Device ------------------------------------------------------------------ + // Full CPU/GPU sync. Expensive by construction — the two callers are an + // IBL precompute and a virtual-geometry ring-buffer wrap. + virtual void WaitForDeviceIdle() = 0; + + // MSAA capability caps. Three separate queries because GL reports them + // separately and they genuinely differ on some drivers: a format may + // support more colour samples than depth samples, and GBuffer must pick + // a count both attachments can carry. + [[nodiscard("Store this!")]] virtual u32 GetMaxFramebufferSamples() const = 0; + [[nodiscard("Store this!")]] virtual u32 GetMaxColorTextureSamples() const = 0; + [[nodiscard("Store this!")]] virtual u32 GetMaxDepthTextureSamples() const = 0; + + // Name-keyed default-block uniform. THIS IS THE ONE VIRTUAL A VULKAN + // BACKEND CANNOT IMPLEMENT FAITHFULLY — SPIR-V has push constants and + // UBO members, not a queryable default uniform block. It has exactly one + // call site (CommandDispatch::DrawInfiniteGrid's u_GridScale) and Phase 6 + // 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; + // GPU capability queries // True when the backend can service resource creation and draws *right diff --git a/OloEngine/src/OloEngine/Renderer/ShaderPack.cpp b/OloEngine/src/OloEngine/Renderer/ShaderPack.cpp index 77de7b4dc..fe2cf288d 100644 --- a/OloEngine/src/OloEngine/Renderer/ShaderPack.cpp +++ b/OloEngine/src/OloEngine/Renderer/ShaderPack.cpp @@ -4,7 +4,6 @@ #include "OloEngine/Renderer/Shader.h" #include "Platform/OpenGL/OpenGLShader.h" -#include #include namespace OloEngine diff --git a/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryPass.cpp b/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryPass.cpp index 1b71c69fc..87d3aa696 100644 --- a/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryPass.cpp @@ -21,8 +21,6 @@ #include "OloEngine/Renderer/VirtualGeometry/VirtualMeshGpuData.h" #include "OloEngine/Renderer/VirtualGeometry/VirtualMeshRegistry.h" -#include - #include #include @@ -273,7 +271,7 @@ namespace OloEngine m_CullShader->SetInt("u_OcclusionEnabled", hzb.IsUsable() ? 1 : 0); if (!hzb.IsUsable()) return; - ::glBindTextureUnit(0, hzb.HZBTextureID); + RenderCommand::BindTexture(0, hzb.HZBTextureID); CommandDispatch::InvalidateTextureSlot(0); m_CullShader->SetInt("u_HZB", 0); m_CullShader->SetMat4("u_OcclusionViewProjection", hzb.PrevViewProjection); @@ -374,38 +372,37 @@ namespace OloEngine targetFB->Bind(); { // All five G-Buffer MRTs, same set SceneRenderPass draws - std::array drawBufs{}; - for (u32 a = 0; a < GBuffer::Count; ++a) - drawBufs[a] = GL_COLOR_ATTACHMENT0 + a; - glNamedFramebufferDrawBuffers(targetFB->GetRendererID(), static_cast(GBuffer::Count), - drawBufs.data()); + RenderCommand::RestoreAllFramebufferDrawAttachments(targetFB->GetRendererID(), GBuffer::Count); } - // Raw GL state, deliberately bypassing the context caches: this pass - // runs between bucket executions whose dispatchers track state in - // their own caches — a cached "already true" here can leave the real - // GL depth test/mask off, which silently drops every depth write - // (color still lands) and downstream sky/overlay passes then overdraw - // the clusters. Same raw-GL discipline as GPUDrivenOcclusionPass. - ::glViewport(0, 0, static_cast(gbuffer->GetWidth()), static_cast(gbuffer->GetHeight())); - ::glEnable(GL_DEPTH_TEST); - ::glDepthFunc(GL_LESS); - ::glDepthMask(GL_TRUE); - ::glDisable(GL_BLEND); - ::glEnable(GL_CULL_FACE); - ::glCullFace(GL_BACK); - ::glDisable(GL_STENCIL_TEST); - ::glDisable(GL_SCISSOR_TEST); - ::glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + // State set UNCONDITIONALLY, deliberately bypassing the context + // caches: this pass runs between bucket executions whose dispatchers + // track state in their own caches — a cached "already true" there + // can leave the real depth test/mask off, which silently drops every + // depth write (color still lands) and downstream sky/overlay passes + // then overdraw the clusters. The facade preserves that property: + // none of these setters early-out on a cached value, they only gate + // the profiler StateChanges counter. Same discipline as + // GPUDrivenOcclusionPass. + RenderCommand::SetViewport(0, 0, gbuffer->GetWidth(), gbuffer->GetHeight()); + RenderCommand::SetDepthTest(true); + RenderCommand::SetDepthFunc(RHI::CompareOp::Less); + RenderCommand::SetDepthMask(true); + RenderCommand::SetBlendState(false); + RenderCommand::EnableCulling(); + RenderCommand::SetCullFace(RHI::CullMode::Back); + RenderCommand::DisableStencilTest(); + RenderCommand::DisableScissorTest(); + RenderCommand::SetPolygonMode(RHI::PolygonMode::Fill); // Per-attachment color masks can be left disabled by earlier passes - // (glColorMaski state is indexed and survives a plain glColorMask); + // (per-attachment mask state is indexed and survives a global mask); // a masked RT1/RT2 silently drops normal/emissive writes while RT0 + // depth land — the lighting pass then shades clusters with cleared // G-Buffer data. - ::glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + RenderCommand::SetColorMask(true, true, true, true); for (u32 attachment = 0; attachment < GBuffer::Count; ++attachment) { - ::glColorMaski(attachment, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + RenderCommand::SetColorMaskForAttachment(attachment, true, true, true, true); } }; @@ -418,8 +415,10 @@ namespace OloEngine if (debugActive) { // Image units 0/1 (separate namespace from the sampler texture units). - ::glBindImageTexture(0, registry.GetDebugColorTextureID(), 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8); - ::glBindImageTexture(1, registry.GetDebugCountTextureID(), 0, GL_FALSE, 0, GL_READ_WRITE, GL_R32UI); + RenderCommand::BindImageTexture(0, registry.GetDebugColorTextureID(), 0, false, 0, + RHI::Access::StorageWrite, RHI::Format::RGBA8UNorm); + RenderCommand::BindImageTexture(1, registry.GetDebugCountTextureID(), 0, false, 0, + RHI::Access::StorageReadWrite, RHI::Format::R32UInt); } m_DrawInfoUBO->Bind(); @@ -442,11 +441,11 @@ namespace OloEngine // toggle the state itself. if (instances[i].TwoSided) { - ::glDisable(GL_CULL_FACE); + RenderCommand::DisableCulling(); } else { - ::glEnable(GL_CULL_FACE); + RenderCommand::EnableCulling(); } RenderCommand::MultiDrawElementsIndirectCountRaw( @@ -456,7 +455,7 @@ namespace OloEngine static_cast((argsInstanceBase + i) * sizeof(VirtualDrawArgs)), instances[i].Gpu.ClusterCount, 32u); } - ::glEnable(GL_CULL_FACE); // restore the pass-wide default + RenderCommand::EnableCulling(); // restore the pass-wide default }; // ── 2. Phase-1 hardware raster ── @@ -476,8 +475,8 @@ namespace OloEngine { // The phase-1 draws just wrote this depth through the fixed-function // pipeline; the Hi-Z build samples it as a texture. Order the - // framebuffer-write -> texture-fetch explicitly (GL 4.5 texture barrier). - ::glTextureBarrier(); + // framebuffer-write -> texture-fetch explicitly (a texture barrier). + RenderCommand::TextureBarrier(); const GPUFrustumCuller::HZBOcclusionInputs currentHZB = Renderer3D::BuildCurrentOcclusionHZB( gbuffer->GetDepthAttachmentID(), gbuffer->GetWidth(), gbuffer->GetHeight()); @@ -517,8 +516,8 @@ namespace OloEngine { registry.GetVertexBuffer()->Bind(); registry.GetVisbufferBuffer()->Bind(); - ::glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ShaderBindingLayout::SSBO_VIRTUAL_INDICES, - registry.GetIndexBufferID()); + RenderCommand::BindStorageBuffer(ShaderBindingLayout::SSBO_VIRTUAL_INDICES, + registry.GetIndexBufferID()); u32 const maxSwRecords = frameClusterCount; u32 const groupsX = std::min(maxSwRecords, 4096u); @@ -572,7 +571,7 @@ namespace OloEngine // HW-rasterized ones from both phases. if (swEnabled) { - ::glDisable(GL_CULL_FACE); // fullscreen triangle + RenderCommand::DisableCulling(); // fullscreen triangle m_ResolveShader->Bind(); registry.GetSwListBuffer()->Bind(); registry.GetVisbufferBuffer()->Bind(); @@ -591,7 +590,7 @@ namespace OloEngine fullscreen->Bind(); context.DrawIndexed(fullscreen); } - ::glEnable(GL_CULL_FACE); + RenderCommand::EnableCulling(); } targetFB->Unbind(); @@ -603,8 +602,10 @@ namespace OloEngine if (debugMode == VirtualDebugMode::Overdraw && m_ColorizeShader) { RenderCommand::MemoryBarrier(MemoryBarrierFlags::ShaderImageAccess); - ::glBindImageTexture(0, registry.GetDebugColorTextureID(), 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8); - ::glBindImageTexture(1, registry.GetDebugCountTextureID(), 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI); + RenderCommand::BindImageTexture(0, registry.GetDebugColorTextureID(), 0, false, 0, + RHI::Access::StorageWrite, RHI::Format::RGBA8UNorm); + RenderCommand::BindImageTexture(1, registry.GetDebugCountTextureID(), 0, false, 0, + RHI::Access::StorageRead, RHI::Format::R32UInt); m_ColorizeShader->Bind(); m_ColorizeShader->SetUint("u_Width", registry.GetDebugWidth()); m_ColorizeShader->SetUint("u_Height", registry.GetDebugHeight()); @@ -634,29 +635,28 @@ namespace OloEngine // and the editor grid see the clusters we just drew — ScenePass copied // its exports before we ran (DeferredGPUOcclusionPass idiom). Handles // that alias the live attachment self-skip. - const auto copyExport = [&context, &gbuffer](const RGTextureHandle handle, u32 sourceTextureID, GLenum target) + const auto copyExport = [&context, &gbuffer](const RGTextureHandle handle, u32 sourceTextureID, + RendererAPI::TextureTargetType target) { if (!handle.IsValid() || sourceTextureID == 0u) return; u32 const exportedID = context.ResolveTexture(handle); if (exportedID == 0u || exportedID == sourceTextureID) return; - ::glCopyImageSubData(sourceTextureID, target, 0, 0, 0, 0, - exportedID, target, 0, 0, 0, 0, - static_cast(gbuffer->GetWidth()), - static_cast(gbuffer->GetHeight()), 1); + RenderCommand::CopyImageSubData(sourceTextureID, target, exportedID, target, + gbuffer->GetWidth(), gbuffer->GetHeight()); }; - copyExport(m_SelectedSceneDepth, gbuffer->GetDepthAttachmentID(), GL_TEXTURE_2D); - copyExport(m_SelectedVelocity, gbuffer->GetColorAttachmentID(GBuffer::Velocity), GL_TEXTURE_2D); - copyExport(m_SelectedGBufferAlbedo, gbuffer->GetColorAttachmentID(GBuffer::Albedo), GL_TEXTURE_2D); - copyExport(m_SelectedGBufferNormal, gbuffer->GetColorAttachmentID(GBuffer::Normal), GL_TEXTURE_2D); - copyExport(m_SelectedGBufferEmissive, gbuffer->GetColorAttachmentID(GBuffer::Emissive), GL_TEXTURE_2D); + copyExport(m_SelectedSceneDepth, gbuffer->GetDepthAttachmentID(), RendererAPI::TextureTargetType::Texture2D); + copyExport(m_SelectedVelocity, gbuffer->GetColorAttachmentID(GBuffer::Velocity), RendererAPI::TextureTargetType::Texture2D); + copyExport(m_SelectedGBufferAlbedo, gbuffer->GetColorAttachmentID(GBuffer::Albedo), RendererAPI::TextureTargetType::Texture2D); + copyExport(m_SelectedGBufferNormal, gbuffer->GetColorAttachmentID(GBuffer::Normal), RendererAPI::TextureTargetType::Texture2D); + copyExport(m_SelectedGBufferEmissive, gbuffer->GetColorAttachmentID(GBuffer::Emissive), RendererAPI::TextureTargetType::Texture2D); // Per-sample lighting samples the MULTISAMPLE G-Buffer, so re-export those // attachments too (they carry the clusters we just drew into the MS FBO). if (perSampleMSAA) { - constexpr GLenum kMS = GL_TEXTURE_2D_MULTISAMPLE; + constexpr auto kMS = RendererAPI::TextureTargetType::Texture2DMultisample; copyExport(m_SelectedSceneDepthMS, gbuffer->GetMSDepthAttachmentID(), kMS); copyExport(m_SelectedVelocityMS, gbuffer->GetMSColorAttachmentID(GBuffer::Velocity), kMS); copyExport(m_SelectedGBufferAlbedoMS, gbuffer->GetMSColorAttachmentID(GBuffer::Albedo), kMS); diff --git a/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryShadow.cpp b/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryShadow.cpp index bb50f1893..c0115e415 100644 --- a/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryShadow.cpp +++ b/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryShadow.cpp @@ -13,7 +13,6 @@ #include "OloEngine/Renderer/VirtualGeometry/VirtualMeshGpuData.h" #include "OloEngine/Renderer/VirtualGeometry/VirtualMeshRegistry.h" -#include #include #include @@ -120,7 +119,7 @@ namespace OloEngine::VirtualGeometryShadow argsBufferID, static_cast(i * sizeof(VirtualDrawArgs)), instances[i].Gpu.ClusterCount, 32u); } - ::glBindVertexArray(0); + RenderCommand::BindVertexArrayRaw(0); } void Shutdown() diff --git a/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualMeshRegistry.cpp b/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualMeshRegistry.cpp index a46f5a803..7b5aaa92a 100644 --- a/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualMeshRegistry.cpp +++ b/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualMeshRegistry.cpp @@ -4,12 +4,11 @@ #include "OloEngine/Renderer/Commands/FrameDataBuffer.h" #include "OloEngine/Renderer/Material.h" #include "OloEngine/Renderer/MeshSource.h" +#include "OloEngine/Renderer/RenderCommand.h" #include "OloEngine/Renderer/ShaderBindingLayout.h" #include "OloEngine/Renderer/StorageBuffer.h" #include "OloEngine/Renderer/VirtualGeometry/VirtualMeshBuilder.h" -#include "Platform/OpenGL/OpenGLUtilities.h" -#include #include #include @@ -181,8 +180,7 @@ namespace OloEngine if (m_RingPtr == nullptr || bytes > m_RingSize) { // Payload larger than the ring (pathological page size): direct upload. - glNamedBufferSubData(targetBufferID, static_cast(targetOffset), - static_cast(bytes), payload); + RenderCommand::UploadBufferSubData(targetBufferID, targetOffset, bytes, payload); return true; } @@ -193,14 +191,12 @@ namespace OloEngine // memcpy), so a wrap only conflicts with copies still in flight // from EARLIER offsets this frame — wait them out. Rare at the // 8 MB ring size vs the per-frame upload cap. - ::glFinish(); + RenderCommand::WaitForDeviceIdle(); m_RingHead = 0; } std::memcpy(m_RingPtr + m_RingHead, payload, bytes); - glCopyNamedBufferSubData(m_RingBufferID, targetBufferID, - static_cast(m_RingHead), static_cast(targetOffset), - static_cast(bytes)); + RenderCommand::CopyBufferSubData(m_RingBufferID, targetBufferID, m_RingHead, targetOffset, bytes); m_RingHead += bytes; return true; } @@ -418,23 +414,21 @@ namespace OloEngine u64 const indexArenaBytes = static_cast(m_SlotIndexCapacity) * slotCount * sizeof(u32); if (m_IndexBufferID == 0) { - glCreateBuffers(1, &m_IndexBufferID); + m_IndexBufferID = RenderCommand::CreateBuffer(); } - glNamedBufferData(m_IndexBufferID, static_cast(indexArenaBytes), nullptr, GL_DYNAMIC_COPY); + RenderCommand::AllocateBufferStorage(m_IndexBufferID, indexArenaBytes, RHI::MemoryResidency::DeviceLocal); if (m_VaoID == 0) { - glCreateVertexArrays(1, &m_VaoID); + m_VaoID = RenderCommand::CreateVertexArray(); } - glVertexArrayElementBuffer(m_VaoID, m_IndexBufferID); + RenderCommand::SetVertexArrayIndexBuffer(m_VaoID, m_IndexBufferID); // Persistent-mapped upload ring if (m_RingBufferID == 0) { - glCreateBuffers(1, &m_RingBufferID); - glNamedBufferStorage(m_RingBufferID, static_cast(kUploadRingBytes), nullptr, - GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); - m_RingPtr = static_cast(glMapNamedBufferRange(m_RingBufferID, 0, static_cast(kUploadRingBytes), - GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT)); + m_RingBufferID = RenderCommand::CreateBuffer(); + m_RingPtr = static_cast( + RenderCommand::AllocatePersistentUploadStorage(m_RingBufferID, kUploadRingBytes)); m_RingSize = (m_RingPtr != nullptr) ? kUploadRingBytes : 0; m_RingHead = 0; } @@ -547,9 +541,8 @@ namespace OloEngine // Clear to "empty" (all bits set: farthest depth + sentinel payload) if (m_VisbufferBuffer) { - Utils::GLClearProgramGuard programGuard; u32 const clearValue = 0xFFFFFFFFu; - glClearNamedBufferData(m_VisbufferBuffer->GetRendererID(), GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, &clearValue); + RenderCommand::ClearBufferUInt(m_VisbufferBuffer->GetRendererID(), clearValue); } } @@ -577,15 +570,14 @@ namespace OloEngine { if (m_ArgsReadbackID != 0) { - glDeleteBuffers(1, &m_ArgsReadbackID); + RenderCommand::DeleteBuffer(m_ArgsReadbackID); } - glCreateBuffers(1, &m_ArgsReadbackID); - glNamedBufferData(m_ArgsReadbackID, static_cast(bytes), nullptr, GL_DYNAMIC_READ); + m_ArgsReadbackID = RenderCommand::CreateBuffer(); + RenderCommand::AllocateBufferStorage(m_ArgsReadbackID, bytes, RHI::MemoryResidency::DeviceToHost); m_ArgsReadbackBytes = bytes; } - glCopyNamedBufferSubData(m_ArgsBuffer->GetRendererID(), m_ArgsReadbackID, 0, 0, - static_cast(bytes)); - glGetNamedBufferSubData(m_ArgsReadbackID, 0, static_cast(bytes), args.data()); + RenderCommand::CopyBufferSubData(m_ArgsBuffer->GetRendererID(), m_ArgsReadbackID, 0, 0, bytes); + RenderCommand::ReadBufferSubData(m_ArgsReadbackID, 0, bytes, args.data()); sizet const phaseStride = m_FrameInstances.size(); for (sizet i = 0; i < args.size(); ++i) { @@ -617,25 +609,21 @@ namespace OloEngine if (m_DebugColorTexID == 0 || m_DebugWidth != viewportWidth || m_DebugHeight != viewportHeight) { if (m_DebugColorTexID != 0) - glDeleteTextures(1, &m_DebugColorTexID); + RenderCommand::DeleteTexture(m_DebugColorTexID); if (m_DebugCountTexID != 0) - glDeleteTextures(1, &m_DebugCountTexID); + RenderCommand::DeleteTexture(m_DebugCountTexID); // RGBA8 colour target — imageStore'd by both raster paths, imported // into the graph as "VirtualGeometryDebug", captured via MCP. - glCreateTextures(GL_TEXTURE_2D, 1, &m_DebugColorTexID); - glTextureStorage2D(m_DebugColorTexID, 1, GL_RGBA8, static_cast(viewportWidth), - static_cast(viewportHeight)); - glTextureParameteri(m_DebugColorTexID, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTextureParameteri(m_DebugColorTexID, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + m_DebugColorTexID = RenderCommand::CreateTexture2D(viewportWidth, viewportHeight, + RHI::Format::RGBA8UNorm); + RenderCommand::SetTextureFilter(m_DebugColorTexID, RHI::Filter::Nearest, RHI::Filter::Nearest); // R32UI overdraw-count target — imageAtomicAdd'd per fragment, then // colorized into the colour target by VirtualDebugColorize.comp. - glCreateTextures(GL_TEXTURE_2D, 1, &m_DebugCountTexID); - glTextureStorage2D(m_DebugCountTexID, 1, GL_R32UI, static_cast(viewportWidth), - static_cast(viewportHeight)); - glTextureParameteri(m_DebugCountTexID, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTextureParameteri(m_DebugCountTexID, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + m_DebugCountTexID = RenderCommand::CreateTexture2D(viewportWidth, viewportHeight, + RHI::Format::R32UInt); + RenderCommand::SetTextureFilter(m_DebugCountTexID, RHI::Filter::Nearest, RHI::Filter::Nearest); m_DebugWidth = viewportWidth; m_DebugHeight = viewportHeight; @@ -653,11 +641,9 @@ namespace OloEngine // // Nothing reads this alpha as colour: the debug capture target is inspected per-RGB. { - Utils::GLClearProgramGuard programGuard; - f32 const clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; - glClearTexImage(m_DebugColorTexID, 0, GL_RGBA, GL_FLOAT, clearColor); - u32 const clearCount = 0u; - glClearTexImage(m_DebugCountTexID, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, &clearCount); + constexpr glm::vec4 kTransparentBlack(0.0f); + RenderCommand::ClearTextureFloat(m_DebugColorTexID, 0, kTransparentBlack); + RenderCommand::ClearTextureUInt(m_DebugCountTexID, 0, 0u); } } @@ -917,7 +903,7 @@ namespace OloEngine m_VisbufferHeight = 0; if (m_ArgsReadbackID != 0) { - glDeleteBuffers(1, &m_ArgsReadbackID); + RenderCommand::DeleteBuffer(m_ArgsReadbackID); m_ArgsReadbackID = 0; m_ArgsReadbackBytes = 0; } @@ -925,32 +911,32 @@ namespace OloEngine { if (m_RingPtr != nullptr) { - glUnmapNamedBuffer(m_RingBufferID); + RenderCommand::UnmapBuffer(m_RingBufferID); m_RingPtr = nullptr; } - glDeleteBuffers(1, &m_RingBufferID); + RenderCommand::DeleteBuffer(m_RingBufferID); m_RingBufferID = 0; m_RingSize = 0; m_RingHead = 0; } if (m_VaoID != 0) { - glDeleteVertexArrays(1, &m_VaoID); + RenderCommand::DeleteVertexArray(m_VaoID); m_VaoID = 0; } if (m_IndexBufferID != 0) { - glDeleteBuffers(1, &m_IndexBufferID); + RenderCommand::DeleteBuffer(m_IndexBufferID); m_IndexBufferID = 0; } if (m_DebugColorTexID != 0) { - glDeleteTextures(1, &m_DebugColorTexID); + RenderCommand::DeleteTexture(m_DebugColorTexID); m_DebugColorTexID = 0; } if (m_DebugCountTexID != 0) { - glDeleteTextures(1, &m_DebugCountTexID); + RenderCommand::DeleteTexture(m_DebugCountTexID); m_DebugCountTexID = 0; } m_DebugWidth = 0; diff --git a/OloEngine/src/OloEngine/SaveGame/ThumbnailCapture.cpp b/OloEngine/src/OloEngine/SaveGame/ThumbnailCapture.cpp index ef1e0ba69..2ada0998e 100644 --- a/OloEngine/src/OloEngine/SaveGame/ThumbnailCapture.cpp +++ b/OloEngine/src/OloEngine/SaveGame/ThumbnailCapture.cpp @@ -1,7 +1,7 @@ #include "OloEnginePCH.h" #include "ThumbnailCapture.h" -#include +#include "OloEngine/Renderer/RenderCommand.h" #define STB_IMAGE_WRITE_IMPLEMENTATION #include @@ -47,12 +47,13 @@ namespace OloEngine } std::vector pixelData(fbWidth * fbHeight * 4); - glGetTextureImage(texID, 0, GL_RGBA, GL_UNSIGNED_BYTE, - static_cast(pixelData.size()), pixelData.data()); - - if (GLenum err = glGetError(); err != GL_NO_ERROR) + // 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, + pixelData.size(), pixelData.data())) { - OLO_CORE_ERROR("[ThumbnailCapture] GL error reading framebuffer: {}", err); + OLO_CORE_ERROR("[ThumbnailCapture] Failed to read back the framebuffer colour attachment"); return {}; } diff --git a/OloEngine/src/OloEngine/Scene/Scene.cpp b/OloEngine/src/OloEngine/Scene/Scene.cpp index 5c9870f07..a8df9949e 100644 --- a/OloEngine/src/OloEngine/Scene/Scene.cpp +++ b/OloEngine/src/OloEngine/Scene/Scene.cpp @@ -5,8 +5,6 @@ // Raw GL below is part of the issue #691 Phase 2 step-2 sweep backlog; the // include is direct rather than transitive through RendererAPI.h, which is // now GL-free. -#include - #include #include #include @@ -9610,10 +9608,9 @@ namespace OloEngine SoftParticleParams softParams; if (auto sceneDepthTextureID = Renderer3D::ResolveFrameGraphTexture(ResourceNames::SceneDepth); sceneDepthTextureID != 0) { - i32 viewportWidth = 0; - i32 viewportHeight = 0; - glGetTextureLevelParameteriv(sceneDepthTextureID, 0, GL_TEXTURE_WIDTH, &viewportWidth); - glGetTextureLevelParameteriv(sceneDepthTextureID, 0, GL_TEXTURE_HEIGHT, &viewportHeight); + u32 viewportWidth = 0; + u32 viewportHeight = 0; + RenderCommand::GetTextureDimensions(sceneDepthTextureID, 0, viewportWidth, viewportHeight); softParams.Enabled = sys.SoftParticlesEnabled; softParams.Distance = sys.SoftParticleDistance; diff --git a/OloEngine/src/OloEngine/UI/UIRenderer.cpp b/OloEngine/src/OloEngine/UI/UIRenderer.cpp index 9fda7e18d..df57ceac7 100644 --- a/OloEngine/src/OloEngine/UI/UIRenderer.cpp +++ b/OloEngine/src/OloEngine/UI/UIRenderer.cpp @@ -4,8 +4,6 @@ // Raw GL below is part of the issue #691 Phase 2 step-2 sweep backlog; the // include is direct rather than transitive through RendererAPI.h, which is // now GL-free. -#include - #include "OloEngine/Renderer/Renderer2D.h" #include "OloEngine/Renderer/RenderCommand.h" #include "OloEngine/Renderer/Font.h" @@ -28,8 +26,12 @@ namespace OloEngine // Clip rect stack for scissor testing struct ClipRect { - GLint x, y; - GLsizei width, height; + // Engine types, not GL ones: these are scissor-rect COORDINATES, not GL + // objects, and RenderCommand::SetScissorBox already takes i32/u32. + // Spelling them GLint/GLsizei was the only reason this translation unit + // needed while making zero GL calls (issue #691). + i32 x, y; + u32 width, height; }; static std::stack s_ClipStack; static f32 s_ViewportHeight = 0.0f; @@ -75,23 +77,23 @@ namespace OloEngine Renderer2D::EndScene(); // Convert from UI space (Y-down) to OpenGL scissor space (Y-up) - GLint x = static_cast(position.x); - GLint y = static_cast(s_ViewportHeight - position.y - size.y); - GLsizei w = static_cast(size.x); - GLsizei h = static_cast(size.y); + i32 x = static_cast(position.x); + i32 y = static_cast(s_ViewportHeight - position.y - size.y); + u32 w = static_cast(size.x); + u32 h = static_cast(size.y); // Intersect with parent clip rect if any if (!s_ClipStack.empty()) { const auto& parent = s_ClipStack.top(); - GLint x2 = glm::max(x, parent.x); - GLint y2 = glm::max(y, parent.y); - GLint right = glm::min(x + static_cast(w), parent.x + static_cast(parent.width)); - GLint top = glm::min(y + static_cast(h), parent.y + static_cast(parent.height)); + i32 x2 = glm::max(x, parent.x); + i32 y2 = glm::max(y, parent.y); + i32 right = glm::min(x + static_cast(w), parent.x + static_cast(parent.width)); + i32 top = glm::min(y + static_cast(h), parent.y + static_cast(parent.height)); x = x2; y = y2; - w = static_cast(glm::max(right - x2, 0)); - h = static_cast(glm::max(top - y2, 0)); + w = static_cast(glm::max(right - x2, 0)); + h = static_cast(glm::max(top - y2, 0)); } s_ClipStack.push({ x, y, w, h }); diff --git a/OloEngine/src/Platform/OpenGL/OpenGLRHIConversions.h b/OloEngine/src/Platform/OpenGL/OpenGLRHIConversions.h index c9441a60c..ea473c489 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLRHIConversions.h +++ b/OloEngine/src/Platform/OpenGL/OpenGLRHIConversions.h @@ -15,11 +15,25 @@ // values "for a free static_cast" is exactly how GL leaked upward the first // time. // -// Every switch carries a `default:` that logs and falls back rather than -// silently emitting 0 (which GL would take as GL_NONE / GL_ZERO / GL_POINTS -// depending on where it landed — a wrong-but-legal value, the silent failure -// mode this phase exists to avoid). Matches ToGLTextureTarget's existing idiom -// in OpenGLRendererAPI.cpp. +// Every switch logs and falls back rather than silently emitting 0 (which GL +// would take as GL_NONE / GL_ZERO / GL_POINTS depending on where it landed — a +// wrong-but-legal value, the silent failure mode this phase exists to avoid). +// +// WHERE that fallback lives is load-bearing, so do not "tidy" it. Sixteen of the +// nineteen switches below put it AFTER the switch and carry no `default:` label +// at all. That is deliberate: without a `default:`, the compiler's +// switch-exhaustiveness warning (clang/clang-cl `-Wswitch`, on by default) fires +// when someone APPENDS an enumerator to one of these enums — and that warning is +// the only thing that catches an append, because RHIEnumLoweringTest's +// last-enumerator `static_assert` cannot (appending leaves the previous last +// member's ordinal unchanged). Adding a `default:` here to "be safe" would trade +// a build error for a silent wrong mapping. Note MSVC's equivalent (C4062) is +// off by default even at /W4, so the clang-cl CI job is what enforces this. +// +// The three exceptions are intentional: ToGLPixelFormat / ToGLPixelType take +// RHI::Format, and ToGLImageAccess takes RHI::Access — enums whose members are +// mostly NOT valid for those particular conversions, so an exhaustive list would +// be noise rather than a guard. // ============================================================================= #include "OloEngine/Core/Log.h" @@ -355,6 +369,97 @@ namespace OloEngine::Utils return GL_UNSIGNED_BYTE; } + [[nodiscard]] inline GLenum ToGL(RHI::IndexType type) + { + switch (type) + { + case RHI::IndexType::UInt16: + return GL_UNSIGNED_SHORT; + case RHI::IndexType::UInt32: + return GL_UNSIGNED_INT; + } + OLO_CORE_ERROR("ToGL(RHI::IndexType): unhandled value {}", static_cast(type)); + return GL_UNSIGNED_INT; + } + + [[nodiscard]] inline GLenum ToGL(RHI::FrontFace face) + { + switch (face) + { + case RHI::FrontFace::CounterClockwise: + return GL_CCW; + case RHI::FrontFace::Clockwise: + return GL_CW; + } + OLO_CORE_ERROR("ToGL(RHI::FrontFace): unhandled value {}", static_cast(face)); + return GL_CCW; + } + + // The GL query target a RHI::QueryType begins/ends against. + [[nodiscard]] inline GLenum ToGL(RHI::QueryType type) + { + switch (type) + { + case RHI::QueryType::OcclusionAnySamples: + return GL_ANY_SAMPLES_PASSED; + case RHI::QueryType::TimeElapsed: + return GL_TIME_ELAPSED; + } + OLO_CORE_ERROR("ToGL(RHI::QueryType): unhandled value {}", static_cast(type)); + return GL_ANY_SAMPLES_PASSED; + } + + // glNamedBufferData's usage hint. GL treats it as a hint only, so a wrong + // value here costs bandwidth rather than correctness — which is precisely why + // it needs a table test: nothing would render wrong, and no other assertion + // in the suite would notice. + [[nodiscard]] inline GLenum ToGL(RHI::MemoryResidency residency) + { + switch (residency) + { + case RHI::MemoryResidency::HostToDevice: + return GL_DYNAMIC_DRAW; + case RHI::MemoryResidency::DeviceLocal: + return GL_DYNAMIC_COPY; + case RHI::MemoryResidency::DeviceToHost: + return GL_DYNAMIC_READ; + } + OLO_CORE_ERROR("ToGL(RHI::MemoryResidency): unhandled value {}", static_cast(residency)); + return GL_DYNAMIC_DRAW; + } + + // glBlitNamedFramebuffer's mask. A GLbitfield rather than a GLenum — the + // return type is the tell that this one is not an enum lowering. + [[nodiscard]] inline GLbitfield ToGLBlitMask(RHI::BlitAspect aspect) + { + switch (aspect) + { + case RHI::BlitAspect::Color: + return GL_COLOR_BUFFER_BIT; + case RHI::BlitAspect::Depth: + return GL_DEPTH_BUFFER_BIT; + case RHI::BlitAspect::Stencil: + return GL_STENCIL_BUFFER_BIT; + case RHI::BlitAspect::DepthStencil: + return GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; + } + OLO_CORE_ERROR("ToGLBlitMask: unhandled RHI::BlitAspect {}", static_cast(aspect)); + return GL_COLOR_BUFFER_BIT; + } + + // A colour-attachment index, or RHI::NoAttachment for "writes nowhere". + // The sentinel is the reason this is not a bare `GL_COLOR_ATTACHMENT0 + i` + // at each call site: GL_NONE is not GL_COLOR_ATTACHMENT0 + anything, and a + // Vulkan backend needs the same distinction for VK_ATTACHMENT_UNUSED. + [[nodiscard]] inline GLenum ToGLColorAttachment(u32 attachmentIndex) + { + if (attachmentIndex == RHI::NoAttachment) + { + return GL_NONE; + } + return GL_COLOR_ATTACHMENT0 + attachmentIndex; + } + // glBindImageTexture's access parameter. Only the three storage accesses are // meaningful here; anything else is a caller bug rather than a lowering gap. [[nodiscard]] inline GLenum ToGLImageAccess(RHI::Access access) diff --git a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp index 3cb4cbe9f..dd72168cd 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp +++ b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp @@ -61,6 +61,10 @@ namespace OloEngine // and glGetIntegerv on every call costs a driver round-trip. glGetIntegerv(GL_MAX_DRAW_BUFFERS, &m_MaxDrawBuffers); + // Same reasoning for the tessellation cap — SetPatchVertexCount runs + // once per terrain/water patch draw. + glGetIntegerv(GL_MAX_PATCH_VERTICES, &m_MaxPatchVertices); + // Detect 64-bit shader integer + atomic support once (issue #629). The // virtualized-geometry software rasterizer uses a single atomicMin on a // packed uint64_t visibility word when BOTH extensions are present, and @@ -270,12 +274,10 @@ namespace OloEngine return; } - GLint maxPatchVerts = 0; - glGetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVerts); - if (patchVertices > static_cast(maxPatchVerts)) + if (m_MaxPatchVertices > 0 && patchVertices > static_cast(m_MaxPatchVertices)) { OLO_CORE_ERROR("OpenGLRendererAPI::DrawIndexedPatches - patchVertices {} exceeds GL_MAX_PATCH_VERTICES {}", - patchVertices, maxPatchVerts); + patchVertices, m_MaxPatchVertices); return; } @@ -357,12 +359,10 @@ namespace OloEngine return; } - GLint maxPatchVerts = 0; - glGetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVerts); - if (patchVertices > static_cast(maxPatchVerts)) + if (m_MaxPatchVertices > 0 && patchVertices > static_cast(m_MaxPatchVertices)) { OLO_CORE_ERROR("OpenGLRendererAPI::DrawIndexedPatchesRaw - patchVertices {} exceeds GL_MAX_PATCH_VERTICES {}", - patchVertices, maxPatchVerts); + patchVertices, m_MaxPatchVertices); return; } @@ -853,6 +853,8 @@ namespace OloEngine return GL_TEXTURE_2D; case RendererAPI::TextureTargetType::TextureCubeMap: return GL_TEXTURE_CUBE_MAP; + case RendererAPI::TextureTargetType::Texture2DMultisample: + return GL_TEXTURE_2D_MULTISAMPLE; default: OLO_CORE_ERROR("ToGLTextureTarget: Unknown TextureTargetType"); return GL_TEXTURE_2D; @@ -1087,4 +1089,744 @@ namespace OloEngine glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &size); return static_cast(size); } + + // ========================================================================= + // Phase 2 step 2 (issue #691) — the operations the call-site sweep found + // the facade had never abstracted. See ADR 0011's "Amendments from Phase 2 + // step 2" for why each has the shape it does. + // ========================================================================= + + // --- Buffer binding points ----------------------------------------------- + + void OpenGLRendererAPI::BindUniformBuffer(u32 bindingPoint, u32 bufferID) + { + OLO_PROFILE_FUNCTION(); + + glBindBufferBase(GL_UNIFORM_BUFFER, bindingPoint, bufferID); + } + + void OpenGLRendererAPI::BindStorageBuffer(u32 bindingPoint, u32 bufferID) + { + OLO_PROFILE_FUNCTION(); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, bindingPoint, bufferID); + } + + // --- Program / VAO / framebuffer binding ---------------------------------- + + void OpenGLRendererAPI::BindShaderProgram(u32 programID) + { + OLO_PROFILE_FUNCTION(); + + glUseProgram(programID); + } + + void OpenGLRendererAPI::BindVertexArrayRaw(u32 vaoID) + { + OLO_PROFILE_FUNCTION(); + + glBindVertexArray(vaoID); + } + + void OpenGLRendererAPI::BindFramebuffer(u32 framebufferID) + { + OLO_PROFILE_FUNCTION(); + + glBindFramebuffer(GL_FRAMEBUFFER, framebufferID); + } + + // --- Draws from already-bound geometry ------------------------------------- + // + // These deliberately do NOT touch RendererProfiler, unlike the + // DrawIndexedRaw family above. The call sites they replace (CommandDispatch's + // raw glDrawElements / glDrawArrays, TiledForwardPlus's debug overlay) never + // incremented it either, and several of them keep their own counters in + // CommandDispatch::Statistics. Adding increments here would silently change + // every profiler-derived number the sweep is supposed to leave untouched. + + namespace + { + // baseIndex is an index COUNT; glDrawElements wants a byte offset into + // the bound element buffer, and the stride depends on the index width. + [[nodiscard]] const void* IndexByteOffset(RHI::IndexType type, u32 baseIndex) + { + const uintptr_t stride = (type == RHI::IndexType::UInt16) ? sizeof(u16) : sizeof(u32); + return reinterpret_cast(static_cast(baseIndex) * stride); + } + } // namespace + + void OpenGLRendererAPI::DrawBoundIndexed(RHI::PrimitiveTopology topology, u32 indexCount, + RHI::IndexType indexType, u32 baseIndex) + { + OLO_PROFILE_FUNCTION(); + + glDrawElements(Utils::ToGL(topology), static_cast(indexCount), Utils::ToGL(indexType), + IndexByteOffset(indexType, baseIndex)); + } + + void OpenGLRendererAPI::DrawBoundIndexedInstanced(RHI::PrimitiveTopology topology, u32 indexCount, + RHI::IndexType indexType, u32 baseIndex, + u32 instanceCount) + { + OLO_PROFILE_FUNCTION(); + + glDrawElementsInstanced(Utils::ToGL(topology), static_cast(indexCount), + Utils::ToGL(indexType), IndexByteOffset(indexType, baseIndex), + static_cast(instanceCount)); + } + + void OpenGLRendererAPI::DrawBoundArrays(RHI::PrimitiveTopology topology, u32 firstVertex, u32 vertexCount) + { + OLO_PROFILE_FUNCTION(); + + glDrawArrays(Utils::ToGL(topology), static_cast(firstVertex), static_cast(vertexCount)); + } + + void OpenGLRendererAPI::SetPatchVertexCount(u32 patchVertices) + { + OLO_PROFILE_FUNCTION(); + + // Validated against the cap cached in Init(). The raw call sites this + // replaces had no check at all, so an out-of-range value became a + // silent GL_INVALID_VALUE and the tessellation draw simply did nothing. + // + // The `m_MaxPatchVertices > 0` term is load-bearing: a zero cap means + // Init() never ran (or the query failed), and rejecting on that would + // turn a missing cache value into "terrain and water silently stop + // rendering" — a far worse failure than the one this guard prevents. + // Fail OPEN on an unknown cap; GL will still report a real violation. + if (patchVertices == 0 || (m_MaxPatchVertices > 0 && patchVertices > static_cast(m_MaxPatchVertices))) + { + OLO_CORE_ERROR("OpenGLRendererAPI::SetPatchVertexCount - {} is outside [1, GL_MAX_PATCH_VERTICES={}]", + patchVertices, m_MaxPatchVertices); + return; + } + glPatchParameteri(GL_PATCH_VERTICES, static_cast(patchVertices)); + } + + // --- Pipeline state the facade was missing ----------------------------------- + + void OpenGLRendererAPI::SetFrontFace(RHI::FrontFace face) + { + OLO_PROFILE_FUNCTION(); + + glFrontFace(Utils::ToGL(face)); + RendererProfiler::GetInstance().IncrementCounter(RendererProfiler::MetricType::StateChanges, 1); + } + + void OpenGLRendererAPI::SetBlendFuncSeparate(RHI::BlendFactor srcRGB, RHI::BlendFactor dstRGB, + RHI::BlendFactor srcAlpha, RHI::BlendFactor dstAlpha) + { + OLO_PROFILE_FUNCTION(); + + glBlendFuncSeparate(Utils::ToGL(srcRGB), Utils::ToGL(dstRGB), + Utils::ToGL(srcAlpha), Utils::ToGL(dstAlpha)); + RendererProfiler::GetInstance().IncrementCounter(RendererProfiler::MetricType::StateChanges, 1); + } + + void OpenGLRendererAPI::SetClearDepth(f32 depth) + { + OLO_PROFILE_FUNCTION(); + + glClearDepth(static_cast(depth)); + } + + // --- Named framebuffers -------------------------------------------------------- + + u32 OpenGLRendererAPI::CreateFramebuffer() + { + OLO_PROFILE_FUNCTION(); + + GLuint fbo = 0; + glCreateFramebuffers(1, &fbo); + return fbo; + } + + void OpenGLRendererAPI::DeleteFramebuffer(u32 framebufferID) + { + OLO_PROFILE_FUNCTION(); + + glDeleteFramebuffers(1, &framebufferID); + } + + void OpenGLRendererAPI::AttachFramebufferColorTexture(u32 framebufferID, u32 attachmentIndex, + u32 textureID, u32 mipLevel) + { + OLO_PROFILE_FUNCTION(); + + glNamedFramebufferTexture(framebufferID, GL_COLOR_ATTACHMENT0 + attachmentIndex, textureID, + static_cast(mipLevel)); + } + + void OpenGLRendererAPI::AttachFramebufferDepthTexture(u32 framebufferID, u32 textureID, u32 mipLevel) + { + OLO_PROFILE_FUNCTION(); + + glNamedFramebufferTexture(framebufferID, GL_DEPTH_ATTACHMENT, textureID, static_cast(mipLevel)); + } + + bool OpenGLRendererAPI::IsFramebufferComplete(u32 framebufferID) + { + OLO_PROFILE_FUNCTION(); + + return glCheckNamedFramebufferStatus(framebufferID, GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; + } + + void OpenGLRendererAPI::SetFramebufferDrawAttachments(u32 framebufferID, std::span attachmentIndices) + { + OLO_PROFILE_FUNCTION(); + + u32 count = static_cast(attachmentIndices.size()); + const u32 maxBuf = static_cast(m_MaxDrawBuffers); + if (count > maxBuf) + { + OLO_CORE_WARN("OpenGLRendererAPI::SetFramebufferDrawAttachments - count {} exceeds " + "GL_MAX_DRAW_BUFFERS {}, clamping", + count, maxBuf); + count = maxBuf; + } + + // 8 covers every framebuffer in the engine (the G-Buffer is the widest + // at 5); the heap path exists only so a future wider target degrades in + // performance rather than in correctness. + if (count <= 8) + { + std::array drawBuffers{}; + for (u32 i = 0; i < count; ++i) + { + drawBuffers[i] = Utils::ToGLColorAttachment(attachmentIndices[i]); + } + glNamedFramebufferDrawBuffers(framebufferID, static_cast(count), drawBuffers.data()); + } + else + { + std::vector drawBuffers(count); + for (u32 i = 0; i < count; ++i) + { + drawBuffers[i] = Utils::ToGLColorAttachment(attachmentIndices[i]); + } + glNamedFramebufferDrawBuffers(framebufferID, static_cast(count), drawBuffers.data()); + } + } + + void OpenGLRendererAPI::RestoreAllFramebufferDrawAttachments(u32 framebufferID, u32 colorAttachmentCount) + { + OLO_PROFILE_FUNCTION(); + + // Build the identity list { 0, 1, ... count-1 } once, here, instead of at + // the nine call sites that used to open-code it. + u32 count = colorAttachmentCount; + const u32 maxBuf = static_cast(m_MaxDrawBuffers); + if (count > maxBuf) + { + OLO_CORE_WARN("OpenGLRendererAPI::RestoreAllFramebufferDrawAttachments - count {} exceeds " + "GL_MAX_DRAW_BUFFERS {}, clamping", + count, maxBuf); + count = maxBuf; + } + + // The stack path covers every framebuffer in the engine (the G-Buffer is + // the widest at 5) but must NOT be a silent cap: this helper's whole + // contract is "restore ALL of them", and quietly truncating is the exact + // failure the comment on the declaration warns about — a narrower list + // drops later fragment outputs. Above 16, allocate rather than clip. + // GL_MAX_DRAW_BUFFERS is the only legitimate ceiling, and it is applied + // above with a warning. + static constexpr u32 kStackCapacity = 16; + if (count <= kStackCapacity) + { + std::array attachments{}; + for (u32 i = 0; i < count; ++i) + { + attachments[i] = i; + } + SetFramebufferDrawAttachments(framebufferID, std::span(attachments.data(), count)); + return; + } + + std::vector attachments(count); + for (u32 i = 0; i < count; ++i) + { + attachments[i] = i; + } + SetFramebufferDrawAttachments(framebufferID, attachments); + } + + void OpenGLRendererAPI::SetFramebufferReadAttachment(u32 framebufferID, u32 attachmentIndex) + { + OLO_PROFILE_FUNCTION(); + + glNamedFramebufferReadBuffer(framebufferID, Utils::ToGLColorAttachment(attachmentIndex)); + } + + void OpenGLRendererAPI::ClearFramebufferColorAttachment(u32 framebufferID, u32 attachmentIndex, + const glm::vec4& color) + { + OLO_PROFILE_FUNCTION(); + + // The clear-program guard lives HERE, not at the call site. It is an + // NVIDIA-driver hazard mitigation (the bound program's vertex shader is + // revalidated against the target at clear time, debug id 131218 — see + // docs/agent-rules/gl-clear-program-revalidation.md), so it is backend + // knowledge. Three passes used to construct it themselves, which forced + // them to include Platform/OpenGL/OpenGLUtilities.h — a backend header in + // the sweep bucket, which would have left `sweep_glad_includes` able to + // reach zero while every one of those TUs could still see all of GL + // transitively. Same placement as ClearDepthOnly()'s existing guard. + Utils::GLClearProgramGuard programGuard; + + // The third parameter of glClearNamedFramebufferfv with GL_COLOR is a + // DRAW BUFFER INDEX, not an attachment enum — hence no ToGLColorAttachment. + glClearNamedFramebufferfv(framebufferID, GL_COLOR, static_cast(attachmentIndex), &color.x); + } + + void OpenGLRendererAPI::ClearFramebufferDepth(u32 framebufferID, f32 depth) + { + OLO_PROFILE_FUNCTION(); + + Utils::GLClearProgramGuard programGuard; + glClearNamedFramebufferfv(framebufferID, GL_DEPTH, 0, &depth); + } + + void OpenGLRendererAPI::BlitFramebuffer(u32 srcFramebufferID, u32 dstFramebufferID, + i32 srcX0, i32 srcY0, i32 srcX1, i32 srcY1, + i32 dstX0, i32 dstY0, i32 dstX1, i32 dstY1, + RHI::BlitAspect aspect, RHI::Filter filter) + { + OLO_PROFILE_FUNCTION(); + + glBlitNamedFramebuffer(srcFramebufferID, dstFramebufferID, + srcX0, srcY0, srcX1, srcY1, + dstX0, dstY0, dstX1, dstY1, + Utils::ToGLBlitMask(aspect), Utils::ToGL(filter)); + } + + // --- Raw buffer lifecycle -------------------------------------------------------- + + u32 OpenGLRendererAPI::CreateBuffer() + { + OLO_PROFILE_FUNCTION(); + + GLuint buffer = 0; + glCreateBuffers(1, &buffer); + return buffer; + } + + void OpenGLRendererAPI::DeleteBuffer(u32 bufferID) + { + OLO_PROFILE_FUNCTION(); + + glDeleteBuffers(1, &bufferID); + } + + void OpenGLRendererAPI::AllocateBufferStorage(u32 bufferID, u64 sizeBytes, RHI::MemoryResidency residency) + { + OLO_PROFILE_FUNCTION(); + + glNamedBufferData(bufferID, static_cast(sizeBytes), nullptr, Utils::ToGL(residency)); + } + + void* OpenGLRendererAPI::AllocatePersistentUploadStorage(u32 bufferID, u64 sizeBytes) + { + OLO_PROFILE_FUNCTION(); + + // Storage flags and map flags must agree or glMapNamedBufferRange fails + // at map time rather than at allocation time — which is why these are + // one call rather than two. + constexpr GLbitfield kFlags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT; + glNamedBufferStorage(bufferID, static_cast(sizeBytes), nullptr, kFlags); + return glMapNamedBufferRange(bufferID, 0, static_cast(sizeBytes), kFlags); + } + + void OpenGLRendererAPI::UnmapBuffer(u32 bufferID) + { + OLO_PROFILE_FUNCTION(); + + glUnmapNamedBuffer(bufferID); + } + + void OpenGLRendererAPI::UploadBufferSubData(u32 bufferID, u64 offsetBytes, u64 sizeBytes, const void* data) + { + OLO_PROFILE_FUNCTION(); + + glNamedBufferSubData(bufferID, static_cast(offsetBytes), static_cast(sizeBytes), data); + } + + void OpenGLRendererAPI::ReadBufferSubData(u32 bufferID, u64 offsetBytes, u64 sizeBytes, void* dest) + { + OLO_PROFILE_FUNCTION(); + + glGetNamedBufferSubData(bufferID, static_cast(offsetBytes), static_cast(sizeBytes), dest); + } + + void OpenGLRendererAPI::CopyBufferSubData(u32 srcBufferID, u32 dstBufferID, + u64 srcOffsetBytes, u64 dstOffsetBytes, u64 sizeBytes) + { + OLO_PROFILE_FUNCTION(); + + glCopyNamedBufferSubData(srcBufferID, dstBufferID, + static_cast(srcOffsetBytes), static_cast(dstOffsetBytes), + static_cast(sizeBytes)); + } + + void OpenGLRendererAPI::ClearBufferUInt(u32 bufferID, u32 value) + { + OLO_PROFILE_FUNCTION(); + + Utils::GLClearProgramGuard programGuard; + glClearNamedBufferData(bufferID, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, &value); + } + + void OpenGLRendererAPI::ClearBufferFloat(u32 bufferID, f32 value) + { + OLO_PROFILE_FUNCTION(); + + Utils::GLClearProgramGuard programGuard; + glClearNamedBufferData(bufferID, GL_R32F, GL_RED, GL_FLOAT, &value); + } + + // --- Vertex array lifecycle --------------------------------------------------------- + + u32 OpenGLRendererAPI::CreateVertexArray() + { + OLO_PROFILE_FUNCTION(); + + GLuint vao = 0; + glCreateVertexArrays(1, &vao); + return vao; + } + + void OpenGLRendererAPI::SetVertexArrayIndexBuffer(u32 vaoID, u32 bufferID) + { + OLO_PROFILE_FUNCTION(); + + glVertexArrayElementBuffer(vaoID, bufferID); + } + + void OpenGLRendererAPI::DeleteVertexArray(u32 vaoID) + { + OLO_PROFILE_FUNCTION(); + + glDeleteVertexArrays(1, &vaoID); + } + + // --- Texture clear / upload / readback ------------------------------------------------ + + void OpenGLRendererAPI::ClearTextureFloat(u32 textureID, u32 mipLevel, const glm::vec4& color) + { + OLO_PROFILE_FUNCTION(); + + Utils::GLClearProgramGuard programGuard; + glClearTexImage(textureID, static_cast(mipLevel), GL_RGBA, GL_FLOAT, &color.x); + } + + void OpenGLRendererAPI::ClearTextureUInt(u32 textureID, u32 mipLevel, u32 value) + { + OLO_PROFILE_FUNCTION(); + + Utils::GLClearProgramGuard programGuard; + glClearTexImage(textureID, static_cast(mipLevel), GL_RED_INTEGER, GL_UNSIGNED_INT, &value); + } + + void OpenGLRendererAPI::UploadTextureSubImage2D(u32 textureID, i32 xOffset, i32 yOffset, + u32 width, u32 height, + RHI::Format sourceFormat, const void* data) + { + OLO_PROFILE_FUNCTION(); + + glTextureSubImage2D(textureID, 0, xOffset, yOffset, + static_cast(width), static_cast(height), + Utils::ToGLPixelFormat(sourceFormat), Utils::ToGLPixelType(sourceFormat), data); + } + + void OpenGLRendererAPI::UploadTextureSubImage3D(u32 textureID, i32 xOffset, i32 yOffset, i32 zOffset, + u32 width, u32 height, u32 depth, + RHI::Format sourceFormat, const void* data) + { + OLO_PROFILE_FUNCTION(); + + glTextureSubImage3D(textureID, 0, xOffset, yOffset, zOffset, + static_cast(width), static_cast(height), static_cast(depth), + Utils::ToGLPixelFormat(sourceFormat), Utils::ToGLPixelType(sourceFormat), data); + } + + namespace + { + // GL's error flag is a sticky global, so an error raised by some earlier + // call would otherwise be attributed to this readback. Drain first, then + // the post-call check describes THIS call — which is what the bool + // return promises. (ADR 0011 amendment (7): glGetError does not become a + // facade entry point, it disappears into these two functions.) + void DrainGLErrors() + { + constexpr u32 kMaxDrain = 32; + for (u32 i = 0; i < kMaxDrain && glGetError() != GL_NO_ERROR; ++i) + { + } + } + } // namespace + + bool OpenGLRendererAPI::ReadTextureImage(u32 textureID, u32 mipLevel, RHI::Format destFormat, + sizet destSizeBytes, void* dest) + { + OLO_PROFILE_FUNCTION(); + + DrainGLErrors(); + glGetTextureImage(textureID, static_cast(mipLevel), + Utils::ToGLPixelFormat(destFormat), Utils::ToGLPixelType(destFormat), + static_cast(destSizeBytes), dest); + return glGetError() == GL_NO_ERROR; + } + + bool OpenGLRendererAPI::ReadTextureSubImage(u32 textureID, u32 mipLevel, i32 x, i32 y, i32 z, + u32 width, u32 height, u32 depth, + RHI::Format destFormat, sizet destSizeBytes, void* dest) + { + OLO_PROFILE_FUNCTION(); + + DrainGLErrors(); + glGetTextureSubImage(textureID, static_cast(mipLevel), x, y, z, + static_cast(width), static_cast(height), static_cast(depth), + Utils::ToGLPixelFormat(destFormat), Utils::ToGLPixelType(destFormat), + static_cast(destSizeBytes), dest); + return glGetError() == GL_NO_ERROR; + } + + void OpenGLRendererAPI::GetTextureDimensions(u32 textureID, u32 mipLevel, u32& outWidth, u32& outHeight) + { + OLO_PROFILE_FUNCTION(); + + GLint width = 0; + GLint height = 0; + glGetTextureLevelParameteriv(textureID, static_cast(mipLevel), GL_TEXTURE_WIDTH, &width); + glGetTextureLevelParameteriv(textureID, static_cast(mipLevel), GL_TEXTURE_HEIGHT, &height); + outWidth = static_cast(std::max(width, 0)); + outHeight = static_cast(std::max(height, 0)); + } + + void OpenGLRendererAPI::TextureBarrier() + { + OLO_PROFILE_FUNCTION(); + + glTextureBarrier(); + } + + // --- Queries ---------------------------------------------------------------------------- + + void OpenGLRendererAPI::CreateQueries(RHI::QueryType type, std::span outQueryIDs) + { + OLO_PROFILE_FUNCTION(); + + if (outQueryIDs.empty()) + { + return; + } + // glCreateQueries rather than glGenQueries: the DSA form binds the + // object to its target at creation, so a subsequent glBeginQuery with a + // mismatched target is an immediate error rather than a latent one. + glCreateQueries(Utils::ToGL(type), static_cast(outQueryIDs.size()), outQueryIDs.data()); + } + + void OpenGLRendererAPI::DeleteQueries(std::span queryIDs) + { + OLO_PROFILE_FUNCTION(); + + if (queryIDs.empty()) + { + return; + } + glDeleteQueries(static_cast(queryIDs.size()), queryIDs.data()); + } + + void OpenGLRendererAPI::BeginQuery(RHI::QueryType type, u32 queryID) + { + OLO_PROFILE_FUNCTION(); + + glBeginQuery(Utils::ToGL(type), queryID); + } + + void OpenGLRendererAPI::EndQuery(RHI::QueryType type) + { + OLO_PROFILE_FUNCTION(); + + glEndQuery(Utils::ToGL(type)); + } + + bool OpenGLRendererAPI::IsQueryResultAvailable(u32 queryID) + { + OLO_PROFILE_FUNCTION(); + + GLint available = 0; + glGetQueryObjectiv(queryID, GL_QUERY_RESULT_AVAILABLE, &available); + return available != 0; + } + + u32 OpenGLRendererAPI::GetQueryResultU32(u32 queryID) + { + OLO_PROFILE_FUNCTION(); + + GLuint result = 0; + glGetQueryObjectuiv(queryID, GL_QUERY_RESULT, &result); + return result; + } + + u64 OpenGLRendererAPI::GetQueryResultU64(u32 queryID) + { + OLO_PROFILE_FUNCTION(); + + GLuint64 result = 0; + glGetQueryObjectui64v(queryID, GL_QUERY_RESULT, &result); + return result; + } + + // --- Fences ------------------------------------------------------------------------------- + // + // GLsync is an opaque pointer; the facade carries it as a u64 so the same + // slot can hold a VkFence (a 64-bit handle) without the callers changing. + + u64 OpenGLRendererAPI::CreateFence() + { + OLO_PROFILE_FUNCTION(); + + GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + return reinterpret_cast(sync); + } + + RHI::FenceStatus OpenGLRendererAPI::ClientWaitFence(u64 fence, u64 timeoutNanoseconds) + { + OLO_PROFILE_FUNCTION(); + + if (fence == 0) + { + return RHI::FenceStatus::Failed; + } + const GLenum result = glClientWaitSync(reinterpret_cast(fence), GL_SYNC_FLUSH_COMMANDS_BIT, + timeoutNanoseconds); + switch (result) + { + case GL_ALREADY_SIGNALED: + return RHI::FenceStatus::AlreadySignaled; + case GL_CONDITION_SATISFIED: + return RHI::FenceStatus::ConditionSatisfied; + case GL_TIMEOUT_EXPIRED: + return RHI::FenceStatus::TimeoutExpired; + default: + return RHI::FenceStatus::Failed; + } + } + + bool OpenGLRendererAPI::IsFenceSignaled(u64 fence) + { + OLO_PROFILE_FUNCTION(); + + if (fence == 0) + { + return false; + } + GLint signaled = 0; + GLsizei length = 0; + glGetSynciv(reinterpret_cast(fence), GL_SYNC_STATUS, sizeof(signaled), &length, &signaled); + return signaled == GL_SIGNALED; + } + + void OpenGLRendererAPI::DestroyFence(u64 fence) + { + OLO_PROFILE_FUNCTION(); + + if (fence != 0) + { + glDeleteSync(reinterpret_cast(fence)); + } + } + + // --- Debug markers ------------------------------------------------------------------------- + + void OpenGLRendererAPI::PushDebugGroup(u32 id, std::string_view label) + { + // The capability check belongs here, not at the call site. RGCommandContext + // used to guard this with `if (GLAD_GL_KHR_debug)` — a glad LOADER symbol, + // which is not a portable way to ask "does this backend support debug + // markers" and which kept a backend dependency in a renderer TU. It also + // does not match `gl[A-Z]`, so the boundary ratchet could never see it + // (issue #691 Phase 2 step 2; same class of problem as SlugFontProcessor's + // `glad_glCreateTextures != nullptr` context probe, replaced in step 1). + if (GLAD_GL_KHR_debug == 0) + { + return; + } + glPushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, id, static_cast(label.size()), label.data()); + } + + void OpenGLRendererAPI::PopDebugGroup() + { + if (GLAD_GL_KHR_debug == 0) + { + return; + } + glPopDebugGroup(); + } + + // --- Device ---------------------------------------------------------------------------------- + + void OpenGLRendererAPI::WaitForDeviceIdle() + { + OLO_PROFILE_FUNCTION(); + + glFinish(); + } + + u32 OpenGLRendererAPI::GetMaxFramebufferSamples() const + { + OLO_PROFILE_FUNCTION(); + + GLint samples = 0; + glGetIntegerv(GL_MAX_SAMPLES, &samples); + return static_cast(std::max(samples, 0)); + } + + u32 OpenGLRendererAPI::GetMaxColorTextureSamples() const + { + OLO_PROFILE_FUNCTION(); + + GLint samples = 0; + glGetIntegerv(GL_MAX_COLOR_TEXTURE_SAMPLES, &samples); + return static_cast(std::max(samples, 0)); + } + + u32 OpenGLRendererAPI::GetMaxDepthTextureSamples() const + { + OLO_PROFILE_FUNCTION(); + + GLint samples = 0; + glGetIntegerv(GL_MAX_DEPTH_TEXTURE_SAMPLES, &samples); + return static_cast(std::max(samples, 0)); + } + + void OpenGLRendererAPI::SetProgramUniformFloat(u32 programID, std::string_view name, f32 value) + { + OLO_PROFILE_FUNCTION(); + + // glGetUniformLocation needs a null-terminated name and string_view does + // not promise one. A stack buffer keeps this allocation-free — the one + // caller runs per frame. + std::array nameBuffer{}; + if (name.size() >= nameBuffer.size()) + { + OLO_CORE_ERROR("OpenGLRendererAPI::SetProgramUniformFloat - uniform name '{}' exceeds {} chars", + name, nameBuffer.size() - 1); + return; + } + std::memcpy(nameBuffer.data(), name.data(), name.size()); + + const GLint location = glGetUniformLocation(programID, nameBuffer.data()); + if (location == -1) + { + // Absent uniform is not an error — the caller uses this to set an + // optional uniform on shaders that may not declare it. + return; + } + // glProgramUniform1f rather than glUniform1f: it names the program + // explicitly instead of acting on whatever happens to be bound. + glProgramUniform1f(programID, location, value); + } } // namespace OloEngine diff --git a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h index 0776217dd..2e6e4993e 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h +++ b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h @@ -99,6 +99,100 @@ namespace OloEngine void BeginConditionalRender(u32 queryID) override; void EndConditionalRender() override; + // --- Phase 2 step 2 additions (issue #691) --------------------------- + void BindUniformBuffer(u32 bindingPoint, u32 bufferID) override; + void BindStorageBuffer(u32 bindingPoint, u32 bufferID) override; + void BindShaderProgram(u32 programID) override; + void BindVertexArrayRaw(u32 vaoID) override; + void BindFramebuffer(u32 framebufferID) override; + + void DrawBoundIndexed(RHI::PrimitiveTopology topology, u32 indexCount, + RHI::IndexType indexType, u32 baseIndex) override; + void DrawBoundIndexedInstanced(RHI::PrimitiveTopology topology, u32 indexCount, + RHI::IndexType indexType, u32 baseIndex, u32 instanceCount) override; + void DrawBoundArrays(RHI::PrimitiveTopology topology, u32 firstVertex, u32 vertexCount) override; + void SetPatchVertexCount(u32 patchVertices) override; + + void SetFrontFace(RHI::FrontFace face) override; + void SetBlendFuncSeparate(RHI::BlendFactor srcRGB, RHI::BlendFactor dstRGB, + RHI::BlendFactor srcAlpha, RHI::BlendFactor dstAlpha) override; + void SetClearDepth(f32 depth) override; + + u32 CreateFramebuffer() override; + void DeleteFramebuffer(u32 framebufferID) override; + void AttachFramebufferColorTexture(u32 framebufferID, u32 attachmentIndex, + u32 textureID, u32 mipLevel) override; + void AttachFramebufferDepthTexture(u32 framebufferID, u32 textureID, u32 mipLevel) override; + [[nodiscard("Store this!")]] bool IsFramebufferComplete(u32 framebufferID) override; + void SetFramebufferDrawAttachments(u32 framebufferID, std::span attachmentIndices) override; + void RestoreAllFramebufferDrawAttachments(u32 framebufferID, u32 colorAttachmentCount) override; + void SetFramebufferReadAttachment(u32 framebufferID, u32 attachmentIndex) override; + void ClearFramebufferColorAttachment(u32 framebufferID, u32 attachmentIndex, + const glm::vec4& color) override; + void ClearFramebufferDepth(u32 framebufferID, f32 depth) override; + void BlitFramebuffer(u32 srcFramebufferID, u32 dstFramebufferID, + i32 srcX0, i32 srcY0, i32 srcX1, i32 srcY1, + i32 dstX0, i32 dstY0, i32 dstX1, i32 dstY1, + RHI::BlitAspect aspect, RHI::Filter filter) override; + + u32 CreateBuffer() override; + void DeleteBuffer(u32 bufferID) override; + void AllocateBufferStorage(u32 bufferID, u64 sizeBytes, RHI::MemoryResidency residency) override; + void* AllocatePersistentUploadStorage(u32 bufferID, u64 sizeBytes) override; + void UnmapBuffer(u32 bufferID) override; + void UploadBufferSubData(u32 bufferID, u64 offsetBytes, u64 sizeBytes, const void* data) override; + void ReadBufferSubData(u32 bufferID, u64 offsetBytes, u64 sizeBytes, void* dest) override; + void CopyBufferSubData(u32 srcBufferID, u32 dstBufferID, + u64 srcOffsetBytes, u64 dstOffsetBytes, u64 sizeBytes) override; + void ClearBufferUInt(u32 bufferID, u32 value) override; + void ClearBufferFloat(u32 bufferID, f32 value) override; + + u32 CreateVertexArray() override; + void SetVertexArrayIndexBuffer(u32 vaoID, u32 bufferID) override; + void DeleteVertexArray(u32 vaoID) override; + + void ClearTextureFloat(u32 textureID, 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, + u32 width, u32 height, + RHI::Format sourceFormat, const void* data) override; + void UploadTextureSubImage3D(u32 textureID, i32 xOffset, i32 yOffset, i32 zOffset, + u32 width, u32 height, u32 depth, + RHI::Format sourceFormat, const void* data) override; + [[nodiscard("Store this!")]] bool ReadTextureImage(u32 textureID, 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; + void GetTextureDimensions(u32 textureID, u32 mipLevel, u32& outWidth, u32& outHeight) override; + void TextureBarrier() override; + + void CreateQueries(RHI::QueryType type, std::span outQueryIDs) override; + void DeleteQueries(std::span queryIDs) override; + void BeginQuery(RHI::QueryType type, u32 queryID) override; + void EndQuery(RHI::QueryType type) override; + [[nodiscard("Store this!")]] bool IsQueryResultAvailable(u32 queryID) override; + [[nodiscard("Store this!")]] u32 GetQueryResultU32(u32 queryID) override; + [[nodiscard("Store this!")]] u64 GetQueryResultU64(u32 queryID) override; + + [[nodiscard("Store this!")]] u64 CreateFence() override; + [[nodiscard("Store this!")]] RHI::FenceStatus ClientWaitFence(u64 fence, u64 timeoutNanoseconds) override; + [[nodiscard("Store this!")]] bool IsFenceSignaled(u64 fence) override; + void DestroyFence(u64 fence) override; + + void PushDebugGroup(u32 id, std::string_view label) override; + void PopDebugGroup() override; + + void WaitForDeviceIdle() override; + [[nodiscard("Store this!")]] u32 GetMaxFramebufferSamples() const override; + [[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; + [[nodiscard("Store this!")]] bool IsDeviceAvailable() const override; [[nodiscard("Store this!")]] u32 GetMaxUniformBlockSize() const override; [[nodiscard("Store this!")]] bool SupportsInt64ShaderAtomics() const override @@ -111,6 +205,9 @@ namespace OloEngine bool m_DepthMaskEnabled = true; bool m_StencilTestEnabled = false; GLint m_MaxDrawBuffers = 0; // Cached from glGetIntegerv(GL_MAX_DRAW_BUFFERS) in Init(). + // Cached from glGetIntegerv(GL_MAX_PATCH_VERTICES) in Init(), so + // SetPatchVertexCount can validate without a per-call driver round-trip. + GLint m_MaxPatchVertices = 0; // Cached in Init(): GL_ARB_gpu_shader_int64 && GL_NV_shader_atomic_int64 (issue #629). bool m_SupportsInt64Atomics = false; }; diff --git a/OloEngine/tests/Rendering/MockRendererAPI.h b/OloEngine/tests/Rendering/MockRendererAPI.h index 10de16cde..af11dc53f 100644 --- a/OloEngine/tests/Rendering/MockRendererAPI.h +++ b/OloEngine/tests/Rendering/MockRendererAPI.h @@ -24,6 +24,7 @@ namespace OloEngine::Testing f32 ParamF32_0 = 0.0f; bool ParamBool_0 = false; glm::vec4 ParamVec4_0 = glm::vec4(0); + std::vector ParamU32List; }; class MockRendererAPI : public RendererAPI @@ -483,6 +484,382 @@ namespace OloEngine::Testing Record("DeleteTexture"); } + // ---------------------------------------------------------------- + // Phase 2 step 2 additions (issue #691). Same recording convention as + // above. Note these make the mock STRICTLY safer than before: the call + // sites they replace issued raw glXxx() through glad, which in a + // headless test is a null function pointer. + // ---------------------------------------------------------------- + void BindUniformBuffer(u32 bindingPoint, u32 bufferID) override + { + RecordedCall c{ "BindUniformBuffer" }; + c.ParamU32_0 = bindingPoint; + c.ParamU32_1 = bufferID; + m_Calls.push_back(c); + ++m_BindCount; + } + void BindStorageBuffer(u32 bindingPoint, u32 bufferID) override + { + RecordedCall c{ "BindStorageBuffer" }; + c.ParamU32_0 = bindingPoint; + c.ParamU32_1 = bufferID; + m_Calls.push_back(c); + ++m_BindCount; + } + void BindShaderProgram(u32 programID) override + { + RecordedCall c{ "BindShaderProgram" }; + c.ParamU32_0 = programID; + m_Calls.push_back(c); + ++m_BindCount; + } + void BindVertexArrayRaw(u32 vaoID) override + { + RecordedCall c{ "BindVertexArrayRaw" }; + c.ParamU32_0 = vaoID; + m_Calls.push_back(c); + ++m_BindCount; + } + void BindFramebuffer(u32 framebufferID) override + { + RecordedCall c{ "BindFramebuffer" }; + c.ParamU32_0 = framebufferID; + m_Calls.push_back(c); + ++m_BindCount; + } + + void DrawBoundIndexed(RHI::PrimitiveTopology /*topology*/, u32 indexCount, + RHI::IndexType /*indexType*/, u32 baseIndex) override + { + RecordedCall c{ "DrawBoundIndexed" }; + c.ParamU32_0 = indexCount; + c.ParamU32_1 = baseIndex; + m_Calls.push_back(c); + ++m_DrawCallCount; + } + void DrawBoundIndexedInstanced(RHI::PrimitiveTopology /*topology*/, u32 indexCount, + RHI::IndexType /*indexType*/, u32 baseIndex, u32 instanceCount) override + { + RecordedCall c{ "DrawBoundIndexedInstanced" }; + c.ParamU32_0 = indexCount; + c.ParamU32_1 = baseIndex; + c.ParamU32_2 = instanceCount; + m_Calls.push_back(c); + ++m_DrawCallCount; + } + void DrawBoundArrays(RHI::PrimitiveTopology /*topology*/, u32 firstVertex, u32 vertexCount) override + { + RecordedCall c{ "DrawBoundArrays" }; + c.ParamU32_0 = firstVertex; + c.ParamU32_1 = vertexCount; + m_Calls.push_back(c); + ++m_DrawCallCount; + } + void SetPatchVertexCount(u32 patchVertices) override + { + RecordedCall c{ "SetPatchVertexCount" }; + c.ParamU32_0 = patchVertices; + m_Calls.push_back(c); + } + + void SetFrontFace(RHI::FrontFace /*face*/) override + { + Record("SetFrontFace"); + } + void SetBlendFuncSeparate(RHI::BlendFactor /*srcRGB*/, RHI::BlendFactor /*dstRGB*/, + RHI::BlendFactor /*srcAlpha*/, RHI::BlendFactor /*dstAlpha*/) override + { + Record("SetBlendFuncSeparate"); + } + void SetClearDepth(f32 depth) override + { + RecordedCall c{ "SetClearDepth" }; + c.ParamF32_0 = depth; + m_Calls.push_back(c); + } + + u32 CreateFramebuffer() override + { + Record("CreateFramebuffer"); + return m_NextFramebufferID++; + } + void DeleteFramebuffer(u32 /*framebufferID*/) override + { + Record("DeleteFramebuffer"); + } + void AttachFramebufferColorTexture(u32 /*fb*/, u32 /*attachmentIndex*/, u32 /*texID*/, u32 /*mip*/) override + { + Record("AttachFramebufferColorTexture"); + } + void AttachFramebufferDepthTexture(u32 /*fb*/, u32 /*texID*/, u32 /*mip*/) override + { + Record("AttachFramebufferDepthTexture"); + } + [[nodiscard("Store this!")]] bool IsFramebufferComplete(u32 /*fb*/) override + { + Record("IsFramebufferComplete"); + return true; + } + void SetFramebufferDrawAttachments(u32 fb, std::span attachmentIndices) override + { + RecordedCall c{ "SetFramebufferDrawAttachments" }; + c.ParamU32_0 = fb; + c.ParamU32_1 = static_cast(attachmentIndices.size()); + // The indices themselves, not just how many: the interesting + // assertions are about WHICH attachment a pass steers a draw into + // (and whether a slot is RHI::NoAttachment), which a count cannot + // distinguish — DecalRenderPass's four modes all pass 5 entries. + c.ParamU32List.assign(attachmentIndices.begin(), attachmentIndices.end()); + m_Calls.push_back(c); + } + void RestoreAllFramebufferDrawAttachments(u32 fb, u32 colorAttachmentCount) override + { + RecordedCall c{ "RestoreAllFramebufferDrawAttachments" }; + c.ParamU32_0 = fb; + c.ParamU32_1 = colorAttachmentCount; + m_Calls.push_back(c); + } + void SetFramebufferReadAttachment(u32 fb, u32 attachmentIndex) override + { + RecordedCall c{ "SetFramebufferReadAttachment" }; + c.ParamU32_0 = fb; + c.ParamU32_1 = attachmentIndex; + m_Calls.push_back(c); + } + void ClearFramebufferColorAttachment(u32 fb, u32 attachmentIndex, const glm::vec4& color) override + { + RecordedCall c{ "ClearFramebufferColorAttachment" }; + c.ParamU32_0 = fb; + c.ParamU32_1 = attachmentIndex; + c.ParamVec4_0 = color; + m_Calls.push_back(c); + } + void ClearFramebufferDepth(u32 fb, f32 depth) override + { + RecordedCall c{ "ClearFramebufferDepth" }; + c.ParamU32_0 = fb; + c.ParamF32_0 = depth; + m_Calls.push_back(c); + } + void BlitFramebuffer(u32 src, u32 dst, i32 /*sx0*/, i32 /*sy0*/, i32 /*sx1*/, i32 /*sy1*/, + i32 /*dx0*/, i32 /*dy0*/, i32 /*dx1*/, i32 /*dy1*/, + RHI::BlitAspect /*aspect*/, RHI::Filter /*filter*/) override + { + RecordedCall c{ "BlitFramebuffer" }; + c.ParamU32_0 = src; + c.ParamU32_1 = dst; + m_Calls.push_back(c); + } + + u32 CreateBuffer() override + { + Record("CreateBuffer"); + return m_NextBufferID++; + } + void DeleteBuffer(u32 /*bufferID*/) override + { + Record("DeleteBuffer"); + } + void AllocateBufferStorage(u32 /*bufferID*/, u64 /*sizeBytes*/, RHI::MemoryResidency /*residency*/) override + { + Record("AllocateBufferStorage"); + } + void* AllocatePersistentUploadStorage(u32 /*bufferID*/, u64 /*sizeBytes*/) override + { + Record("AllocatePersistentUploadStorage"); + // Null is the documented "mapping failed" answer, and every caller + // already has a fallback path for it (VirtualMeshRegistry falls back + // to direct uploads). Handing back a fake pointer the caller would + // memcpy into is the option that would actually crash a test. + return nullptr; + } + void UnmapBuffer(u32 /*bufferID*/) override + { + Record("UnmapBuffer"); + } + void UploadBufferSubData(u32 /*bufferID*/, u64 /*offset*/, u64 /*size*/, const void* /*data*/) override + { + Record("UploadBufferSubData"); + } + void ReadBufferSubData(u32 /*bufferID*/, u64 /*offset*/, u64 /*size*/, void* /*dest*/) override + { + Record("ReadBufferSubData"); + } + void CopyBufferSubData(u32 /*src*/, u32 /*dst*/, u64 /*srcOff*/, u64 /*dstOff*/, u64 /*size*/) override + { + Record("CopyBufferSubData"); + } + void ClearBufferUInt(u32 /*bufferID*/, u32 /*value*/) override + { + Record("ClearBufferUInt"); + } + void ClearBufferFloat(u32 /*bufferID*/, f32 /*value*/) override + { + Record("ClearBufferFloat"); + } + + u32 CreateVertexArray() override + { + Record("CreateVertexArray"); + return m_NextVertexArrayID++; + } + void SetVertexArrayIndexBuffer(u32 /*vaoID*/, u32 /*bufferID*/) override + { + Record("SetVertexArrayIndexBuffer"); + } + void DeleteVertexArray(u32 /*vaoID*/) override + { + Record("DeleteVertexArray"); + } + + void ClearTextureFloat(u32 texID, u32 /*mip*/, const glm::vec4& color) override + { + RecordedCall c{ "ClearTextureFloat" }; + c.ParamU32_0 = texID; + c.ParamVec4_0 = color; + m_Calls.push_back(c); + } + void ClearTextureUInt(u32 texID, u32 /*mip*/, u32 value) override + { + RecordedCall c{ "ClearTextureUInt" }; + c.ParamU32_0 = texID; + c.ParamU32_1 = value; + m_Calls.push_back(c); + } + void UploadTextureSubImage2D(u32 /*texID*/, i32 /*x*/, i32 /*y*/, u32 /*w*/, u32 /*h*/, + RHI::Format /*sourceFormat*/, const void* /*data*/) override + { + Record("UploadTextureSubImage2DOffset"); + } + void UploadTextureSubImage3D(u32 /*texID*/, i32 /*x*/, i32 /*y*/, i32 /*z*/, + u32 /*w*/, u32 /*h*/, u32 /*d*/, + RHI::Format /*sourceFormat*/, const void* /*data*/) override + { + Record("UploadTextureSubImage3D"); + } + [[nodiscard("Store this!")]] bool ReadTextureImage(u32 /*texID*/, u32 /*mip*/, RHI::Format /*fmt*/, + sizet /*destSizeBytes*/, void* /*dest*/) override + { + Record("ReadTextureImage"); + // False, not true: there is no device behind the mock, so `dest` is + // untouched. Claiming success would let a caller consume + // uninitialised memory and call it a readback. + return false; + } + [[nodiscard("Store this!")]] bool ReadTextureSubImage(u32 /*texID*/, u32 /*mip*/, i32 /*x*/, i32 /*y*/, i32 /*z*/, + u32 /*w*/, u32 /*h*/, u32 /*d*/, RHI::Format /*fmt*/, + sizet /*destSizeBytes*/, void* /*dest*/) override + { + Record("ReadTextureSubImage"); + return false; + } + void GetTextureDimensions(u32 /*texID*/, u32 /*mip*/, u32& outWidth, u32& outHeight) override + { + Record("GetTextureDimensions"); + outWidth = m_Viewport.width; + outHeight = m_Viewport.height; + } + void TextureBarrier() override + { + Record("TextureBarrier"); + } + + void CreateQueries(RHI::QueryType /*type*/, std::span outQueryIDs) override + { + Record("CreateQueries"); + for (u32& id : outQueryIDs) + { + id = m_NextQueryID++; + } + } + void DeleteQueries(std::span /*queryIDs*/) override + { + Record("DeleteQueries"); + } + void BeginQuery(RHI::QueryType /*type*/, u32 queryID) override + { + RecordedCall c{ "BeginQuery" }; + c.ParamU32_0 = queryID; + m_Calls.push_back(c); + } + void EndQuery(RHI::QueryType /*type*/) override + { + Record("EndQuery"); + } + [[nodiscard("Store this!")]] bool IsQueryResultAvailable(u32 /*queryID*/) override + { + Record("IsQueryResultAvailable"); + return false; + } + [[nodiscard("Store this!")]] u32 GetQueryResultU32(u32 /*queryID*/) override + { + Record("GetQueryResultU32"); + return 0; + } + [[nodiscard("Store this!")]] u64 GetQueryResultU64(u32 /*queryID*/) override + { + Record("GetQueryResultU64"); + return 0; + } + + [[nodiscard("Store this!")]] u64 CreateFence() override + { + Record("CreateFence"); + // A non-zero opaque handle, so the SUCCESS path is what tests + // exercise by default. Returning 0 made every caller take its + // creation-failed branch, which meant the mock could only ever + // cover the error path (and made FrameResourceManager log an error + // on a perfectly healthy test). + return m_NextFenceHandle++; + } + [[nodiscard("Store this!")]] RHI::FenceStatus ClientWaitFence(u64 /*fence*/, u64 /*timeoutNs*/) override + { + Record("ClientWaitFence"); + return RHI::FenceStatus::AlreadySignaled; + } + [[nodiscard("Store this!")]] bool IsFenceSignaled(u64 /*fence*/) override + { + Record("IsFenceSignaled"); + return true; + } + void DestroyFence(u64 /*fence*/) override + { + Record("DestroyFence"); + } + + void PushDebugGroup(u32 /*id*/, std::string_view /*label*/) override + { + Record("PushDebugGroup"); + } + void PopDebugGroup() override + { + Record("PopDebugGroup"); + } + + void WaitForDeviceIdle() override + { + Record("WaitForDeviceIdle"); + } + [[nodiscard("Store this!")]] u32 GetMaxFramebufferSamples() const override + { + return 8; + } + [[nodiscard("Store this!")]] u32 GetMaxColorTextureSamples() const override + { + return 8; + } + [[nodiscard("Store this!")]] u32 GetMaxDepthTextureSamples() const override + { + return 8; + } + void SetProgramUniformFloat(u32 programID, std::string_view /*name*/, f32 value) override + { + RecordedCall c{ "SetProgramUniformFloat" }; + c.ParamU32_0 = programID; + c.ParamF32_0 = value; + m_Calls.push_back(c); + } + private: void Record(const std::string& name) { @@ -493,6 +870,11 @@ namespace OloEngine::Testing u32 m_BindCount = 0; u32 m_DrawCallCount = 0; u32 m_NextTextureID = 1; + u32 m_NextFramebufferID = 1; + u32 m_NextBufferID = 1; + u32 m_NextVertexArrayID = 1; + u32 m_NextQueryID = 1; + u64 m_NextFenceHandle = 1; u32 m_MaxUniformBlockSize = 65536u; bool m_SupportsInt64Atomics = false; Viewport m_Viewport{ 0, 0, 1920, 1080 }; diff --git a/OloEngine/tests/Rendering/RHIEnumLoweringTest.cpp b/OloEngine/tests/Rendering/RHIEnumLoweringTest.cpp index 51a754a95..becf88160 100644 --- a/OloEngine/tests/Rendering/RHIEnumLoweringTest.cpp +++ b/OloEngine/tests/Rendering/RHIEnumLoweringTest.cpp @@ -16,11 +16,24 @@ // 1. Every enumerator lowers to the exact GL constant it names. Checked // against the literal GL_* token rather than a numeric value, so the test // states the intended mapping rather than restating whatever the code does. -// 2. Each enum's member COUNT is pinned by a static_assert on its last -// enumerator. Adding a member without extending ToGL() would otherwise fall -// through to the switch's error path, which logs and returns a plausible -// default — a silent wrong mapping, exactly what (1) cannot catch on its -// own because the new member has no table row. +// 2. Each enum's shape is pinned by a static_assert on its last enumerator's +// ordinal. Be precise about what this does and does not catch, because the +// two halves are covered by different mechanisms: +// +// * INSERTING a member mid-enum, REMOVING one, or REORDERING them all +// shift the last ordinal, so the static_assert fires. That is its job. +// * APPENDING a member after the current last one leaves that ordinal +// unchanged, so the static_assert CANNOT see it. What catches an append +// is the compiler: the lowering switches in OpenGLRHIConversions.h +// deliberately carry no `default:` label, so `-Wswitch` errors on the +// unhandled enumerator. That is why adding a `default:` there would be +// a downgrade, and why the clang-cl CI job is load-bearing (MSVC's +// C4062 is off by default even at /W4). +// +// Either way the failure being prevented is the same: a new member falling +// through to the error path, which logs and returns a plausible value — a +// silent wrong mapping that (1) cannot catch on its own, because the new +// member has no table row. // // No GL context is required: the conversions are pure switches. @@ -68,6 +81,26 @@ namespace "RHI::Access changed — update ToGLImageAccess() and ImageAccessLowering"); static_assert(static_cast(RHI::PrimitiveTopology::PatchList) == 5, "RHI::PrimitiveTopology changed — update ToGL() and PrimitiveTopologyLowering"); + + // Phase 2 step 2 vocabulary (ADR 0011 amendment (10)). Same tripwire + // discipline: IndexType in particular has only two members, which makes a + // swapped mapping look harmless right up until a u16-indexed mesh reads its + // element buffer at 4-byte stride and renders as scattered triangles. + static_assert(static_cast(RHI::IndexType::UInt32) == 1, + "RHI::IndexType changed — update ToGL() and IndexTypeLowering"); + static_assert(static_cast(RHI::FrontFace::Clockwise) == 1, + "RHI::FrontFace changed — update ToGL() and FrontFaceLowering"); + static_assert(static_cast(RHI::QueryType::TimeElapsed) == 1, + "RHI::QueryType changed — update ToGL() and QueryTypeLowering"); + static_assert(static_cast(RHI::MemoryResidency::DeviceToHost) == 2, + "RHI::MemoryResidency changed — update ToGL() and MemoryResidencyLowering"); + static_assert(static_cast(RHI::BlitAspect::DepthStencil) == 3, + "RHI::BlitAspect changed — update ToGLBlitMask() and BlitAspectLowering"); + // FenceStatus has no ToGL() — it is produced BY the backend, not consumed by + // it — so its tripwire lives with ClientWaitFence's switch instead. Pinned + // here anyway so a new member is noticed at the same place as its siblings. + static_assert(static_cast(RHI::FenceStatus::Failed) == 3, + "RHI::FenceStatus changed — update OpenGLRendererAPI::ClientWaitFence"); } // namespace TEST(RHIEnumLowering, CompareOpLowersToTheNamedGLConstant) @@ -207,3 +240,70 @@ TEST(RHIEnumLowering, PrimitiveTopologyLowersToTheNamedGLPrimitive) EXPECT_EQ(Utils::ToGL(RHI::PrimitiveTopology::PointList), GLenum{ GL_POINTS }); EXPECT_EQ(Utils::ToGL(RHI::PrimitiveTopology::PatchList), GLenum{ GL_PATCHES }); } + +// --------------------------------------------------------------------------- +// Phase 2 step 2 vocabulary (ADR 0011 amendment (10)). +// --------------------------------------------------------------------------- + +TEST(RHIEnumLowering, IndexTypeLowersToTheNamedGLType) +{ + // Two members, so a swap is invisible to a reviewer and catastrophic at run + // time: a u16 index buffer read at u32 stride draws from the wrong vertices + // AND overruns the buffer's tail. + EXPECT_EQ(Utils::ToGL(RHI::IndexType::UInt16), GLenum{ GL_UNSIGNED_SHORT }); + EXPECT_EQ(Utils::ToGL(RHI::IndexType::UInt32), GLenum{ GL_UNSIGNED_INT }); +} + +TEST(RHIEnumLowering, FrontFaceLowersToTheNamedGLWinding) +{ + // PlanarReflectionRenderPass flips this to compensate for the mirror matrix + // reversing triangle winding; a swap silently culls exactly the faces that + // should be visible in the reflection. + EXPECT_EQ(Utils::ToGL(RHI::FrontFace::CounterClockwise), GLenum{ GL_CCW }); + EXPECT_EQ(Utils::ToGL(RHI::FrontFace::Clockwise), GLenum{ GL_CW }); +} + +TEST(RHIEnumLowering, QueryTypeLowersToTheNamedGLTarget) +{ + EXPECT_EQ(Utils::ToGL(RHI::QueryType::OcclusionAnySamples), GLenum{ GL_ANY_SAMPLES_PASSED }); + EXPECT_EQ(Utils::ToGL(RHI::QueryType::TimeElapsed), GLenum{ GL_TIME_ELAPSED }); +} + +TEST(RHIEnumLowering, MemoryResidencyLowersToTheNamedGLHint) +{ + // GL treats these as hints, so a wrong entry costs bandwidth rather than + // correctness — which is exactly why it needs a table test. Nothing would + // render wrong and no other assertion in the suite would notice. + EXPECT_EQ(Utils::ToGL(RHI::MemoryResidency::HostToDevice), GLenum{ GL_DYNAMIC_DRAW }); + EXPECT_EQ(Utils::ToGL(RHI::MemoryResidency::DeviceLocal), GLenum{ GL_DYNAMIC_COPY }); + EXPECT_EQ(Utils::ToGL(RHI::MemoryResidency::DeviceToHost), GLenum{ GL_DYNAMIC_READ }); +} + +TEST(RHIEnumLowering, BlitAspectLowersToTheNamedGLBitfield) +{ + EXPECT_EQ(Utils::ToGLBlitMask(RHI::BlitAspect::Color), GLbitfield{ GL_COLOR_BUFFER_BIT }); + EXPECT_EQ(Utils::ToGLBlitMask(RHI::BlitAspect::Depth), GLbitfield{ GL_DEPTH_BUFFER_BIT }); + EXPECT_EQ(Utils::ToGLBlitMask(RHI::BlitAspect::Stencil), GLbitfield{ GL_STENCIL_BUFFER_BIT }); + EXPECT_EQ(Utils::ToGLBlitMask(RHI::BlitAspect::DepthStencil), + GLbitfield{ GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT }); +} + +// The sentinel is the whole reason draw-attachment lists go through a lowering +// function instead of `GL_COLOR_ATTACHMENT0 + i` at each call site: GL_NONE is +// not GL_COLOR_ATTACHMENT0 + anything. DecalRenderPass writes lists like +// { attachment 0, NONE, NONE, NONE, NONE } to steer a decal into exactly one +// G-Buffer target, and folding the sentinel into the arithmetic would turn +// "writes nowhere" into "writes to attachment 4294967295" — a GL_INVALID_ENUM +// that drops the whole draw-buffer assignment, leaving the PREVIOUS list live. +TEST(RHIEnumLowering, ColorAttachmentLoweringHonoursTheNoAttachmentSentinel) +{ + EXPECT_EQ(Utils::ToGLColorAttachment(0), GLenum{ GL_COLOR_ATTACHMENT0 }); + EXPECT_EQ(Utils::ToGLColorAttachment(1), GLenum{ GL_COLOR_ATTACHMENT1 }); + EXPECT_EQ(Utils::ToGLColorAttachment(4), GLenum{ GL_COLOR_ATTACHMENT4 }); + EXPECT_EQ(Utils::ToGLColorAttachment(RHI::NoAttachment), GLenum{ GL_NONE }); + + // GL_NONE is 0, and so is GL_COLOR_ATTACHMENT0 + 0 in no sane reading — + // pin that they are genuinely different values so the sentinel cannot be + // "simplified" into attachment 0. + EXPECT_NE(Utils::ToGLColorAttachment(RHI::NoAttachment), Utils::ToGLColorAttachment(0)); +} diff --git a/OloEngine/tests/Rendering/rhi_boundary_baseline.json b/OloEngine/tests/Rendering/rhi_boundary_baseline.json index d749f39a0..0dc0193ef 100644 --- a/OloEngine/tests/Rendering/rhi_boundary_baseline.json +++ b/OloEngine/tests/Rendering/rhi_boundary_baseline.json @@ -65,13 +65,50 @@ "Debug/Instrumentor.h included , and Instrumentor.h is", "in OloEnginePCH.h — so every TU in the engine saw the whole GL API. The", "OLO_PROFILE_GPU* macros it served were used nowhere, so the include was", - "deleted. A per-file #include scan cannot see a PCH; check it by hand." + "deleted. A per-file #include scan cannot see a PCH; check it by hand.", + "", + "PHASE 2 STEP 2 (2026-07-30): sweep_gl_calls 313 -> 0 and", + "sweep_glad_includes 39 -> 0. The sweep bucket now contains no raw OpenGL", + "call and no translation unit in it can name a GL symbol at all. Phase 2's", + "headline target is met.", + "", + "BOTH ZEROES ARE NOW FLOORS, NOT TARGETS. A rise in either is a real", + "regression with no remaining excuse: the facade gained ~60 virtuals in this", + "step precisely so every operation the engine performs has a neutral", + "spelling (buffer binding points, buffer/framebuffer/VAO/query/fence", + "lifecycle, named-framebuffer draw+read attachment selection, blits, texture", + "clear/upload/readback, debug markers, device idle, MSAA caps). If you find", + "an operation with no facade entry point, add the virtual — do not reach for", + "glad.", + "", + "Three things this step is worth remembering for:", + "", + "1. The counter reaching zero was NOT the end of the work. A Platform/OpenGL", + " header included from the sweep bucket leaks the whole GL API just as", + " effectively as , and this scan cannot see it. Three passes", + " (FluidIntermediatesPass, WaterRenderPass, VirtualMeshRegistry) included", + " Platform/OpenGL/OpenGLUtilities.h for GLClearProgramGuard. The fix was to", + " move the guard INTO the backend clear implementations where it belongs —", + " not to delete the glad include and call it done, which would have zeroed", + " this counter while proving nothing. Check for Platform// includes", + " by hand, exactly like the PCH.", + "", + "2. Zero GL CALLS does not mean zero GL. UIRenderer.cpp had no calls but typed", + " its clip-rect stack in GLint/GLsizei, so it genuinely could not compile", + " without the header. Removing an include is only safe when the file names", + " no GL* identifier of any kind — and only a compile proves it.", + "", + "3. A loader-symbol probe is invisible here by construction. RGCommandContext", + " guarded its debug markers with `if (GLAD_GL_KHR_debug)`, which does not", + " match gl[A-Z] and so never appeared in this count. The capability check", + " moved into OpenGLRendererAPI::PushDebugGroup. Same class as step 1's", + " `glad_glCreateTextures != nullptr` context probe." ], - "measured_on_commit": "6d18846e + phase-2 step 1 (working tree)", + "measured_on_commit": "11d7cea7", "measured_on_date": "2026-07-30", - "sweep_gl_calls": 313, - "sweep_glad_includes": 39, + "sweep_gl_calls": 0, + "sweep_glad_includes": 0, "tools_gl_calls": 236, "debug_escape_hatch": 0 } diff --git a/docs/adr/0011-rhi-neutral-resource-and-binding-model.md b/docs/adr/0011-rhi-neutral-resource-and-binding-model.md index 718f2c09b..14f0edd57 100644 --- a/docs/adr/0011-rhi-neutral-resource-and-binding-model.md +++ b/docs/adr/0011-rhi-neutral-resource-and-binding-model.md @@ -411,6 +411,15 @@ Three counters, all monotonically non-increasing, all baselined in | `sweep_glad_includes` — files there including `` | 70 | **0** | 2 | | `tools_gl_calls` — GL calls in `OloEngine/Renderer/Debug/` | 236 | 0 (relocated) | 8 | +**Status (2026-07-30): both Phase 2 counters reached zero.** Step 1 took +`sweep_glad_includes` 70 → 39 by stripping the `GLenum`/`GLuint` virtuals (which +is what made the counter meaningful at all, per the ordering constraint below); +step 2 took `sweep_gl_calls` 313 → 0 and `sweep_glad_includes` 39 → 0. See +"Amendments from Phase 2 step 2" for what the sweep cost — chiefly that the +facade had to grow ~60 virtuals, because it was not merely GL-typed but +incomplete. `tools_gl_calls` is unchanged at 236 and remains Phase 8's +relocation, not an exemption (§1.6). + `sweep_glad_includes` is the counter that actually *proves* the property. A call count is a progress measure that a clever workaround can game (wrap the call in a helper that still lives in `Renderer/`); a translation unit that cannot see @@ -800,6 +809,175 @@ matters more than the first: --- +## Amendments from Phase 2 step 2 (2026-07-30) — the call-site sweep + +Step 1 converted the facade's *vocabulary*; step 2 swept the 313 raw `glXxx()` +call sites in the sweep bucket to zero. The headline finding is that **the +facade was not merely GL-typed, it was incomplete**: 84 distinct GL entry points +appear at those call sites, and **54 of them had no `RendererAPI` equivalent at +all**. Closing that gap took **60 new virtuals**. + +Those two numbers are deliberately not folded into one percentage, because they +count different things: an entry point can expand into more than one virtual +(`glClearTexImage` becomes a float clear and a uint clear, mirroring +`VkClearColorValue`'s union; the two readbacks each gained a `bool` return). 54 +is the size of the *gap*; 60 is the size of the *fix*. Quoting 60 against 84 as a +ratio would silently compare an operation count to an API count. + +### (5) The facade grows 60 virtuals, and that number is the real measurement + +§1.7 framed Phase 2 as "strip the `GLenum`s, then sweep". That undersells it. +Stripping the enums (step 1) touched 74 existing virtuals; the sweep needed +**60 new ones**, because whole categories of GPU work had simply never been +abstracted and every pass reached past the facade to do them: + +| Category | New virtuals | Why it had no facade entry | +| --- | ---: | --- | +| Buffer binding points (`glBindBufferBase`) | 2 | The single biggest gap — 26 call sites, UBO and SSBO | +| Buffer lifecycle (create / storage / map / copy / clear / readback / delete) | 9 | `UniformBuffer` / `StorageBuffer` wrap *their* buffers; `VirtualMeshRegistry` hand-rolls an arena + a persistent-mapped upload ring | +| Named-framebuffer state (draw/read attachment, clear, blit, attach, completeness) | 10 | `SetDrawBuffers` existed but only for the *bound* FBO; every call site names a specific one via DSA | +| Queries (occlusion + timer) | 7 | `BeginConditionalRender` existed; the pools that feed it did not | +| Fences | 4 | `FrameResourceManager` used `GLsync` directly | +| Draws from bound geometry | 4 | The `*Raw(vaoID, …)` family binds its own VAO; `CommandDispatch` keeps a redundant-bind cache and needs a draw that does not re-bind | +| Program / VAO / framebuffer binding | 5 | `Shader::Bind()` exists, but the POD dispatcher holds only a `u32` program id | +| Texture clear / offset upload / readback / dimensions / barrier | 8 | — | +| Vertex-array lifecycle | 3 | — | +| Debug groups, device idle, sample-count caps, separate blend func, front face, clear depth, patch count | 8 | — | + +*Generalisable, and the thing to carry into Phase 5:* **an abstraction's +completeness is not measured by how many call sites it already serves, but by +how many distinct operations the layer above performs.** 74 virtuals looked like +a thorough facade while 60 operations went around it, because the ones that went +around it were each rare enough (1–3 sites) to feel like a special case. The +`glBindBufferBase` count (26 sites, one missing pair of virtuals) is the +counter-example that shows frequency was never the signal either. + +### (6) Named framebuffers need a "writes nowhere" sentinel + +`glNamedFramebufferDrawBuffers` is 24 of the 313, and the interesting half of +them (`DecalRenderPass`) pass arrays containing `GL_NONE` — *slot i writes +nothing* — to steer a decal into exactly one G-Buffer attachment. The existing +`SetDrawBuffers(std::span)` maps `attachments[i] → +GL_COLOR_ATTACHMENT0 + attachments[i]` and **cannot express that**. + +`RHI::NoAttachment` (a `u32` sentinel, `numeric_limits::max()`) is added and +honoured by every draw-attachment lowering. This matters beyond GL: a Vulkan +backend maps the same list onto `VkSubpassDescription::pColorAttachments` where +the equivalent is `VK_ATTACHMENT_UNUSED` — also a sentinel, also not +representable as an index. Both APIs need it; only the neutral layer was missing +it. + +`glNamedFramebufferDrawBuffer` (singular) folds into the same virtual as a +one-element span — it sets draw slot 0 to the named attachment, which is exactly +what a one-element list does. + +### (7) `glGetError` disappears rather than being abstracted + +`ThumbnailCapture` reads a texture back and then checks `glGetError()`. A +neutral `GetError()` would be the wrong shape twice over: GL's error model is a +global sticky flag, Vulkan's is a per-call `VkResult`, and exposing either forces +the other backend to fake it. + +The readback virtuals therefore **return `bool`** and swallow the check inside +the backend. One entry point vanished from the sweep with no replacement, which +is the outcome to prefer whenever a GL call exists only to interrogate a +GL-specific mechanism. Same reasoning as amendment (1)'s `SetPolygonMode` face: +check whether the parameter/call is a fossil before translating it. + +### (8) Draws that do *not* bind their geometry are the Vulkan-shaped ones + +`CommandDispatch` keeps a `CurrentBoundVAO` cache and calls `glDrawElements` +directly, so routing it through the existing `DrawIndexedRaw(vaoID, …)` family +would have made the backend re-bind on every draw and defeated the cache. + +The new `DrawBoundIndexed` / `DrawBoundIndexedInstanced` / `DrawBoundArrays` draw +from *previously bound* geometry — which is not a GL-ism to be apologised for, +it is the **native Vulkan shape** (`vkCmdBindVertexBuffers` + +`vkCmdBindIndexBuffer` then `vkCmdDrawIndexed`). The combined `*Raw(vaoID, …)` +form that binds-and-draws is the less portable of the two. They also carry +`RHI::PrimitiveTopology` and `RHI::IndexType` explicitly rather than hard-coding +`GL_TRIANGLES`/`GL_UNSIGNED_INT` as the `*Raw` family does. + +`SetPatchVertexCount` is split out rather than folded into a patch-draw variant, +because the tessellation call sites set it once and draw many times. + +### (9) One recorded debt: `SetProgramUniformFloat` is not portable, deliberately + +`CommandDispatch::DrawInfiniteGrid` does `glGetUniformLocation(program, +"u_GridScale")` + `glUniform1f`. A name-keyed default-block uniform has **no +Vulkan counterpart** — SPIR-V has push constants and UBO members, not a +queryable default uniform block. + +Three options were weighed: move `u_GridScale` into the camera/grid UBO (a +shader change, and this branch is a call-site sweep whose safety net is +golden-image parity — a shader edit forfeits that), reach for the `Shader` class +(the dispatcher holds a `u32` program id by design, not a `Ref`), or add +the virtual and record the debt. The third is taken: `SetProgramUniformFloat(u32 +programID, std::string_view name, f32 value)` exists, has exactly one call site, +and is **the one virtual on the facade that a Vulkan backend cannot implement +faithfully.** Phase 6 must fold `u_GridScale` into a UBO and delete it. It is +called out here rather than left as a surprise, because a single unimplementable +virtual discovered during Phase 7 bring-up reads as a design failure when it is +actually a scheduled one. + +### (10) New `RHITypes.h` vocabulary + +`RHI::QueryType` (`OcclusionAnySamples`, `TimeElapsed` — the two the engine +actually uses; deliberately not a mirror of GL's target space), +`RHI::FenceStatus` (`AlreadySignaled` / `ConditionSatisfied` / `TimeoutExpired` / +`Failed`, matching `glClientWaitSync`'s four returns and `vkWaitForFences`' +`VK_SUCCESS`/`VK_TIMEOUT` split), `RHI::BlitAspect`, and `RHI::NoAttachment`. + +**`MemoryResidency` moved from `RHIResources.h` to `RHITypes.h`, and the near-miss +is the lesson.** `AllocateBufferStorage` needs to say how a buffer's memory is +used, and the sweep started inventing a `RHI::BufferUsage` enum +(`DynamicDraw`/`DynamicCopy`/`DynamicRead`) for it — a straight transcription of +GL's usage hints. Phase 1 had **already designed exactly this concept**, better, +as `MemoryResidency` (`DeviceLocal` / `HostToDevice` / `DeviceToHost`): named by +intent rather than by GL's spelling, and the three members map one-to-one onto +what the sweep needed. It was invisible because it sat next to `BufferDesc` in a +header nothing consumed yet, and because `RendererAPI.h` includes only +`RHITypes.h`. + +What surfaced it was not review — it was a **name collision**: `RHIResources.h` +already had a `BufferUsage`, a *bind-flags* enum (`Vertex`/`Index`/`Uniform`/ +`Storage`/…), and the two could not coexist. The engine library compiled fine +(nothing in it includes `RHIResources.h`); only the ratchet test, which includes +that header precisely so the declaration-only vocabulary keeps compiling, caught +it. + +*Generalisable:* **a declaration-only header from an earlier phase must be read +for the vocabulary you are about to invent, not just for the types you consume.** +Phase 1 wrote that header so Phase 2 would have "a fixed target to convert +toward"; the sweep nearly added a second, worse spelling of one of its concepts +anyway. The collision was luck. The habit that would not need luck is: before +adding an enum to `RHITypes.h`, grep `Renderer/RHI/` for the concept, not the +name. + +Note this also resolves what would otherwise have been recorded as debt against +`StorageBufferUsage` (`StorageBuffer.h`, `DynamicDraw`/`DynamicCopy`): that +engine-wrapper option and `MemoryResidency` are now the only two spellings, and +Phase 5 collapses them when `StorageBuffer` moves onto `RHI::ResourceHandle`. + +Every new enum is pinned by the same last-ordinal `static_assert` + literal-token +table in `RHIEnumLoweringTest.cpp` that the "One new guard" paragraph above +established (not amendment (4), which is about `UploadTextureSubImage2D`'s +source-buffer format). + +One correction to that guard's stated reach, found in step 2: the last-ordinal +`static_assert` catches an enumerator being **inserted, removed or reordered**, +but *not* one **appended** after the current last member — appending leaves the +asserted ordinal unchanged. Appends are caught by the compiler instead: the +lowering switches in `OpenGLRHIConversions.h` deliberately carry no `default:` +label, so `-Wswitch` errors on the unhandled enumerator. That makes the absence +of `default:` load-bearing rather than an oversight, and makes the clang-cl CI +job the one that enforces it (MSVC's C4062 is off by default even at `/W4`). +A `Count` sentinel per enum was considered and rejected: it makes an invalid +value representable in the neutral vocabulary and forces a dead `case` in every +lowering switch. + +--- + ## Consequences - The renderer carries **four** boundary concepts where it carries one today diff --git a/docs/agent-rules/rhi-abstraction-boundary.md b/docs/agent-rules/rhi-abstraction-boundary.md index 08c6d36e5..5fb8ffd66 100644 --- a/docs/agent-rules/rhi-abstraction-boundary.md +++ b/docs/agent-rules/rhi-abstraction-boundary.md @@ -140,6 +140,50 @@ The safe predicate for removing an include is "zero GL calls **and** zero ratchet's call pattern, which is deliberately narrower because it is measuring something else. +Step 2 finished with exactly four files still including ``, and they +split along this line. Three — `BloomRenderPass.cpp`, `OITResolveRenderPass.cpp` +and `ShaderPack.cpp` — named no `GL*` identifier at all, so the include just fell +out. The fourth hit the second case: `UIRenderer.cpp` made **zero** GL calls but +typed its clip-rect stack in `GLint`/`GLsizei`. Those values are scissor-rect +*coordinates* — `i32`/`u32` — and `RenderCommand::SetScissorBox` already took +engine types, so the GL spelling was pure inertia. It was nonetheless +load-bearing: delete the include without retyping the struct and the file does +not compile. + +### A `Platform//` include leaks just as much, and this scan cannot see it either + +The PCH is the transitive path everyone warns about. There is a second one that +is much easier to walk into, because it looks like ordinary engine code: + +> Three passes — `FluidIntermediatesPass`, `WaterRenderPass`, +> `VirtualMeshRegistry` — included `Platform/OpenGL/OpenGLUtilities.h` to +> construct `Utils::GLClearProgramGuard` around their clears. That header +> includes ``. Deleting each file's *direct* glad include would have +> driven `sweep_glad_includes` to zero while all three TUs could still name +> every symbol in OpenGL. + +That is the counter-gaming the baseline's own `_comment` warns about, arrived at +honestly rather than deliberately — which is what makes it worth recording. + +**The fix is a layering question, not an include question.** `GLClearProgramGuard` +exists because an NVIDIA driver revalidates the bound program at clear time +(`gl-clear-program-revalidation.md`). That is *backend knowledge*. It belongs +inside `OpenGLRendererAPI::Clear{Texture,Buffer,FramebufferColorAttachment, +FramebufferDepth}` — where `ClearDepthOnly()` had been carrying it correctly all +along. Once the guard moved down, the passes needed no backend header at all. + +Generalisable: when a module outside a boundary constructs a helper from inside +it, the helper is usually on the wrong side. Ask what knowledge the helper +encodes; if the answer names a vendor, a driver or an API, it belongs to the +backend, and the call site should be getting the behaviour for free rather than +opting into it. + +Audit `Platform//` includes from the sweep bucket **by hand**, on the +same schedule as the PCH. Note the *factory* files (`Texture.cpp`, `Shader.cpp`, +`VertexBuffer.cpp`, …) legitimately include their backend counterparts to +construct one in `Create()` — those are the pattern working as intended, not +leaks, and Phase 4 adds a Vulkan branch beside them. + ### Expect to ADD a few includes, and do not read that as a regression ADR 0011 §0 measured that 3 of the 42 GL-calling files had no direct @@ -154,6 +198,94 @@ files behind a transitive include. --- +## 2b. The facade was not just GL-typed, it was **incomplete** — and that is the bigger number + +Step 1 rewrote the vocabulary of `RendererAPI`'s 74 existing virtuals. Step 2 +then discovered that stripping `GLenum` was the smaller half of the job: + +> **84 distinct GL entry points** appear across the 313 swept call sites, and +> **54 of them had no `RendererAPI` equivalent at all.** Closing that gap took +> **60 new virtuals** — nearly doubling a 74-virtual facade. + +Keep those two numbers distinct rather than folding them into one percentage: an +entry point can expand into more than one virtual (`glClearTexImage` becomes a +float clear and a uint clear; the readbacks each gained a `bool` return). 54 is +the size of the gap, 60 is the size of the fix. + +Whole categories had simply never been abstracted, so every pass reached past the +facade to perform them: buffer binding points (`glBindBufferBase`, 26 sites), +raw buffer lifecycle (the virtual-geometry arena + persistent-mapped upload +ring), named-framebuffer state (draw/read attachment selection, clears, blits, +attachment, completeness), occlusion and timer queries, fences, VAO lifecycle, +texture clear/readback, debug markers. + +**The lesson, and it generalises past this phase:** an abstraction's +completeness is not measured by how many call sites it already serves, but by +how many distinct *operations* the layer above performs. 74 virtuals looked like +a thorough facade while 60 operations went around it — because each of those was +rare enough (1–3 sites) to read as a special case. Frequency was not the signal +either: `glBindBufferBase` had 26 sites and was still missing. + +Practical consequence for a future phase: before starting a sweep, histogram the +**distinct entry points**, not the call count. The call count tells you how much +typing you face; the entry-point histogram tells you how much *designing* you +face, and that is the part that cannot be delegated or hurried. + +### Read the previous phase's declaration-only header for VOCABULARY, not just for types + +The sweep needed to tell `AllocateBufferStorage` how a buffer's memory is used, +and began inventing `RHI::BufferUsage { DynamicDraw, DynamicCopy, DynamicRead }` +— a straight transcription of GL's `glNamedBufferData` hints, which is exactly +the mistake this whole phase exists to stop. + +Phase 1 had **already designed that concept**, and better: `RHI::MemoryResidency +{ DeviceLocal, HostToDevice, DeviceToHost }`, named by intent rather than by GL's +spelling, three members mapping one-to-one onto the need. It was invisible +because it sat beside `BufferDesc` in `RHIResources.h` — a header nothing +consumed yet — while `RendererAPI.h` includes only `RHITypes.h`. + +**What caught it was a name collision, not review.** `RHIResources.h` also had a +`BufferUsage` (the *bind-flags* enum: `Vertex`/`Index`/`Storage`/…), so the two +could not coexist. And the collision only fired in the **test** build: the engine +library compiles without ever including `RHIResources.h`; only +`RHIBoundaryRatchetTest` includes it, precisely so the declaration-only +vocabulary keeps compiling. Rename either enum and the duplicate concept ships +silently. + +Method, for any phase that inherits a declaration-only header: + +1. Before adding a type to the shared vocabulary header, grep the whole + `Renderer/RHI/` directory for the **concept**, not the name you picked. +2. Treat "this header is declaration-only, nothing consumes it" as a reason to + read it *more* carefully, not less — unconsumed means uncorrected, so it holds + the design intent at its cleanest and its most easily missed. +3. If the concept exists but lives in the wrong header for your consumer, **move + it** rather than duplicating it. `MemoryResidency` moved to `RHITypes.h`; it + was vocabulary all along, filed under resource description. + +### Behaviour deltas a sweep introduces even when it changes no logic + +Three showed up here. None is a bug, all three are visible, and a reviewer +should know to expect them: + +- **The backend's own state cache becomes truthful.** `VirtualGeometryPass` set + depth state with raw `glEnable(GL_DEPTH_TEST)`, which left + `OpenGLRendererAPI::m_DepthTestEnabled` stale; `Clear()` derives its + `GLbitfield` from that member. Routing the pass through `SetDepthTest(true)` + fixes the divergence — which means a later `Clear()` can now clear depth where + it previously did not. Verify visually rather than reasoning about it. +- **Profiler counters move.** The facade's state setters bump + `RendererProfiler::StateChanges`; the raw calls they replaced did not. Draw + counters deliberately did *not* move: the new `DrawBound*` family does not + touch `RendererProfiler`, because the call sites it replaced never did and + several keep their own `CommandDispatch::Statistics`. +- **The mock gets safer, and records more.** Call sites that used to issue raw + `glXxx()` under a `MockRendererAPI` were calling a null glad function pointer + in a headless test. They now land on the mock. Tests asserting *exact* recorded + call counts will need updating; ones using `HasCall` / `GE` will not. + +--- + ## 3. `Renderer/Debug/` is 43% of the problem and is *not* exempt 236 of the 549 calls are in 7 files under `Renderer/Debug/`: `GLStateGuard`,