Эх сурвалжийг харах

fixed several todos, wayland driver no longer blocks while waiting for compositor events, added real engine audio response, effects that start hidden now get their passes built, TEMP-DIAG cleanup

UwU 5 өдөр өмнө
parent
commit
9c6a5fd978

+ 8 - 0
src/WallpaperEngine/Application/WallpaperApplication.cpp

@@ -2174,6 +2174,14 @@ void WallpaperApplication::render () {
 	m_videoDriver->getInputContext ().update ();
 	m_videoDriver->getInputContext ().update ();
 	m_videoDriver->dispatchEventQueue ();
 	m_videoDriver->dispatchEventQueue ();
 
 
+	// read every frame, --fps can change with a hotswap
+	const float minimumTime = 1.0f / std::max (1, this->m_context.settings.render.maximumFPS);
+	const float frameTime = m_videoDriver->getRenderTime () - rawTimeNow;
+
+	if (frameTime < minimumTime) {
+	    std::this_thread::sleep_for (std::chrono::duration<float> (minimumTime - frameTime));
+	}
+
 	if (m_videoDriver->closeRequested ()) {
 	if (m_videoDriver->closeRequested ()) {
 	    sLog.out ("Stop requested by driver");
 	    sLog.out ("Stop requested by driver");
 	    this->m_context.state.general.keepRunning = false;
 	    this->m_context.state.general.keepRunning = false;

+ 73 - 11
src/WallpaperEngine/Audio/Drivers/Recorders/PulseAudioPlaybackRecorder.cpp

@@ -1,5 +1,6 @@
 #include "PulseAudioPlaybackRecorder.h"
 #include "PulseAudioPlaybackRecorder.h"
 #include "WallpaperEngine/Logging/Log.h"
 #include "WallpaperEngine/Logging/Log.h"
+#include <pulse/rtclock.h>
 #include <chrono>
 #include <chrono>
 #include <cmath>
 #include <cmath>
 #include <cstring>
 #include <cstring>
@@ -24,12 +25,55 @@ std::string wallClockTimestamp () {
 }
 }
 } // namespace
 } // namespace
 
 
-void pa_stream_notify_cb (pa_stream* stream, void* /*userdata*/) {
+void pa_server_info_cb (pa_context* ctx, const pa_server_info* info, void* userdata);
+
+void pa_retry_capture_cb (pa_mainloop_api* api, pa_time_event* event, const struct timeval* /*tv*/, void* userdata) {
+    auto* recorder = static_cast<PulseAudioPlaybackRecorder::PulseAudioData*> (userdata);
+
+    api->time_free (event);
+
+    if (recorder->captureStream == nullptr) {
+	return;
+    }
+
+    pa_context* ctx = pa_stream_get_context (recorder->captureStream);
+
+    if (pa_context_get_state (ctx) != PA_CONTEXT_READY) {
+	return;
+    }
+
+    if (pa_operation* o = pa_context_get_server_info (ctx, &pa_server_info_cb, userdata)) {
+	pa_operation_unref (o);
+    }
+}
+
+void pa_stream_notify_cb (pa_stream* stream, void* userdata) {
+    auto* recorder = static_cast<PulseAudioPlaybackRecorder::PulseAudioData*> (userdata);
+
     switch (pa_stream_get_state (stream)) {
     switch (pa_stream_get_state (stream)) {
 	case PA_STREAM_FAILED:
 	case PA_STREAM_FAILED:
-	    sLog.error ("Cannot open stream for capture. Audio processing is disabled");
+	case PA_STREAM_TERMINATED:
+	    if (stream != recorder->captureStream) {
+		break;
+	    }
+
+	    // the server can drop the monitor stream while sinks change state (a hotswap starting or stopping
+	    // sounds) and nothing else may follow to re-take it, so retry on our own
+	    if (!recorder->captureLost) {
+		recorder->captureLost = true;
+		sLog.error ("Audio capture stream lost, retrying every second");
+	    }
+
+	    pa_context_rttime_new (
+		pa_stream_get_context (stream), pa_rtclock_now () + PA_USEC_PER_SEC, &pa_retry_capture_cb, userdata
+	    );
 	    break;
 	    break;
 	case PA_STREAM_READY:
 	case PA_STREAM_READY:
+	    if (recorder->captureLost) {
+		recorder->captureLost = false;
+		sLog.out ("Audio capture stream restored");
+	    }
+
 	    sLog.debug ("[", wallClockTimestamp (), "] Audio processing: capture stream ready");
 	    sLog.debug ("[", wallClockTimestamp (), "] Audio processing: capture stream ready");
 	    break;
 	    break;
 	default:
 	default:
@@ -102,29 +146,42 @@ void pa_stream_read_cb (pa_stream* stream, const size_t /*nbytes*/, void* userda
 }
 }
 
 
 void pa_server_info_cb (pa_context* ctx, const pa_server_info* info, void* userdata) {
 void pa_server_info_cb (pa_context* ctx, const pa_server_info* info, void* userdata) {
-    if (info == nullptr) {
+    if (info == nullptr || info->default_sink_name == nullptr) {
 	return;
 	return;
     }
     }
 
 
     auto* recorder = static_cast<PulseAudioPlaybackRecorder::PulseAudioData*> (userdata);
     auto* recorder = static_cast<PulseAudioPlaybackRecorder::PulseAudioData*> (userdata);
+    const std::string monitor_name = std::string (info->default_sink_name) + ".monitor";
+
+    if (recorder->captureStream) {
+	// sink/source events also fire for volume and state changes (another wallpaper starting its sounds),
+	// only re-take the stream when the default sink moved or the old one died
+	const bool alive = PA_STREAM_IS_GOOD (pa_stream_get_state (recorder->captureStream));
+
+	if (alive && monitor_name == recorder->monitorName) {
+	    return;
+	}
+
+	// the context keeps a connected stream alive, unref alone left it feeding the same buffer
+	pa_stream_set_state_callback (recorder->captureStream, nullptr, nullptr);
+	pa_stream_set_read_callback (recorder->captureStream, nullptr, nullptr);
+	pa_stream_disconnect (recorder->captureStream);
+	pa_stream_unref (recorder->captureStream);
+	recorder->captureStream = nullptr;
+    }
+
+    recorder->monitorName = monitor_name;
 
 
     pa_sample_spec spec;
     pa_sample_spec spec;
     spec.format = PA_SAMPLE_U8;
     spec.format = PA_SAMPLE_U8;
     spec.rate = 44100;
     spec.rate = 44100;
     spec.channels = 1;
     spec.channels = 1;
 
 
-    if (recorder->captureStream) {
-	pa_stream_unref (recorder->captureStream);
-    }
-
     recorder->captureStream = pa_stream_new (ctx, "output monitor", &spec, nullptr);
     recorder->captureStream = pa_stream_new (ctx, "output monitor", &spec, nullptr);
 
 
     pa_stream_set_state_callback (recorder->captureStream, &pa_stream_notify_cb, userdata);
     pa_stream_set_state_callback (recorder->captureStream, &pa_stream_notify_cb, userdata);
     pa_stream_set_read_callback (recorder->captureStream, &pa_stream_read_cb, userdata);
     pa_stream_set_read_callback (recorder->captureStream, &pa_stream_read_cb, userdata);
 
 
-    std::string monitor_name (info->default_sink_name);
-    monitor_name += ".monitor";
-
     pa_buffer_attr attr {};
     pa_buffer_attr attr {};
 
 
     // 10 = latency msecs, 750 = max msecs to store
     // 10 = latency msecs, 750 = max msecs to store
@@ -154,7 +211,10 @@ void pa_context_notify_cb (pa_context* ctx, void* userdata) {
 	    {
 	    {
 		pa_context_set_subscribe_callback (ctx, pa_context_subscribe_cb, userdata);
 		pa_context_set_subscribe_callback (ctx, pa_context_subscribe_cb, userdata);
 		pa_operation* o = pa_context_subscribe (
 		pa_operation* o = pa_context_subscribe (
-		    ctx, static_cast<pa_subscription_mask_t> (PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE),
+		    ctx,
+		    static_cast<pa_subscription_mask_t> (
+			PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE | PA_SUBSCRIPTION_MASK_SERVER
+		    ),
 		    nullptr, nullptr
 		    nullptr, nullptr
 		);
 		);
 
 
@@ -222,6 +282,8 @@ PulseAudioPlaybackRecorder::~PulseAudioPlaybackRecorder () {
     }
     }
 
 
     if (m_captureData.captureStream) {
     if (m_captureData.captureStream) {
+	pa_stream_set_state_callback (m_captureData.captureStream, nullptr, nullptr);
+	pa_stream_disconnect (m_captureData.captureStream);
 	pa_stream_unref (m_captureData.captureStream);
 	pa_stream_unref (m_captureData.captureStream);
     }
     }
 
 

+ 3 - 0
src/WallpaperEngine/Audio/Drivers/Recorders/PulseAudioPlaybackRecorder.h

@@ -6,6 +6,7 @@
 #include <SDL.h>
 #include <SDL.h>
 #include <atomic>
 #include <atomic>
 #include <chrono>
 #include <chrono>
+#include <string>
 #include <pulse/pulseaudio.h>
 #include <pulse/pulseaudio.h>
 
 
 #define WAVE_BUFFER_SIZE 1024
 #define WAVE_BUFFER_SIZE 1024
@@ -25,6 +26,8 @@ public:
 	size_t currentWritePointer;
 	size_t currentWritePointer;
 	bool fullFrameReady;
 	bool fullFrameReady;
 	pa_stream* captureStream;
 	pa_stream* captureStream;
+	std::string monitorName;
+	bool captureLost;
     };
     };
 
 
     PulseAudioPlaybackRecorder ();
     PulseAudioPlaybackRecorder ();

+ 11 - 3
src/WallpaperEngine/Data/Model/Object.h

@@ -149,7 +149,7 @@ struct ParticleEmitter {
     float delay;
     float delay;
     float duration;
     float duration;
     glm::vec2 audioProcessingBounds;
     glm::vec2 audioProcessingBounds;
-    int audioProcessingExponent;
+    float audioProcessingExponent;
     int audioProcessingFrequencyStart;
     int audioProcessingFrequencyStart;
     int audioProcessingFrequencyEnd;
     int audioProcessingFrequencyEnd;
     int audioProcessingMode;
     int audioProcessingMode;
@@ -374,7 +374,9 @@ public:
 	UserSettingUniquePtr distanceInner, UserSettingUniquePtr distanceOuter, UserSettingUniquePtr speedInner,
 	UserSettingUniquePtr distanceInner, UserSettingUniquePtr distanceOuter, UserSettingUniquePtr speedInner,
 	UserSettingUniquePtr speedOuter, UserSettingUniquePtr centerForce, UserSettingUniquePtr ringRadius,
 	UserSettingUniquePtr speedOuter, UserSettingUniquePtr centerForce, UserSettingUniquePtr ringRadius,
 	UserSettingUniquePtr ringWidth, UserSettingUniquePtr ringPullDistance, UserSettingUniquePtr ringPullForce,
 	UserSettingUniquePtr ringWidth, UserSettingUniquePtr ringPullDistance, UserSettingUniquePtr ringPullForce,
-	UserSettingUniquePtr audioProcessingMode, UserSettingUniquePtr audioProcessingBounds
+	UserSettingUniquePtr audioProcessingMode, UserSettingUniquePtr audioProcessingBounds,
+	UserSettingUniquePtr audioProcessingExponent, UserSettingUniquePtr audioProcessingFrequencyStart,
+	UserSettingUniquePtr audioProcessingFrequencyEnd
     ) :
     ) :
 	controlPoint (controlPoint), flags (flags), axis (std::move (axis)), offset (std::move (offset)),
 	controlPoint (controlPoint), flags (flags), axis (std::move (axis)), offset (std::move (offset)),
 	distanceInner (std::move (distanceInner)), distanceOuter (std::move (distanceOuter)),
 	distanceInner (std::move (distanceInner)), distanceOuter (std::move (distanceOuter)),
@@ -382,7 +384,10 @@ public:
 	ringRadius (std::move (ringRadius)), ringWidth (std::move (ringWidth)),
 	ringRadius (std::move (ringRadius)), ringWidth (std::move (ringWidth)),
 	ringPullDistance (std::move (ringPullDistance)), ringPullForce (std::move (ringPullForce)),
 	ringPullDistance (std::move (ringPullDistance)), ringPullForce (std::move (ringPullForce)),
 	audioProcessingMode (std::move (audioProcessingMode)),
 	audioProcessingMode (std::move (audioProcessingMode)),
-	audioProcessingBounds (std::move (audioProcessingBounds)) { }
+	audioProcessingBounds (std::move (audioProcessingBounds)),
+	audioProcessingExponent (std::move (audioProcessingExponent)),
+	audioProcessingFrequencyStart (std::move (audioProcessingFrequencyStart)),
+	audioProcessingFrequencyEnd (std::move (audioProcessingFrequencyEnd)) { }
     int controlPoint;
     int controlPoint;
     int flags; // 1 = infinite axis, 2 = maintain distance to center, 4 = ring shape
     int flags; // 1 = infinite axis, 2 = maintain distance to center, 4 = ring shape
     UserSettingUniquePtr axis;
     UserSettingUniquePtr axis;
@@ -398,6 +403,9 @@ public:
     UserSettingUniquePtr ringPullForce; // Ring mode: strength of ring attraction
     UserSettingUniquePtr ringPullForce; // Ring mode: strength of ring attraction
     UserSettingUniquePtr audioProcessingMode;
     UserSettingUniquePtr audioProcessingMode;
     UserSettingUniquePtr audioProcessingBounds;
     UserSettingUniquePtr audioProcessingBounds;
+    UserSettingUniquePtr audioProcessingExponent;
+    UserSettingUniquePtr audioProcessingFrequencyStart;
+    UserSettingUniquePtr audioProcessingFrequencyEnd;
 };
 };
 
 
 class ControlPointAttractOperator : public ParticleOperatorBase {
 class ControlPointAttractOperator : public ParticleOperatorBase {

+ 8 - 5
src/WallpaperEngine/Data/Parsers/ObjectParser.cpp

@@ -611,7 +611,7 @@ ParticleEmitter ObjectParser::parseParticleEmitter (const JSON& it) {
 	    .delay = it.optional ("delay", 0.0f),
 	    .delay = it.optional ("delay", 0.0f),
 	    .duration = it.optional ("duration", 0.0f),
 	    .duration = it.optional ("duration", 0.0f),
 	    .audioProcessingBounds = parseVec2 ("audioprocessingbounds", glm::vec2 (0.8f, 1.0f)),
 	    .audioProcessingBounds = parseVec2 ("audioprocessingbounds", glm::vec2 (0.8f, 1.0f)),
-	    .audioProcessingExponent = it.optional ("audioprocessingexponent", 2),
+	    .audioProcessingExponent = it.optional ("audioprocessingexponent", 2.0f),
 	    .audioProcessingFrequencyStart = it.optional ("audioprocessingfrequencystart", 0),
 	    .audioProcessingFrequencyStart = it.optional ("audioprocessingfrequencystart", 0),
 	    .audioProcessingFrequencyEnd = it.optional ("audioprocessingfrequencyend", 1),
 	    .audioProcessingFrequencyEnd = it.optional ("audioprocessingfrequencyend", 1),
 	    .audioProcessingMode = it.optional ("audioprocessingmode", 0),
 	    .audioProcessingMode = it.optional ("audioprocessingmode", 0),
@@ -716,10 +716,10 @@ ParticleOperatorUniquePtr ObjectParser::parseParticleOperator (const JSON& it, c
 	    it.user ("speedmax", properties, 1000.0f), it.user ("timescale", properties, 0.01f),
 	    it.user ("speedmax", properties, 1000.0f), it.user ("timescale", properties, 0.01f),
 	    it.user ("mask", properties, glm::vec3 (1.0f, 1.0f, 0.0f)), it.user ("phasemin", properties, 0.0f),
 	    it.user ("mask", properties, glm::vec3 (1.0f, 1.0f, 0.0f)), it.user ("phasemin", properties, 0.0f),
 	    it.user ("phasemax", properties, 0.0f), it.user ("audioprocessingmode", properties, 0),
 	    it.user ("phasemax", properties, 0.0f), it.user ("audioprocessingmode", properties, 0),
-	    it.user ("audioprocessingbounds", properties, glm::vec2 (0.0f, 1.0f)),
-	    it.user ("audioprocessingexponent", properties, 1.0f),
+	    it.user ("audioprocessingbounds", properties, glm::vec2 (0.8f, 1.0f)),
+	    it.user ("audioprocessingexponent", properties, 2.0f),
 	    it.user ("audioprocessingfrequencystart", properties, 0),
 	    it.user ("audioprocessingfrequencystart", properties, 0),
-	    it.user ("audioprocessingfrequencyend", properties, 15)
+	    it.user ("audioprocessingfrequencyend", properties, 1)
 	);
 	);
     } else if (name == "vortex" || name == "vortex_v2") {
     } else if (name == "vortex" || name == "vortex_v2") {
 	return std::make_unique<VortexOperator> (
 	return std::make_unique<VortexOperator> (
@@ -732,7 +732,10 @@ ParticleOperatorUniquePtr ObjectParser::parseParticleOperator (const JSON& it, c
 	    it.user ("ringradius", properties, 300.0f), it.user ("ringwidth", properties, 50.0f),
 	    it.user ("ringradius", properties, 300.0f), it.user ("ringwidth", properties, 50.0f),
 	    it.user ("ringpulldistance", properties, 50.0f), it.user ("ringpullforce", properties, 10.0f),
 	    it.user ("ringpulldistance", properties, 50.0f), it.user ("ringpullforce", properties, 10.0f),
 	    it.user ("audioprocessingmode", properties, 0),
 	    it.user ("audioprocessingmode", properties, 0),
-	    it.user ("audioprocessingbounds", properties, glm::vec2 (0.0f, 1.0f))
+	    it.user ("audioprocessingbounds", properties, glm::vec2 (0.8f, 1.0f)),
+	    it.user ("audioprocessingexponent", properties, 2.0f),
+	    it.user ("audioprocessingfrequencystart", properties, 0),
+	    it.user ("audioprocessingfrequencyend", properties, 1)
 	);
 	);
     } else if (name == "controlpointattract") {
     } else if (name == "controlpointattract") {
 	return std::make_unique<ControlPointAttractOperator> (
 	return std::make_unique<ControlPointAttractOperator> (

+ 0 - 13
src/WallpaperEngine/Render/Drivers/GLFWOpenGLDriver.cpp

@@ -15,8 +15,6 @@
 #include <cstdlib>
 #include <cstdlib>
 #include <unistd.h>
 #include <unistd.h>
 
 
-#include <algorithm>
-
 using namespace WallpaperEngine::Render::Drivers;
 using namespace WallpaperEngine::Render::Drivers;
 
 
 void CustomGLFWErrorHandler (int errorCode, const char* reason) { sLog.error ("GLFW error ", errorCode, ": ", reason); }
 void CustomGLFWErrorHandler (int errorCode, const char* reason) { sLog.error ("GLFW error ", errorCode, ": ", reason); }
@@ -108,10 +106,6 @@ glm::ivec2 GLFWOpenGLDriver::getFramebufferSize () const {
 uint32_t GLFWOpenGLDriver::getFrameCounter () const { return this->m_frameCounter; }
 uint32_t GLFWOpenGLDriver::getFrameCounter () const { return this->m_frameCounter; }
 
 
 void GLFWOpenGLDriver::dispatchEventQueue () {
 void GLFWOpenGLDriver::dispatchEventQueue () {
-    static float startTime, endTime;
-    // read every frame, --fps can change with a hotswap
-    const float minimumTime = 1.0f / std::max (1, this->m_context.settings.render.maximumFPS);
-    startTime = this->getRenderTime ();
     glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
     glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
 
     for (const auto& [screen, viewport] : this->m_output->getViewports ()) {
     for (const auto& [screen, viewport] : this->m_output->getViewports ()) {
@@ -139,17 +133,10 @@ void GLFWOpenGLDriver::dispatchEventQueue () {
 	}
 	}
     }
     }
 
 
-    // TODO: frametime control should go back to CWallpaperApplication once actual particles are
-    // implemented, as those will likely require a different processing rate
     this->m_output->updateRender ();
     this->m_output->updateRender ();
     glfwSwapBuffers (this->m_window);
     glfwSwapBuffers (this->m_window);
     glfwPollEvents ();
     glfwPollEvents ();
     this->m_frameCounter++;
     this->m_frameCounter++;
-    endTime = this->getRenderTime ();
-
-    if ((endTime - startTime) < minimumTime) {
-	usleep ((minimumTime - (endTime - startTime)) * CLOCKS_PER_SEC);
-    }
 }
 }
 
 
 void* GLFWOpenGLDriver::getProcAddress (const char* name) const {
 void* GLFWOpenGLDriver::getProcAddress (const char* name) const {

+ 32 - 14
src/WallpaperEngine/Render/Drivers/WaylandOpenGLDriver.cpp

@@ -20,11 +20,15 @@ extern "C" {
 #undef static
 #undef static
 
 
 #include <algorithm>
 #include <algorithm>
+#include <cerrno>
+#include <poll.h>
 #include <string.h>
 #include <string.h>
 #include <unistd.h>
 #include <unistd.h>
 
 
 using namespace WallpaperEngine::Render::Drivers;
 using namespace WallpaperEngine::Render::Drivers;
 
 
+constexpr int EVENT_WAIT_TIMEOUT_MS = 100;
+
 static void handlePointerEnter (
 static void handlePointerEnter (
     void* data, struct wl_pointer* wl_pointer, uint32_t serial, struct wl_surface* surface, wl_fixed_t surface_x,
     void* data, struct wl_pointer* wl_pointer, uint32_t serial, struct wl_surface* surface, wl_fixed_t surface_x,
     wl_fixed_t surface_y
     wl_fixed_t surface_y
@@ -440,26 +444,40 @@ void WaylandOpenGLDriver::dispatchEventQueue () {
 	}
 	}
     }
     }
 
 
-    // TODO: frametime control should go back to CWallpaperApplication once actual particles are
-    // implemented, as those will likely require a different processing rate
-    // TODO: write a non-blocking version of this once particle simulation starts working, otherwise
-    // wl_display_dispatch will block if no surfaces are being drawn
-    static float startTime, endTime;
-    // read every frame, --fps can change with a hotswap
-    const float minimumTime = 1.0f / std::max (1, this->m_context.settings.render.maximumFPS);
-    startTime = this->getRenderTime ();
+    // the compositor stops sending frame callbacks to hidden surfaces, a blocking dispatch would stall the main loop
+    wl_display* display = m_waylandContext.display;
 
 
-    if (wl_display_dispatch (m_waylandContext.display) == -1) {
-	m_requestedExit = true;
+    while (wl_display_prepare_read (display) != 0) {
+	if (wl_display_dispatch_pending (display) == -1) {
+	    m_requestedExit = true;
+	    return;
+	}
     }
     }
 
 
-    m_frameCounter++;
+    wl_display_flush (display);
+
+    pollfd fd = { .fd = wl_display_get_fd (display), .events = POLLIN, .revents = 0 };
+    const int ready = poll (&fd, 1, EVENT_WAIT_TIMEOUT_MS);
+
+    if (ready > 0) {
+	if (wl_display_read_events (display) == -1) {
+	    m_requestedExit = true;
+	    return;
+	}
+    } else {
+	wl_display_cancel_read (display);
 
 
-    endTime = this->getRenderTime ();
+	if (ready == -1 && errno != EINTR) {
+	    m_requestedExit = true;
+	    return;
+	}
+    }
 
 
-    if ((endTime - startTime) < minimumTime) {
-	usleep ((minimumTime - (endTime - startTime)) * CLOCKS_PER_SEC);
+    if (wl_display_dispatch_pending (display) == -1) {
+	m_requestedExit = true;
     }
     }
+
+    m_frameCounter++;
 }
 }
 
 
 Output::Output& WaylandOpenGLDriver::getOutput () { return this->m_output; }
 Output::Output& WaylandOpenGLDriver::getOutput () { return this->m_output; }

+ 178 - 256
src/WallpaperEngine/Render/Objects/CImage.cpp

@@ -283,8 +283,7 @@ std::optional<double> scorePuppetMeshCoherence (const PuppetMeshData& data) {
 // plausible stride is tried against every candidate mesh header found in the file, and whichever
 // plausible stride is tried against every candidate mesh header found in the file, and whichever
 // combination produces the most coherent triangulated mesh wins.
 // combination produces the most coherent triangulated mesh wins.
 std::optional<PuppetVertexLayout> resolvePuppetVertexLayout (
 std::optional<PuppetVertexLayout> resolvePuppetVertexLayout (
-    const BinaryReader& reader, size_t markerSize, size_t mdlsOffset, size_t meshHeaderSize,
-    const std::string& debugName
+    const BinaryReader& reader, size_t markerSize, size_t mdlsOffset, size_t meshHeaderSize
 ) {
 ) {
     constexpr size_t minVertexStride = 20; // position (12 bytes) + uv (8 bytes), no bone data at all
     constexpr size_t minVertexStride = 20; // position (12 bytes) + uv (8 bytes), no bone data at all
     constexpr size_t maxVertexStride = 256; // generous upper bound, comfortably covers multi-bone rigs
     constexpr size_t maxVertexStride = 256; // generous upper bound, comfortably covers multi-bone rigs
@@ -295,9 +294,6 @@ std::optional<PuppetVertexLayout> resolvePuppetVertexLayout (
     std::optional<PuppetVertexLayout> best;
     std::optional<PuppetVertexLayout> best;
     double bestScore = std::numeric_limits<double>::max ();
     double bestScore = std::numeric_limits<double>::max ();
 
 
-    // TEMP-DIAG: score of every (block, stride) attempt
-    std::vector<std::tuple<double, size_t, size_t, size_t>> allScored;
-
     for (const auto& block : candidates) {
     for (const auto& block : candidates) {
 	for (size_t stride = minVertexStride; stride <= maxVertexStride; stride += strideStep) {
 	for (size_t stride = minVertexStride; stride <= maxVertexStride; stride += strideStep) {
 	    const auto data = readPuppetMeshData (reader, block, meshHeaderSize, stride);
 	    const auto data = readPuppetMeshData (reader, block, meshHeaderSize, stride);
@@ -310,10 +306,6 @@ std::optional<PuppetVertexLayout> resolvePuppetVertexLayout (
 		continue;
 		continue;
 	    }
 	    }
 
 
-	    if (!debugName.empty ()) {
-		allScored.emplace_back (*score, block.headerOffset, stride, data->positions.size () / 3);
-	    }
-
 	    if (*score >= bestScore) {
 	    if (*score >= bestScore) {
 		continue;
 		continue;
 	    }
 	    }
@@ -323,18 +315,6 @@ std::optional<PuppetVertexLayout> resolvePuppetVertexLayout (
 	}
 	}
     }
     }
 
 
-    if (!debugName.empty ()) {
-	std::sort (allScored.begin (), allScored.end ());
-	sLog.out ("TEMP-DIAG vertex layout candidates for ", debugName, ": total=", allScored.size ());
-	for (size_t i = 0; i < allScored.size () && i < 10; i++) {
-	    const auto& [score, headerOffset, stride, vertexCount] = allScored[i];
-	    sLog.out (
-		"TEMP-DIAG   #", i, " score=", score, " headerOffset=", headerOffset, " stride=", stride,
-		" vertexCount=", vertexCount
-	    );
-	}
-    }
-
     return best;
     return best;
 }
 }
 
 
@@ -577,7 +557,8 @@ std::optional<size_t> findNextPuppetClipHeader (
 
 
 // Parses every baked animation clip out of the MDLA section (see docs/rendering/MDL_FILES.md).
 // Parses every baked animation clip out of the MDLA section (see docs/rendering/MDL_FILES.md).
 std::vector<PuppetAnimationClip> parsePuppetAnimationClips (
 std::vector<PuppetAnimationClip> parsePuppetAnimationClips (
-    const std::vector<char>& data, const BinaryReader& reader, size_t mdlaOffset, uint32_t expectedBoneCount
+    const std::vector<char>& data, const BinaryReader& reader, size_t mdlaOffset, uint32_t expectedBoneCount,
+    bool dumpBone29
 ) {
 ) {
     reader.base ().seekg (static_cast<std::streamoff> (mdlaOffset), std::ios::beg);
     reader.base ().seekg (static_cast<std::streamoff> (mdlaOffset), std::ios::beg);
 
 
@@ -652,7 +633,7 @@ std::vector<PuppetAnimationClip> parsePuppetAnimationClips (
 	    }
 	    }
 
 
 	    // TEMP-DIAG: raw keyframe dump for bone 29
 	    // TEMP-DIAG: raw keyframe dump for bone 29
-	    if (boneIndex == 29 && expectedBoneCount > 29) {
+	    if (dumpBone29 && boneIndex == 29 && expectedBoneCount > 29) {
 		float minRotZ = std::numeric_limits<float>::max (), maxRotZ = std::numeric_limits<float>::lowest ();
 		float minRotZ = std::numeric_limits<float>::max (), maxRotZ = std::numeric_limits<float>::lowest ();
 		float minPosX = std::numeric_limits<float>::max (), maxPosX = std::numeric_limits<float>::lowest ();
 		float minPosX = std::numeric_limits<float>::max (), maxPosX = std::numeric_limits<float>::lowest ();
 		for (const auto& kf : track) {
 		for (const auto& kf : track) {
@@ -837,7 +818,7 @@ CImage::ResolvedTransform CImage::resolveTransform (const Object& object) const
 	local.scale.y *= anchorScale.y;
 	local.scale.y *= anchorScale.y;
 	resolved = { local.origin, local.scale * resolved.scale, local.angle + anchorAngle, meshPivotAngle };
 	resolved = { local.origin, local.scale * resolved.scale, local.angle + anchorAngle, meshPivotAngle };
 
 
-	if ((chain[i]->id == 422 || chain[i]->id == 134) && !this->m_finalOriginLogged.contains (chain[i]->id)) {
+	if (chain[i]->id == 134 && !this->m_finalOriginLogged.contains (chain[i]->id)) {
 	    this->m_finalOriginLogged.insert (chain[i]->id);
 	    this->m_finalOriginLogged.insert (chain[i]->id);
 	    sLog.out (
 	    sLog.out (
 		"TEMP-DIAG final resolved origin for ", chain[i]->name, " (", chain[i]->id, "): anchorOrigin=(",
 		"TEMP-DIAG final resolved origin for ", chain[i]->name, " (", chain[i]->id, "): anchorOrigin=(",
@@ -1127,11 +1108,12 @@ CImage::~CImage () {
     this->m_texture->decrementUsageCount ();
     this->m_texture->decrementUsageCount ();
 
 
     // delete passes first as they depend on the image's data
     // delete passes first as they depend on the image's data
-    for (auto* pass : this->m_passes) {
+    for (auto* pass : this->m_allPasses.empty () ? this->m_passes : this->m_allPasses) {
 	delete pass;
 	delete pass;
     }
     }
 
 
     this->m_passes.clear ();
     this->m_passes.clear ();
+    this->m_allPasses.clear ();
 
 
     glDeleteBuffers (1, &this->m_sceneSpacePosition);
     glDeleteBuffers (1, &this->m_sceneSpacePosition);
     glDeleteBuffers (1, &this->m_copySpacePosition);
     glDeleteBuffers (1, &this->m_copySpacePosition);
@@ -1179,9 +1161,7 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 
 
 	const bool isDiagTarget
 	const bool isDiagTarget
 	    = this->getImage ().name == "bodyhairkochuru" || this->getImage ().name == "spiritblossomahribase";
 	    = this->getImage ().name == "bodyhairkochuru" || this->getImage ().name == "spiritblossomahribase";
-	const auto layout = resolvePuppetVertexLayout (
-	    reader, markerSize, mdlsOffset, meshHeaderSize, isDiagTarget ? this->getImage ().name : ""
-	);
+	const auto layout = resolvePuppetVertexLayout (reader, markerSize, mdlsOffset, meshHeaderSize);
 	if (!layout.has_value ()) {
 	if (!layout.has_value ()) {
 	    sLog.error ("Could not find a usable MDLV mesh block in ", *this->getImage ().model->puppet);
 	    sLog.error ("Could not find a usable MDLV mesh block in ", *this->getImage ().model->puppet);
 	    return false;
 	    return false;
@@ -1318,8 +1298,9 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 
 
 		std::vector<PuppetAnimationClip> clips;
 		std::vector<PuppetAnimationClip> clips;
 		if (mdlaOffsetLooksValid) {
 		if (mdlaOffsetLooksValid) {
-		    clips
-			= parsePuppetAnimationClips (data, reader, mdlaOffset, static_cast<uint32_t> (this->m_puppetBones.size ()));
+		    clips = parsePuppetAnimationClips (
+			data, reader, mdlaOffset, static_cast<uint32_t> (this->m_puppetBones.size ()), isDiagTarget
+		    );
 		} else {
 		} else {
 		    sLog.error (
 		    sLog.error (
 			"Puppet MDLS data for ", *this->getImage ().model->puppet,
 			"Puppet MDLS data for ", *this->getImage ().model->puppet,
@@ -1570,7 +1551,7 @@ void CImage::updatePuppetSkinning () {
 	animatedLocals[i] = local;
 	animatedLocals[i] = local;
 
 
 	// TEMP-DIAG: bones 29/30 logged every frame
 	// TEMP-DIAG: bones 29/30 logged every frame
-	if (!this->m_boneTrackDiagLogged || ((i == 29 || i == 30) && this->getImage ().name == "bodyhairkochuru")) {
+	if ((i == 29 || i == 30) && this->getImage ().name == "bodyhairkochuru") {
 	    sLog.out (
 	    sLog.out (
 		"TEMP-DIAG bone anim for ", this->getImage ().name, " (", this->getId (), ") i=", i, " parent=",
 		"TEMP-DIAG bone anim for ", this->getImage ().name, " (", this->getId (), ") i=", i, " parent=",
 		bone.parent, " bindLocalPos=(", bone.bindLocal[3].x, ",", bone.bindLocal[3].y, ") animatedPos=(",
 		bone.parent, " bindLocalPos=(", bone.bindLocal[3].x, ",", bone.bindLocal[3].y, ") animatedPos=(",
@@ -1581,8 +1562,6 @@ void CImage::updatePuppetSkinning () {
 	}
 	}
     }
     }
 
 
-    this->m_boneTrackDiagLogged = true;
-
     const std::vector<glm::mat4> worldAnimated = composeBoneWorldTransforms (animatedParents, animatedLocals);
     const std::vector<glm::mat4> worldAnimated = composeBoneWorldTransforms (animatedParents, animatedLocals);
 
 
     // attachment points (see getAttachmentPointMeshTransform) need the live bone transforms independently
     // attachment points (see getAttachmentPointMeshTransform) need the live bone transforms independently
@@ -1769,47 +1748,9 @@ void CImage::setupPuppetGeometryCallback (Effects::CPass* pass) const {
 		);
 		);
 	    }
 	    }
 
 
-	    if (this->getId () == 418) {
-		static bool uniformChecked = false;
-		if (!uniformChecked) {
-		    uniformChecked = true;
-		    const GLint loc = glGetUniformLocation (pass->getProgramID (), "g_ModelViewProjectionMatrix");
-		    GLfloat uniformVals[16] = {};
-		    if (loc >= 0) {
-			glGetUniformfv (pass->getProgramID (), loc, uniformVals);
-		    }
-		    sLog.out (
-			"TEMP-DIAG uniform check for koshinibody: programID=", pass->getProgramID (),
-			" mvpLocation=", loc, " uniformCol0=(", uniformVals[0], ",", uniformVals[1], ",",
-			uniformVals[2], ",", uniformVals[3], ") uniformCol3=(", uniformVals[12], ",", uniformVals[13],
-			",", uniformVals[14], ",", uniformVals[15], ")"
-		    );
-		}
-	    }
-
 	    glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, this->m_puppetIndices);
 	    glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, this->m_puppetIndices);
 	    glDrawElements (GL_TRIANGLES, this->m_puppetIndexCount, GL_UNSIGNED_SHORT, nullptr);
 	    glDrawElements (GL_TRIANGLES, this->m_puppetIndexCount, GL_UNSIGNED_SHORT, nullptr);
 
 
-	    {
-		static int liveDiagCounter = 0;
-		if (this->getId () == 418 && (liveDiagCounter++ % 25 == 0)) {
-		    GLfloat liveBuf[9] = {};
-		    glBindBuffer (GL_ARRAY_BUFFER, this->m_puppetSpacePosition);
-		    glGetBufferSubData (GL_ARRAY_BUFFER, 0, sizeof (liveBuf), liveBuf);
-		    const glm::mat4& mvp = this->m_modelViewProjectionScreen;
-		    const glm::vec4 c0 = mvp * glm::vec4 (liveBuf[0], liveBuf[1], liveBuf[2], 1.0f);
-		    const glm::vec4 c1 = mvp * glm::vec4 (liveBuf[3], liveBuf[4], liveBuf[5], 1.0f);
-		    const glm::vec4 c2 = mvp * glm::vec4 (liveBuf[6], liveBuf[7], liveBuf[8], 1.0f);
-		    sLog.out (
-			"TEMP-DIAG live-ndc frame=", liveDiagCounter, " for ", this->getImage ().name, " (",
-			this->getId (), "): buf=[", liveBuf[0], " ", liveBuf[1], " ", liveBuf[2], "|", liveBuf[3], " ",
-			liveBuf[4], " ", liveBuf[5], "|", liveBuf[6], " ", liveBuf[7], " ", liveBuf[8], "] ndc0=(",
-			c0.x / c0.w, ",", c0.y / c0.w, ") ndc1=(", c1.x / c1.w, ",", c1.y / c1.w, ") ndc2=(",
-			c2.x / c2.w, ",", c2.y / c2.w, ")"
-		    );
-		}
-	    }
-
 	    {
 	    {
 		static int mikasaEyeDumpCounter = 0;
 		static int mikasaEyeDumpCounter = 0;
 		if (this->getId () == 603 && mikasaEyeDumpCounter++ == 5) {
 		if (this->getId () == 603 && mikasaEyeDumpCounter++ == 5) {
@@ -1831,34 +1772,6 @@ void CImage::setupPuppetGeometryCallback (Effects::CPass* pass) const {
 		}
 		}
 	    }
 	    }
 
 
-	    {
-		static int dumpCounter = 0;
-		if (this->getId () == 418 && dumpCounter++ == 100) {
-		    const glm::mat4& copyProj = this->m_modelViewProjectionCopy;
-		    sLog.out (
-			"TEMP-DIAG modelViewProjectionCopy for koshinibody: col0=(", copyProj[0][0], ",", copyProj[0][1],
-			",", copyProj[0][2], ",", copyProj[0][3], ") col1=(", copyProj[1][0], ",", copyProj[1][1], ",",
-			copyProj[1][2], ",", copyProj[1][3], ") col3=(", copyProj[3][0], ",", copyProj[3][1], ",",
-			copyProj[3][2], ",", copyProj[3][3], ") m_size=(", this->m_size.x, ",", this->m_size.y, ")"
-		    );
-		    GLint vp[4] = {};
-		    glGetIntegerv (GL_VIEWPORT, vp);
-		    const int w = vp[2], h = vp[3];
-		    if (w > 0 && h > 0 && w < 8192 && h < 8192) {
-			std::vector<unsigned char> pixels (static_cast<size_t> (w) * h * 4);
-			glReadPixels (0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data ());
-			FILE* f = fopen ("/tmp/koshini_fbo_dump.raw", "wb");
-			if (f) {
-			    fwrite (&w, sizeof (int), 1, f);
-			    fwrite (&h, sizeof (int), 1, f);
-			    fwrite (pixels.data (), 1, pixels.size (), f);
-			    fclose (f);
-			    sLog.out ("TEMP-DIAG dumped FBO contents for koshinibody: ", w, "x", h, " to /tmp/koshini_fbo_dump.raw");
-			}
-		    }
-		}
-	    }
-
 	    if (!this->m_puppetDrawErrorChecked) {
 	    if (!this->m_puppetDrawErrorChecked) {
 		this->m_puppetDrawErrorChecked = true;
 		this->m_puppetDrawErrorChecked = true;
 		GLint boundFBO = 0;
 		GLint boundFBO = 0;
@@ -1891,42 +1804,6 @@ void CImage::setupPuppetGeometryCallback (Effects::CPass* pass) const {
 		    " colorMask=(", (int) colorMask[0], ",", (int) colorMask[1], ",", (int) colorMask[2], ",",
 		    " colorMask=(", (int) colorMask[0], ",", (int) colorMask[1], ",", (int) colorMask[2], ",",
 		    (int) colorMask[3], ")"
 		    (int) colorMask[3], ")"
 		);
 		);
-
-		const size_t vertexCount = this->m_puppetRawPositions.size () / 3;
-		std::vector<GLushort> idx (this->m_puppetIndexCount);
-		glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, this->m_puppetIndices);
-		glGetBufferSubData (GL_ELEMENT_ARRAY_BUFFER, 0, idx.size () * sizeof (GLushort), idx.data ());
-
-		std::vector<GLfloat> pos (vertexCount * 3);
-		glBindBuffer (GL_ARRAY_BUFFER, this->m_puppetSpacePosition);
-		glGetBufferSubData (GL_ARRAY_BUFFER, 0, pos.size () * sizeof (GLfloat), pos.data ());
-
-		double totalArea = 0.0, minArea = 1e30, maxArea = 0.0;
-		size_t degenerate = 0, outOfRange = 0;
-		for (size_t t = 0; t + 2 < idx.size (); t += 3) {
-		    const GLushort ia = idx[t], ib = idx[t + 1], ic = idx[t + 2];
-		    if (ia >= vertexCount || ib >= vertexCount || ic >= vertexCount) {
-			outOfRange++;
-			continue;
-		    }
-		    const glm::vec3 a (pos[ia * 3], pos[ia * 3 + 1], pos[ia * 3 + 2]);
-		    const glm::vec3 b (pos[ib * 3], pos[ib * 3 + 1], pos[ib * 3 + 2]);
-		    const glm::vec3 c (pos[ic * 3], pos[ic * 3 + 1], pos[ic * 3 + 2]);
-		    const double area = 0.5 * glm::length (glm::cross (b - a, c - a));
-		    if (area < 1e-6) {
-			degenerate++;
-		    }
-		    totalArea += area;
-		    minArea = std::min (minArea, area);
-		    maxArea = std::max (maxArea, area);
-		}
-		const size_t triCount = idx.size () / 3;
-		sLog.out (
-		    "TEMP-DIAG mesh coherence for ", this->getImage ().name, " (", this->getId (), "): vertexCount=",
-		    vertexCount, " triCount=", triCount, " degenerate=", degenerate, " outOfRange=", outOfRange,
-		    " minArea=", minArea, " maxArea=", maxArea, " avgArea=", (triCount > 0 ? totalArea / triCount : 0.0),
-		    " totalArea=", totalArea
-		);
 	    }
 	    }
 	},
 	},
 	[pass] () {
 	[pass] () {
@@ -1944,6 +1821,83 @@ void CImage::setupPuppetGeometryCallback (Effects::CPass* pass) const {
     );
     );
 }
 }
 
 
+void CImage::addEffectPasses (const ImageEffect& effect) {
+    const auto fboProvider = std::make_shared<FBOProvider> (this);
+
+    for (const auto& fbo : effect.effect->fbos) {
+	fboProvider->create (
+	    *fbo,
+	    this->m_image.model->passthrough ? (this->m_texture->getFlags () | TextureFlags_ClampUVs)
+					      : this->m_texture->getFlags (),
+	    this->getSize ()
+	);
+    }
+
+    // TODO: MAKE USE OF ZIP OPERATOR IN BOOST? WAY OVERKILL JUST FOR THIS...
+
+    auto curEffect = effect.effect->passes.begin ();
+    auto endEffect = effect.effect->passes.end ();
+    auto curOverride = effect.passOverrides.begin ();
+    auto endOverride = effect.passOverrides.end ();
+
+    for (; curEffect != endEffect; ++curEffect) {
+	if (!(*curEffect)->material.has_value ()) {
+	    if (!(*curEffect)->command.has_value ()) {
+		sLog.error ("Pass without material and command not supported");
+		continue;
+	    }
+
+	    if (!(*curEffect)->source.has_value ()) {
+		sLog.error ("Pass without material and source not supported");
+		continue;
+	    }
+
+	    if (!(*curEffect)->target.has_value ()) {
+		sLog.error ("Pass without material and target not supported");
+		continue;
+	    }
+
+	    if ((*curEffect)->command != Command_Copy) {
+		sLog.error ("Only copy command is supported for pass without material");
+		continue;
+	    }
+
+	    auto virtualPass
+		= std::make_unique<MaterialPass> (MaterialPass { .blending = BlendingMode_Normal,
+								 .cullmode = CullingMode_Disable,
+								 .depthtest = DepthtestMode_Disabled,
+								 .depthwrite = DepthwriteMode_Disabled,
+								 .shader = "commands/copy",
+								 .textures = { { 0, *(*curEffect)->source } },
+								 .combos = {},
+								 .constants = {} });
+
+	    const auto& config = *this->m_virtualPassess.emplace_back (std::move (virtualPass));
+
+	    this->m_passes.push_back (new CPass (
+		*this, fboProvider, config, std::nullopt, std::nullopt, (*curEffect)->target.value ()
+	    ));
+	} else {
+	    for (auto& pass : (*curEffect)->material.value ()->passes) {
+		const auto override = curOverride != endOverride
+		    ? **curOverride
+		    : std::optional<std::reference_wrapper<const ImageEffectPassOverride>> (std::nullopt);
+		const auto target = (*curEffect)->target.has_value ()
+		    ? *(*curEffect)->target
+		    : std::optional<std::reference_wrapper<std::string>> (std::nullopt);
+
+		this->m_passes.push_back (
+		    new CPass (*this, fboProvider, *pass, override, (*curEffect)->binds, target)
+		);
+	    }
+
+	    if (curOverride != endOverride) {
+		++curOverride;
+	    }
+	}
+    }
+}
+
 void CImage::setup () {
 void CImage::setup () {
     if (this->m_initialized) {
     if (this->m_initialized) {
 	return;
 	return;
@@ -1952,24 +1906,9 @@ void CImage::setup () {
     // TODO: CHECK ORDER OF THINGS, 2419444134'S ID 27 DEPENDS ON 104'S COMPOSITE_A WHEN OUR LAST RENDER IS ON
     // TODO: CHECK ORDER OF THINGS, 2419444134'S ID 27 DEPENDS ON 104'S COMPOSITE_A WHEN OUR LAST RENDER IS ON
     // COMPOSITE_B
     // COMPOSITE_B
     // TODO: SUPPORT PASSTHROUGH (IT'S A SHADER)
     // TODO: SUPPORT PASSTHROUGH (IT'S A SHADER)
-    if (this->m_image.model->passthrough) {
-	// passthrough without effects has nothing to draw
-	if (this->m_image.effects.empty ()) {
-	    return;
-	}
-
-	// some scenes declare effects with visible set to false
-	bool allEffectsInvisible = true;
-	for (const auto& cur : this->m_image.effects) {
-	    if (cur->visible->value->getBool ()) {
-		allEffectsInvisible = false;
-		break;
-	    }
-	}
-
-	if (allEffectsInvisible) {
-	    return;
-	}
+    // passthrough without effects has nothing to draw
+    if (this->m_image.model->passthrough && this->m_image.effects.empty ()) {
+	return;
     }
     }
 
 
     const auto& debug = this->getScene ().getContext ().getApp ().getContext ().settings.render.debug;
     const auto& debug = this->getScene ().getContext ().getApp ().getContext ().settings.render.debug;
@@ -1980,6 +1919,9 @@ void CImage::setup () {
 	);
 	);
     }
     }
 
 
+    std::vector<const DynamicValue*> passVisibility (this->m_passes.size (), nullptr);
+    std::vector<bool> passFromEffect (this->m_passes.size (), false);
+
     if (!debug.baseOnly && !this->getImage ().effects.empty ()) {
     if (!debug.baseOnly && !this->getImage ().effects.empty ()) {
 	for (const auto& cur : this->m_image.effects) {
 	for (const auto& cur : this->m_image.effects) {
 	    if (std::find (debug.skipEffects.begin (), debug.skipEffects.end (), static_cast<int> (cur->id))
 	    if (std::find (debug.skipEffects.begin (), debug.skipEffects.end (), static_cast<int> (cur->id))
@@ -1992,84 +1934,42 @@ void CImage::setup () {
 	    );
 	    );
 
 
 	    // an explicit --disable-effect/--enable-effect override wins over the scene's own visibility
 	    // an explicit --disable-effect/--enable-effect override wins over the scene's own visibility
-	    if (effectVisibility.has_value () ? !*effectVisibility : !cur->visible->value->getBool ()) {
+	    if (effectVisibility.has_value () && !*effectVisibility) {
 		continue;
 		continue;
 	    }
 	    }
 
 
-	    const auto fboProvider = std::make_shared<FBOProvider> (this);
+	    // scripts can toggle hidden effects at runtime; puppets can't, their mesh pass layout
+	    // depends on the pass count
+	    const bool followsVisibility = !effectVisibility.has_value () && !this->m_hasPuppetMesh;
 
 
-	    for (const auto& fbo : cur->effect->fbos) {
-		fboProvider->create (
-		    *fbo,
-		    this->m_image.model->passthrough ? (this->m_texture->getFlags () | TextureFlags_ClampUVs)
-						      : this->m_texture->getFlags (),
-		    this->getSize ()
-		);
+	    if (!followsVisibility && !effectVisibility.has_value () && !cur->visible->value->getBool ()) {
+		continue;
 	    }
 	    }
 
 
-	    // TODO: MAKE USE OF ZIP OPERATOR IN BOOST? WAY OVERKILL JUST FOR THIS...
-
-	    auto curEffect = cur->effect->passes.begin ();
-	    auto endEffect = cur->effect->passes.end ();
-	    auto curOverride = cur->passOverrides.begin ();
-	    auto endOverride = cur->passOverrides.end ();
-
-	    for (; curEffect != endEffect; ++curEffect) {
-		if (!(*curEffect)->material.has_value ()) {
-		    if (!(*curEffect)->command.has_value ()) {
-			sLog.error ("Pass without material and command not supported");
-			continue;
-		    }
+	    const DynamicValue* visibleValue = followsVisibility ? cur->visible->value.get () : nullptr;
+	    const size_t firstEffectPass = this->m_passes.size ();
 
 
-		    if (!(*curEffect)->source.has_value ()) {
-			sLog.error ("Pass without material and source not supported");
-			continue;
-		    }
-
-		    if (!(*curEffect)->target.has_value ()) {
-			sLog.error ("Pass without material and target not supported");
-			continue;
-		    }
-
-		    if ((*curEffect)->command != Command_Copy) {
-			sLog.error ("Only copy command is supported for pass without material");
-			continue;
-		    }
-
-		    auto virtualPass
-			= std::make_unique<MaterialPass> (MaterialPass { .blending = BlendingMode_Normal,
-									 .cullmode = CullingMode_Disable,
-									 .depthtest = DepthtestMode_Disabled,
-									 .depthwrite = DepthwriteMode_Disabled,
-									 .shader = "commands/copy",
-									 .textures = { { 0, *(*curEffect)->source } },
-									 .combos = {},
-									 .constants = {} });
-
-		    const auto& config = *this->m_virtualPassess.emplace_back (std::move (virtualPass));
-
-		    this->m_passes.push_back (new CPass (
-			*this, fboProvider, config, std::nullopt, std::nullopt, (*curEffect)->target.value ()
-		    ));
-		} else {
-		    for (auto& pass : (*curEffect)->material.value ()->passes) {
-			const auto override = curOverride != endOverride
-			    ? **curOverride
-			    : std::optional<std::reference_wrapper<const ImageEffectPassOverride>> (std::nullopt);
-			const auto target = (*curEffect)->target.has_value ()
-			    ? *(*curEffect)->target
-			    : std::optional<std::reference_wrapper<std::string>> (std::nullopt);
-
-			this->m_passes.push_back (
-			    new CPass (*this, fboProvider, *pass, override, (*curEffect)->binds, target)
-			);
-		    }
+	    try {
+		this->addEffectPasses (*cur);
+	    } catch (const std::exception& e) {
+		if (visibleValue == nullptr || visibleValue->getBool ()) {
+		    throw;
+		}
 
 
-		    if (curOverride != endOverride) {
-			++curOverride;
-		    }
+		for (size_t i = firstEffectPass; i < this->m_passes.size (); i++) {
+		    delete this->m_passes[i];
 		}
 		}
+
+		this->m_passes.resize (firstEffectPass);
+		sLog.error (
+		    "Dropping hidden effect ", cur->id, " (", cur->name, ") on ", this->getImage ().name, ": ",
+		    e.what ()
+		);
+		continue;
 	    }
 	    }
+
+	    passVisibility.resize (this->m_passes.size (), visibleValue);
+	    passFromEffect.resize (this->m_passes.size (), true);
 	}
 	}
     }
     }
 
 
@@ -2151,6 +2051,52 @@ void CImage::setup () {
 	}
 	}
     }
     }
 
 
+    passVisibility.resize (this->m_passes.size (), nullptr);
+    passFromEffect.resize (this->m_passes.size (), false);
+
+    for (size_t i = 0; i < this->m_passes.size (); i++) {
+	this->m_allPassStates.push_back (
+	    { passVisibility[i], this->m_passes[i]->getBlendingMode (), passFromEffect[i] }
+	);
+    }
+
+    this->m_allPasses = this->m_passes;
+
+    CRenderable::setup ();
+
+    this->rebuildActivePasses ();
+    this->m_initialized = true;
+}
+
+bool CImage::effectVisibilityChanged () const {
+    for (size_t i = 0; i < this->m_allPassStates.size (); i++) {
+	const auto* visible = this->m_allPassStates[i].visible;
+
+	if (visible != nullptr && visible->getBool () != this->m_activePassMask[i]) {
+	    return true;
+	}
+    }
+
+    return false;
+}
+
+void CImage::rebuildActivePasses () {
+    this->m_passes.clear ();
+    this->m_activePassMask.assign (this->m_allPasses.size (), false);
+    this->m_hasActiveEffectPass = false;
+
+    for (size_t i = 0; i < this->m_allPasses.size (); i++) {
+	const auto& state = this->m_allPassStates[i];
+
+	this->m_allPasses[i]->setBlendingMode (state.blending);
+
+	if (state.visible == nullptr || state.visible->getBool ()) {
+	    this->m_activePassMask[i] = true;
+	    this->m_hasActiveEffectPass |= state.fromEffect;
+	    this->m_passes.push_back (this->m_allPasses[i]);
+	}
+    }
+
     // if there's more than one pass the blendmode has to be moved from the beginning to the end
     // if there's more than one pass the blendmode has to be moved from the beginning to the end
     if (this->m_passes.size () > 1) {
     if (this->m_passes.size () > 1) {
 	const auto first = this->m_passes.begin ();
 	const auto first = this->m_passes.begin ();
@@ -2160,10 +2106,11 @@ void CImage::setup () {
 	(*first)->setBlendingMode (BlendingMode_Normal);
 	(*first)->setBlendingMode (BlendingMode_Normal);
     }
     }
 
 
-    CRenderable::setup ();
+    // setupPasses() ping-pongs these, every rebuild has to start from the same pair
+    this->m_currentMainFBO = this->m_mainFBO;
+    this->m_currentSubFBO = this->m_subFBO;
 
 
     this->setupPasses ();
     this->setupPasses ();
-    this->m_initialized = true;
 }
 }
 
 
 void CImage::setupPasses () {
 void CImage::setupPasses () {
@@ -2178,9 +2125,6 @@ void CImage::setupPasses () {
     std::shared_ptr<const TextureProvider> effectInput = nullptr;
     std::shared_ptr<const TextureProvider> effectInput = nullptr;
 
 
     for (; cur != end; ++cur) {
     for (; cur != end; ++cur) {
-	// TODO: PROPERLY CHECK EFFECT'S VISIBILITY AND TAKE IT INTO ACCOUNT
-	// TODO: THIS REQUIRES ON-THE-FLY EVALUATION OF EFFECTS VISIBILITY TO FIGURE OUT
-	// TODO: WHICH ONE IS THE LAST + A FEW OTHER THINGS
 	Effects::CPass* pass = *cur;
 	Effects::CPass* pass = *cur;
 	std::shared_ptr<const CFBO> prevDrawTo = drawTo;
 	std::shared_ptr<const CFBO> prevDrawTo = drawTo;
 	bool writesToTarget = false;
 	bool writesToTarget = false;
@@ -2207,7 +2151,6 @@ void CImage::setupPasses () {
 	writesToTarget = this->configurePassTarget (pass, drawTo, asInput, effectInput, inTargetEffectSequence);
 	writesToTarget = this->configurePassTarget (pass, drawTo, asInput, effectInput, inTargetEffectSequence);
 	// TODO: PROPERLY CHECK IF THIS IS ALL THAT'S NEEDED
 	// TODO: PROPERLY CHECK IF THIS IS ALL THAT'S NEEDED
 	if (!writesToTarget && this->shouldRenderFinalPass (std::next (cur) == end)) {
 	if (!writesToTarget && this->shouldRenderFinalPass (std::next (cur) == end)) {
-	    // TODO: PROPERLY CHECK EFFECT'S VISIBILITY AND TAKE IT INTO ACCOUNT
 	    drawTo = this->getScene ().getFBO ();
 	    drawTo = this->getScene ().getFBO ();
 
 
 	    // A puppet with no effects has its geometry pass be both the first AND the last pass, drawn
 	    // A puppet with no effects has its geometry pass be both the first AND the last pass, drawn
@@ -2224,35 +2167,6 @@ void CImage::setupPasses () {
 	    // WE's final pass inverse lands in the layer's local space (origin at its center, unscaled
 	    // WE's final pass inverse lands in the layer's local space (origin at its center, unscaled
 	    // pixels); older shaders like the bundled xray.vert unproject the pointer through it
 	    // pixels); older shaders like the bundled xray.vert unproject the pointer through it
 	    inverseProjection = &this->m_objectSpaceProjectionInverse;
 	    inverseProjection = &this->m_objectSpaceProjectionInverse;
-
-	    if (this->m_hasPuppetMesh) {
-		GLfloat bufDump[18] = {};
-		glBindBuffer (GL_ARRAY_BUFFER, spacePosition);
-		glGetBufferSubData (GL_ARRAY_BUFFER, 0, sizeof (bufDump), bufDump);
-		sLog.out (
-		    "TEMP-DIAG final-pass-branch for ", this->getImage ().name, " (", this->getId (),
-		    "): isFirstPass=", isFirstPass, " m_pos=(", this->m_pos.x, ",", this->m_pos.y, ",", this->m_pos.z,
-		    ",", this->m_pos.w, ") spacePosition=", spacePosition, " sceneSpacePositionBuffer=",
-		    this->getSceneSpacePosition (), " passCount=", this->m_passes.size (), " bufVerts=[",
-		    bufDump[0], " ", bufDump[1], " ", bufDump[2], " | ", bufDump[3], " ", bufDump[4], " ", bufDump[5],
-		    " | ", bufDump[6], " ", bufDump[7], " ", bufDump[8], " | ", bufDump[9], " ", bufDump[10], " ",
-		    bufDump[11], " | ", bufDump[12], " ", bufDump[13], " ", bufDump[14], " | ", bufDump[15], " ",
-		    bufDump[16], " ", bufDump[17], "]"
-		);
-
-		const glm::mat4& mvp = *projection;
-		const glm::vec4 c0 = mvp * glm::vec4 (bufDump[0], bufDump[1], bufDump[2], 1.0f);
-		const glm::vec4 c1 = mvp * glm::vec4 (bufDump[3], bufDump[4], bufDump[5], 1.0f);
-		const glm::vec4 c2 = mvp * glm::vec4 (bufDump[6], bufDump[7], bufDump[8], 1.0f);
-		sLog.out (
-		    "TEMP-DIAG projection for ", this->getImage ().name, " (", this->getId (), "): mvpRow0=(", mvp[0][0],
-		    ",", mvp[1][0], ",", mvp[2][0], ",", mvp[3][0], ") mvpRow1=(", mvp[0][1], ",", mvp[1][1], ",",
-		    mvp[2][1], ",", mvp[3][1], ") clip0=(", c0.x, ",", c0.y, ",", c0.z, ",", c0.w, ") ndc0=(",
-		    c0.x / c0.w, ",", c0.y / c0.w, ") clip1=(", c1.x, ",", c1.y, ",", c1.z, ",", c1.w, ") ndc1=(",
-		    c1.x / c1.w, ",", c1.y / c1.w, ") clip2=(", c2.x, ",", c2.y, ",", c2.z, ",", c2.w, ") ndc2=(",
-		    c2.x / c2.w, ",", c2.y / c2.w, ")"
-		);
-	    }
 	}
 	}
 
 
 	pass->setDestination (drawTo);
 	pass->setDestination (drawTo);
@@ -2348,6 +2262,14 @@ void CImage::render () {
 	return;
 	return;
     }
     }
 
 
+    if (this->effectVisibilityChanged ()) {
+	this->rebuildActivePasses ();
+    }
+
+    if (this->m_image.model->passthrough && !this->m_hasActiveEffectPass) {
+	return;
+    }
+
     glColorMask (true, true, true, true);
     glColorMask (true, true, true, true);
 
 
     this->updateScreenSpacePosition ();
     this->updateScreenSpacePosition ();

+ 13 - 1
src/WallpaperEngine/Render/Objects/CImage.h

@@ -124,6 +124,9 @@ public:
 
 
 protected:
 protected:
     void setupPasses ();
     void setupPasses ();
+    void rebuildActivePasses ();
+    void addEffectPasses (const ImageEffect& effect);
+    [[nodiscard]] bool effectVisibilityChanged () const;
 
 
     void updateScreenSpacePosition ();
     void updateScreenSpacePosition ();
     void updateEffectTextureProjection ();
     void updateEffectTextureProjection ();
@@ -182,7 +185,6 @@ private:
     bool m_transformDiagnosticLogged = false;
     bool m_transformDiagnosticLogged = false;
     mutable std::set<int> m_attachmentDiagnosticLogged = {};
     mutable std::set<int> m_attachmentDiagnosticLogged = {};
     mutable std::set<int> m_finalOriginLogged = {};
     mutable std::set<int> m_finalOriginLogged = {};
-    bool m_boneTrackDiagLogged = false;
     std::vector<GLfloat> m_puppetRawPositions = {};
     std::vector<GLfloat> m_puppetRawPositions = {};
     /** This object's current resolved scale, mirrored here so updatePuppetSkinning() (called after
     /** This object's current resolved scale, mirrored here so updatePuppetSkinning() (called after
      *  updateGeometryBuffers() each frame, see render()) can fold it into puppet vertex positions
      *  updateGeometryBuffers() each frame, see render()) can fold it into puppet vertex positions
@@ -236,6 +238,16 @@ private:
     mutable float m_alphaCache = 1.0f;
     mutable float m_alphaCache = 1.0f;
 
 
     std::vector<Effects::CPass*> m_passes = {};
     std::vector<Effects::CPass*> m_passes = {};
+    std::vector<Effects::CPass*> m_allPasses = {};
+    struct PassState {
+	/** nullptr for passes that always render */
+	const DynamicValue* visible;
+	BlendingMode blending;
+	bool fromEffect;
+    };
+    std::vector<PassState> m_allPassStates = {};
+    std::vector<bool> m_activePassMask = {};
+    bool m_hasActiveEffectPass = false;
     std::vector<MaterialPassUniquePtr> m_virtualPassess = {};
     std::vector<MaterialPassUniquePtr> m_virtualPassess = {};
 
 
     glm::vec4 m_pos = {};
     glm::vec4 m_pos = {};

+ 68 - 17
src/WallpaperEngine/Render/Objects/CParticle.cpp

@@ -396,6 +396,39 @@ void CParticle::setupEmitters () {
     }
     }
 }
 }
 
 
+float CParticle::sampleAudio (
+    int mode, const glm::vec2& bounds, float exponent, int frequencyStart, int frequencyEnd
+) const {
+    // same curve as wallpaper64.exe. Modes 1/2/3 pick left/right/averaged channels there, the recorder is mono
+    if (mode == 0) {
+	return 1.0f;
+    }
+
+    int first = std::clamp (frequencyStart, 0, 15);
+    int last = std::clamp (frequencyEnd, 0, 15);
+
+    if (last < first) {
+	std::swap (first, last);
+    }
+
+    const auto& recorder = this->getScene ().getAudioContext ().getRecorder ();
+    float peak = 0.0f;
+
+    recorder.lock ();
+    for (int i = first; i <= last; i++) {
+	peak = std::max (peak, recorder.audio16[i]);
+    }
+    recorder.unlock ();
+
+    float t = (peak - bounds.x) / (bounds.y - bounds.x);
+    // NaN from equal bounds ends up as 0 like the original
+    t = t >= 1.0f ? 1.0f : (t >= 0.0f ? t : 0.0f);
+
+    const float response = std::pow (t * t * (3.0f - 2.0f * t), exponent);
+
+    return response >= 1.0f ? 1.0f : (response >= 0.0f ? response : 0.0f);
+}
+
 EmitterFunc CParticle::createBoxEmitter (const ParticleEmitter& emitter) {
 EmitterFunc CParticle::createBoxEmitter (const ParticleEmitter& emitter) {
     float rate = emitter.rate * m_particle.instanceOverride.rate->value->getFloat ();
     float rate = emitter.rate * m_particle.instanceOverride.rate->value->getFloat ();
 
 
@@ -462,8 +495,6 @@ EmitterFunc CParticle::createBoxEmitter (const ParticleEmitter& emitter) {
 		}
 		}
 	    }
 	    }
 
 
-	    // TODO: audio processing (audioProcessingMode, audioProcessingBounds, etc.)
-
 	    uint32_t toEmit = 0;
 	    uint32_t toEmit = 0;
 	    if (emitter.instantaneous > 0 && !instantaneousEmitted) {
 	    if (emitter.instantaneous > 0 && !instantaneousEmitted) {
 		toEmit = emitter.instantaneous;
 		toEmit = emitter.instantaneous;
@@ -471,7 +502,11 @@ EmitterFunc CParticle::createBoxEmitter (const ParticleEmitter& emitter) {
 	    }
 	    }
 
 
 	    if (emitter.rate > 0.0f) {
 	    if (emitter.rate > 0.0f) {
-		emissionTimer += dt * rate;
+		const float audio = sampleAudio (
+		    emitter.audioProcessingMode, emitter.audioProcessingBounds, emitter.audioProcessingExponent,
+		    emitter.audioProcessingFrequencyStart, emitter.audioProcessingFrequencyEnd
+		);
+		emissionTimer += dt * rate * audio;
 		uint32_t rateEmit = static_cast<uint32_t> (emissionTimer);
 		uint32_t rateEmit = static_cast<uint32_t> (emissionTimer);
 		emissionTimer -= static_cast<float> (rateEmit);
 		emissionTimer -= static_cast<float> (rateEmit);
 		// limitOnePerFrame (flags bit 1): cap at 1 to prevent rope artifacts
 		// limitOnePerFrame (flags bit 1): cap at 1 to prevent rope artifacts
@@ -567,7 +602,11 @@ EmitterFunc CParticle::createSphereEmitter (const ParticleEmitter& emitter) {
 	    return;
 	    return;
 	}
 	}
 
 
-	emissionTimer += dt * rate;
+	const float audio = sampleAudio (
+	    emitter.audioProcessingMode, emitter.audioProcessingBounds, emitter.audioProcessingExponent,
+	    emitter.audioProcessingFrequencyStart, emitter.audioProcessingFrequencyEnd
+	);
+	emissionTimer += dt * rate * audio;
 	uint32_t toEmit = static_cast<uint32_t> (emissionTimer);
 	uint32_t toEmit = static_cast<uint32_t> (emissionTimer);
 	emissionTimer -= static_cast<float> (toEmit);
 	emissionTimer -= static_cast<float> (toEmit);
 	// limitOnePerFrame (flags bit 1): cap at 1 to prevent rope artifacts
 	// limitOnePerFrame (flags bit 1): cap at 1 to prevent rope artifacts
@@ -1232,8 +1271,11 @@ OperatorFunc CParticle::createTurbulenceOperator (const TurbulenceOperator& op)
     DynamicValue* phaseMinValue = op.phaseMin->value.get ();
     DynamicValue* phaseMinValue = op.phaseMin->value.get ();
     DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
     DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
     DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
     DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
-
-    // TODO: audio processing support (audioProcessingMode/Bounds/Exponent/FrequencyStart/FrequencyEnd)
+    DynamicValue* audioModeValue = op.audioProcessingMode->value.get ();
+    DynamicValue* audioBoundsValue = op.audioProcessingBounds->value.get ();
+    DynamicValue* audioExponentValue = op.audioProcessingExponent->value.get ();
+    DynamicValue* audioStartValue = op.audioProcessingFrequencyStart->value.get ();
+    DynamicValue* audioEndValue = op.audioProcessingFrequencyEnd->value.get ();
 
 
     // Phase and speed are randomized once per operator instance, not per particle
     // Phase and speed are randomized once per operator instance, not per particle
     const float phase
     const float phase
@@ -1241,7 +1283,8 @@ OperatorFunc CParticle::createTurbulenceOperator (const TurbulenceOperator& op)
     const float turbSpeed
     const float turbSpeed
 	= WallpaperEngine::Maths::randomFloat (m_rng, speedMinValue->getFloat (), speedMaxValue->getFloat ());
 	= WallpaperEngine::Maths::randomFloat (m_rng, speedMinValue->getFloat (), speedMaxValue->getFloat ());
 
 
-    return [scaleValue, timeScaleValue, maskValue, speedOverride, phase, turbSpeed] (
+    return [this, scaleValue, timeScaleValue, maskValue, speedOverride, audioModeValue, audioBoundsValue,
+	    audioExponentValue, audioStartValue, audioEndValue, phase, baseTurbSpeed = turbSpeed] (
 	       std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&,
 	       std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&,
 	       float currentTime, float dt
 	       float currentTime, float dt
 	   ) {
 	   ) {
@@ -1249,6 +1292,11 @@ OperatorFunc CParticle::createTurbulenceOperator (const TurbulenceOperator& op)
 	const float timeScale = timeScaleValue->getFloat ();
 	const float timeScale = timeScaleValue->getFloat ();
 	const glm::vec3 mask = maskValue->getVec3 ();
 	const glm::vec3 mask = maskValue->getVec3 ();
 	const float speed = speedOverride->getFloat ();
 	const float speed = speedOverride->getFloat ();
+	const float audio = sampleAudio (
+	    audioModeValue->getInt (), audioBoundsValue->getVec2 (), audioExponentValue->getFloat (),
+	    audioStartValue->getInt (), audioEndValue->getInt ()
+	);
+	const float turbSpeed = baseTurbSpeed * audio;
 
 
 	if (turbSpeed <= 0.0001f) {
 	if (turbSpeed <= 0.0001f) {
 	    return;
 	    return;
@@ -1291,24 +1339,29 @@ OperatorFunc CParticle::createVortexOperator (const VortexOperator& op) {
     DynamicValue* ringPullDistanceValue = op.ringPullDistance->value.get ();
     DynamicValue* ringPullDistanceValue = op.ringPullDistance->value.get ();
     DynamicValue* ringPullForceValue = op.ringPullForce->value.get ();
     DynamicValue* ringPullForceValue = op.ringPullForce->value.get ();
     DynamicValue* audioModeValue = op.audioProcessingMode->value.get ();
     DynamicValue* audioModeValue = op.audioProcessingMode->value.get ();
+    DynamicValue* audioBoundsValue = op.audioProcessingBounds->value.get ();
+    DynamicValue* audioExponentValue = op.audioProcessingExponent->value.get ();
+    DynamicValue* audioStartValue = op.audioProcessingFrequencyStart->value.get ();
+    DynamicValue* audioEndValue = op.audioProcessingFrequencyEnd->value.get ();
     DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
     DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
 
 
-    int audioMode = static_cast<int> (audioModeValue->getFloat ());
-
     bool infiniteAxis = (flags & 1) != 0;
     bool infiniteAxis = (flags & 1) != 0;
     bool maintainDistance = (flags & 2) != 0;
     bool maintainDistance = (flags & 2) != 0;
     bool ringShape = (flags & 4) != 0;
     bool ringShape = (flags & 4) != 0;
 
 
     return [controlPoint, axisValue, offsetValue, distanceInnerValue, distanceOuterValue, speedInnerValue,
     return [controlPoint, axisValue, offsetValue, distanceInnerValue, distanceOuterValue, speedInnerValue,
 	    speedOuterValue, centerForceValue, ringRadiusValue, ringWidthValue, ringPullDistanceValue,
 	    speedOuterValue, centerForceValue, ringRadiusValue, ringWidthValue, ringPullDistanceValue,
-	    ringPullForceValue, audioMode, infiniteAxis, maintainDistance, ringShape, speedOverride] (
+	    ringPullForceValue, audioModeValue, audioBoundsValue, audioExponentValue, audioStartValue, audioEndValue,
+	    infiniteAxis, maintainDistance, ringShape, speedOverride, this] (
 	       std::vector<ParticleInstance>& particles, uint32_t count,
 	       std::vector<ParticleInstance>& particles, uint32_t count,
 	       const std::vector<ControlPointData>& controlPoints, float, float dt
 	       const std::vector<ControlPointData>& controlPoints, float, float dt
 	   ) {
 	   ) {
-	float audioAmplitude = 0.0f; // TODO: sample from AudioContext once audio processing is implemented
+	const float audioResponse = sampleAudio (
+	    audioModeValue->getInt (), audioBoundsValue->getVec2 (), audioExponentValue->getFloat (),
+	    audioStartValue->getInt (), audioEndValue->getInt ()
+	);
 
 
-	// Audio mode enabled but no audio available yet - skip vortex entirely
-	if (audioMode > 0 && audioAmplitude == 0.0f) {
+	if (audioResponse <= 0.0f) {
 	    return;
 	    return;
 	}
 	}
 
 
@@ -1324,10 +1377,8 @@ OperatorFunc CParticle::createVortexOperator (const VortexOperator& op) {
 	float ringPullDistance = ringPullDistanceValue->getFloat ();
 	float ringPullDistance = ringPullDistanceValue->getFloat ();
 	float ringPullForce = ringPullForceValue->getFloat ();
 	float ringPullForce = ringPullForceValue->getFloat ();
 
 
-	if (audioMode > 0) {
-	    speedInner *= (1.0f + audioAmplitude);
-	    speedOuter *= (1.0f + audioAmplitude);
-	}
+	speedInner *= audioResponse;
+	speedOuter *= audioResponse;
 
 
 	glm::vec3 center = glm::vec3 (0.0f);
 	glm::vec3 center = glm::vec3 (0.0f);
 	if (controlPoint >= 0 && controlPoint < static_cast<int> (controlPoints.size ())) {
 	if (controlPoint >= 0 && controlPoint < static_cast<int> (controlPoints.size ())) {

+ 4 - 0
src/WallpaperEngine/Render/Objects/CParticle.h

@@ -114,6 +114,10 @@ protected:
     EmitterFunc createBoxEmitter (const ParticleEmitter& emitter);
     EmitterFunc createBoxEmitter (const ParticleEmitter& emitter);
     EmitterFunc createSphereEmitter (const ParticleEmitter& emitter);
     EmitterFunc createSphereEmitter (const ParticleEmitter& emitter);
 
 
+    [[nodiscard]] float sampleAudio (
+	int mode, const glm::vec2& bounds, float exponent, int frequencyStart, int frequencyEnd
+    ) const;
+
     InitializerFunc createColorRandomInitializer (const ColorRandomInitializer& init);
     InitializerFunc createColorRandomInitializer (const ColorRandomInitializer& init);
     InitializerFunc createSizeRandomInitializer (const SizeRandomInitializer& init);
     InitializerFunc createSizeRandomInitializer (const SizeRandomInitializer& init);
     InitializerFunc createAlphaRandomInitializer (const AlphaRandomInitializer& init);
     InitializerFunc createAlphaRandomInitializer (const AlphaRandomInitializer& init);

+ 35 - 2
src/WallpaperEngine/Render/Shaders/ShaderUnit.cpp

@@ -684,6 +684,39 @@ std::string ShaderUnit::applyFragmentVaryingShadowCompatibility (std::string sou
     return source;
     return source;
 }
 }
 
 
+std::string ShaderUnit::applyNonConstantConstCompatibility (std::string source) const {
+    // locals only, globals sit at column 0
+    static const std::regex constLocal (R"((^|\n)([ \t]+)const\s+([^;=]+=([^;]*);))");
+    static const std::regex nonConstant (R"(\b(?:texSample2D\w*|texture\w*|g_\w+|v_\w+)\b)");
+
+    std::string result;
+    size_t count = 0;
+    auto last = source.cbegin ();
+
+    for (auto it = std::sregex_iterator (source.cbegin (), source.cend (), constLocal); it != std::sregex_iterator ();
+	 ++it) {
+	const auto& match = *it;
+
+	if (!std::regex_search (match[4].first, match[4].second, nonConstant)) {
+	    continue;
+	}
+
+	result.append (last, match[0].first);
+	result += match[1].str () + match[2].str () + match[3].str ();
+	last = match[0].second;
+	count++;
+    }
+
+    if (count == 0) {
+	return source;
+    }
+
+    result.append (last, source.cend ());
+    sLog.out ("Dropped const from ", count, " non-constant local(s) in ", this->m_file);
+
+    return result;
+}
+
 void ShaderUnit::parseComboConfiguration (const std::string& content, const int defaultValue) {
 void ShaderUnit::parseComboConfiguration (const std::string& content, const int defaultValue) {
     // TODO: SUPPORT REQUIRES SO WE PROPERLY FOLLOW THE REQUIRED CHAIN
     // TODO: SUPPORT REQUIRES SO WE PROPERLY FOLLOW THE REQUIRED CHAIN
     JSON data;
     JSON data;
@@ -995,11 +1028,11 @@ const std::string& ShaderUnit::compile () {
 	}
 	}
     }
     }
 
 
-    const std::string compat = this->applyFloatConditionCompatibility (
+    const std::string compat = this->applyNonConstantConstCompatibility (this->applyFloatConditionCompatibility (
 	this->applyVectorTruncationCompatibility (this->applyFragmentVaryingShadowCompatibility (
 	this->applyVectorTruncationCompatibility (this->applyFragmentVaryingShadowCompatibility (
 	    this->applyFragmentTexCoordCompatibility (this->applyLinkedVaryingCompatibility (this->m_preprocessed))
 	    this->applyFragmentTexCoordCompatibility (this->applyLinkedVaryingCompatibility (this->m_preprocessed))
 	))
 	))
-    );
+    ));
 
 
     {
     {
 	std::lock_guard lock (cacheMutex);
 	std::lock_guard lock (cacheMutex);

+ 3 - 0
src/WallpaperEngine/Render/Shaders/ShaderUnit.h

@@ -71,6 +71,9 @@ private:
     /** HLSL converts a float to bool implicitly (`cond ? a : b`, `if (cond)`), GLSL needs a real bool.
     /** HLSL converts a float to bool implicitly (`cond ? a : b`, `if (cond)`), GLSL needs a real bool.
      *  Rewrites a bare float variable used as such a condition to `(cond != 0.0)`. */
      *  Rewrites a bare float variable used as such a condition to `(cond != 0.0)`. */
     [[nodiscard]] std::string applyFloatConditionCompatibility (std::string source) const;
     [[nodiscard]] std::string applyFloatConditionCompatibility (std::string source) const;
+    /** HLSL accepts a `const` local initialized from a texture sample, uniform or varying, GLSL only allows
+     *  constant expressions there. Drops the `const` from such locals. */
+    [[nodiscard]] std::string applyNonConstantConstCompatibility (std::string source) const;
 
 
     void parseComboConfiguration (const std::string& content, int defaultValue = 0);
     void parseComboConfiguration (const std::string& content, int defaultValue = 0);
     void parseParameterConfiguration (const std::string& type, const std::string& name, const std::string& content);
     void parseParameterConfiguration (const std::string& type, const std::string& name, const std::string& content);

+ 0 - 8
src/WallpaperEngine/Testing/Render/TestingOpenGLDriver.cpp

@@ -4,8 +4,6 @@
 
 
 #include "WallpaperEngine/Render/Drivers/Output/GLFWWindowOutput.h"
 #include "WallpaperEngine/Render/Drivers/Output/GLFWWindowOutput.h"
 
 
-#include <algorithm>
-
 using namespace WallpaperEngine::Testing::Render;
 using namespace WallpaperEngine::Testing::Render;
 
 
 void TestingCustomGLFWErrorHandler (int errorCode, const char* reason) {
 void TestingCustomGLFWErrorHandler (int errorCode, const char* reason) {
@@ -92,10 +90,6 @@ glm::ivec2 TestingOpenGLDriver::getFramebufferSize () const {
 
 
 uint32_t TestingOpenGLDriver::getFrameCounter () const { return this->m_frameCounter; }
 uint32_t TestingOpenGLDriver::getFrameCounter () const { return this->m_frameCounter; }
 void TestingOpenGLDriver::dispatchEventQueue () {
 void TestingOpenGLDriver::dispatchEventQueue () {
-    static float startTime, endTime;
-    // read every frame, --fps can change with a hotswap
-    const float minimumTime = 1.0f / std::max (1, this->m_context.settings.render.maximumFPS);
-    startTime = this->getRenderTime ();
     glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
     glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
 
     for (const auto& [screen, viewport] : this->m_output->getViewports ()) {
     for (const auto& [screen, viewport] : this->m_output->getViewports ()) {
@@ -123,10 +117,8 @@ void TestingOpenGLDriver::dispatchEventQueue () {
 	}
 	}
     }
     }
 
 
-    // TODO: frametime control should move back to CWallpaperApplication once real particles need their own processing rate
     this->m_output->updateRender ();
     this->m_output->updateRender ();
     glfwSwapBuffers (this->m_window);
     glfwSwapBuffers (this->m_window);
     glfwPollEvents ();
     glfwPollEvents ();
     this->m_frameCounter++;
     this->m_frameCounter++;
-    endTime = this->getRenderTime ();
 }
 }