diff --git a/src/api/include/projectM-4/callbacks.h b/src/api/include/projectM-4/callbacks.h index 3ba9e1bcb8..3d2935f68d 100644 --- a/src/api/include/projectM-4/callbacks.h +++ b/src/api/include/projectM-4/callbacks.h @@ -104,7 +104,7 @@ PROJECTM_EXPORT void projectm_set_preset_switch_failed_event_callback(projectm_h * and will delete it (via glDeleteTextures) when it is no longer needed. Do not * delete the texture yourself or reuse the texture ID after passing it here. * - * @since 4.2.0 + * @since 4.3.0 */ typedef struct projectm_texture_load_data { const unsigned char* data; /**< Pointer to raw pixel data in standard OpenGL format (first row is bottom of image). Can be NULL. */ @@ -139,7 +139,7 @@ typedef struct projectm_texture_load_data { * @param texture_name The name of the texture being requested, as used in the preset. * @param[out] data Pointer to a structure where the application should place texture data. * @param user_data A user-defined data pointer that was provided when registering the callback. - * @since 4.2.0 + * @since 4.3.0 */ typedef void (*projectm_texture_load_event)(const char* texture_name, projectm_texture_load_data* data, @@ -155,12 +155,60 @@ typedef void (*projectm_texture_load_event)(const char* texture_name, * @param callback A pointer to the callback function. * @param user_data A pointer to any data that will be sent back in the callback, e.g. context * information. - * @since 4.2.0 + * @since 4.3.0 */ PROJECTM_EXPORT void projectm_set_texture_load_event_callback(projectm_handle instance, projectm_texture_load_event callback, void* user_data); +/** + * @brief Optional hooks for caching the preprocessed-HLSL text of Milkdrop preset shaders. + * + * The Milkdrop shader transpiler runs a text preprocessor (macro expansion) over each + * preset shader before parsing it. That step is pure text and deterministic for a given + * input, so its result can be cached across runs. When these hooks are registered, projectM + * computes a stable key for the preprocessor input and consults the cache before running the + * (comparatively expensive) preprocessor. + * + * The cache is fully opt-in. If no hooks are registered, behaviour is byte-identical to a + * build without this feature. projectM owns nothing referenced by the value pointers after a + * callback returns; the get sink is only valid for the duration of the sink call. + * + * All hook invocations happen on the same thread that calls the projectM rendering + * functions (the transpile runs there), so no additional synchronization is required. + * + * @since 4.3.0 + */ +typedef struct projectm_preprocess_cache_hooks { + /** + * @brief Lookup hook. On a hit, invoke @p sink exactly once with the cached value (valid + * only for the duration of that call) and return true. On a miss, return false and + * do not call @p sink. + */ + bool (*get)(void* user, const char* key, size_t keylen, + void* sinkctx, void (*sink)(void* sinkctx, const char* data, size_t len)); + /** + * @brief Store hook. Copy @p vallen bytes at @p value under the @p keylen-byte @p key. + * The callee owns no passed-in pointer after returning. + */ + void (*put)(void* user, const char* key, size_t keylen, const char* value, size_t vallen); + void* user; /**< User-defined pointer passed back to get/put. */ +} projectm_preprocess_cache_hooks; + +/** + * @brief Registers (or clears) the preprocessed-HLSL cache hooks for this instance. + * + * The hooks structure is copied by value; the caller need not keep it alive, but the @c user + * pointer and anything the callbacks touch must outlive the projectM instance (or until the + * hooks are cleared). Passing NULL clears any previously registered hooks. + * + * @param instance The projectM instance handle. + * @param hooks A pointer to the hooks structure, or NULL to clear. + * @since 4.3.0 + */ +PROJECTM_EXPORT void projectm_set_preprocess_cache(projectm_handle instance, + const projectm_preprocess_cache_hooks* hooks); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/libprojectM/MilkdropPreset/CMakeLists.txt b/src/libprojectM/MilkdropPreset/CMakeLists.txt index d6682ae8a1..e0b8a03cbc 100644 --- a/src/libprojectM/MilkdropPreset/CMakeLists.txt +++ b/src/libprojectM/MilkdropPreset/CMakeLists.txt @@ -62,6 +62,8 @@ add_library(MilkdropPreset OBJECT MilkdropPresetExceptions.hpp MilkdropShader.cpp MilkdropShader.hpp + MilkdropShaderPreprocess.cpp + MilkdropShaderPreprocess.hpp MilkdropStaticShaders.cpp.in MilkdropStaticShaders.hpp.in MotionVectors.cpp diff --git a/src/libprojectM/MilkdropPreset/MilkdropShader.cpp b/src/libprojectM/MilkdropPreset/MilkdropShader.cpp index 10cf919ca2..0191b9c51f 100644 --- a/src/libprojectM/MilkdropPreset/MilkdropShader.cpp +++ b/src/libprojectM/MilkdropPreset/MilkdropShader.cpp @@ -1,5 +1,6 @@ #include "MilkdropShader.hpp" +#include "MilkdropShaderPreprocess.hpp" #include "PresetState.hpp" #include "Utils.hpp" @@ -331,182 +332,13 @@ auto MilkdropShader::Shader() -> Renderer::Shader& void MilkdropShader::PreprocessPresetShader(std::string& program) { - std::string shaderTypeString = "composite"; - if (m_type == ShaderType::WarpShader) - { - shaderTypeString = "warp"; - } - - if (program.length() <= 0) - { - throw Renderer::ShaderException("[MilkdropShader] Preset " + shaderTypeString + " shader is declared, but empty."); - } - - size_t found; - - // Find "sampler_state" overrides and remove them first, as they're not supported by GLSL. - // The logic isn't totally fool-proof, but should work in general. - // Use a comment-stripped copy for searching so commented-out sampler_state blocks are skipped. - // StripComments preserves string length, so positions map 1:1 to the original. - std::string stripped = Utils::StripComments(program); - found = stripped.find("sampler_state"); - while (found != std::string::npos) - { - // Now go backwards and find the assignment - found = stripped.rfind('=', found); - auto startPos = found; - - // Find closing brace and semicolon - found = stripped.find('}', found); - found = stripped.find(';', found); - - if (found != std::string::npos) - { - stripped.replace(startPos, found - startPos, ""); - } - else - { - // No closing brace and semicolon. - break; - } - - found = stripped.find("sampler_state"); - } - - // replace shader_body with entry point function - // Use the stripped copy so a commented-out shader_body is not matched. - found = stripped.find("shader_body"); - if (found != std::string::npos) - { - if (m_type == ShaderType::WarpShader) - { - program.replace(int(found), 11, R"( -void PS(float4 _vDiffuse : COLOR, - float4 _uv : TEXCOORD0, - float2 _rad_ang : TEXCOORD1, - out float4 _return_value : COLOR0, - out float4 _mv_tex_coords : COLOR1) -)"); - } - else - { - program.replace(int(found), 11, R"( -void PS(float4 _vDiffuse : COLOR, - float2 _uv : TEXCOORD0, - float2 _rad_ang : TEXCOORD1, - out float4 _return_value : COLOR) -)"); - } - } - else - { - LOG_DEBUG("[MilkdropShader] Failed " + shaderTypeString + " shader code:\n" + program); - throw Renderer::ShaderException("[MilkdropShader] Preset " + shaderTypeString + " shader is missing \"shader_body\" entry point."); - } - - // replace the "{" immediately following shader_body with some variable declarations - found = program.find('{', found); - if (found != std::string::npos) - { - std::string progMain = "{\nfloat3 ret = 0;\n"; - if (m_type == ShaderType::WarpShader) - { - progMain.append("_mv_tex_coords.xy = _uv.xy;\n"); - } - program.replace(int(found), 1, progMain); - } - else - { - LOG_DEBUG("[MilkdropShader] Failed " + shaderTypeString + " shader code:\n" + program); - throw Renderer::ShaderException("[MilkdropShader] Preset " + shaderTypeString + " shader has no opening braces."); - } - - // replace "}" with return statement (this can probably be optimized for the GLSL conversion...) - found = program.rfind('}'); - if (found != std::string::npos) - { - program.replace(int(found), 1, "_return_value = float4(ret.xyz, 1.0);\n" - "}\n"); - } - else - { - LOG_DEBUG("[MilkdropShader] Failed " + shaderTypeString + " shader code:\n" + program); - throw Renderer::ShaderException("[MilkdropShader] Preset " + shaderTypeString + " shader has no closing brace."); - } - - // Find matching closing brace and cut off excess text after shader's main function - int bracesOpen = 1; - size_t pos = found + 1; - for (; pos < program.length() && bracesOpen > 0; ++pos) - { - switch (program.at(pos)) - { - case '/': - // Skip line comments until EoL to prevent false counting - if (pos < program.length() - 1 && program.at(pos + 1) == '/') - { - for (; pos < program.length(); ++pos) - { - if (program.at(pos) == '\n') - { - break; - } - } - } - // Skip block comments to prevent false counting - else if (pos < program.length() - 1 && program.at(pos + 1) == '*') - { - pos += 2; - for (; pos < program.length() - 1; ++pos) - { - if (program.at(pos) == '*' && program.at(pos + 1) == '/') - { - ++pos; // skip past '/' - break; - } - } - } - continue; - - case '{': - bracesOpen++; - continue; - - case '}': - bracesOpen--; - } - } - - if (pos < program.length() - 1) - { - program.resize(pos); - } - - std::string fullSource; //!< Full shader source before translation, includes all uniforms etc. - - // First copy the generic "header" into the shader. Includes uniforms and some defines - // to unwrap the packed 4-element uniforms into single values. - fullSource.append(MilkdropStaticShaders::Get()->GetPresetShaderHeader()); - - if (m_type == ShaderType::WarpShader) - { - fullSource.append("#define rad _rad_ang.x\n" - "#define ang _rad_ang.y\n" - "#define uv _uv.xy\n" - "#define uv_orig _uv.zw\n"); - } - else - { - fullSource.append("#define rad _rad_ang.x\n" - "#define ang _rad_ang.y\n" - "#define uv _uv.xy\n" - "#define uv_orig _uv.xy\n" - "#define hue_shader _vDiffuse.xyz\n"); - } - - fullSource.append(program); - - program = fullSource; + // The pure-text assembly lives in the GL-free free function + // AssembleApplyPreprocessorInput(type, body) so that external tooling (e.g. a build-time + // cache precompute step) and the runtime produce byte-identical preprocessor input. This + // wrapper preserves the original in/out contract (mutates `program` in place). + // GetReferencedSamplers still runs separately in LoadCode, so m_samplerNames population + // is unchanged. + program = AssembleApplyPreprocessorInput(m_type, program); } void MilkdropShader::GetReferencedSamplers(const std::string& program) @@ -615,12 +447,45 @@ void MilkdropShader::TranspileHLSLShader(const PresetState& presetState, std::st M4::HLSLTree tree(&allocator); M4::HLSLParser parser(&allocator, &tree); - // Preprocess define macros + // Preprocess define macros. Optionally short-circuit via the app-provided preprocessed- + // HLSL cache: the preprocessor output is a pure, deterministic function of `program`, so + // a hit lets us skip ApplyPreprocessor entirely. With no hooks registered this path is + // byte-identical to the historical behaviour. std::string sourcePreprocessed; - if (!parser.ApplyPreprocessor("", program.c_str(), program.size(), sourcePreprocessed)) + const auto* preprocessCacheHooks = presetState.renderContext.preprocessCache; + bool preprocessCacheHit = false; + std::string preprocessCacheKey; + if (preprocessCacheHooks != nullptr && (preprocessCacheHooks->get != nullptr || preprocessCacheHooks->put != nullptr)) { - LOG_DEBUG("[MilkdropShader] Failed " + shaderTypeString + " shader code:\n" + program); - throw Renderer::ShaderException("Error translating HLSL " + shaderTypeString + " shader: Preprocessing failed."); + preprocessCacheKey = ComputePreprocessCacheKey(program); + } + if (preprocessCacheHooks != nullptr && preprocessCacheHooks->get != nullptr) + { + // Sink appends the cached bytes into sourcePreprocessed for the duration of the call. + auto appendSink = [](void* sinkctx, const char* data, size_t len) { + auto* out = static_cast(sinkctx); + out->append(data, len); + }; + preprocessCacheHit = preprocessCacheHooks->get(preprocessCacheHooks->user, + preprocessCacheKey.data(), preprocessCacheKey.size(), + &sourcePreprocessed, appendSink); + } + + if (!preprocessCacheHit) + { + sourcePreprocessed.clear(); + if (!parser.ApplyPreprocessor("", program.c_str(), program.size(), sourcePreprocessed)) + { + LOG_DEBUG("[MilkdropShader] Failed " + shaderTypeString + " shader code:\n" + program); + throw Renderer::ShaderException("Error translating HLSL " + shaderTypeString + " shader: Preprocessing failed."); + } + + if (preprocessCacheHooks != nullptr && preprocessCacheHooks->put != nullptr) + { + preprocessCacheHooks->put(preprocessCacheHooks->user, + preprocessCacheKey.data(), preprocessCacheKey.size(), + sourcePreprocessed.data(), sourcePreprocessed.size()); + } } // Remove previous shader declarations diff --git a/src/libprojectM/MilkdropPreset/MilkdropShaderPreprocess.cpp b/src/libprojectM/MilkdropPreset/MilkdropShaderPreprocess.cpp new file mode 100644 index 0000000000..ebb5e7622b --- /dev/null +++ b/src/libprojectM/MilkdropPreset/MilkdropShaderPreprocess.cpp @@ -0,0 +1,251 @@ +#include "MilkdropShaderPreprocess.hpp" + +#include "Utils.hpp" + +#include + +#include +#include + +#include +#include +#include +#include + +namespace libprojectM { +namespace MilkdropPreset { + +using libprojectM::MilkdropPreset::MilkdropStaticShaders; + +std::string AssembleApplyPreprocessorInput(MilkdropShader::ShaderType type, + const std::string& presetShaderBody) +{ + // Byte-for-byte extraction of the pure-text assembly that used to live inline in + // MilkdropShader::PreprocessPresetShader. Do NOT alter any transformation, ordering, + // whitespace, header or #define content — external tooling and the runtime rely on + // producing identical bytes. + std::string program = presetShaderBody; + + std::string shaderTypeString = "composite"; + if (type == MilkdropShader::ShaderType::WarpShader) + { + shaderTypeString = "warp"; + } + + if (program.length() <= 0) + { + throw Renderer::ShaderException("[MilkdropShader] Preset " + shaderTypeString + " shader is declared, but empty."); + } + + size_t found; + + // Find "sampler_state" overrides and remove them first, as they're not supported by GLSL. + // The logic isn't totally fool-proof, but should work in general. + // Use a comment-stripped copy for searching so commented-out sampler_state blocks are skipped. + // StripComments preserves string length, so positions map 1:1 to the original. + std::string stripped = Utils::StripComments(program); + found = stripped.find("sampler_state"); + while (found != std::string::npos) + { + // Now go backwards and find the assignment + found = stripped.rfind('=', found); + auto startPos = found; + + // Find closing brace and semicolon + found = stripped.find('}', found); + found = stripped.find(';', found); + + if (found != std::string::npos) + { + stripped.replace(startPos, found - startPos, ""); + } + else + { + // No closing brace and semicolon. + break; + } + + found = stripped.find("sampler_state"); + } + + // replace shader_body with entry point function + // Use the stripped copy so a commented-out shader_body is not matched. + found = stripped.find("shader_body"); + if (found != std::string::npos) + { + if (type == MilkdropShader::ShaderType::WarpShader) + { + program.replace(int(found), 11, R"( +void PS(float4 _vDiffuse : COLOR, + float4 _uv : TEXCOORD0, + float2 _rad_ang : TEXCOORD1, + out float4 _return_value : COLOR0, + out float4 _mv_tex_coords : COLOR1) +)"); + } + else + { + program.replace(int(found), 11, R"( +void PS(float4 _vDiffuse : COLOR, + float2 _uv : TEXCOORD0, + float2 _rad_ang : TEXCOORD1, + out float4 _return_value : COLOR) +)"); + } + } + else + { + LOG_DEBUG("[MilkdropShader] Failed " + shaderTypeString + " shader code:\n" + program); + throw Renderer::ShaderException("[MilkdropShader] Preset " + shaderTypeString + " shader is missing \"shader_body\" entry point."); + } + + // replace the "{" immediately following shader_body with some variable declarations + found = program.find('{', found); + if (found != std::string::npos) + { + std::string progMain = "{\nfloat3 ret = 0;\n"; + if (type == MilkdropShader::ShaderType::WarpShader) + { + progMain.append("_mv_tex_coords.xy = _uv.xy;\n"); + } + program.replace(int(found), 1, progMain); + } + else + { + LOG_DEBUG("[MilkdropShader] Failed " + shaderTypeString + " shader code:\n" + program); + throw Renderer::ShaderException("[MilkdropShader] Preset " + shaderTypeString + " shader has no opening braces."); + } + + // replace "}" with return statement (this can probably be optimized for the GLSL conversion...) + found = program.rfind('}'); + if (found != std::string::npos) + { + program.replace(int(found), 1, "_return_value = float4(ret.xyz, 1.0);\n" + "}\n"); + } + else + { + LOG_DEBUG("[MilkdropShader] Failed " + shaderTypeString + " shader code:\n" + program); + throw Renderer::ShaderException("[MilkdropShader] Preset " + shaderTypeString + " shader has no closing brace."); + } + + // Find matching closing brace and cut off excess text after shader's main function + int bracesOpen = 1; + size_t pos = found + 1; + for (; pos < program.length() && bracesOpen > 0; ++pos) + { + switch (program.at(pos)) + { + case '/': + // Skip line comments until EoL to prevent false counting + if (pos < program.length() - 1 && program.at(pos + 1) == '/') + { + for (; pos < program.length(); ++pos) + { + if (program.at(pos) == '\n') + { + break; + } + } + } + // Skip block comments to prevent false counting + else if (pos < program.length() - 1 && program.at(pos + 1) == '*') + { + pos += 2; + for (; pos < program.length() - 1; ++pos) + { + if (program.at(pos) == '*' && program.at(pos + 1) == '/') + { + ++pos; // skip past '/' + break; + } + } + } + continue; + + case '{': + bracesOpen++; + continue; + + case '}': + bracesOpen--; + } + } + + if (pos < program.length() - 1) + { + program.resize(pos); + } + + std::string fullSource; //!< Full shader source before translation, includes all uniforms etc. + + // First copy the generic "header" into the shader. Includes uniforms and some defines + // to unwrap the packed 4-element uniforms into single values. + fullSource.append(MilkdropStaticShaders::Get()->GetPresetShaderHeader()); + + if (type == MilkdropShader::ShaderType::WarpShader) + { + fullSource.append("#define rad _rad_ang.x\n" + "#define ang _rad_ang.y\n" + "#define uv _uv.xy\n" + "#define uv_orig _uv.zw\n"); + } + else + { + fullSource.append("#define rad _rad_ang.x\n" + "#define ang _rad_ang.y\n" + "#define uv _uv.xy\n" + "#define uv_orig _uv.xy\n" + "#define hue_shader _vDiffuse.xyz\n"); + } + + fullSource.append(program); + + return fullSource; +} + +namespace { + +// Compile-time salt. Bump this string whenever anything that changes the produced GLSL for +// a given input changes: the static preset shader header, the hlslparser/ApplyPreprocessor +// implementation, or AssembleApplyPreprocessorInput above. Bumping invalidates every stale +// cache entry (old keys can no longer be produced). +constexpr const char* kPreprocessCacheSalt = "pmpp-v1:"; + +uint64_t Fnv1a64(const std::string& data, uint64_t offsetBasis) +{ + constexpr uint64_t prime = 1099511628211ull; + uint64_t hash = offsetBasis; + for (const char c : data) + { + hash ^= static_cast(static_cast(c)); + hash *= prime; + } + return hash; +} + +} // namespace + +std::string ComputePreprocessCacheKey(const std::string& applyPreprocessorInput) +{ + // Two independent FNV-1a passes with different offset bases (the second is the standard + // basis byte-reversed) give ~128 bits of effective hash; the length is embedded too. + const uint64_t h1 = Fnv1a64(applyPreprocessorInput, 14695981039346656037ull); + const uint64_t h2 = Fnv1a64(applyPreprocessorInput, 0x84222325cbf29ce4ull); + + char buffer[64]; + std::snprintf(buffer, sizeof(buffer), "%016llx%016llx-%zx", + static_cast(h1), + static_cast(h2), + applyPreprocessorInput.size()); + + return std::string(kPreprocessCacheSalt) + buffer; +} + +std::string PreprocessCacheSalt() +{ + return kPreprocessCacheSalt; +} + +} // namespace MilkdropPreset +} // namespace libprojectM diff --git a/src/libprojectM/MilkdropPreset/MilkdropShaderPreprocess.hpp b/src/libprojectM/MilkdropPreset/MilkdropShaderPreprocess.hpp new file mode 100644 index 0000000000..b1827766dc --- /dev/null +++ b/src/libprojectM/MilkdropPreset/MilkdropShaderPreprocess.hpp @@ -0,0 +1,74 @@ +/** + * @file MilkdropShaderPreprocess.hpp + * @brief GL-free, pure-text assembly of the input passed to the HLSL preprocessor. + * + * Extracted from MilkdropShader::PreprocessPresetShader so the exact string handed to + * HLSLParser::ApplyPreprocessor can be produced without a MilkdropShader instance, a GL + * context, textures, or PresetState. This is the shared foundation for a runtime + * shader-transpile cache and a build-time (host) precompute tool: both call this function + * and must get byte-identical output. + */ +#pragma once + +#include "MilkdropShader.hpp" + +#include + +namespace libprojectM { +namespace MilkdropPreset { + +/** + * @brief Assembles the pure-text shader source that is later passed to the HLSL + * preprocessor (HLSLParser::ApplyPreprocessor). + * + * This is a pure function of (shader type, preset shader body): it renames the preset's + * shader entry point to PS(), strips unsupported sampler_state{} blocks, injects the + * per-type variable declarations, trims trailing text after the shader's main function, + * and prepends the static preset shader header plus the type-specific #defines. It does + * NOT touch GL, Renderer::Shader, textures, or PresetState. + * + * The output is byte-for-byte identical to what MilkdropShader::PreprocessPresetShader + * historically produced in-place. + * + * @param type The preset shader type (warp or composite). + * @param presetShaderBody The raw preset shader body (as loaded from the preset). + * @return The fully assembled shader source ready for the HLSL preprocessor. + * @throws Renderer::ShaderException If the body is empty or malformed (missing + * "shader_body", opening brace, or closing brace) — same messages/type as before. + */ +std::string AssembleApplyPreprocessorInput(MilkdropShader::ShaderType type, + const std::string& presetShaderBody); + +/** + * @brief Computes a version-salted, collision-safe cache key for a preprocessed-HLSL + * lookup, from the exact bytes handed to HLSLParser::ApplyPreprocessor. + * + * GL-free and side-effect-free, so the runtime transpile path and a future build-time + * precompute tool compute byte-identical keys for identical inputs. + * + * Collision safety: the key embeds a compile-time salt (bumped whenever the static shader + * header, the hlslparser, or the assembly transform changes — invalidating stale entries), + * the input length, and two independent 64-bit FNV-1a hashes seeded with different offset + * bases. That is ~128 bits of effective hash plus an exact length check, so accidental + * collisions are astronomically unlikely (and a length mismatch alone rules most out). + * The value stored under a key is deterministic text, so even in the impossible event of a + * full collision the only effect would be reusing a valid-but-wrong transpile — never a + * crash. Callers may treat a hit as authoritative. + * + * @param applyPreprocessorInput The exact string that would be passed to ApplyPreprocessor + * (i.e. the return value of AssembleApplyPreprocessorInput). + * @return A printable ASCII cache key. + */ +std::string ComputePreprocessCacheKey(const std::string& applyPreprocessorInput); + +/** + * @brief Returns the compile-time version salt embedded in every cache key. + * + * Exposed so a build-time precompute tool can stamp the exact same salt into its resource + * header without duplicating the constant (the tool and ComputePreprocessCacheKey must not + * drift). Bumping the salt in the .cpp invalidates every stale precomputed/runtime entry. + */ +std::string PreprocessCacheSalt(); + +} // namespace MilkdropPreset +} // namespace libprojectM diff --git a/src/libprojectM/ProjectM.cpp b/src/libprojectM/ProjectM.cpp index a042f244c6..728678492c 100644 --- a/src/libprojectM/ProjectM.cpp +++ b/src/libprojectM/ProjectM.cpp @@ -573,6 +573,20 @@ void ProjectM::TouchDestroyAll() // UNIMPLEMENTED } +void ProjectM::SetPreprocessCacheHooks(const projectm_preprocess_cache_hooks* hooks) +{ + if (hooks != nullptr) + { + m_preprocessCacheHooks = *hooks; + m_preprocessCacheHooksSet = true; + } + else + { + m_preprocessCacheHooks = projectm_preprocess_cache_hooks{}; + m_preprocessCacheHooksSet = false; + } +} + auto ProjectM::GetRenderContext() -> Renderer::RenderContext { Renderer::RenderContext ctx{}; @@ -595,6 +609,7 @@ auto ProjectM::GetRenderContext() -> Renderer::RenderContext ctx.textureManager = m_textureManager.get(); ctx.shaderCache = m_shaderCache.get(); + ctx.preprocessCache = m_preprocessCacheHooksSet ? &m_preprocessCacheHooks : nullptr; if (m_transition) { diff --git a/src/libprojectM/ProjectM.hpp b/src/libprojectM/ProjectM.hpp index 9879e77f0c..a718b40364 100644 --- a/src/libprojectM/ProjectM.hpp +++ b/src/libprojectM/ProjectM.hpp @@ -281,6 +281,14 @@ class PROJECTM_CXX_EXPORT ProjectM */ void BurnInTexture(uint32_t openGlTextureId, int left, int top, int width, int height); + /** + * @brief Registers (or clears) app-provided preprocessed-HLSL cache hooks. + * + * The struct is copied by value. Passing nullptr clears any registered hooks. + * @param hooks Pointer to the hooks struct, or nullptr to clear. + */ + void SetPreprocessCacheHooks(const projectm_preprocess_cache_hooks* hooks); + private: void Initialize(); @@ -331,6 +339,9 @@ class PROJECTM_CXX_EXPORT ProjectM 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. + + projectm_preprocess_cache_hooks m_preprocessCacheHooks{}; //!< App-provided preprocessed-HLSL cache hooks (copied by value). + bool m_preprocessCacheHooksSet{false}; //!< True once hooks are registered; controls RenderContext propagation. }; } // namespace libprojectM diff --git a/src/libprojectM/ProjectMCWrapper.cpp b/src/libprojectM/ProjectMCWrapper.cpp index cd2bca2f2e..f7751f72e3 100644 --- a/src/libprojectM/ProjectMCWrapper.cpp +++ b/src/libprojectM/ProjectMCWrapper.cpp @@ -171,6 +171,13 @@ void projectm_set_texture_load_event_callback(projectm_handle instance, } } +void projectm_set_preprocess_cache(projectm_handle instance, + const projectm_preprocess_cache_hooks* hooks) +{ + auto projectMInstance = handle_to_instance(instance); + projectMInstance->SetPreprocessCacheHooks(hooks); +} + void projectm_set_texture_search_paths(projectm_handle instance, const char** texture_search_paths, size_t count) diff --git a/src/libprojectM/Renderer/RenderContext.hpp b/src/libprojectM/Renderer/RenderContext.hpp index 9fec280dc8..881b7e03e9 100644 --- a/src/libprojectM/Renderer/RenderContext.hpp +++ b/src/libprojectM/Renderer/RenderContext.hpp @@ -5,6 +5,7 @@ #pragma once #include +#include namespace libprojectM { namespace Renderer { @@ -38,6 +39,10 @@ class PROJECTM_CXX_EXPORT RenderContext TextureManager* textureManager{nullptr}; //!< Holds all loaded textures for shader access. ShaderCache* shaderCache{nullptr}; //!< The shader chace of this projectM instance. + + //!< Optional, app-provided preprocessed-HLSL cache hooks. Null unless registered via + //!< projectm_set_preprocess_cache. Points at storage owned by the ProjectM instance. + const projectm_preprocess_cache_hooks* preprocessCache{nullptr}; }; } // namespace Renderer