From 8e2b4584119599f65faf099f8fd2dbe3e547e608 Mon Sep 17 00:00:00 2001 From: Ole Bueker Date: Tue, 7 Jul 2026 15:31:10 +0200 Subject: [PATCH 1/2] fix(shader): recover gracefully from GLSL compile/link failures instead of crashing (#568) OpenGLShader treated a user-authored GLSL compile/link failure as an engineering invariant violation, calling OLO_CORE_VERIFY(false, ...) which OLO_DEBUGBREAK()s in Debug builds and kills the editor whenever a developer edits a shader into a broken state. Replace all 5 crash sites with the existing OLO_CORE_CRITICAL log plus a clean transition to ShaderCompilationStatus::Failed, propagating failure through both constructors and Reload() so no further compile/link stage runs on stale data. Also fixes a second, previously-latent crash this change exposed: the parallel-compile lambdas in CompileOrGetVulkanBinaries / CompileOrGetOpenGLBinaries assigned their result's Stage field *after* the hasError early-exit check, so a task that never ran left its result default-constructed (Stage=0). Once the fail-fast VERIFY no longer aborted on the first failure, the sequential collect loop would reach that never-run result and call GLShaderStageToString(0), hitting its own switch-default assert. Moved the Stage assignment before the early-exit check in both lambdas. Adds a regression test driving real broken GLSL through the compile path and documents the ParallelFor result-struct gotcha in docs/agent-rules/cpp-coding-quality.md for future parallel-aggregation refactors. Co-Authored-By: Claude Sonnet 5 --- .../src/Platform/OpenGL/OpenGLShader.cpp | 134 ++++++++++++----- OloEngine/src/Platform/OpenGL/OpenGLShader.h | 12 +- OloEngine/tests/CMakeLists.txt | 1 + .../ShaderCompileFailureRecoveryTest.cpp | 136 ++++++++++++++++++ docs/agent-rules/cpp-coding-quality.md | 32 +++++ 5 files changed, 278 insertions(+), 37 deletions(-) create mode 100644 OloEngine/tests/Rendering/PropertyTests/ShaderCompileFailureRecoveryTest.cpp diff --git a/OloEngine/src/Platform/OpenGL/OpenGLShader.cpp b/OloEngine/src/Platform/OpenGL/OpenGLShader.cpp index 0b24227b8..9f23effad 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLShader.cpp +++ b/OloEngine/src/Platform/OpenGL/OpenGLShader.cpp @@ -227,9 +227,14 @@ namespace OloEngine const Timer timer; OLO_CORE_INFO("Compiling shader '{}' from '{}'", m_Name, filepath); - CompileOrGetVulkanBinaries(shaderSources); - - if (Utils::IsAmdGpu()) + if (!CompileOrGetVulkanBinaries(shaderSources)) + { + // A user-authored GLSL syntax/compile error is not an engineering + // invariant violation — surface it as a Failed shader instead of + // crashing the editor (issue #568). Already logged via OLO_CORE_CRITICAL. + m_CompilationStatus = ShaderCompilationStatus::Failed; + } + else if (Utils::IsAmdGpu()) { std::string fullVersion(reinterpret_cast(glGetString(GL_VERSION))); @@ -249,22 +254,28 @@ namespace OloEngine { CreateProgramForAmd(); } - else + else if (CompileOrGetOpenGLBinaries()) { - CompileOrGetOpenGLBinaries(); CreateProgram(); } + else + { + m_CompilationStatus = ShaderCompilationStatus::Failed; + } } else { OLO_CORE_ERROR("Could not find driver version in string: '{0}'", fullVersion); } } - else + else if (CompileOrGetOpenGLBinaries()) { - CompileOrGetOpenGLBinaries(); CreateProgram(); } + else + { + m_CompilationStatus = ShaderCompilationStatus::Failed; + } const f64 compilationTime = timer.ElapsedMillis(); // If the shader is still Compiling (async path), report that; otherwise it's Ready/Failed @@ -307,15 +318,23 @@ namespace OloEngine { // AMD path: compile GLSL source strings directly (no SPIR-V) m_OriginalSourceCode = sources; - CompileOrGetVulkanBinaries(sources); - CreateProgramForAmd(); + if (CompileOrGetVulkanBinaries(sources)) + { + CreateProgramForAmd(); + } + else + { + m_CompilationStatus = ShaderCompilationStatus::Failed; + } } - else + else if (CompileOrGetVulkanBinaries(sources) && CompileOrGetOpenGLBinaries()) { - CompileOrGetVulkanBinaries(sources); - CompileOrGetOpenGLBinaries(); CreateProgram(); } + else + { + m_CompilationStatus = ShaderCompilationStatus::Failed; + } // Source-string shaders (boot/fallback) must be ready immediately — // the boot shader is needed to render the warmup progress bar, and @@ -634,7 +653,7 @@ namespace OloEngine return shaderSources; } - void OpenGLShader::CompileOrGetVulkanBinaries(const std::unordered_map& shaderSources) + bool OpenGLShader::CompileOrGetVulkanBinaries(const std::unordered_map& shaderSources) { glCreateProgram(); @@ -676,13 +695,18 @@ namespace OloEngine static_cast(stageSourcePairs.size()), [this, &hasError, &stageSourcePairs, &results, &cacheDirectory, &disableCache](i32 index) { - if (hasError.load(std::memory_order_relaxed)) - return; // Early exit if another stage failed - + // Record the stage before the early-out below so the collect loop can + // still identify (and log) this result even if this task never runs — + // the default-constructed CompilationResult::Stage is 0, which isn't a + // valid GLenum and would otherwise hit GLShaderStageToString's own + // switch-default assert when logging the failure. const auto& [stage, source] = stageSourcePairs[index]; CompilationResult& result = results[index]; result.Stage = stage; + if (hasError.load(std::memory_order_relaxed)) + return; // Early exit if another stage failed + const std::filesystem::path shaderFilePath = m_FilePath; const std::filesystem::path cachedPath = cacheDirectory / (shaderFilePath.filename().string() + Utils::GLShaderStageCachedVulkanFileExtension(stage)); result.CachePath = cachedPath; @@ -740,14 +764,21 @@ namespace OloEngine result.NeedsCache = !disableCache; }); - // Collect results and write cache (sequential to avoid map race conditions) + // Collect results and write cache (sequential to avoid map race conditions). + // This loop itself is single-threaded (ParallelFor above already joined all + // workers), so accumulating into a plain local bool is race-free — the + // subtlety is only that a later *successful* stage must not make the overall + // result look like a success after an earlier stage failed. + bool anyStageFailed = false; for (const auto& result : results) { if (!result.Success) { + // A stage that never ran because an earlier stage already tripped + // |hasError| carries an empty ErrorMessage — still a real failure. OLO_CORE_CRITICAL("[OpenGL] SPIR-V compilation failed for '{}' (stage {}): {}", m_FilePath, Utils::GLShaderStageToString(result.Stage), result.ErrorMessage); - OLO_CORE_VERIFY(false, "Shader SPIR-V compilation failure"); + anyStageFailed = true; continue; } @@ -767,13 +798,18 @@ namespace OloEngine } } + if (anyStageFailed) + return false; + for (auto&& [stage, data] : shaderData) { Reflect(stage, data); } + + return true; } - void OpenGLShader::CompileOrGetOpenGLBinaries() + bool OpenGLShader::CompileOrGetOpenGLBinaries() { auto& shaderData = m_OpenGLSPIRV; @@ -812,13 +848,15 @@ namespace OloEngine static_cast(stageSpirvPairs.size()), [this, &hasError, &stageSpirvPairs, &results, &cacheDirectory, &disableCache](i32 index) { - if (hasError.load(std::memory_order_relaxed)) - return; - + // Record the stage before the early-out below — see the matching + // comment in CompileOrGetVulkanBinaries's ParallelFor lambda. const auto& [stage, vulkanSpirv] = stageSpirvPairs[index]; OpenGLCompilationResult& result = results[index]; result.Stage = stage; + if (hasError.load(std::memory_order_relaxed)) + return; + const std::filesystem::path shaderFilePath = m_FilePath; const std::filesystem::path cachedPath = cacheDirectory / (shaderFilePath.filename().string() + Utils::GLShaderStageCachedOpenGLFileExtension(stage)); result.CachePath = cachedPath; @@ -917,14 +955,16 @@ namespace OloEngine result.NeedsCache = !disableCache; }); - // Collect results and write cache (sequential) + // Collect results and write cache (sequential). Single-threaded here too + // (ParallelFor above already joined), so this bool accumulation is race-free. + bool anyStageFailed = false; for (const auto& result : results) { if (!result.Success) { OLO_CORE_CRITICAL("[OpenGL] SPIR-V cross-compilation failed for '{}' (stage {}): {}", m_FilePath, Utils::GLShaderStageToString(result.Stage), result.ErrorMessage); - OLO_CORE_VERIFY(false, "Shader SPIR-V cross-compilation failure"); + anyStageFailed = true; continue; } @@ -947,6 +987,8 @@ namespace OloEngine } } } + + return !anyStageFailed; } void OpenGLShader::FinalizeProgram(GLenum const& program, const std::unordered_map>& spirvMap) @@ -1062,7 +1104,6 @@ namespace OloEngine std::vector infoLog(maxLength); glGetProgramInfoLog(program, maxLength, &maxLength, infoLog.data()); OLO_CORE_CRITICAL("[OpenGL] Shader linking failed for '{}':\n{}", m_FilePath, infoLog.data()); - OLO_CORE_VERIFY(false, "Shader program linking failure"); glDeleteProgram(program); @@ -1294,7 +1335,6 @@ namespace OloEngine glGetProgramInfoLog(program, maxLength, &maxLength, &infoLog[0]); OLO_CORE_CRITICAL("[OpenGL] Shader link failure for '{}': {}", filePath, infoLog.data()); - OLO_CORE_VERIFY(false, "Shader program linking failure"); return false; } return true; @@ -1326,7 +1366,14 @@ namespace OloEngine program = glCreateProgram(); std::array glShadersIDs{}; - CompileOpenGLBinariesForAmd(program, glShadersIDs); + if (!CompileOpenGLBinariesForAmd(program, glShadersIDs)) + { + // CompileOpenGLBinariesForAmd already cleaned up any shaders it attached + // before failing; |program| itself is still ours to delete. + glDeleteProgram(program); + m_CompilationStatus = ShaderCompilationStatus::Failed; + return; + } // Required by spec for glGetProgramBinary; prevents Mesa/radeonsi crash. glProgramParameteri(program, GL_PROGRAM_BINARY_RETRIEVABLE_HINT, GL_TRUE); glLinkProgram(program); @@ -1374,7 +1421,7 @@ namespace OloEngine m_CompilationStatus = ShaderCompilationStatus::Ready; } - void OpenGLShader::CompileOpenGLBinariesForAmd(GLenum const& program, std::array& glShadersIDs) const + bool OpenGLShader::CompileOpenGLBinariesForAmd(GLenum const& program, std::array& glShadersIDs) const { int glShaderIDIndex = 0; for (auto&& [stage, spirv] : m_VulkanSPIRV) @@ -1425,12 +1472,22 @@ namespace OloEngine OLO_CORE_CRITICAL("[OpenGL] Shader compilation failed for '{}' (stage {}): {}", m_FilePath, Utils::GLShaderStageToString(stage), infoLog.data()); - OLO_CORE_VERIFY(false, "Shader source compilation failure"); - return; + + // Clean up any stages already compiled+attached during this call so the + // caller is left with a clean, empty |program| rather than a partially + // attached one it doesn't know about. + for (int i = 0; i < glShaderIDIndex; ++i) + { + glDetachShader(program, glShadersIDs[i]); + glDeleteShader(glShadersIDs[i]); + } + glShadersIDs.fill(0); + return false; } glAttachShader(program, shader); glShadersIDs[glShaderIDIndex++] = shader; } + return true; } void OpenGLShader::Reflect(const GLenum stage, const std::vector& shaderData) @@ -1532,16 +1589,25 @@ namespace OloEngine try { - CompileOrGetVulkanBinaries(shaderSources); - if (Utils::IsAmdGpu()) + if (!CompileOrGetVulkanBinaries(shaderSources)) + { + // Broken GLSL in the edited file — already logged via + // OLO_CORE_CRITICAL. Fall through to Failed; the restore-old-program + // block below keeps the previously-working shader bound (issue #568). + m_CompilationStatus = ShaderCompilationStatus::Failed; + } + else if (Utils::IsAmdGpu()) { CreateProgramForAmd(); } - else + else if (CompileOrGetOpenGLBinaries()) { - CompileOrGetOpenGLBinaries(); CreateProgram(); } + else + { + m_CompilationStatus = ShaderCompilationStatus::Failed; + } // For hot-reload we want synchronous completion so the new shader // is immediately usable — force-finish any async link. diff --git a/OloEngine/src/Platform/OpenGL/OpenGLShader.h b/OloEngine/src/Platform/OpenGL/OpenGLShader.h index 9278cec70..b6efb3d90 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLShader.h +++ b/OloEngine/src/Platform/OpenGL/OpenGLShader.h @@ -152,11 +152,17 @@ namespace OloEngine static std::string ProcessIncludesInternal(const std::string& source, const std::string& directory, std::unordered_set& includedFiles); std::unordered_map PreProcess(std::string_view source); - void CompileOrGetVulkanBinaries(const std::unordered_map& shaderSources); - void CompileOrGetOpenGLBinaries(); + // Returns false if any shader stage failed to compile (already logged via + // OLO_CORE_CRITICAL). Callers must not proceed to link/finalize on failure — + // set m_CompilationStatus to Failed and bail instead (see issue #568). + [[nodiscard]] bool CompileOrGetVulkanBinaries(const std::unordered_map& shaderSources); + [[nodiscard]] bool CompileOrGetOpenGLBinaries(); void CreateProgram(); - void CompileOpenGLBinariesForAmd(GLenum const& program, std::array& glShadersIDs) const; + // Returns false if any stage failed to compile; any shader objects already + // attached to |program| during this call are detached/deleted before + // returning so the caller is left with a clean, empty program. + [[nodiscard]] bool CompileOpenGLBinariesForAmd(GLenum const& program, std::array& glShadersIDs) const; void CreateProgramForAmd(); void Reflect(GLenum stage, const std::vector& shaderData); diff --git a/OloEngine/tests/CMakeLists.txt b/OloEngine/tests/CMakeLists.txt index e8859d72e..afb65e0c2 100644 --- a/OloEngine/tests/CMakeLists.txt +++ b/OloEngine/tests/CMakeLists.txt @@ -194,6 +194,7 @@ add_executable(OloEngine-Tests Rendering/PropertyTests/DeferredOverlayPassTests.cpp Rendering/PropertyTests/OITPropertyTests.cpp Rendering/PropertyTests/ShaderCompilationTest.cpp + Rendering/PropertyTests/ShaderCompileFailureRecoveryTest.cpp Rendering/PropertyTests/AssetPreviewRendererTest.cpp Rendering/PropertyTests/RendererAttachedTest.cpp Rendering/PropertyTests/RendererAttachedSmokeTest.cpp diff --git a/OloEngine/tests/Rendering/PropertyTests/ShaderCompileFailureRecoveryTest.cpp b/OloEngine/tests/Rendering/PropertyTests/ShaderCompileFailureRecoveryTest.cpp new file mode 100644 index 000000000..b1bb7ffaf --- /dev/null +++ b/OloEngine/tests/Rendering/PropertyTests/ShaderCompileFailureRecoveryTest.cpp @@ -0,0 +1,136 @@ +// ============================================================================= +// ShaderCompileFailureRecoveryTest.cpp +// +// Regression test for issue #568 — a user-authored GLSL compile/link failure +// used to be treated as an engineering invariant violation: `OpenGLShader` +// logged the error and then called `OLO_CORE_VERIFY(false, ...)`, which +// `OLO_DEBUGBREAK()`s in Debug builds and crashes the editor whenever a +// developer edits a shader into a broken state (the normal hot-reload +// iteration loop). This drives the real compile path — `Shader::Create` +// through `OpenGLShader`'s source-string constructor — with deliberately +// broken GLSL and asserts the process survives and the shader lands in +// `ShaderCompilationStatus::Failed` instead. +// +// Classification: shaderpipe (within-shader-compile-pipeline contract, not +// GLSL math correctness (L2) or GPU state (L4)). +// ============================================================================= +// OLO_TEST_LAYER: shaderpipe +#include "OloEnginePCH.h" + +#include "RenderPropertyTest.h" + +#include "OloEngine/Renderer/Shader.h" +#include "OloEngine/Renderer/ShaderDebugUtils.h" + +#include + +namespace OloEngine::Tests +{ + namespace + { + // These tests construct throwaway shaders with pseudo file paths (the + // source-string constructor uses the shader name as its "path"), so a + // real on-disk shader cache entry for them would have no corresponding + // production .glsl and permanently trip + // AssetContentValidity.ShaderCacheEntriesAllHaveLiveGlslSources. Disable + // the process-wide cache for the lifetime of the test and restore + // whatever state it had on entry. + struct ScopedDisableShaderCache + { + bool m_WasDisabled = ShaderDebugUtils::IsShaderCacheDisabled(); + ScopedDisableShaderCache() + { + ShaderDebugUtils::SetDisableShaderCache(true); + } + ~ScopedDisableShaderCache() + { + ShaderDebugUtils::SetDisableShaderCache(m_WasDisabled); + } + }; + + constexpr const char* kValidVertexSrc = R"( + #version 450 core + layout(location = 0) in vec3 a_Position; + void main() + { + gl_Position = vec4(a_Position, 1.0); + } + )"; + + constexpr const char* kValidFragmentSrc = R"( + #version 450 core + layout(location = 0) out vec4 o_Color; + void main() + { + o_Color = vec4(1.0); + } + )"; + + // Deliberate GLSL syntax error: an undeclared type and a missing + // semicolon. shaderc must reject this at the SPIR-V compile stage — + // the same stage that crashed in the issue's repro. + constexpr const char* kBrokenVertexSrc = R"( + #version 450 core + layout(location = 0) in vec3 a_Position; + void main() + { + gl_Position = totally_not_a_type(a_Position, 1.0) + } + )"; + } // namespace + + TEST(ShaderCompileFailureRecovery, BrokenVertexSourceDoesNotCrashAndReachesFailedStatus) + { + OLO_ENSURE_GPU_OR_SKIP(); + ScopedDisableShaderCache noCache; + + // Previously this call would OLO_DEBUGBREAK() and terminate the test + // process before ASSERT_TRUE below ever ran — the crash itself was + // the bug. Surviving to this point is already the primary assertion. + Ref shader = Shader::Create("BrokenVertexTestShader", kBrokenVertexSrc, kValidFragmentSrc); + + ASSERT_TRUE(shader != nullptr); + EXPECT_EQ(shader->GetCompilationStatus(), ShaderCompilationStatus::Failed); + EXPECT_FALSE(shader->IsReady()); + // A failed compile must not hand out a program id that looks usable. + EXPECT_EQ(shader->GetRendererID(), 0u); + } + + TEST(ShaderCompileFailureRecovery, BrokenFragmentSourceDoesNotCrashAndReachesFailedStatus) + { + OLO_ENSURE_GPU_OR_SKIP(); + ScopedDisableShaderCache noCache; + + // Mirrors the vertex-stage case but breaks the fragment stage instead, + // exercising the other half of the parallel-compile aggregation. + constexpr const char* kBrokenFragmentSrc = R"( + #version 450 core + layout(location = 0) out vec4 o_Color; + void main() + { + o_Color = still_not_a_type(1.0) + } + )"; + + Ref shader = Shader::Create("BrokenFragmentTestShader", kValidVertexSrc, kBrokenFragmentSrc); + + ASSERT_TRUE(shader != nullptr); + EXPECT_EQ(shader->GetCompilationStatus(), ShaderCompilationStatus::Failed); + EXPECT_EQ(shader->GetRendererID(), 0u); + } + + TEST(ShaderCompileFailureRecovery, ValidSourceStillCompilesSuccessfully) + { + OLO_ENSURE_GPU_OR_SKIP(); + ScopedDisableShaderCache noCache; + + // Sanity check alongside the failure cases above: the fix must not + // regress the happy path. + Ref shader = Shader::Create("ValidTestShader", kValidVertexSrc, kValidFragmentSrc); + + ASSERT_TRUE(shader != nullptr); + EXPECT_EQ(shader->GetCompilationStatus(), ShaderCompilationStatus::Ready); + EXPECT_TRUE(shader->IsReady()); + EXPECT_NE(shader->GetRendererID(), 0u); + } +} // namespace OloEngine::Tests diff --git a/docs/agent-rules/cpp-coding-quality.md b/docs/agent-rules/cpp-coding-quality.md index 89d420600..6e2a70ce4 100644 --- a/docs/agent-rules/cpp-coding-quality.md +++ b/docs/agent-rules/cpp-coding-quality.md @@ -251,3 +251,35 @@ If a feature-gate ever seems mysteriously inert on Windows, check for a bare `__ A render pass that publishes a raw GL name (`u32` texture/buffer id) into process-wide renderer state (`Renderer3D::Set*TextureID`) **must not rely on its own `Execute()` to clear it** — the render graph can cull the pass entirely, so its "clear at top of Execute" never runs and last frame's (or last *test's*) id survives while the owning framebuffer is deleted by a resize/rebuild. Any consumer that binds the published id then re-binds a dead name: `GL_INVALID_OPERATION " is not a valid texture name"`, every frame, with visually-correct output (issue #505 — `WaterSurfaceDepthTextureID` / `PlanarReflectionTextureID`, consumed by `ToneMapRenderPass`). Rule: a raw-id publication is **per-frame state** — reset it in `RenderPipeline::PrepareFrame` (BeginScene) alongside the other per-frame rotations, and let the executing pass re-publish. Holding a `Ref` instead sidesteps the dangling-name problem but extends resource lifetime; the per-frame reset is the default. Debugging technique that pins this class of bug in minutes: the GL debug context is synchronous in Debug builds, so `OpenGLMessageCallback` logs a `std::stacktrace` for every ERROR-type message — the offending `glBindTextureUnit` call site is in the log with file:line. + +--- + +## 12. `ParallelFor` result structs: populate identifying fields *before* the early-exit check, not after + +A `ParallelFor` "collect results" pattern — a per-task lambda that writes into `results[index]`, guarded by `if (hasError.load()) return;` at the top to stop new work once any task has failed — must assign the result's identifying field (the enum/key the sequential collect loop uses to label a failure, e.g. a `GLenum Stage`) **before** that early-exit check, not after: + +```cpp +// BAD — early-exit skips the Stage assignment; results[index].Stage stays +// default-constructed (0) if this task never runs the real work below. +[&](i32 index) +{ + if (hasError.load(std::memory_order_relaxed)) + return; + const auto& [stage, source] = pairs[index]; + results[index].Stage = stage; // never reached on early-exit + ... +}; + +// GOOD — the identifying field is set unconditionally, so a task that +// never runs its real work still leaves a labelled (if empty) result. +[&](i32 index) +{ + const auto& [stage, source] = pairs[index]; + results[index].Stage = stage; // always runs + if (hasError.load(std::memory_order_relaxed)) + return; + ... +}; +``` + +Why this matters: once one task fails and sets `hasError`, every other in-flight/not-yet-started task takes the early-exit path and its `results[index]` stays default-constructed. If the sequential collect loop then logs *every* failed result (not just the first) — including a task that never ran — it uses that default field value, e.g. `GLShaderStageToString(0)`. A "stage" switch/lookup helper with an `OLO_CORE_ASSERT(false)`/no default case in its fallback branch then crashes on a value that was never a real shader stage, not because the original failure was mishandled, but because a *second*, unrelated result's bookkeeping field was never populated. This exact bug shipped alongside the fix for issue #568: replacing a fail-fast `OLO_CORE_VERIFY(false, ...)` with a "log every failure, don't stop at the first" collect loop made the sequential loop actually reach the second (never-run) result for the first time — previously the immediate crash on the first failure masked it. Any refactor from "crash/return on the first failure" to "collect and report every failure" in a `ParallelFor`-based aggregation needs the same audit: check every field the collect loop reads is populated on **every** path through the per-task lambda, including the early-exit one. From 2ed360cc6203ac5dabe06b39d1ff1a3953a1cc91 Mon Sep 17 00:00:00 2001 From: Ole Bueker Date: Tue, 7 Jul 2026 15:49:11 +0200 Subject: [PATCH 2/2] fix(shader): address review findings on the #568 crash-fix diff - Remove a stray glCreateProgram() call in CompileOrGetVulkanBinaries() whose result was immediately discarded, leaking a GL program object on every shader compile. - Set m_CompilationStatus = Failed in the AMD driver-version-parse failure branch (missing space in the GL_VERSION string), matching every other failure path touched by this fix instead of leaving the shader stuck at Pending. Co-Authored-By: Claude Sonnet 5 --- OloEngine/src/Platform/OpenGL/OpenGLShader.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OloEngine/src/Platform/OpenGL/OpenGLShader.cpp b/OloEngine/src/Platform/OpenGL/OpenGLShader.cpp index 9f23effad..5e1b4f60c 100644 --- a/OloEngine/src/Platform/OpenGL/OpenGLShader.cpp +++ b/OloEngine/src/Platform/OpenGL/OpenGLShader.cpp @@ -266,6 +266,7 @@ namespace OloEngine else { OLO_CORE_ERROR("Could not find driver version in string: '{0}'", fullVersion); + m_CompilationStatus = ShaderCompilationStatus::Failed; } } else if (CompileOrGetOpenGLBinaries()) @@ -655,8 +656,6 @@ namespace OloEngine bool OpenGLShader::CompileOrGetVulkanBinaries(const std::unordered_map& shaderSources) { - glCreateProgram(); - // Store original preprocessed source code for debugging m_OriginalSourceCode = shaderSources;