From 2be41da9b668bf2e3d389d969147fccc3388ffe3 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 27 Apr 2021 23:33:50 +0200 Subject: [PATCH] Add compatibility option to force flushing to zero This prevents Kush Audio REDDI from taking down the DAW when the host passes it denormalized audio to process. I've discovered that the issue with this plugin had to do with denormals in the issue linked below, but I didn't realize that we can just enable the FTZ flag for plugins that don't already do so. https://github.com/osxmidi/LinVst/issues/174 --- CHANGELOG.md | 9 +++++++++ README.md | 8 ++++++++ src/common/configuration.cpp | 6 ++++++ src/common/configuration.h | 10 ++++++++++ src/common/utils.h | 4 +--- src/plugin/bridges/common.h | 3 +++ src/wine-host/bridges/vst2.cpp | 6 ++++++ src/wine-host/bridges/vst3.cpp | 8 ++++++++ 8 files changed, 51 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a36d6c1..2628e00e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,15 @@ Versioning](https://semver.org/spec/v2.0.0.html). that have undesirable or broken DAW-specific behaviour. See the [known issues](https://github.com/robbert-vdh/yabridge#runtime-dependencies-and-known-issues) section of the readme for more information on when this may be useful. +- Added another new [compatibility + option](https://github.com/robbert-vdh/yabridge#compatibility-options) to + force flushing denormals to zero during audio processing. Some plugins, such + as _Kush Audio REDDI_, have significantly increased DSP usage when processing + audio that's almost but not quite silent. When this happens, you will notice + that a plugin's DSP usage increases exponentially after playback has stopped + or when the plugin starts processing silence. Enabling this new option will + force your CPU to flush this almost silent audio to silence, potentially + fixing the issue. ### Changed diff --git a/README.md b/README.md index 1a2cbd59..757b3d6d 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,7 @@ plugin._ | `editor_double_embed` | `{true,false}` | Compatibility option for plugins that rely on the absolute screen coordinates of the window they're embedded in. Since the Wine window gets embedded inside of a window provided by your DAW, these coordinates won't match up and the plugin would end up drawing in the wrong location without this option. Currently the only known plugins that require this option are _PSPaudioware_ plugins with expandable GUIs, such as E27. Defaults to `false`. | | `editor_force_dnd` | `{true,false}` | This option forcefully enables drag-and-drop support in _REAPER_. Because REAPER's FX window supports drag-and-drop itself, dragging a file onto a plugin editor will cause the drop to be intercepted by the FX window. This makes it impossible to drag files onto plugins in REAPER under normal circumstances. Setting this option to `true` will strip drag-and-drop support from the FX window, thus allowing files to be dragged onto the plugin again. Defaults to `false`. | | `editor_xembed` | `{true,false}` | Use Wine's XEmbed implementation instead of yabridge's normal window embedding method. Some plugins will have redrawing issues when using XEmbed and editor resizing won't always work properly with it, but it could be useful in certain setups. You may need to use [this Wine patch](https://github.com/psycha0s/airwave/blob/master/fix-xembed-wine-windows.patch) if you're getting blank editor windows. Defaults to `false`. | +| `force_ftz` | `{true,false}` | Enable the CPU's FTZ flag while processing audio, flushing denormals to zero. This can fix issues with plugins that start showing exponentially increasing DSP load when playback is stopped or when they start processing silence. Defaults to `false`. | | `frame_rate` | `` | The rate at which Win32 events are being handled and usually also the refresh rate of a plugin's editor GUI. When using plugin groups all plugins share the same event handling loop, so in those the last loaded plugin will set the refresh rate. Defaults to `60`. | | `hide_daw` | `{true,false}` | Don't report the name of the actual DAW to the plugin. See the [known issues](#runtime-dependencies-and-known-issues) section for a list of situations where this may be useful. This affects both VST2 and VST3 plugins. Defaults to `false`. | | `vst3_no_scaling` | `{true,false}` | Disable HiDPI scaling for VST3 plugins. Wine currently does not have proper fractional HiDPI support, so you might have to enable this option if you're using a HiDPI display. In most cases setting the font DPI in `winecfg`'s graphics tab to 192 will cause plugins to scale correctly at 200% size. Defaults to `false`. | @@ -333,6 +334,9 @@ editor_xembed = true ["Chromaphone 3.so"] hide_daw = true +["Kush Audio/REDDI.so"] +force_ftz = true + ["SWAM Cello 64bit.so"] cache_time_info = true @@ -444,6 +448,10 @@ include: _Bitwig Studio_, text entry will cause the plugin to crash because Chromaphone uses a different text entry method when it detects Bitwig. You can use the `hide_daw` [compatibility option](#compatibility-options) to work around this. +- **Kush Audio REDDI** suffers from exponentially increasing DSP usage when the + plugin starts processing silence, halting playback and potentially crashing + the DAW. You can enable the `force_ftz` [compatibility + option](#compatibility-options) for the plugin to work around this bug. - The VST2 version of **SWAM Cello** has a bug where it asks the host for the current buffer's time and tempo information for every sample it processes instead of doing it only once per buffer, resulting in very bad performance. diff --git a/src/common/configuration.cpp b/src/common/configuration.cpp index 919b1157..01c8a9ae 100644 --- a/src/common/configuration.cpp +++ b/src/common/configuration.cpp @@ -108,6 +108,12 @@ Configuration::Configuration(const fs::path& config_path, } else { invalid_options.push_back(key); } + } else if (key == "force_ftz") { + if (const auto parsed_value = value.as_boolean()) { + force_ftz = parsed_value->get(); + } else { + invalid_options.push_back(key); + } } else if (key == "frame_rate") { if (const auto parsed_value = value.as_floating_point()) { frame_rate = parsed_value->get(); diff --git a/src/common/configuration.h b/src/common/configuration.h index 5d07bc15..0c0bee81 100644 --- a/src/common/configuration.h +++ b/src/common/configuration.h @@ -126,6 +126,15 @@ class Configuration { */ bool editor_xembed = false; + /** + * When using this option, we'll enable the flush-to-zero flag during audio + * processing. This can be useful when the host passes denormals to a plugin + * that doesn't handle those well. An example of such a plugin is REDDI by + * Kush Audio. Some hosts, like Bitwig, will already snap denormals to zero + * for us so this may not be necessary with every host. + */ + bool force_ftz = false; + /** * The number of times per second we'll handle the event loop. In most * plugins this also controls the plugin editor GUI's refresh rate. @@ -205,6 +214,7 @@ class Configuration { s.value1b(editor_double_embed); s.value1b(editor_force_dnd); s.value1b(editor_xembed); + s.value1b(force_ftz); s.ext(frame_rate, bitsery::ext::StdOptional(), [](S& s, auto& v) { s.value4b(v); }); s.value1b(hide_daw); diff --git a/src/common/utils.h b/src/common/utils.h index cbac74d0..0e654138 100644 --- a/src/common/utils.h +++ b/src/common/utils.h @@ -90,15 +90,13 @@ bool set_realtime_priority(bool sched_fifo, int priority = 5); * previously when it drops out of scope. */ class ScopedFlushToZero { + public: ScopedFlushToZero(); ~ScopedFlushToZero(); ScopedFlushToZero(const ScopedFlushToZero&) = delete; ScopedFlushToZero& operator=(const ScopedFlushToZero&) = delete; - ScopedFlushToZero(ScopedFlushToZero&&) = delete; - ScopedFlushToZero& operator=(ScopedFlushToZero&&) = delete; - private: /** * The previous FTZ mode. When we use this on the Wine side, this should diff --git a/src/plugin/bridges/common.h b/src/plugin/bridges/common.h index 2c91dc9f..15da2336 100644 --- a/src/plugin/bridges/common.h +++ b/src/plugin/bridges/common.h @@ -187,6 +187,9 @@ class PluginBridge { if (config.editor_xembed) { other_options.push_back("editor: XEmbed"); } + if (config.force_ftz) { + other_options.push_back("hack: force flush-to-zero"); + } if (config.frame_rate) { std::ostringstream option; option << "frame rate: " << std::setprecision(2) diff --git a/src/wine-host/bridges/vst2.cpp b/src/wine-host/bridges/vst2.cpp index a2af16e6..8752a6f6 100644 --- a/src/wine-host/bridges/vst2.cpp +++ b/src/wine-host/bridges/vst2.cpp @@ -180,6 +180,12 @@ Vst2Bridge::Vst2Bridge(MainContext& main_context, sockets.host_vst_process_replacing.receive_multi( [&](AudioBuffers request, std::vector& buffer) { + // HACK: When a plugin doesn't handle denormals properly, we can + // force the FTZ flag to be set to work around this + std::optional ftz_guard = + config.force_ftz ? std::make_optional() + : std::nullopt; + // As suggested by Jack Winter, we'll synchronize this thread's // audio processing priority with that of the host's audio // thread every once in a while diff --git a/src/wine-host/bridges/vst3.cpp b/src/wine-host/bridges/vst3.cpp index c9fd0708..8baa19f4 100644 --- a/src/wine-host/bridges/vst3.cpp +++ b/src/wine-host/bridges/vst3.cpp @@ -1211,6 +1211,14 @@ size_t Vst3Bridge::register_object_instance( }, [&](YaAudioProcessor::Process& request) -> YaAudioProcessor::Process::Response { + // HACK: When a plugin doesn't handle denormals + // properly, we can force the FTZ flag to be set + // to work around this + std::optional ftz_guard = + config.force_ftz + ? std::make_optional() + : std::nullopt; + // As suggested by Jack Winter, we'll synchronize this // thread's audio processing priority with that of the // host's audio thread every once in a while