From c08b810502cece50c280f582b255163efbacd24c Mon Sep 17 00:00:00 2001 From: Coastrnaut Date: Sun, 8 Nov 2026 21:11:47 -0500 Subject: [PATCH] Skip equation code that doesn't compile instead of failing the preset If a code block of a preset doesn't compile, Milkdrop shows a warning, leaves that block disabled and still uses the preset (RecompileExpressions() in state.cpp). If the init code fails, it resets the q variables (or the t variables of a custom wave or shape) that would have been set by it. projectM threw an exception instead, so the whole preset failed to load. Some presets in the common packs contain broken code that Milkdrop also rejects, e.g. "0.99l", ".-.4" or a stray "per_frame_1=" pasted into a line, often in a block that has little or no visible effect. Log a warning and skip the block instead, and reset the variables like Milkdrop does. --- .../MilkdropPreset/CustomShape.hpp | 2 +- .../MilkdropPreset/CustomWaveform.hpp | 2 +- .../MilkdropPreset/PerFrameContext.cpp | 13 ++++- .../MilkdropPreset/PerFrameContext.hpp | 4 +- .../MilkdropPreset/PerPixelContext.cpp | 4 +- .../MilkdropPreset/PerPixelContext.hpp | 2 +- .../MilkdropPreset/ShapePerFrameContext.cpp | 12 +++- .../MilkdropPreset/ShapePerFrameContext.hpp | 4 +- .../WaveformPerFrameContext.cpp | 12 +++- .../WaveformPerFrameContext.hpp | 4 +- .../WaveformPerPointContext.cpp | 4 +- tests/libprojectM/CMakeLists.txt | 4 ++ .../libprojectM/EquationCompileErrorTest.cpp | 57 +++++++++++++++++++ 13 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 tests/libprojectM/EquationCompileErrorTest.cpp diff --git a/src/libprojectM/MilkdropPreset/CustomShape.hpp b/src/libprojectM/MilkdropPreset/CustomShape.hpp index 5090b37eb7..2456ad8ce9 100644 --- a/src/libprojectM/MilkdropPreset/CustomShape.hpp +++ b/src/libprojectM/MilkdropPreset/CustomShape.hpp @@ -35,7 +35,7 @@ class CustomShape /** * @brief Compiles all code blocks and runs the init expression. - * @throws MilkdropCompileException Thrown if one of the code blocks couldn't be compiled. + * If one of the code blocks can't be compiled, a warning is logged and the code is skipped, like in Milkdrop. */ void CompileCodeAndRunInitExpressions(); diff --git a/src/libprojectM/MilkdropPreset/CustomWaveform.hpp b/src/libprojectM/MilkdropPreset/CustomWaveform.hpp index 7c4c3df1a2..e563bdb544 100644 --- a/src/libprojectM/MilkdropPreset/CustomWaveform.hpp +++ b/src/libprojectM/MilkdropPreset/CustomWaveform.hpp @@ -33,7 +33,7 @@ class CustomWaveform /** * @brief Compiles all code blocks and runs the init expression. - * @throws MilkdropCompileException Thrown if one of the code blocks couldn't be compiled. + * If one of the code blocks can't be compiled, a warning is logged and the code is skipped, like in Milkdrop. * @param presetPerFrameContext The per-frame context to retrieve the init Q vars from. */ void CompileCodeAndRunInitExpressions(const PerFrameContext& presetPerFrameContext); diff --git a/src/libprojectM/MilkdropPreset/PerFrameContext.cpp b/src/libprojectM/MilkdropPreset/PerFrameContext.cpp index b2063825be..d8ecc21f34 100644 --- a/src/libprojectM/MilkdropPreset/PerFrameContext.cpp +++ b/src/libprojectM/MilkdropPreset/PerFrameContext.cpp @@ -139,7 +139,14 @@ void PerFrameContext::EvaluateInitCode(PresetState& state) error = "[PerFrameContext] Could not compile per-frame init code."; } LOG_DEBUG("[PerFrameContext] Failed per-frame INIT code:\n" + state.perFrameInitCode); - throw MilkdropCompileException(error); + // Like Milkdrop, only this code block is disabled, the preset is still used. + LOG_WARN(error); + for (int q = 0; q < QVarCount; q++) + { + q_values_after_init_code[q] = 0; + state.frameQVariables[q] = 0; + } + return; } projectm_eval_code_execute(initCode); @@ -260,7 +267,9 @@ void PerFrameContext::CompilePerFrameCode(const std::string& perFrameCode) error = "[PerFrameContext] Could not compile per-frame code."; } LOG_DEBUG("[PerFrameContext] Failed per-frame code:\n" + perFrameCode); - throw MilkdropCompileException(error); + // Like Milkdrop, only this code block is disabled, the preset is still used. + LOG_WARN(error); + return; } } diff --git a/src/libprojectM/MilkdropPreset/PerFrameContext.hpp b/src/libprojectM/MilkdropPreset/PerFrameContext.hpp index aca786ba88..17f36dce85 100644 --- a/src/libprojectM/MilkdropPreset/PerFrameContext.hpp +++ b/src/libprojectM/MilkdropPreset/PerFrameContext.hpp @@ -46,14 +46,14 @@ class PerFrameContext /** * @brief Compiles and runs the preset init code. - * @throws MilkdropCompileException Thrown if the per-frame init code couldn't be compiled. + * If the per-frame init code can't be compiled, a warning is logged and the code is skipped, like in Milkdrop. * @param perFrameInitCode The init code. */ void EvaluateInitCode(PresetState& state); /** * @brief Compiles the per-frame code and stores the code handle in the class. - * @throws MilkdropCompileException Thrown if the per-frame code couldn't be compiled. + * If the per-frame code can't be compiled, a warning is logged and the code is skipped, like in Milkdrop. * @param perFrameCode The code to compile. */ void CompilePerFrameCode(const std::string& perFrameCode); diff --git a/src/libprojectM/MilkdropPreset/PerPixelContext.cpp b/src/libprojectM/MilkdropPreset/PerPixelContext.cpp index 2942a7f2fe..16bab2d2cb 100644 --- a/src/libprojectM/MilkdropPreset/PerPixelContext.cpp +++ b/src/libprojectM/MilkdropPreset/PerPixelContext.cpp @@ -124,7 +124,9 @@ void PerPixelContext::CompilePerPixelCode(const std::string& perPixelCode) error = "[PerPixelContext] Could not compile per-pixel code."; } LOG_DEBUG("[PerPixelContext] Failed per-pixel code:\n" + perPixelCode); - throw MilkdropCompileException(error); + // Like Milkdrop, only this code block is disabled, the preset is still used. + LOG_WARN(error); + return; } } diff --git a/src/libprojectM/MilkdropPreset/PerPixelContext.hpp b/src/libprojectM/MilkdropPreset/PerPixelContext.hpp index e20acadb21..2c9e167bfc 100644 --- a/src/libprojectM/MilkdropPreset/PerPixelContext.hpp +++ b/src/libprojectM/MilkdropPreset/PerPixelContext.hpp @@ -48,7 +48,7 @@ class PerPixelContext /** * @brief Compiles the per-pixel code and stores the code handle in the class. - * @throws MilkdropCompileException Thrown if the per-pixel code couldn't be compiled. + * If the per-pixel code can't be compiled, a warning is logged and the code is skipped, like in Milkdrop. * @param perPixelCode The code to compile. */ void CompilePerPixelCode(const std::string& perPixelCode); diff --git a/src/libprojectM/MilkdropPreset/ShapePerFrameContext.cpp b/src/libprojectM/MilkdropPreset/ShapePerFrameContext.cpp index e31f5332db..e86c5e9386 100644 --- a/src/libprojectM/MilkdropPreset/ShapePerFrameContext.cpp +++ b/src/libprojectM/MilkdropPreset/ShapePerFrameContext.cpp @@ -160,7 +160,13 @@ void ShapePerFrameContext::EvaluateInitCode(const std::string& perFrameInitCode, error = "[ShapePerFrameContext] Could not compile custom shape " + std::to_string(shape.m_index) + " per-frame init code."; } LOG_DEBUG("[ShapePerFrameContext] Failed custom shape per-frame INIT code:\n" + perFrameInitCode); - throw MilkdropCompileException(error); + // Like Milkdrop, only this code block is disabled, the preset is still used. + LOG_WARN(error); + for (auto* tVar : t_vars) + { + *tVar = 0; + } + return; } projectm_eval_code_execute(initCode); @@ -195,7 +201,9 @@ void ShapePerFrameContext::CompilePerFrameCode(const std::string& perFrameCode, error = "[ShapePerFrameContext] Could not compile custom shape " + std::to_string(shape.m_index) + " per-frame code."; } LOG_DEBUG("[ShapePerFrameContext] Failed custom shape per-frame code:\n" + perFrameCode); - throw MilkdropCompileException(error); + // Like Milkdrop, only this code block is disabled, the preset is still used. + LOG_WARN(error); + return; } } diff --git a/src/libprojectM/MilkdropPreset/ShapePerFrameContext.hpp b/src/libprojectM/MilkdropPreset/ShapePerFrameContext.hpp index f52095ebe7..e047f8ef31 100644 --- a/src/libprojectM/MilkdropPreset/ShapePerFrameContext.hpp +++ b/src/libprojectM/MilkdropPreset/ShapePerFrameContext.hpp @@ -41,7 +41,7 @@ class ShapePerFrameContext /** * @brief Compiles and runs the preset init code. - * @throws MilkdropCompileException Thrown if one of the custom shape init code couldn't be compiled. + * If one of the custom shape init code can't be compiled, a warning is logged and the code is skipped, like in Milkdrop. * @param perFrameInitCode The init code. * @param shape The shape this context belongs to. */ @@ -49,7 +49,7 @@ class ShapePerFrameContext /** * @brief Compiles the per-frame code and stores the code handle in the class. - * @throws MilkdropCompileException Thrown if one of the per-frame code couldn't be compiled. + * If one of the per-frame code can't be compiled, a warning is logged and the code is skipped, like in Milkdrop. * @param perFrameCode The code to compile. * @param shape The shape this context belongs to. */ diff --git a/src/libprojectM/MilkdropPreset/WaveformPerFrameContext.cpp b/src/libprojectM/MilkdropPreset/WaveformPerFrameContext.cpp index c80a006474..0b9e35540e 100644 --- a/src/libprojectM/MilkdropPreset/WaveformPerFrameContext.cpp +++ b/src/libprojectM/MilkdropPreset/WaveformPerFrameContext.cpp @@ -122,7 +122,13 @@ void WaveformPerFrameContext::EvaluateInitCode(const std::string& perFrameInitCo error = "[WaveformPerFrameContext] Could not compile custom wave " + std::to_string(waveform.m_index) + " per-frame init code."; } LOG_DEBUG("[WaveformPerFrameContext] Failed custom wave per-frame INIT code:\n" + perFrameInitCode); - throw MilkdropCompileException(error); + // Like Milkdrop, only this code block is disabled, the preset is still used. + LOG_WARN(error); + for (auto* tVar : t_vars) + { + *tVar = 0; + } + return; } projectm_eval_code_execute(initCode); @@ -157,7 +163,9 @@ void WaveformPerFrameContext::CompilePerFrameCode(const std::string& perFrameCod error = "[WaveformPerFrameContext] Could not compile custom wave " + std::to_string(waveform.m_index) + " per-frame code."; } LOG_DEBUG("[WaveformPerFrameContext] Failed custom wave per-frame code:\n" + perFrameCode); - throw MilkdropCompileException(error); + // Like Milkdrop, only this code block is disabled, the preset is still used. + LOG_WARN(error); + return; } } diff --git a/src/libprojectM/MilkdropPreset/WaveformPerFrameContext.hpp b/src/libprojectM/MilkdropPreset/WaveformPerFrameContext.hpp index 9993f042da..658004f674 100644 --- a/src/libprojectM/MilkdropPreset/WaveformPerFrameContext.hpp +++ b/src/libprojectM/MilkdropPreset/WaveformPerFrameContext.hpp @@ -35,7 +35,7 @@ class WaveformPerFrameContext /** * @brief Compiles and runs the preset init code. - * @throws MilkdropCompileException Thrown if the custom wave init code couldn't be compiled. + * If the custom wave init code can't be compiled, a warning is logged and the code is skipped, like in Milkdrop. * @param perFrameInitCode The init code. * @param waveform The waveform this context belongs to. */ @@ -43,7 +43,7 @@ class WaveformPerFrameContext /** * @brief Compiles the per-frame code and stores the code handle in the class. - * @throws MilkdropCompileException Thrown if the custom wave per-frame code couldn't be compiled. + * If the custom wave per-frame code can't be compiled, a warning is logged and the code is skipped, like in Milkdrop. * @param perFrameCode The code to compile. * @param waveform The waveform this context belongs to. */ diff --git a/src/libprojectM/MilkdropPreset/WaveformPerPointContext.cpp b/src/libprojectM/MilkdropPreset/WaveformPerPointContext.cpp index e73fed4aec..71bac492c3 100644 --- a/src/libprojectM/MilkdropPreset/WaveformPerPointContext.cpp +++ b/src/libprojectM/MilkdropPreset/WaveformPerPointContext.cpp @@ -110,7 +110,9 @@ void WaveformPerPointContext::CompilePerPointCode(const std::string& perPointCod error = "[WaveformPerPointContext] Could not compile custom wave " + std::to_string(waveform.m_index) + " per-point code."; } LOG_DEBUG("[WaveformPerPointContext] Failed custom wave " + std::to_string(waveform.m_index) + " per-point code:\n" + perPointCode); - throw MilkdropCompileException(error); + // Like Milkdrop, only this code block is disabled, the preset is still used. + LOG_WARN(error); + return; } } diff --git a/tests/libprojectM/CMakeLists.txt b/tests/libprojectM/CMakeLists.txt index 29df19fcf3..044e39b55b 100644 --- a/tests/libprojectM/CMakeLists.txt +++ b/tests/libprojectM/CMakeLists.txt @@ -30,6 +30,7 @@ add_executable(projectM-unittest HLSLParserTest.cpp HLSLModuloTest.cpp LoggingTest.cpp + EquationCompileErrorTest.cpp MilkdropShaderCommentParsingTest.cpp PCMTest.cpp PresetFileParserTest.cpp @@ -48,6 +49,7 @@ add_executable(projectM-unittest target_compile_definitions(projectM-unittest PRIVATE PROJECTM_TEST_DATA_DIR="${CMAKE_CURRENT_LIST_DIR}/data" + $ ) # Test includes a header file from libprojectM with its full path in the source dir. @@ -56,6 +58,8 @@ target_include_directories(projectM-unittest "${PROJECTM_SOURCE_DIR}/src/libprojectM" "${PROJECTM_SOURCE_DIR}" "${PROJECTM_SOURCE_DIR}/vendor/hlslparser/src" + # For tests using the expression contexts directly. + $ ) target_link_libraries(projectM-unittest diff --git a/tests/libprojectM/EquationCompileErrorTest.cpp b/tests/libprojectM/EquationCompileErrorTest.cpp new file mode 100644 index 0000000000..576b214512 --- /dev/null +++ b/tests/libprojectM/EquationCompileErrorTest.cpp @@ -0,0 +1,57 @@ +#include + +#include + +#include + +using libprojectM::MilkdropPreset::PerFrameContext; + +namespace { + +/** + * PresetState needs an OpenGL context, so the expression context is set up with its own memory here. + */ +class EquationCompileErrorTest : public testing::Test +{ +protected: + void SetUp() override + { + memory = projectm_eval_memory_buffer_create(); + context = std::make_unique(memory, ®isters); + context->RegisterBuiltinVariables(); + } + + void TearDown() override + { + context.reset(); + projectm_eval_memory_buffer_destroy(memory); + } + + projectm_eval_mem_buffer memory{}; + PRJM_EVAL_F registers[100]{}; + std::unique_ptr context; +}; + +} // namespace + +/** + * Like Milkdrop, a code block that can't be compiled is skipped, and the rest of the preset is still used. + */ +TEST_F(EquationCompileErrorTest, InvalidPerFrameCodeIsSkipped) +{ + // "0.99l" isn't a valid number, so this doesn't compile. + EXPECT_NO_THROW(context->CompilePerFrameCode("additive = above(sin(time * 200), 0.99l);")); + EXPECT_EQ(context->perFrameCodeHandle, nullptr); + + // Executing the skipped code does nothing. + EXPECT_NO_THROW(context->ExecutePerFrameCode()); +} + +TEST_F(EquationCompileErrorTest, ValidPerFrameCodeIsCompiledAndExecuted) +{ + context->CompilePerFrameCode("zoom = 1.25;"); + ASSERT_NE(context->perFrameCodeHandle, nullptr); + + context->ExecutePerFrameCode(); + EXPECT_EQ(*context->zoom, 1.25); +}