Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions src/api/include/projectM-4/core.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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.
*
Expand Down
28 changes: 28 additions & 0 deletions src/libprojectM/MilkdropPreset/FinalComposite.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>(MilkdropShader::ShaderType::CompositeShader);
m_compositeShader->LoadCode(defaultCompositeShader);
m_compositeShader->LoadTexturesAndCompile(presetState);
}
}
}

void FinalComposite::Draw(const PresetState& presetState, const PerFrameContext& perFrameContext)
{
if (m_compositeShader)
Expand Down
13 changes: 13 additions & 0 deletions src/libprojectM/MilkdropPreset/FinalComposite.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions src/libprojectM/MilkdropPreset/MilkdropPreset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,17 @@ void MilkdropPreset::DrawInitialImage(const std::shared_ptr<Renderer::Texture>&
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)
Expand Down
11 changes: 11 additions & 0 deletions src/libprojectM/MilkdropPreset/MilkdropPreset.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
23 changes: 23 additions & 0 deletions src/libprojectM/MilkdropPreset/PerPixelMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions src/libprojectM/MilkdropPreset/PerPixelMesh.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions src/libprojectM/Preset.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
113 changes: 112 additions & 1 deletion src/libprojectM/ProjectM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

#include <Renderer/CopyTexture.hpp>
#include <Renderer/PresetTransition.hpp>
#include <Renderer/Shader.hpp>
#include <Renderer/ShaderCache.hpp>
#include <Renderer/TextureManager.hpp>
#include <Renderer/TransitionShaderManager.hpp>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -286,6 +389,11 @@ void ProjectM::SetWindowSize(uint32_t width, uint32_t height)
}

void ProjectM::StartPresetTransition(std::unique_ptr<Preset>&& preset, bool hardCut)
{
StartPresetTransitionInternal(std::move(preset), hardCut, false);
}

void ProjectM::StartPresetTransitionInternal(std::unique_ptr<Preset>&& preset, bool hardCut, bool alreadyInitialized)
{
m_presetChangeNotified = m_presetLocked;

Expand All @@ -294,7 +402,10 @@ void ProjectM::StartPresetTransition(std::unique_ptr<Preset>&& preset, bool hard
return;
}

preset->Initialize(GetRenderContext());
if (!alreadyInitialized)
{
preset->Initialize(GetRenderContext());
}

// If already in a transition, force immediate completion.
if (m_transitioningPreset != nullptr)
Expand Down
Loading