From a4e36420a64c22032936a467fc0531818d7f908b Mon Sep 17 00:00:00 2001 From: keeper-of-memes Date: Mon, 6 Jul 2026 11:49:28 +0100 Subject: [PATCH] Add preset preload API with optional background shader compilation Applications that own their preset rotation (know current/next ahead of time) previously had no way to prepare the next preset before switching: projectm_load_preset_file() creates AND initialises the preset inside the switch, so complex Milkdrop presets stall the render thread for hundreds of milliseconds (600-1200 ms measured on an Apple TV 4K / A15) at the exact moment the transition starts. New C API (all opt-in; existing behaviour unchanged): - projectm_preload_preset_file(): create and fully initialise a preset (including shader compilation) without switching to it. One preset can be staged at a time. - projectm_activate_preloaded_preset(): switch to the staged preset, skipping re-initialisation. Returns false (caller falls back to a normal load) if nothing is staged or the window size changed since the preload. - projectm_has_preloaded_preset(): query whether a preset is staged. - projectm_preloaded_preset_compile_ready(): non-blocking readiness poll for the deferred background shader compiles (below). During preload, Renderer::Shader defers everything past glLinkProgram. Drivers that implement GL_KHR_parallel_shader_compile (ANGLE does) background the compile/link on a worker pool, but ANY program-observing call synchronously resolves the link on the calling thread. That includes the glDetachShader immediately after glLinkProgram in the normal path. With the deferral, the link keeps running in the background during the preload lead time; readiness is polled via the non-resolving GL_COMPLETION_STATUS_KHR query, and activation finalises the program (status check, detach/delete, and the same per-shader failure fallbacks as the synchronous path). Bind()/Validate() finalise lazily as a safety net, so a pending shader is always safe to use. On drivers without background compilation the deferral is harmless; the link is simply already complete at finalise time. Texture purging moves to activation (not preload) so a speculative preload can't age/evict textures the active preset still uses. Measured on tvOS/ANGLE (Metal backend): preset-switch initialisation spike drops from ~651 ms to ~133 ms average with preload alone; with deferred compilation the render-thread cost of glLinkProgram drops from ~33-55 ms to 0.04-0.10 ms per preset, with the background link completing over 1-4 frames and finalisation at activation taking 0.003-3.1 ms. --- src/api/include/projectM-4/core.h | 60 +++++++- .../MilkdropPreset/FinalComposite.cpp | 28 ++++ .../MilkdropPreset/FinalComposite.hpp | 13 ++ .../MilkdropPreset/MilkdropPreset.cpp | 11 ++ .../MilkdropPreset/MilkdropPreset.hpp | 11 ++ .../MilkdropPreset/PerPixelMesh.cpp | 23 +++ .../MilkdropPreset/PerPixelMesh.hpp | 12 ++ src/libprojectM/Preset.hpp | 17 +++ src/libprojectM/ProjectM.cpp | 113 +++++++++++++- src/libprojectM/ProjectM.hpp | 46 ++++++ src/libprojectM/ProjectMCWrapper.cpp | 24 +++ src/libprojectM/Renderer/Shader.cpp | 138 ++++++++++++++++++ src/libprojectM/Renderer/Shader.hpp | 52 +++++++ 13 files changed, 545 insertions(+), 3 deletions(-) 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