On Linux, ClapAsVst3::onIdle() runs only while the plug-in's editor window is open. With no window — a headless instance, a plug-in the user never opened, or one whose window was closed again — it does not run at all, and everything the wrapper defers to it is silently dropped.
Why
src/detail/os/linux.cpp has no timer. LinuxHelper::init() and terminate() are empty bodies, attach/detach only push to and erase from _plugs, and LinuxHelper::executeDefered() — the function that would call onIdle() on each attached plug — has no caller. (The one textual reference at line 120 is inside the #if 0 Windows block.) So _os_attached.on() in ClapAsVst3::setActive(true) is a no-op on Linux.
The other two platforms do have it:
detail/os/macos.mm — MacOSHelper::attach does CFRunLoopTimerCreate + CFRunLoopAddTimer(CFRunLoopGetMain(), ...), callback → executeDefered() → onIdle().
detail/os/windows.cpp — WindowsHelper::init creates a message window and SetTimer(_msgWin, 0, 20, NULL); WM_TIMER → executeDefered() → onIdle().
Both pump for the lifetime of the instance, independent of any editor. On Linux the only driver is the IdleHandler timer registered against the host's IRunLoop, and the wrapper only obtains that from the wrapped view — attachTimers(_wrappedview->getRunLoop()) in the createView attach callback, and detachTimers(...) with _iRunLoop = nullptr when the view goes away.
The comment already in onIdle() notes it in passing:
#if LIN
if (!_iRunLoop) // don't process timers if we have a runloop.
// (but if we don't have a runloop on linux onIdle isn't called
// anyway so consider just not having this at all once we decide
What it costs
With no editor open on Linux, all of these are dead, and become dead again as soon as the window is closed:
clap_host::request_restart never reaches restartComponent — restartPlugin() only sets _requestRestart, which onIdle() consumes
clap_host::request_callback never reaches clap_plugin::on_main_thread
clap_params_host::request_flush never flushes
- the wrapper's own
_queueToUI is never drained
Concretely: a plug-in that defers reallocation to its next activate cycle — a changed FFT size, latency, or oversampling factor — asks for a restart that never arrives, and runs on stale settings indefinitely. A plug-in that defers anything to on_main_thread (retiring objects handed back by the audio thread, mark_dirty, deferred parameter rescans) never runs it, so those either pile up or are lost. This is the same class of gap as #521/#524 for AUv2, but Linux/VST3 and only in the no-window case.
The awkward part
I don't think there is a clean fix purely inside the wrapper, and I'd rather raise it than guess. IComponentHandler::restartComponent is [UI-thread & Connected], and on Linux the only UI thread the plug-in can reach is the host's run loop, which VST3 exposes solely through IPlugFrame::queryInterface(IRunLoop) — i.e. only when a view exists. A wrapper-owned thread could service the [thread-safe] CLAP callbacks but must not call restartComponent or performEdit from there.
So possibly the honest answers are some mix of: pump the thread-safe subset from a wrapper-owned timer thread; opportunistically drain at setActive/setProcessing/process boundaries for the parts that allow it; and document that the rest requires an open editor on Linux. Happy to prototype whichever direction you prefer.
Found while tracing why a CLAP plug-in's request_restart never lands in Ardour — that turned out to be an Ardour bug as well (it answers kIoChanged with kNotImplemented, the handler is #if 0), but this one is ours and is independent of the host. Separate from #527, which fixes two unrelated one-liners in the same file.
On Linux,
ClapAsVst3::onIdle()runs only while the plug-in's editor window is open. With no window — a headless instance, a plug-in the user never opened, or one whose window was closed again — it does not run at all, and everything the wrapper defers to it is silently dropped.Why
src/detail/os/linux.cpphas no timer.LinuxHelper::init()andterminate()are empty bodies,attach/detachonly push to and erase from_plugs, andLinuxHelper::executeDefered()— the function that would callonIdle()on each attached plug — has no caller. (The one textual reference at line 120 is inside the#if 0Windows block.) So_os_attached.on()inClapAsVst3::setActive(true)is a no-op on Linux.The other two platforms do have it:
detail/os/macos.mm—MacOSHelper::attachdoesCFRunLoopTimerCreate+CFRunLoopAddTimer(CFRunLoopGetMain(), ...), callback →executeDefered()→onIdle().detail/os/windows.cpp—WindowsHelper::initcreates a message window andSetTimer(_msgWin, 0, 20, NULL);WM_TIMER→executeDefered()→onIdle().Both pump for the lifetime of the instance, independent of any editor. On Linux the only driver is the
IdleHandlertimer registered against the host'sIRunLoop, and the wrapper only obtains that from the wrapped view —attachTimers(_wrappedview->getRunLoop())in thecreateViewattach callback, anddetachTimers(...)with_iRunLoop = nullptrwhen the view goes away.The comment already in
onIdle()notes it in passing:What it costs
With no editor open on Linux, all of these are dead, and become dead again as soon as the window is closed:
clap_host::request_restartnever reachesrestartComponent—restartPlugin()only sets_requestRestart, whichonIdle()consumesclap_host::request_callbacknever reachesclap_plugin::on_main_threadclap_params_host::request_flushnever flushes_queueToUIis never drainedConcretely: a plug-in that defers reallocation to its next activate cycle — a changed FFT size, latency, or oversampling factor — asks for a restart that never arrives, and runs on stale settings indefinitely. A plug-in that defers anything to
on_main_thread(retiring objects handed back by the audio thread,mark_dirty, deferred parameter rescans) never runs it, so those either pile up or are lost. This is the same class of gap as #521/#524 for AUv2, but Linux/VST3 and only in the no-window case.The awkward part
I don't think there is a clean fix purely inside the wrapper, and I'd rather raise it than guess.
IComponentHandler::restartComponentis[UI-thread & Connected], and on Linux the only UI thread the plug-in can reach is the host's run loop, which VST3 exposes solely throughIPlugFrame::queryInterface(IRunLoop)— i.e. only when a view exists. A wrapper-owned thread could service the[thread-safe]CLAP callbacks but must not callrestartComponentorperformEditfrom there.So possibly the honest answers are some mix of: pump the thread-safe subset from a wrapper-owned timer thread; opportunistically drain at
setActive/setProcessing/processboundaries for the parts that allow it; and document that the rest requires an open editor on Linux. Happy to prototype whichever direction you prefer.Found while tracing why a CLAP plug-in's
request_restartnever lands in Ardour — that turned out to be an Ardour bug as well (it answerskIoChangedwithkNotImplemented, the handler is#if 0), but this one is ours and is independent of the host. Separate from #527, which fixes two unrelated one-liners in the same file.