From 11d7cea7830cd66fbfdeb345a8c6d2f91fd20913 Mon Sep 17 00:00:00 2001 From: Ole Bueker Date: Thu, 30 Jul 2026 22:55:07 +0200 Subject: [PATCH 1/3] refactor(rhi): sweep raw GL call sites out of OloEngine/src (#691 Phase 2 step 2) Drives both Phase 2 ratchet counters to zero: sweep_gl_calls 313 -> 0 sweep_glad_includes 39 -> 0 No translation unit in the sweep bucket can name a GL symbol any more, which is a compiler-enforced property rather than a grep. tools_gl_calls stays at 236 (Renderer/Debug/, Phase 8 relocation) and debug_escape_hatch stays at 0. The headline finding is that the facade was not merely GL-typed, it was INCOMPLETE. 84 distinct GL entry points appear across the 313 call sites and roughly 60% of them had no RendererAPI equivalent at all, so closing the sweep needed ~60 new virtuals against an existing 74. Whole categories had never been abstracted and every pass reached past the facade to perform them: buffer binding points (glBindBufferBase, 26 sites), raw buffer lifecycle, named- framebuffer draw/read attachment selection, blits, clears, completeness, occlusion + timer queries, fences, VAO lifecycle, texture clear/upload/readback, debug markers, device idle and MSAA caps. Shape decisions are recorded as ADR 0011 amendments (5)-(10): * RHI::NoAttachment - a "this draw slot writes nowhere" sentinel. DecalRenderPass steers a decal into exactly one G-Buffer attachment with GL_NONE entries, which an attachment index cannot express; both backends need it (GL_NONE / VK_ATTACHMENT_UNUSED). * glGetError DISAPPEARS rather than being abstracted. GL's error model is a sticky global flag and Vulkan's is a per-call result, so exposing either forces the other backend to fake it. The readback virtuals return bool. * DrawBoundIndexed / DrawBoundIndexedInstanced / DrawBoundArrays draw from previously bound geometry, which is the native Vulkan shape (vkCmdBindIndexBuffer + vkCmdDrawIndexed), not a GL-ism. They carry topology and index width explicitly instead of hard-coding triangles + u32. * SetProgramUniformFloat is a recorded, deliberate debt: the one virtual a Vulkan backend cannot implement faithfully, since SPIR-V has no name-queryable default uniform block. One call site (DrawInfiniteGrid's u_GridScale); Phase 6 folds it into a UBO and deletes it. * MemoryResidency moved from RHIResources.h to RHITypes.h. The sweep started reinventing it as a GL-transcribed "BufferUsage" and only a name collision with RHIResources.h's bind-flag BufferUsage surfaced that Phase 1 had already designed the concept, better. RendererAPI.h includes only RHITypes.h, so it belongs there. Two boundary leaks the ratchet cannot see were also closed: * Three passes included Platform/OpenGL/OpenGLUtilities.h for GLClearProgramGuard. Deleting their direct glad includes would have zeroed sweep_glad_includes while all three TUs could still see the whole GL API. The guard is an NVIDIA driver hazard, so it moved INTO the backend clear implementations, where ClearDepthOnly() had been carrying it correctly all along. * RGCommandContext guarded its debug markers with `if (GLAD_GL_KHR_debug)`, a glad loader symbol that does not match gl[A-Z] and so never appeared in any count. The capability check moved into OpenGLRendererAPI::PushDebugGroup. Every new enum is pinned by the same last-ordinal static_assert plus literal-GL_* token table that step 1's amendment (4) established, including a test that the NoAttachment sentinel cannot be folded into attachment-0 arithmetic. Verification: full suite 5195 run / 5189 passed / 0 failed / 6 skipped, with the goldens and visual-evidence tests executing (not skipping) and OLOENGINE_GOLDEN_REBASE unset. Nine evidence PNGs were regenerated; every one differs from HEAD by at or below its own run-to-run noise measured on the identical binary, so all were restored and this commit changes no pixels. Live editor: renders correctly, zero shader compile/link errors, zero GL_INVALID_*, and the frame breakdown shows the rewritten dispatch path executing with real GPU timings. Step 3 (TransientPool's RendererID split) is deliberately not in this commit. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 2 +- .../MorphTargets/MorphTargetEvaluator.cpp | 23 +- .../Precipitation/PrecipitationSystem.cpp | 17 +- .../Renderer/Commands/CommandDispatch.cpp | 231 +++--- .../Commands/FrameResourceManager.cpp | 45 +- .../Renderer/DDGI/DDGIProbeUpdatePass.cpp | 20 +- OloEngine/src/OloEngine/Renderer/GBuffer.cpp | 62 +- .../src/OloEngine/Renderer/IBLPrecompute.cpp | 6 +- .../Renderer/Instancing/GPUFrustumCuller.cpp | 16 +- .../LightCulling/TiledForwardPlus.cpp | 18 +- .../OloEngine/Renderer/LightProbeBaker.cpp | 11 +- .../Renderer/Occlusion/OcclusionQueryPool.cpp | 18 +- .../OloEngine/Renderer/Ocean/OceanFFTGpu.cpp | 19 +- .../Renderer/Passes/BloomRenderPass.cpp | 2 - .../Renderer/Passes/DecalRenderPass.cpp | 91 ++- .../Passes/DeferredGPUOcclusionPass.cpp | 45 +- .../Renderer/Passes/DeferredLightingPass.cpp | 58 +- .../Passes/DeferredOpaqueDecalPass.cpp | 19 +- .../Renderer/Passes/FluidCompositePass.cpp | 9 +- .../Passes/FluidIntermediatesPass.cpp | 102 ++- .../Passes/ForwardOverlayRenderPass.cpp | 28 +- .../Passes/GPUDrivenOcclusionPass.cpp | 29 +- .../Renderer/Passes/GTAORenderPass.cpp | 9 +- .../Renderer/Passes/OITPrepareRenderPass.cpp | 22 +- .../Renderer/Passes/OITResolveRenderPass.cpp | 2 - .../Passes/PlanarReflectionRenderPass.cpp | 6 +- .../Renderer/Passes/SSAORenderPass.cpp | 9 +- .../Renderer/Passes/SceneRenderPass.cpp | 88 ++- .../Renderer/Passes/WaterRenderPass.cpp | 24 +- .../OloEngine/Renderer/RGCommandContext.cpp | 21 +- .../src/OloEngine/Renderer/RHI/RHIResources.h | 14 +- .../src/OloEngine/Renderer/RHI/RHITypes.h | 77 ++ .../Renderer/ReflectionProbeBaker.cpp | 12 +- .../src/OloEngine/Renderer/RenderCommand.h | 342 +++++++++ .../src/OloEngine/Renderer/RenderGraph.cpp | 40 +- .../Renderer/Renderer3DLifecycle.cpp | 13 +- .../src/OloEngine/Renderer/RendererAPI.h | 186 ++++- .../src/OloEngine/Renderer/ShaderPack.cpp | 1 - .../VirtualGeometry/VirtualGeometryPass.cpp | 103 +-- .../VirtualGeometry/VirtualGeometryShadow.cpp | 3 +- .../VirtualGeometry/VirtualMeshRegistry.cpp | 84 +-- .../OloEngine/SaveGame/ThumbnailCapture.cpp | 13 +- OloEngine/src/OloEngine/Scene/Scene.cpp | 9 +- OloEngine/src/OloEngine/UI/UIRenderer.cpp | 30 +- .../Platform/OpenGL/OpenGLRHIConversions.h | 91 +++ .../src/Platform/OpenGL/OpenGLRendererAPI.cpp | 703 ++++++++++++++++++ .../src/Platform/OpenGL/OpenGLRendererAPI.h | 96 +++ OloEngine/tests/Rendering/MockRendererAPI.h | 363 +++++++++ .../tests/Rendering/RHIEnumLoweringTest.cpp | 87 +++ .../Rendering/rhi_boundary_baseline.json | 45 +- ...-rhi-neutral-resource-and-binding-model.md | 157 ++++ docs/agent-rules/rhi-abstraction-boundary.md | 124 +++ 52 files changed, 2896 insertions(+), 749 deletions(-) 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..684422af5 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,18 +292,18 @@ 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::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; + case RHI::FenceStatus::AlreadySignaled: + case RHI::FenceStatus::ConditionSatisfied: + break; } return true; @@ -314,13 +314,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 +322,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..78e766962 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,13 @@ 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())) + { + OLO_CORE_WARN("DDGIProbeUpdatePass: probe hit-geo tile readback failed"); + } DDGI::ProbeHitAggregates agg; i32 backfaceCount = 0; @@ -792,7 +795,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..4a7ec0004 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,33 @@ 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); + static constexpr std::array fullDrawBufs = { 0u, 1u, 2u, 3u, 4u }; + RenderCommand::SetFramebufferDrawAttachments( + dstFB, std::span(fullDrawBufs.data(), std::to_underlying(Count))); } u32 GBuffer::GetColorAttachmentID(AttachmentIndex index) const @@ -227,14 +219,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..bd83d8207 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 @@ -69,9 +68,13 @@ 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()); + if (!readOk) + { + OLO_CORE_WARN("LightProbeBaker: probe face readback failed"); + } fbo->Unbind(); 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..a8f6ecca9 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,12 @@ 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())) + { + OLO_CORE_WARN("OceanFFTGpu: butterfly readback failed"); + } 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..7f60f7a50 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,11 @@ namespace OloEngine targetFB->Bind(); if (colorAttachmentCount > 0) { - std::array drawBufs{}; + 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()); + drawBufs[i] = i; + RenderCommand::SetFramebufferDrawAttachments(targetFBID, std::span(drawBufs.data(), n)); } context.SetDepthTest(true); context.SetDepthMask(true); @@ -151,7 +149,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 +177,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 +196,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 +211,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 +225,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..1dd255041 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,11 @@ namespace OloEngine // GL_COLOR_ATTACHMENT3 entries. if (sceneColorAttachmentCount > 0) { - std::array fullDrawBufs{}; + 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()); + fullDrawBufs[i] = i; + RenderCommand::SetFramebufferDrawAttachments(sceneFBID, std::span(fullDrawBufs.data(), n)); } context.SetDepthTest(true); @@ -380,11 +381,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 +401,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 +415,11 @@ namespace OloEngine // attachments to be available on bind). if (sceneColorAttachmentCount > 0) { - std::array fullDrawBufs{}; + 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()); + fullDrawBufs[i] = i; + RenderCommand::SetFramebufferDrawAttachments(sceneFBID, std::span(fullDrawBufs.data(), n)); } } @@ -432,8 +433,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 +484,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 +515,15 @@ namespace OloEngine } if (colorCount > 0) { - std::array fullDrawBufs{}; + 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()); + fullDrawBufs[i] = i; + RenderCommand::SetFramebufferDrawAttachments(sceneFBID, std::span(fullDrawBufs.data(), n)); } - ::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..edaeb2771 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,11 @@ namespace OloEngine // attachment count differs from the previous 4-entry hardcoded list. if (sceneColorAttachmentCount > 0) { - std::array fullDrawBufs{}; + 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()); + fullDrawBufs[i] = i; + RenderCommand::SetFramebufferDrawAttachments(sceneFBID, std::span(fullDrawBufs.data(), n)); } // Restores cull face + polygon mode too — skybox / debug commands inside @@ -164,15 +167,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..87c72c9be 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,11 @@ namespace OloEngine m_SceneFramebuffer->Bind(); if (sceneColorAttachmentCount > 0) { - std::array drawBufs{}; + 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()); + drawBufs[i] = i; + RenderCommand::SetFramebufferDrawAttachments(sceneFBID, std::span(drawBufs.data(), n)); } context.SetDepthTest(true); context.ResetOpaqueForwardDrawState(); @@ -172,7 +170,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 +200,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..c1ae31253 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,10 +586,10 @@ namespace OloEngine if (!isDepth && att.TextureFormat != FramebufferTextureFormat::None) ++targetColorCount; } - std::array fullDrawBufs{}; + 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; + fullDrawBufs[i] = i; // Channel 3 (RMA) needs data from TWO attachments — RT0.a (metallic) // and RT1.zw (roughness, AO). glBlitFramebuffer cannot swizzle, so @@ -604,8 +604,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 +627,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::SetFramebufferDrawAttachments(dstFB, std::span(fullDrawBufs.data(), fullN)); 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 +674,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::SetFramebufferDrawAttachments(dstFB, std::span(fullDrawBufs.data(), fullN)); // 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 +740,27 @@ namespace OloEngine if (!isDepth && att.TextureFormat != FramebufferTextureFormat::None) ++colorCount; } - std::array prevDrawBufs{}; + 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; + prevDrawBufs[i] = 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::SetFramebufferDrawAttachments(fb, std::span(prevDrawBufs.data(), n)); // 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..078e9bec9 100644 --- a/OloEngine/src/OloEngine/Renderer/RenderCommand.h +++ b/OloEngine/src/OloEngine/Renderer/RenderCommand.h @@ -417,6 +417,348 @@ 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 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..fe242a62d 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,182 @@ 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; + 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..4844dd18b 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,40 @@ namespace OloEngine targetFB->Bind(); { // All five G-Buffer MRTs, same set SceneRenderPass draws - std::array drawBufs{}; + 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()); + drawBufs[a] = a; + RenderCommand::SetFramebufferDrawAttachments(targetFB->GetRendererID(), drawBufs); } - // 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 +418,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 +444,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 +458,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 +478,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 +519,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 +574,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 +593,7 @@ namespace OloEngine fullscreen->Bind(); context.DrawIndexed(fullscreen); } - ::glEnable(GL_CULL_FACE); + RenderCommand::EnableCulling(); } targetFB->Unbind(); @@ -603,8 +605,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 +638,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..f8a2bdb70 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; + // i32/u32, not i32/u32: these are scissor-rect COORDINATES, not + // GL objects, and RenderCommand::SetScissorBox already takes engine + // types. Naming them in GL types 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..c278fd396 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLRHIConversions.h +++ b/OloEngine/src/Platform/OpenGL/OpenGLRHIConversions.h @@ -355,6 +355,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..6f6462c97 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 @@ -853,6 +857,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 +1093,701 @@ 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::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..c4295cb9f 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h +++ b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h @@ -99,6 +99,99 @@ 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 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 +204,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..77fdb4e2b 100644 --- a/OloEngine/tests/Rendering/MockRendererAPI.h +++ b/OloEngine/tests/Rendering/MockRendererAPI.h @@ -483,6 +483,365 @@ 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()); + 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"); + return 0; // "no fence" — callers already handle a failed creation. + } + [[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 +852,10 @@ 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; 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..70f409d26 100644 --- a/OloEngine/tests/Rendering/RHIEnumLoweringTest.cpp +++ b/OloEngine/tests/Rendering/RHIEnumLoweringTest.cpp @@ -68,6 +68,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 +227,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..0a15196b6 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": "188fea68 + phase-2 step 2 (working tree)", "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..b9b215f78 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,154 @@ 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 only ~40% of them had any `RendererAPI` +equivalent at all. The other ~60% is what this section designs. + +### (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 amendment (4) established. + +--- + ## 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..a4e765fb3 100644 --- a/docs/agent-rules/rhi-abstraction-boundary.md +++ b/docs/agent-rules/rhi-abstraction-boundary.md @@ -140,6 +140,47 @@ 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 hit the second case again, in the last four files standing: +`UIRenderer.cpp` made **zero** GL calls but typed its clip-rect stack in +`GLint`/`GLsizei`. The 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 +195,89 @@ 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 +> roughly **60% of them had no `RendererAPI` equivalent at all.** Closing the +> sweep needed ~60 *new* virtuals — nearly doubling the facade. + +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`, From b2606d12e6b4ae252016478907ac3ee441b11c8b Mon Sep 17 00:00:00 2001 From: Ole Bueker Date: Fri, 31 Jul 2026 00:41:09 +0200 Subject: [PATCH 2/3] refactor(rhi): address review findings on #691 Phase 2 step 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two of these were load-bearing and one was a bug introduced by the sweep. * UIRenderer.cpp's clip-rect comment was corrupted by the sweep's own bulk GLint->i32 rewrite: it rendered as "i32/u32, not i32/u32" and "issue //691". Rewritten to say what it meant. * Nine call sites open-coded the same identity draw-attachment list (a std::array, a fill loop and a span). The facade was simply missing the method: RestoreAllFramebufferDrawAttachments(framebufferID, count) is the named-framebuffer counterpart of the RestoreAllDrawBuffers(u32) that already existed for the BOUND framebuffer. All nine collapse to one line, across SceneRenderPass, DeferredLightingPass (x3), ForwardOverlayRenderPass, GPUDrivenOcclusionPass, DeferredGPUOcclusionPass, VirtualGeometryPass and GBuffer. Restoring a narrower list than the target actually has silently drops later shader outputs, which is exactly the off-by-one an open-coded loop invites. * Three migrated readbacks logged failure and then used the buffer anyway — a half-fix from the sweep. DDGI now skips that probe for the frame (the update is amortized, so it retries); OceanFFTGpu::DebugInverseFFT2D returns empty; and LightProbeBaker, the one that matters, reports the probe through its existing outValid out-param instead of projecting undefined pixels into SH coefficients that get PERSISTED into LightProbeVolumeAsset. * SetPatchVertexCount cached GL_MAX_PATCH_VERTICES but the two pre-existing patch-draw sites still queried it per call. Both use the cache now. * MockRendererAPI records the draw-attachment INDICES, not just how many (DecalRenderPass's four modes all pass five entries, so a count cannot tell them apart), and CreateFence() returns a non-zero handle so tests exercise the success path rather than only the creation-failed branch. * rhi_boundary_baseline.json pins measured_on_commit to 11d7cea7 instead of naming a working tree. * The 84 / 54 / 60 figures are stated as three distinct quantities in all three places that carried them. An entry point can expand into more than one virtual (glClearTexImage becomes a float and a uint clear; the readbacks each gained a bool return), so dividing 60 by 84 compared an operation count with an API count. 54 is the size of the gap, 60 the size of the fix. Also fixes a wrong internal cross-reference and a sentence that promised four files and named one. Two findings were fixed differently than suggested, both to avoid trading a compile-time guarantee for a runtime one: * A `default:` on the RHI::FenceStatus switch would suppress the compiler's exhaustiveness warning, which is what actually catches a new enumerator. The switch stays exhaustive with no `default:`; the post-switch fallthrough is the backstop and now FAILS CLOSED, because a fence gates reuse of double-buffered GPU resources and reporting success for an unrecognized status is the one answer that could hand a caller memory the GPU still owns. * The last-ordinal static_assert genuinely cannot detect an APPENDED enumerator. Chasing that turned up something worse: 16 of the 19 lowering switches deliberately carry no `default:` label — which is what makes clang's -Wswitch catch an append — while OpenGLRHIConversions.h's header comment claimed "Every switch carries a `default:`". Reconciling the code to that comment would have removed the only append guard, engine-wide. The comment is corrected and the division of labour is now documented in the header, the test and the ADR: static_assert catches insert/remove/reorder, absent-`default:` + -Wswitch catches append, and MSVC's C4062 being off by default at /W4 makes the clang-cl CI job the one that enforces it. A per-enum Count sentinel was considered and rejected: it makes an invalid value representable in the neutral vocabulary and forces a dead case in every lowering switch. Full suite 5195 run / 5191 passed / 0 failed / 4 skipped; the RHI ratchet still reports 0 / 0 / 236 / 0. Co-Authored-By: Claude Opus 5 (1M context) --- .../Commands/FrameResourceManager.cpp | 15 ++++++-- .../Renderer/DDGI/DDGIProbeUpdatePass.cpp | 9 ++++- OloEngine/src/OloEngine/Renderer/GBuffer.cpp | 4 +- .../OloEngine/Renderer/LightProbeBaker.cpp | 27 +++++++++++--- .../src/OloEngine/Renderer/LightProbeBaker.h | 4 +- .../OloEngine/Renderer/Ocean/OceanFFTGpu.cpp | 6 ++- .../Passes/DeferredGPUOcclusionPass.cpp | 6 +-- .../Renderer/Passes/DeferredLightingPass.cpp | 18 ++------- .../Passes/ForwardOverlayRenderPass.cpp | 6 +-- .../Passes/GPUDrivenOcclusionPass.cpp | 6 +-- .../Renderer/Passes/SceneRenderPass.cpp | 16 ++------ .../src/OloEngine/Renderer/RenderCommand.h | 5 +++ .../src/OloEngine/Renderer/RendererAPI.h | 9 +++++ .../VirtualGeometry/VirtualGeometryPass.cpp | 5 +-- OloEngine/src/OloEngine/UI/UIRenderer.cpp | 8 ++-- .../Platform/OpenGL/OpenGLRHIConversions.h | 24 +++++++++--- .../src/Platform/OpenGL/OpenGLRendererAPI.cpp | 37 +++++++++++++++---- .../src/Platform/OpenGL/OpenGLRendererAPI.h | 1 + OloEngine/tests/Rendering/MockRendererAPI.h | 21 ++++++++++- .../tests/Rendering/RHIEnumLoweringTest.cpp | 23 +++++++++--- .../Rendering/rhi_boundary_baseline.json | 2 +- ...-rhi-neutral-resource-and-binding-model.md | 29 +++++++++++++-- docs/agent-rules/rhi-abstraction-boundary.md | 24 ++++++++---- 23 files changed, 207 insertions(+), 98 deletions(-) diff --git a/OloEngine/src/OloEngine/Renderer/Commands/FrameResourceManager.cpp b/OloEngine/src/OloEngine/Renderer/Commands/FrameResourceManager.cpp index 684422af5..59719b848 100644 --- a/OloEngine/src/OloEngine/Renderer/Commands/FrameResourceManager.cpp +++ b/OloEngine/src/OloEngine/Renderer/Commands/FrameResourceManager.cpp @@ -295,18 +295,25 @@ namespace OloEngine constexpr u64 TIMEOUT_NS = 1000000000ULL; // 1 second switch (RenderCommand::ClientWaitFence(fenceId, TIMEOUT_NS)) { + 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; - case RHI::FenceStatus::AlreadySignaled: - case RHI::FenceStatus::ConditionSatisfied: - break; } - 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 diff --git a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp index 78e766962..9c89bdd98 100644 --- a/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp +++ b/OloEngine/src/OloEngine/Renderer/DDGI/DDGIProbeUpdatePass.cpp @@ -722,7 +722,14 @@ namespace OloEngine RHI::Format::RGBA32Float, texels.size() * sizeof(glm::vec4), texels.data())) { - OLO_CORE_WARN("DDGIProbeUpdatePass: probe hit-geo tile readback failed"); + // 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; diff --git a/OloEngine/src/OloEngine/Renderer/GBuffer.cpp b/OloEngine/src/OloEngine/Renderer/GBuffer.cpp index 4a7ec0004..1b1493762 100644 --- a/OloEngine/src/OloEngine/Renderer/GBuffer.cpp +++ b/OloEngine/src/OloEngine/Renderer/GBuffer.cpp @@ -175,9 +175,7 @@ namespace OloEngine // Restore full draw-buffer set on the resolved FB so subsequent // passes that bind it for composition get all attachments. - static constexpr std::array fullDrawBufs = { 0u, 1u, 2u, 3u, 4u }; - RenderCommand::SetFramebufferDrawAttachments( - dstFB, std::span(fullDrawBufs.data(), std::to_underlying(Count))); + RenderCommand::RestoreAllFramebufferDrawAttachments(dstFB, std::to_underlying(Count)); } u32 GBuffer::GetColorAttachmentID(AttachmentIndex index) const diff --git a/OloEngine/src/OloEngine/Renderer/LightProbeBaker.cpp b/OloEngine/src/OloEngine/Renderer/LightProbeBaker.cpp index bd83d8207..ef5ed9730 100644 --- a/OloEngine/src/OloEngine/Renderer/LightProbeBaker.cpp +++ b/OloEngine/src/OloEngine/Renderer/LightProbeBaker.cpp @@ -32,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, @@ -71,13 +71,19 @@ namespace OloEngine const bool readOk = RenderCommand::ReadTextureImage( colorAttachmentID, 0, RHI::Format::RGBA32Float, rgbaBuffer.size() * sizeof(f32), rgbaBuffer.data()); + + fbo->Unbind(); + if (!readOk) { - OLO_CORE_WARN("LightProbeBaker: probe face readback failed"); + // 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; } - fbo->Unbind(); - // 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) @@ -88,6 +94,8 @@ namespace OloEngine rgbaBuffer[i * 4 + 2]); } } + + return true; } SHCoefficients LightProbeBaker::ProjectToSH( @@ -186,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/Ocean/OceanFFTGpu.cpp b/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTGpu.cpp index a8f6ecca9..3da5250cb 100644 --- a/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTGpu.cpp +++ b/OloEngine/src/OloEngine/Renderer/Ocean/OceanFFTGpu.cpp @@ -288,7 +288,11 @@ namespace OloEngine::Ocean N, N, 1, RHI::Format::RGBA32Float, count * sizeof(glm::vec4), readback.data())) { - OLO_CORE_WARN("OceanFFTGpu: butterfly readback failed"); + // 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)); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/DeferredGPUOcclusionPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/DeferredGPUOcclusionPass.cpp index 7f60f7a50..24bbc1988 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/DeferredGPUOcclusionPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/DeferredGPUOcclusionPass.cpp @@ -125,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] = i; - RenderCommand::SetFramebufferDrawAttachments(targetFBID, std::span(drawBufs.data(), n)); + RenderCommand::RestoreAllFramebufferDrawAttachments(targetFBID, colorAttachmentCount); } context.SetDepthTest(true); context.SetDepthMask(true); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/DeferredLightingPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/DeferredLightingPass.cpp index 1dd255041..671d66758 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/DeferredLightingPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/DeferredLightingPass.cpp @@ -362,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] = i; - RenderCommand::SetFramebufferDrawAttachments(sceneFBID, std::span(fullDrawBufs.data(), n)); + RenderCommand::RestoreAllFramebufferDrawAttachments(sceneFBID, sceneColorAttachmentCount); } context.SetDepthTest(true); @@ -415,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] = i; - RenderCommand::SetFramebufferDrawAttachments(sceneFBID, std::span(fullDrawBufs.data(), n)); + RenderCommand::RestoreAllFramebufferDrawAttachments(sceneFBID, sceneColorAttachmentCount); } } @@ -515,11 +507,7 @@ 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] = i; - RenderCommand::SetFramebufferDrawAttachments(sceneFBID, std::span(fullDrawBufs.data(), n)); + RenderCommand::RestoreAllFramebufferDrawAttachments(sceneFBID, colorCount); } RenderCommand::BindVertexArrayRaw(0); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/ForwardOverlayRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/ForwardOverlayRenderPass.cpp index edaeb2771..72a3806cb 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/ForwardOverlayRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/ForwardOverlayRenderPass.cpp @@ -150,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] = i; - RenderCommand::SetFramebufferDrawAttachments(sceneFBID, std::span(fullDrawBufs.data(), n)); + RenderCommand::RestoreAllFramebufferDrawAttachments(sceneFBID, sceneColorAttachmentCount); } // Restores cull face + polygon mode too — skybox / debug commands inside diff --git a/OloEngine/src/OloEngine/Renderer/Passes/GPUDrivenOcclusionPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/GPUDrivenOcclusionPass.cpp index 87c72c9be..0a44ca6c6 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/GPUDrivenOcclusionPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/GPUDrivenOcclusionPass.cpp @@ -132,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] = i; - RenderCommand::SetFramebufferDrawAttachments(sceneFBID, std::span(drawBufs.data(), n)); + RenderCommand::RestoreAllFramebufferDrawAttachments(sceneFBID, sceneColorAttachmentCount); } context.SetDepthTest(true); context.ResetOpaqueForwardDrawState(); diff --git a/OloEngine/src/OloEngine/Renderer/Passes/SceneRenderPass.cpp b/OloEngine/src/OloEngine/Renderer/Passes/SceneRenderPass.cpp index c1ae31253..9c6c2b387 100644 --- a/OloEngine/src/OloEngine/Renderer/Passes/SceneRenderPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/Passes/SceneRenderPass.cpp @@ -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] = 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. @@ -627,7 +622,7 @@ 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. - RenderCommand::SetFramebufferDrawAttachments(dstFB, std::span(fullDrawBufs.data(), fullN)); + RenderCommand::RestoreAllFramebufferDrawAttachments(dstFB, targetColorCount); RenderCommand::SetDepthMask(true); RenderCommand::SetDepthTest(true); @@ -689,7 +684,7 @@ namespace OloEngine // 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. - RenderCommand::SetFramebufferDrawAttachments(dstFB, std::span(fullDrawBufs.data(), fullN)); + RenderCommand::RestoreAllFramebufferDrawAttachments(dstFB, targetColorCount); // Also copy depth so downstream passes (post-process, selection // outline, UI) have a coherent depth buffer. @@ -740,11 +735,6 @@ 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] = i; - RenderCommand::SetFramebufferReadAttachment(fb, 3); RenderCommand::SetFramebufferDrawAttachments(fb, kAttachment0Only); @@ -756,7 +746,7 @@ namespace OloEngine // Restore the scene FB's full multi-attachment draw-buffer list for // downstream passes (post-process, UI composite); see comment above. - RenderCommand::SetFramebufferDrawAttachments(fb, std::span(prevDrawBufs.data(), n)); + 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. diff --git a/OloEngine/src/OloEngine/Renderer/RenderCommand.h b/OloEngine/src/OloEngine/Renderer/RenderCommand.h index 078e9bec9..2e3830b10 100644 --- a/OloEngine/src/OloEngine/Renderer/RenderCommand.h +++ b/OloEngine/src/OloEngine/Renderer/RenderCommand.h @@ -518,6 +518,11 @@ namespace OloEngine 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); diff --git a/OloEngine/src/OloEngine/Renderer/RendererAPI.h b/OloEngine/src/OloEngine/Renderer/RendererAPI.h index fe242a62d..4aabfa29a 100644 --- a/OloEngine/src/OloEngine/Renderer/RendererAPI.h +++ b/OloEngine/src/OloEngine/Renderer/RendererAPI.h @@ -252,6 +252,15 @@ namespace OloEngine 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; diff --git a/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryPass.cpp b/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryPass.cpp index 4844dd18b..87d3aa696 100644 --- a/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryPass.cpp +++ b/OloEngine/src/OloEngine/Renderer/VirtualGeometry/VirtualGeometryPass.cpp @@ -372,10 +372,7 @@ 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] = a; - RenderCommand::SetFramebufferDrawAttachments(targetFB->GetRendererID(), drawBufs); + RenderCommand::RestoreAllFramebufferDrawAttachments(targetFB->GetRendererID(), GBuffer::Count); } // State set UNCONDITIONALLY, deliberately bypassing the context diff --git a/OloEngine/src/OloEngine/UI/UIRenderer.cpp b/OloEngine/src/OloEngine/UI/UIRenderer.cpp index f8a2bdb70..df57ceac7 100644 --- a/OloEngine/src/OloEngine/UI/UIRenderer.cpp +++ b/OloEngine/src/OloEngine/UI/UIRenderer.cpp @@ -26,10 +26,10 @@ namespace OloEngine // Clip rect stack for scissor testing struct ClipRect { - // i32/u32, not i32/u32: these are scissor-rect COORDINATES, not - // GL objects, and RenderCommand::SetScissorBox already takes engine - // types. Naming them in GL types was the only reason this translation - // unit needed while making zero GL calls (issue //691). + // 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; }; diff --git a/OloEngine/src/Platform/OpenGL/OpenGLRHIConversions.h b/OloEngine/src/Platform/OpenGL/OpenGLRHIConversions.h index c278fd396..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" diff --git a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp index 6f6462c97..c63a365ff 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp +++ b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp @@ -274,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; } @@ -361,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; } @@ -1313,6 +1309,31 @@ namespace OloEngine } } + 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; + } + + std::array attachments{}; + count = std::min(count, static_cast(attachments.size())); + for (u32 i = 0; i < count; ++i) + { + attachments[i] = i; + } + SetFramebufferDrawAttachments(framebufferID, std::span(attachments.data(), count)); + } + void OpenGLRendererAPI::SetFramebufferReadAttachment(u32 framebufferID, u32 attachmentIndex) { OLO_PROFILE_FUNCTION(); diff --git a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h index c4295cb9f..2e6e4993e 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h +++ b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.h @@ -125,6 +125,7 @@ namespace OloEngine 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; diff --git a/OloEngine/tests/Rendering/MockRendererAPI.h b/OloEngine/tests/Rendering/MockRendererAPI.h index 77fdb4e2b..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 @@ -604,6 +605,18 @@ namespace OloEngine::Testing 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 @@ -792,7 +805,12 @@ namespace OloEngine::Testing [[nodiscard("Store this!")]] u64 CreateFence() override { Record("CreateFence"); - return 0; // "no fence" — callers already handle a failed creation. + // 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 { @@ -856,6 +874,7 @@ namespace OloEngine::Testing 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 70f409d26..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. diff --git a/OloEngine/tests/Rendering/rhi_boundary_baseline.json b/OloEngine/tests/Rendering/rhi_boundary_baseline.json index 0a15196b6..0dc0193ef 100644 --- a/OloEngine/tests/Rendering/rhi_boundary_baseline.json +++ b/OloEngine/tests/Rendering/rhi_boundary_baseline.json @@ -104,7 +104,7 @@ " moved into OpenGLRendererAPI::PushDebugGroup. Same class as step 1's", " `glad_glCreateTextures != nullptr` context probe." ], - "measured_on_commit": "188fea68 + phase-2 step 2 (working tree)", + "measured_on_commit": "11d7cea7", "measured_on_date": "2026-07-30", "sweep_gl_calls": 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 b9b215f78..14f0edd57 100644 --- a/docs/adr/0011-rhi-neutral-resource-and-binding-model.md +++ b/docs/adr/0011-rhi-neutral-resource-and-binding-model.md @@ -814,10 +814,17 @@ matters more than the first: 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 only ~40% of them had any `RendererAPI` -equivalent at all. The other ~60% is what this section designs. +appear at those call sites, and **54 of them had no `RendererAPI` equivalent at +all**. Closing that gap took **60 new virtuals**. -### (5) The facade grows ~60 virtuals, and that number is the real measurement +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 @@ -953,7 +960,21 @@ 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 amendment (4) established. +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. --- diff --git a/docs/agent-rules/rhi-abstraction-boundary.md b/docs/agent-rules/rhi-abstraction-boundary.md index a4e765fb3..5fb8ffd66 100644 --- a/docs/agent-rules/rhi-abstraction-boundary.md +++ b/docs/agent-rules/rhi-abstraction-boundary.md @@ -140,12 +140,15 @@ 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 hit the second case again, in the last four files standing: -`UIRenderer.cpp` made **zero** GL calls but typed its clip-rect stack in -`GLint`/`GLsizei`. The 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. +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 @@ -201,8 +204,13 @@ 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 -> roughly **60% of them had no `RendererAPI` equivalent at all.** Closing the -> sweep needed ~60 *new* virtuals — nearly doubling the facade. +> **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), From 2d229261e1bf2aa94e268552c55603491e7f1076 Mon Sep 17 00:00:00 2001 From: Ole Bueker Date: Fri, 31 Jul 2026 01:45:06 +0200 Subject: [PATCH 3/3] fix(rhi): do not silently cap RestoreAllFramebufferDrawAttachments at 16 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The helper added in the previous commit contradicted its own declaration comment: that comment warns that restoring a narrower list than the target actually has silently drops later fragment outputs, and then the implementation clipped the list to a 16-entry stack array whenever GL_MAX_DRAW_BUFFERS was larger. Above 16 it now allocates instead of clipping, so GL_MAX_DRAW_BUFFERS is the only ceiling — and that one already clamps with a warning. The stack path still covers every framebuffer in the engine (the G-Buffer is the widest at 5), so this costs nothing in practice; it just stops the cap being silent. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/Platform/OpenGL/OpenGLRendererAPI.cpp | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp index c63a365ff..dd72168cd 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp +++ b/OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp @@ -1325,13 +1325,31 @@ namespace OloEngine count = maxBuf; } - std::array attachments{}; - count = std::min(count, static_cast(attachments.size())); + // 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, std::span(attachments.data(), count)); + SetFramebufferDrawAttachments(framebufferID, attachments); } void OpenGLRendererAPI::SetFramebufferReadAttachment(u32 framebufferID, u32 attachmentIndex)