diff --git a/src/api/include/projectM-4/core.h b/src/api/include/projectM-4/core.h index 9a42545ac0..c321f2cbbf 100644 --- a/src/api/include/projectM-4/core.h +++ b/src/api/include/projectM-4/core.h @@ -40,7 +40,7 @@ extern "C" { * @param name The name of the function to resolve. * @param user_data A user-defined data pointer that is passed along with the load proc call, * e.g. context information. - * @since 4.2.0 + * @since 4.3.0 */ typedef void* (*projectm_load_proc)(const char* name, void* user_data); @@ -73,7 +73,7 @@ PROJECTM_EXPORT projectm_handle projectm_create(); * @param user_data Custom user data pointer to pass along to the load_proc call, e.g. context information. Optional, may be NULL. * @return A projectM handle for the newly created instance that must be used in subsequent API calls. * NULL if the instance could not be created successfully. - * @since 4.2.0 + * @since 4.3.0 */ PROJECTM_EXPORT projectm_handle projectm_create_with_opengl_load_proc(projectm_load_proc load_proc, void* user_data); @@ -108,6 +108,62 @@ PROJECTM_EXPORT void projectm_destroy(projectm_handle instance); PROJECTM_EXPORT void projectm_load_preset_file(projectm_handle instance, const char* filename, bool smooth_transition); +/** + * @brief Loads and initializes a preset in the background without switching to it. + * + * The preset is fully created and initialized (including shader compilation), then retained + * until projectm_activate_preloaded_preset() is called. This lets applications that control + * the preset rotation themselves prepare the next preset ahead of time, so the visible + * switch no longer pays the load/initialize cost (which can reach hundreds of milliseconds + * for complex presets on slower devices). + * + * Only one preset can be preloaded at a time; a subsequent call replaces the previous one. + * If the preset can't be loaded, the preset switch failed callback is invoked and no preset + * is retained. Must be called from the rendering thread with the GL context active, like + * projectm_load_preset_file(). + * + * @param instance The projectM instance handle. + * @param filename The preset filename to preload. + * @since 4.3.0 + */ +PROJECTM_EXPORT void projectm_preload_preset_file(projectm_handle instance, const char* filename); + +/** + * @brief Switches to the previously preloaded preset without re-initializing it. + * + * Returns false if no preset is preloaded, or if the window size changed since the preload + * (the prepared preset is then discarded, as its GL resources were sized for the old + * surface). In both cases the caller should fall back to projectm_load_preset_file(). + * + * @param instance The projectM instance handle. + * @param smooth_transition If true, the new preset is smoothly blended over. + * @return True if the preloaded preset was activated. + * @since 4.3.0 + */ +PROJECTM_EXPORT bool projectm_activate_preloaded_preset(projectm_handle instance, + bool smooth_transition); + +/** + * @brief Returns true if a preset is currently preloaded and awaiting activation. + * + * @param instance The projectM instance handle. + * @return True if a preset is preloaded. + * @since 4.3.0 + */ +PROJECTM_EXPORT bool projectm_has_preloaded_preset(projectm_handle instance); + +/** + * @brief Non-blocking: true once the preloaded preset's deferred background shader + * compiles have finished (GL_KHR_parallel_shader_compile). False if nothing is + * preloaded. Gate projectm_activate_preloaded_preset() on this to keep the + * finalize step instant. + * + * @param instance The projectM instance handle. + * @return True if the preloaded preset is ready for instant activation. + * @since 4.3.0 + */ +PROJECTM_EXPORT bool projectm_preloaded_preset_compile_ready(projectm_handle instance); + /** * @brief Loads a preset from the data pointer. * diff --git a/src/libprojectM/MilkdropPreset/FinalComposite.cpp b/src/libprojectM/MilkdropPreset/FinalComposite.cpp index 693df44935..9aa3153a6c 100644 --- a/src/libprojectM/MilkdropPreset/FinalComposite.cpp +++ b/src/libprojectM/MilkdropPreset/FinalComposite.cpp @@ -94,6 +94,34 @@ void FinalComposite::CompileCompositeShader(PresetState& presetState) } } +auto FinalComposite::CompositeShaderCompileReady() const -> bool +{ + return !m_compositeShader || m_compositeShader->Shader().PendingCompileReady(); +} + +void FinalComposite::FinalizeCompositeShaderCompile(PresetState& presetState) +{ + if (m_compositeShader && m_compositeShader->Shader().HasPendingCompile()) + { + try + { + m_compositeShader->Shader().FinalizeCompile(); + LOG_DEBUG("[FinalComposite] Successfully finalized deferred composite shader compile."); + } + catch (Renderer::ShaderException&) + { + // Same failure handling as CompileCompositeShader. Deferral is off by + // finalize time, so the fallback compiles synchronously (static source, + // rare path — only broken presets reach it). + LOG_WARN("[FinalComposite] Error compiling composite warp shader code (deferred) - Using fallback shader."); + + m_compositeShader = std::make_unique(MilkdropShader::ShaderType::CompositeShader); + m_compositeShader->LoadCode(defaultCompositeShader); + m_compositeShader->LoadTexturesAndCompile(presetState); + } + } +} + void FinalComposite::Draw(const PresetState& presetState, const PerFrameContext& perFrameContext) { if (m_compositeShader) diff --git a/src/libprojectM/MilkdropPreset/FinalComposite.hpp b/src/libprojectM/MilkdropPreset/FinalComposite.hpp index 9701447706..d33a84562e 100644 --- a/src/libprojectM/MilkdropPreset/FinalComposite.hpp +++ b/src/libprojectM/MilkdropPreset/FinalComposite.hpp @@ -31,6 +31,19 @@ class FinalComposite */ void CompileCompositeShader(PresetState& presetState); + /** + * True once the composite shader's deferred + * background compile (if any) has finished. Non-blocking. + */ + auto CompositeShaderCompileReady() const -> bool; + + /** + * Finalizes a deferred composite-shader compile, + * replaying CompileCompositeShader's failure handling (fall back to the default + * composite shader, compiled synchronously). + */ + void FinalizeCompositeShaderCompile(PresetState& presetState); + /** * @brief Renders the composite quad with the appropriate effects or shaders. * @param presetState The preset state to retrieve the configuration values from. diff --git a/src/libprojectM/MilkdropPreset/MilkdropPreset.cpp b/src/libprojectM/MilkdropPreset/MilkdropPreset.cpp index ef1716115b..ca32f852bb 100755 --- a/src/libprojectM/MilkdropPreset/MilkdropPreset.cpp +++ b/src/libprojectM/MilkdropPreset/MilkdropPreset.cpp @@ -179,6 +179,17 @@ void MilkdropPreset::DrawInitialImage(const std::shared_ptr& m_flipTexture.Draw(*renderContext.shaderCache, image, m_framebuffer, m_previousFrameBuffer); } +auto MilkdropPreset::PendingShaderCompileReady() const -> bool +{ + return m_perPixelMesh.WarpShaderCompileReady() && m_finalComposite.CompositeShaderCompileReady(); +} + +void MilkdropPreset::FinalizePendingShaderCompile() +{ + m_perPixelMesh.FinalizeWarpShaderCompile(); + m_finalComposite.FinalizeCompositeShaderCompile(m_state); +} + void MilkdropPreset::BindFramebuffer() { if (m_framebuffer.Width() > 0 && m_framebuffer.Height() > 0) diff --git a/src/libprojectM/MilkdropPreset/MilkdropPreset.hpp b/src/libprojectM/MilkdropPreset/MilkdropPreset.hpp index db8eb43ba4..d0e10e440d 100644 --- a/src/libprojectM/MilkdropPreset/MilkdropPreset.hpp +++ b/src/libprojectM/MilkdropPreset/MilkdropPreset.hpp @@ -86,6 +86,17 @@ class MilkdropPreset : public ::libprojectM::Preset void BindFramebuffer() override; + /** + * Non-blocking readiness of the warp/composite shaders' deferred background compiles. + */ + auto PendingShaderCompileReady() const -> bool override; + + /** + * Finalizes deferred warp/composite compiles with the same per-shader failure + * fallbacks as the synchronous compile path. + */ + void FinalizePendingShaderCompile() override; + private: void PerFrameUpdate(); diff --git a/src/libprojectM/MilkdropPreset/PerPixelMesh.cpp b/src/libprojectM/MilkdropPreset/PerPixelMesh.cpp index 0c4a7b9a15..c19379955b 100644 --- a/src/libprojectM/MilkdropPreset/PerPixelMesh.cpp +++ b/src/libprojectM/MilkdropPreset/PerPixelMesh.cpp @@ -83,6 +83,29 @@ void PerPixelMesh::CompileWarpShader(PresetState& presetState) } } +auto PerPixelMesh::WarpShaderCompileReady() const -> bool +{ + return !m_warpShader || m_warpShader->Shader().PendingCompileReady(); +} + +void PerPixelMesh::FinalizeWarpShaderCompile() +{ + if (m_warpShader && m_warpShader->Shader().HasPendingCompile()) + { + try + { + m_warpShader->Shader().FinalizeCompile(); + LOG_DEBUG("[PerPixelMesh] Successfully finalized deferred warp shader compile."); + } + catch (Renderer::ShaderException&) + { + // Same failure handling as CompileWarpShader: fall back to no warp shader. + LOG_ERROR("[PerPixelMesh] Error compiling warp shader code (deferred)."); + m_warpShader.reset(); + } + } +} + void PerPixelMesh::Draw(const PresetState& presetState, const PerFrameContext& perFrameContext, PerPixelContext& perPixelContext) diff --git a/src/libprojectM/MilkdropPreset/PerPixelMesh.hpp b/src/libprojectM/MilkdropPreset/PerPixelMesh.hpp index dda77f114b..fee61b3fcc 100644 --- a/src/libprojectM/MilkdropPreset/PerPixelMesh.hpp +++ b/src/libprojectM/MilkdropPreset/PerPixelMesh.hpp @@ -41,6 +41,18 @@ class PerPixelMesh */ void CompileWarpShader(PresetState& presetState); + /** + * True once the warp shader's deferred background compile (if any) has finished. + * Non-blocking. + */ + auto WarpShaderCompileReady() const -> bool; + + /** + * Finalizes a deferred warp-shader compile, replaying CompileWarpShader's failure + * handling (drop the warp shader). + */ + void FinalizeWarpShaderCompile(); + /** * @brief Renders the transformation mesh. * @param presetState The preset state to retrieve the configuration values from. diff --git a/src/libprojectM/Preset.hpp b/src/libprojectM/Preset.hpp index c96b1bf7f2..4563286098 100644 --- a/src/libprojectM/Preset.hpp +++ b/src/libprojectM/Preset.hpp @@ -60,6 +60,23 @@ class Preset */ virtual void BindFramebuffer() = 0; + /** + * Non-blocking — true once any deferred background + * shader compiles started during Initialize() have finished (or if the preset has + * none). Default: nothing deferred. + */ + virtual auto PendingShaderCompileReady() const -> bool + { + return true; + } + + /** + * Finalizes deferred shader compiles (status check + + * cleanup + per-shader failure fallback). Blocks if a compile is still running — + * callers should gate on PendingShaderCompileReady(). Default: no-op. + */ + virtual void FinalizePendingShaderCompile() {} + inline void SetFilename(const std::string& filename) { m_filename = filename; diff --git a/src/libprojectM/ProjectM.cpp b/src/libprojectM/ProjectM.cpp index a042f244c6..aa130372bd 100644 --- a/src/libprojectM/ProjectM.cpp +++ b/src/libprojectM/ProjectM.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -71,6 +72,108 @@ void ProjectM::LoadPresetFile(const std::string& presetFilename, bool smoothTran } } +void ProjectM::PreloadPresetFile(const std::string& presetFilename) +{ + try + { + auto preset = m_presetFactoryManager->CreatePresetFromFile(presetFilename); + { + // Preload-scoped deferred GL compile: shader compiles started during + // Initialize() may run on the driver's worker pool + // (GL_KHR_parallel_shader_compile); the status checks + cleanup happen in + // FinalizePendingShaderCompile() once PreloadedPresetCompileReady() reports + // the background work done. + struct DeferGuard + { + DeferGuard() { Renderer::Shader::SetDeferCompilation(true); } + ~DeferGuard() { Renderer::Shader::SetDeferCompilation(false); } + } deferGuard; + preset->Initialize(GetRenderContext()); + } + + m_preloadedPreset = std::move(preset); + m_preloadedPresetName = presetFilename; + m_preloadedPresetWindowWidth = m_windowWidth; + m_preloadedPresetWindowHeight = m_windowHeight; + } + catch (const std::exception& ex) + { + m_preloadedPreset.reset(); + m_preloadedPresetName.clear(); + m_preloadedPresetWindowWidth = 0; + m_preloadedPresetWindowHeight = 0; + LOG_ERROR(ex.what()); + PresetSwitchFailedEvent(presetFilename, ex.what()); + } +} + +auto ProjectM::ActivatePreloadedPreset(bool smoothTransition) -> bool +{ + if (m_preloadedPreset == nullptr) + { + return false; + } + + if (m_preloadedPresetWindowWidth != m_windowWidth || m_preloadedPresetWindowHeight != m_windowHeight) + { + // Preset::Initialize() bakes the current render context into GL resources. If the + // surface changed after preload, discard the prepared preset and let the caller + // fall back to the normal load path for the new size. + m_preloadedPreset.reset(); + m_preloadedPresetName.clear(); + m_preloadedPresetWindowWidth = 0; + m_preloadedPresetWindowHeight = 0; + return false; + } + + // Settle any deferred background shader compiles before handing the preset to the + // transition. Callers gate activation on PreloadedPresetCompileReady(), so this is + // normally an instant resolve; if a compile is genuinely still running it blocks here + // (correctness over smoothness). Per-shader failures fall back inside + // FinalizePendingShaderCompile; anything that still escapes discards the preload so + // the caller does a normal load instead. + try + { + m_preloadedPreset->FinalizePendingShaderCompile(); + } + catch (const std::exception& ex) + { + LOG_ERROR(ex.what()); + PresetSwitchFailedEvent(m_preloadedPresetName, ex.what()); + m_preloadedPreset.reset(); + m_preloadedPresetName.clear(); + m_preloadedPresetWindowWidth = 0; + m_preloadedPresetWindowHeight = 0; + return false; + } + + // Purge at handoff rather than preload: PurgeTextures() deliberately ages textures + // once per real preset load and may evict cache entries. Preloading happens while the + // active preset is still rendering, so aging early would count a speculative load as + // an active switch and could evict reusable textures before the handoff. + m_textureManager->PurgeTextures(); + StartPresetTransitionInternal(std::move(m_preloadedPreset), !smoothTransition, true); + m_preloadedPresetName.clear(); + m_preloadedPresetWindowWidth = 0; + m_preloadedPresetWindowHeight = 0; + return true; +} + +auto ProjectM::HasPreloadedPreset() const -> bool +{ + return m_preloadedPreset != nullptr; +} + +auto ProjectM::PreloadedPresetCompileReady() const -> bool +{ + return m_preloadedPreset != nullptr && m_preloadedPreset->PendingShaderCompileReady(); +} + +auto ProjectM::PreloadedPresetName() const -> const std::string& +{ + return m_preloadedPresetName; +} + void ProjectM::LoadPresetData(std::istream& presetData, bool smoothTransition) { try @@ -286,6 +389,11 @@ void ProjectM::SetWindowSize(uint32_t width, uint32_t height) } void ProjectM::StartPresetTransition(std::unique_ptr&& preset, bool hardCut) +{ + StartPresetTransitionInternal(std::move(preset), hardCut, false); +} + +void ProjectM::StartPresetTransitionInternal(std::unique_ptr&& preset, bool hardCut, bool alreadyInitialized) { m_presetChangeNotified = m_presetLocked; @@ -294,7 +402,10 @@ void ProjectM::StartPresetTransition(std::unique_ptr&& preset, bool hard return; } - preset->Initialize(GetRenderContext()); + if (!alreadyInitialized) + { + preset->Initialize(GetRenderContext()); + } // If already in a transition, force immediate completion. if (m_transitioningPreset != nullptr) diff --git a/src/libprojectM/ProjectM.hpp b/src/libprojectM/ProjectM.hpp index 9879e77f0c..7d500e460a 100644 --- a/src/libprojectM/ProjectM.hpp +++ b/src/libprojectM/ProjectM.hpp @@ -89,6 +89,46 @@ class PROJECTM_CXX_EXPORT ProjectM */ void LoadPresetFile(const std::string& presetFilename, bool smoothTransition); + /** + * @brief Loads and initializes a preset in the background without switching to it. + * + * The preset is fully created and initialized (including shader compilation), then + * retained until ActivatePreloadedPreset() is called. Only one preset can be + * preloaded at a time; a subsequent call replaces the previous one. On failure, + * PresetSwitchFailedEvent() is fired and no preset is retained. + * + * @param presetFilename The preset filename to preload. + */ + void PreloadPresetFile(const std::string& presetFilename); + + /** + * @brief Switches to the previously preloaded preset without re-initializing it. + * + * Returns false (and discards the preloaded preset where appropriate) if no preset + * is preloaded or the window size changed since the preload, in which case the + * caller should fall back to LoadPresetFile(). + * + * @param smoothTransition If set to true, old and new presets will be blended over smoothly. + * @return True if the preloaded preset was activated. + */ + auto ActivatePreloadedPreset(bool smoothTransition) -> bool; + + /** + * @brief Returns true if a preset is currently preloaded and awaiting activation. + */ + auto HasPreloadedPreset() const -> bool; + + /** + * @brief Non-blocking — true once the preloaded preset's deferred background shader + * compiles have finished. False if no preset is preloaded. + */ + auto PreloadedPresetCompileReady() const -> bool; + + /** + * @brief Returns the filename the preloaded preset was created from (empty if none). + */ + auto PreloadedPresetName() const -> const std::string&; + /** * @brief Loads the given preset data and performs a smooth or immediate transition. * @@ -288,6 +328,8 @@ class PROJECTM_CXX_EXPORT ProjectM void StartPresetTransition(std::unique_ptr&& preset, bool hardCut); + void StartPresetTransitionInternal(std::unique_ptr&& preset, bool hardCut, bool alreadyInitialized); + void LoadIdlePreset(); auto GetRenderContext() -> Renderer::RenderContext; @@ -328,6 +370,10 @@ class PROJECTM_CXX_EXPORT ProjectM std::unique_ptr m_textureCopier; //!< Class that copies textures 1:1 to another texture or framebuffer. std::unique_ptr m_activePreset; //!< Currently loaded preset. std::unique_ptr m_transitioningPreset; //!< Destination preset when smooth preset switching. + std::unique_ptr m_preloadedPreset; //!< Initialized next preset retained for low-latency switching. + std::string m_preloadedPresetName; //!< Filename used to create the preloaded preset. + uint32_t m_preloadedPresetWindowWidth{0}; //!< Window width used during preloaded preset initialization. + uint32_t m_preloadedPresetWindowHeight{0}; //!< Window height used during preloaded preset initialization. std::unique_ptr m_transition; //!< Transition effect used for blending. std::unique_ptr m_timeKeeper; //!< Keeps the different timers used to render and switch presets. std::unique_ptr m_spriteManager; //!< Manages all types of user sprites. diff --git a/src/libprojectM/ProjectMCWrapper.cpp b/src/libprojectM/ProjectMCWrapper.cpp index cd2bca2f2e..a37e9f0a9e 100644 --- a/src/libprojectM/ProjectMCWrapper.cpp +++ b/src/libprojectM/ProjectMCWrapper.cpp @@ -114,6 +114,30 @@ void projectm_load_preset_file(projectm_handle instance, const char* filename, projectMInstance->LoadPresetFile(filename, smooth_transition); } +void projectm_preload_preset_file(projectm_handle instance, const char* filename) +{ + auto projectMInstance = handle_to_instance(instance); + projectMInstance->PreloadPresetFile(filename); +} + +bool projectm_activate_preloaded_preset(projectm_handle instance, bool smooth_transition) +{ + auto projectMInstance = handle_to_instance(instance); + return projectMInstance->ActivatePreloadedPreset(smooth_transition); +} + +bool projectm_has_preloaded_preset(projectm_handle instance) +{ + auto projectMInstance = handle_to_instance(instance); + return projectMInstance->HasPreloadedPreset(); +} + +bool projectm_preloaded_preset_compile_ready(projectm_handle instance) +{ + auto projectMInstance = handle_to_instance(instance); + return projectMInstance->PreloadedPresetCompileReady(); +} + void projectm_load_preset_data(projectm_handle instance, const char* data, bool smooth_transition) { diff --git a/src/libprojectM/Renderer/Shader.cpp b/src/libprojectM/Renderer/Shader.cpp index 58e03ae792..d9a0bbe0d2 100644 --- a/src/libprojectM/Renderer/Shader.cpp +++ b/src/libprojectM/Renderer/Shader.cpp @@ -5,9 +5,21 @@ #include +// Non-blocking link-completion poll (GL_KHR_parallel_shader_compile). +// Value per the KHR extension spec. +#ifndef GL_COMPLETION_STATUS_KHR +#define GL_COMPLETION_STATUS_KHR 0x91B1 +#endif + namespace libprojectM { namespace Renderer { +namespace { +// When set (render/GL thread only, around preset +// preload), CompileProgram leaves the link in flight instead of resolving it. +bool s_deferCompilation{false}; +} // namespace + Shader::Shader() : m_shaderProgram(glCreateProgram()) { @@ -17,19 +29,132 @@ Shader::~Shader() { if (m_shaderProgram) { + // Note: deleting a program with a pending background link resolves (blocks on) + // the link inside ANGLE first. Acceptable: only reached when a preload is + // discarded, and by then the compile is normally long finished. glDeleteProgram(m_shaderProgram); } + if (m_pendingVertexShader) + { + glDeleteShader(m_pendingVertexShader); + } + if (m_pendingFragmentShader) + { + glDeleteShader(m_pendingFragmentShader); + } +} + +void Shader::SetDeferCompilation(bool defer) +{ + s_deferCompilation = defer; +} + +auto Shader::HasPendingCompile() const -> bool +{ + return m_pendingLink; +} + +auto Shader::PendingCompileReady() const -> bool +{ + if (!m_pendingLink) + { + return true; + } + + // Explicitly non-resolving in ANGLE: reports whether the background link (including + // the Metal shader-library subtasks) has finished, without waiting for it. + GLint completed{GL_FALSE}; + glGetProgramiv(m_shaderProgram, GL_COMPLETION_STATUS_KHR, &completed); + return completed == GL_TRUE; +} + +void Shader::FinalizeCompile() const +{ + if (!m_pendingLink) + { + return; + } + m_pendingLink = false; + + // The detach/delete below is what resolves the link inside ANGLE (blocking if the + // background compile is still running — callers gate on PendingCompileReady()). + glDetachShader(m_shaderProgram, m_pendingVertexShader); + glDetachShader(m_shaderProgram, m_pendingFragmentShader); + glDeleteShader(m_pendingVertexShader); + glDeleteShader(m_pendingFragmentShader); + m_pendingVertexShader = 0; + m_pendingFragmentShader = 0; + + std::string vertexShaderSource; + std::string fragmentShaderSource; + std::swap(vertexShaderSource, m_pendingVertexSource); + std::swap(fragmentShaderSource, m_pendingFragmentSource); + + GLint programLinked{}; + glGetProgramiv(m_shaderProgram, GL_LINK_STATUS, &programLinked); + if (programLinked == GL_TRUE) + { + return; + } + + GLint infoLogLength{}; + glGetProgramiv(m_shaderProgram, GL_INFO_LOG_LENGTH, &infoLogLength); + std::vector message(infoLogLength + 1); + glGetProgramInfoLog(m_shaderProgram, infoLogLength, nullptr, message.data()); + + std::string linkError = "[Shader] Error linking deferred shader program: " + std::string(message.data()); + LOG_ERROR(linkError); + LOG_DEBUG("[Shader] Vertex shader source: " + vertexShaderSource); + LOG_DEBUG("[Shader] Fragment shader source: " + fragmentShaderSource); + throw ShaderException(linkError); +} + +void Shader::EnsureFinalizedNoThrow() const noexcept +{ + if (!m_pendingLink) + { + return; + } + try + { + FinalizeCompile(); + } + catch (const ShaderException& ex) + { + // Safety net only: real preload paths finalize explicitly (with fallback + // handling) before first use. A shader that fails here draws nothing. + LOG_ERROR(std::string("[Shader] Deferred compile failed at first use: ") + ex.message()); + } } void Shader::CompileProgram(const std::string& vertexShaderSource, const std::string& fragmentShaderSource) { + // Recompiling a shader that still has a deferred link in flight: settle it first. + EnsureFinalizedNoThrow(); + auto vertexShader = CompileShader(vertexShaderSource, GL_VERTEX_SHADER); auto fragmentShader = CompileShader(fragmentShaderSource, GL_FRAGMENT_SHADER); glAttachShader(m_shaderProgram, vertexShader); glAttachShader(m_shaderProgram, fragmentShader); + if (s_deferCompilation) + { + // Leave the link running on the driver's worker pool. Everything past + // glLinkProgram that touches the program — INCLUDING the glDetachShader in the + // normal path below — may synchronously resolve the link on this thread (ANGLE + // does), so all of it moves to FinalizeCompile(). + glLinkProgram(m_shaderProgram); + + m_pendingLink = true; + m_pendingVertexShader = vertexShader; + m_pendingFragmentShader = fragmentShader; + m_pendingVertexSource = vertexShaderSource; + m_pendingFragmentSource = fragmentShaderSource; + return; + } + glLinkProgram(m_shaderProgram); // Shader objects are no longer needed after linking, free the memory. @@ -59,6 +184,8 @@ void Shader::CompileProgram(const std::string& vertexShaderSource, bool Shader::Validate(std::string& validationMessage) const { + EnsureFinalizedNoThrow(); + GLint result{GL_FALSE}; int infoLogLength; @@ -78,6 +205,10 @@ bool Shader::Validate(std::string& validationMessage) const void Shader::Bind() const { + // Every render path binds before setting uniforms, so finalizing here covers the + // deferred-compile safety net for all program uses. + EnsureFinalizedNoThrow(); + if (m_shaderProgram > 0) { glUseProgram(m_shaderProgram); @@ -199,6 +330,13 @@ GLuint Shader::CompileShader(const std::string& source, GLenum type) glCompileShader(shader); + if (s_deferCompilation) + { + // The status query would synchronously resolve the background translate job. + // Compile errors surface as a link failure in FinalizeCompile() instead. + return shader; + } + glGetShaderiv(shader, GL_COMPILE_STATUS, &shaderCompiled); if (shaderCompiled == GL_TRUE) { diff --git a/src/libprojectM/Renderer/Shader.hpp b/src/libprojectM/Renderer/Shader.hpp index 79b2f15cac..6ce1431a2e 100644 --- a/src/libprojectM/Renderer/Shader.hpp +++ b/src/libprojectM/Renderer/Shader.hpp @@ -79,6 +79,44 @@ class Shader void CompileProgram(const std::string& vertexShaderSource, const std::string& fragmentShaderSource); + /** + * Deferred GL compilation (GL_KHR_parallel_shader_compile): + * + * ANGLE backgrounds glCompileShader/glLinkProgram on a worker pool + * (GL_KHR_parallel_shader_compile), but ANY program-observing call — including the + * glDetachShader right after glLinkProgram in CompileProgram — synchronously resolves + * the link on the calling thread. While the defer flag is set (only around preset + * preload), CompileProgram skips the status queries AND the detach/delete cleanup, + * leaving the link in flight; callers poll PendingCompileReady() (non-blocking + * GL_COMPLETION_STATUS_KHR) and call FinalizeCompile() once ready. Bind()/Validate()/ + * SetUniform* finalize lazily as a safety net, so a pending shader is always safe to + * use — first use just blocks until the background compile finishes. + */ + + /** + * @brief Globally enables/disables deferred compilation for subsequent CompileProgram calls. + * Render/GL thread only. Only set around preset preloading. + */ + static void SetDeferCompilation(bool defer); + + /** + * @brief Returns true if this shader has a deferred, not-yet-finalized link. + */ + auto HasPendingCompile() const -> bool; + + /** + * @brief Non-blocking: true once the deferred link has completed in the background + * (or if there is no pending compile). Does NOT resolve the link. + */ + auto PendingCompileReady() const -> bool; + + /** + * @brief Performs the deferred status check + shader cleanup for a pending compile. + * No-op if nothing is pending. Blocks if the background compile is still running. + * @throws ShaderException Thrown if the deferred compilation or linking failed. + */ + void FinalizeCompile() const; + /** * @brief Validates that the program can run in the current state. * @param validationMessage The error message if validation failed. @@ -194,7 +232,21 @@ class Shader */ auto CompileShader(const std::string& source, GLenum type) -> GLuint; + /** + * @brief Lazily finalizes a pending deferred compile, swallowing (but logging) errors. + * Safety net for Bind()/Validate()/SetUniform* so render paths never throw. + */ + void EnsureFinalizedNoThrow() const noexcept; + GLuint m_shaderProgram{}; //!< The program ID. + + // Deferred-compile state. Mutable because the lazy + // finalize safety net runs from const accessors (Bind/SetUniform are const). + mutable bool m_pendingLink{false}; //!< A deferred link has not been finalized yet. + mutable GLuint m_pendingVertexShader{}; //!< Attached vertex shader awaiting detach/delete. + mutable GLuint m_pendingFragmentShader{}; //!< Attached fragment shader awaiting detach/delete. + mutable std::string m_pendingVertexSource; //!< Kept for the error log if the deferred link fails. + mutable std::string m_pendingFragmentSource; //!< Kept for the error log if the deferred link fails. }; } // namespace Renderer