From 67e73a2a8548a0847587e56368f626379a2af6bc Mon Sep 17 00:00:00 2001 From: locphamtran Date: Sat, 15 Aug 2026 17:50:18 +0700 Subject: [PATCH] =?UTF-8?q?fix=20GUI=20VSTi=20mute/reopen/not-load:=20reve?= =?UTF-8?q?rt=20walkthrough=202=20ve=20hanh=20vi=200cec3d3=20(reloadForGUI?= =?UTF-8?q?=20reload-on-reopen,=20closeGUI=20khong=20goi=20view->removed()?= =?UTF-8?q?,=20post=5Fclose=5Fgui=20reload-on-close=20+=20same-plugin=20si?= =?UTF-8?q?lence=20+=20destroy=20children=20+=20pathBusy,=20editorOpenPred?= =?UTF-8?q?icate=20refcount=20mute,=20CoInitializeEx=20MTA,=20window=20mai?= =?UTF-8?q?n-thread),=20giu=20cai=20tien=20an=20toan=20(WM=5FERASEBKGND/PA?= =?UTF-8?q?INT/SIZE=20chong=20trang,=20resizeView,=20call=5Fonce,=20jobEve?= =?UTF-8?q?nt=5F/MsgWaitForMultipleObjects=20pump=20128,=20ABOVE=5FNORMAL?= =?UTF-8?q?=20+=20PowerThrottling=20worker,=20setBypassed=20quanh=20assign?= =?UTF-8?q?/unload);=20queryInterface=20giu=20kNoInterface=20nhu=200cec3d3?= =?UTF-8?q?=20=E2=80=94=20VERIFIED:=20probe=5Ffix=5Fflow=20PASS=20(mo=20GU?= =?UTF-8?q?I=20Nexus=20->=20close=20->=20reopen=20khong=20mute,=20load=20A?= =?UTF-8?q?mple=20khi=20GUI=20dang=20mo=20OK,=20screenshot=20khong=20trang?= =?UTF-8?q?);=20warning:=20driver=20stress=20test=206=20Nexus=20cung=20luc?= =?UTF-8?q?=20crash=20xac=20suat=20CA=20tren=20exe=20stable=200cec3d3=20(k?= =?UTF-8?q?hong=20phai=20regression)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- native_bridge/src/Vst3Instrument.cpp | 48 +++---- native_bridge/src/main.cpp | 191 ++++++++++++++++++--------- 2 files changed, 154 insertions(+), 85 deletions(-) diff --git a/native_bridge/src/Vst3Instrument.cpp b/native_bridge/src/Vst3Instrument.cpp index 822fca3..a78d937 100644 --- a/native_bridge/src/Vst3Instrument.cpp +++ b/native_bridge/src/Vst3Instrument.cpp @@ -97,13 +97,7 @@ constexpr ParamID kHostMidiProgramChange = 0x3000; // Minimal IComponentHandler so the plugin can inform the host of param edits. class HostComponentHandler : public IComponentHandler { public: - tresult queryInterface(const char* iid, void** v) override { - if (std::memcmp(iid, Steinberg::Vst::IComponentHandler::iid, 16) == 0 || - std::memcmp(iid, Steinberg::FUnknown::iid, 16) == 0) { - *v = static_cast(this); - addRef(); - return kResultOk; - } + tresult queryInterface(const char*, void** v) override { *v = nullptr; return kNoInterface; } @@ -118,13 +112,7 @@ public: // Minimal IPlugFrame so plugins can resize their editor view. class HostPlugFrame : public Steinberg::IPlugFrame { public: - tresult queryInterface(const char* iid, void** v) override { - if (std::memcmp(iid, Steinberg::IPlugFrame::iid, 16) == 0 || - std::memcmp(iid, Steinberg::FUnknown::iid, 16) == 0) { - *v = static_cast(this); - addRef(); - return kResultOk; - } + tresult queryInterface(const char*, void** v) override { *v = nullptr; return kNoInterface; } @@ -543,6 +531,18 @@ bool Vst3Instrument::reloadForGUI() { #else auto* s = static_cast(state_); if (!s || !s->controller) return false; + if (s->view && guiAttached_) return true; + if (hasAttachedOnce_ && !guiAttached_) { + // Reopen GUI sau khi dong: mot so plugin (Nexus...) hang o + // view->attached() LAN 2 tren cung component instance. Tao lai inst + // moi tren CUNG worker thread (COM STA con song) roi attach lai. + // PHAN NAY PHAI chay tren worker thread — apartment cua channel song + // o day; chay tren thread tam thi apartment moi chet ngay sau do + // (reopen lan sau -> treo nhu bridge_like). + if (!reload()) return false; + s = static_cast(state_); + if (!s || !s->controller) return false; + } return true; #endif } @@ -642,16 +642,16 @@ void Vst3Instrument::closeGUI() { return; #else auto* s = static_cast(state_); - if (s && s->view) { - if (guiAttached_) { - try { - s->view->removed(); - } catch (...) { - std::cerr << "[Vst3Instrument] Exception inside view->removed()" << std::endl; - } - } - s->view = nullptr; - } + // Do NOT call s->view->removed(): Nexus's removed() enters an internal + // modal wait for a message only this thread's message pump can dispatch + // — but this thread is inside removed(), so it deadlocks forever + // (observed: closeGUI job never returns; wdisp flood from the editor). + // Nulling the view and letting the next terminate() (reload / destructor) + // tear the editor windows down on this same thread is the deterministic + // path out. Callers (post_close_gui / load job) additionally destroy the + // editor's child windows right here on this thread so the plugin stops + // pumping; the leaked editor object is inert once its windows are gone. + if (s) s->view = nullptr; guiAttached_ = false; #endif } diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index 859e628..220aff7 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -116,6 +116,7 @@ static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam) // bi huy → WM_DESTROY tren con tro dangling → crash/hang bridge). class ChannelWorker; // fwd — WM_DESTROY posts closeGUI() to the channel worker static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker +static void post_resize_view(uint32_t ch, int w, int h); // defined after ChannelWorker static bool is_teardown_window(HWND hwnd, uint32_t pch); // fwd — defined after ChannelWorker static uint32_t pump_window_channel(HWND hwnd); // fwd — defined after ChannelWorker static InstrumentEngineManager* g_engine = nullptr; @@ -210,11 +211,8 @@ static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARA auto it = g_hwndToCh.find(hwnd); if (it != g_hwndToCh.end()) ch = it->second; } - if (ch != UINT32_MAX && g_engine) { - auto* inst = g_engine->get(ch); - if (inst) { - inst->resizeView(w, h); - } + if (ch != UINT32_MAX) { + post_resize_view(ch, w, h); } return 0; } @@ -450,6 +448,21 @@ private: #endif }; +// resizeView() must run on the channel worker (the view was attached in its +// COM STA apartment), never on the main pump thread. +static void post_resize_view(uint32_t ch, int w, int h) { + if (g_workers) { + auto wit = g_workers->find(ch); + if (wit != g_workers->end()) { + wit->second->post([ch, w, h]() { + if (!g_engine) return; + auto* i = g_engine->get(ch); + if (i) i->resizeView(w, h); + }); + } + } +} + // --- same-plugin-DLL silence helpers ---------------------------------------- // Two threads inside one VST3 DLL (Nexus) crash/deadlock: the audio loop's // process() on one instance must not race createInstance/createView/terminate @@ -638,18 +651,46 @@ static void post_close_gui(uint32_t ch, HWND hwnd) { // Silence first: the audio loop must not process() the instance // while we tear its editor down on this thread. g_engine->setReloading(ch, true); + // Close-in-flight: until the fresh instance is loaded, no other + // job may unmute this channel (its plugin is being torn down). { std::lock_guard lk(g_attachMutex); g_closeInFlight[ch] = true; } if (auto* i = g_engine->get(ch)) { + // closeGUI() nulls the view WITHOUT view->removed() (Nexus + // removed() deadlocks this worker - modal wait for a message + // only its own pump can dispatch, but it is inside removed()). try { i->closeGUI(); } catch (...) { std::cerr << "[NativeBridge] closeGUI EXCEPTION ch=" << ch << " — plugin threw" << std::endl; } + // CRASH FIX: the editor was dropped WITHOUT view->removed() — + // the instance's editor state is dangling, so the audio loop + // must NOT process() it. Rebuild a fresh instance NOW on this + // worker (its COM STA apartment is alive); reload() == + // terminate + loadPlugin (createInstance). Same-plugin + // channels are silenced so no second thread enters this DLL + // during createInstance (2 threads in one DLL -> Nexus exits + // silently, observed in probes). Serialized with other loads + // by g_loadMutex. + std::vector sp = same_plugin_channels(ch); + for (uint32_t y : sp) { + g_engine->setReloading(y, true); + } #ifdef _WIN32 + // CRASH FIX (0xc000041d): destroy the editor's child windows + // on the thread that OWNS them (the plugin's JUCE + // MessageManager thread - a single worker), never + // cross-thread. Wait for each destroy to finish (owner worker + // serializes it with its pump) BEFORE reload() - otherwise + // reload's createInstance enters the DLL while the owner + // thread is still inside the plugin wndproc (2 threads in one + // DLL -> Nexus crash). Kept OUTSIDE g_loadMutex: the wait + // must not block other loads (the owner worker may itself be + // waiting on that lock). disable_ime_contexts(hwnd); if (hwnd && IsWindow(hwnd)) { while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) { @@ -657,13 +698,51 @@ static void post_close_gui(uint32_t ch, HWND hwnd) { } } #endif - } - clear_editor_open(ch); - { + { + // reload() = terminate + loadPlugin (createInstance) — + // serialized with all other plugin-DLL entry points. + // Nexus may throw a C++ exception here too; catch it, + // treat as failed reload, and let the restore below + // unmute the channel (instance may be broken; next + // load/assign rebuilds it). + std::lock_guard lg(g_loadMutex); + try { + i->reload(); + } catch (...) { + std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch + << " — plugin threw (createInstance)" << std::endl; + } + } + // Editor detached + fresh instance loaded: drop editor-open + // state (channelQuiet falls back to reloading flags only). + clear_editor_open(ch); + // createInstance is done: unmuting is safe again. But if an + // attach for the SAME plugin path is still running (its + // createView must not race process() on any same-path + // channel), leave the silence in place — the attach job's + // restore unmutes everything once attachView finished. + { + std::lock_guard lk(g_attachMutex); + g_closeInFlight[ch] = false; + } + bool pathBusy = false; + { + std::lock_guard lk(g_attachMutex); + std::string p = lower_plugin_path(ch); + pathBusy = !p.empty() && + std::find(g_attachPaths.begin(), g_attachPaths.end(), p) != g_attachPaths.end(); + } + if (!pathBusy) { + for (uint32_t y : sp) unmute_if_not_closing(y, "closeGUI"); + g_engine->setReloading(ch, false); + } + } else { + // Instrument already gone (unloaded) — nothing to rebuild. + g_engine->setReloading(ch, false); + clear_editor_open(ch); std::lock_guard lk(g_attachMutex); g_closeInFlight[ch] = false; } - g_engine->setReloading(ch, false); }); } } @@ -671,8 +750,6 @@ static void post_close_gui(uint32_t ch, HWND hwnd) { int main(int argc, char* argv[]) { std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl; #ifdef _WIN32 - CoInitializeEx(nullptr, COINIT_MULTITHREADED); - // Disable Power Throttling (Efficiency Mode) for this background process #ifndef PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION #define PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION 0x00000004 @@ -686,9 +763,6 @@ int main(int argc, char* argv[]) { powerState.StateMask = 0; // Turn off throttling SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &powerState, sizeof(powerState)); - // Elevate main audio processing thread to time-critical priority - SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); - // DPI: declare per-monitor v2 FIRST (before any window is created). // Without it, on scaled displays (125/150%) the plugin's own renderer // paints at 100% into an OS-scaled window -> blank/white panel with dark @@ -768,8 +842,10 @@ int main(int argc, char* argv[]) { // Editor-open predicate: channelQuiet() checks whether ANY editor is // attached for the channel's plugin DLL path (refcounted in // g_editorPathCount, updated by the open/close GUI jobs). - instruments.setEditorOpenPredicate([](const std::string& /*lp*/) { - return false; + instruments.setEditorOpenPredicate([](const std::string& lp) { + std::lock_guard lk(g_editorMutex); + auto it = g_editorPathCount.find(lp); + return it != g_editorPathCount.end() && it->second > 0; }); // Per-channel persistent workers: loadPlugin + openGUI run on the SAME // thread whose COM STA apartment stays alive for the channel's lifetime @@ -883,9 +959,44 @@ int main(int argc, char* argv[]) { if (!workers[guiCh]) { workers[guiCh] = std::make_unique(); } - workers[guiCh]->post([&instruments, guiCh, arg1, arg2 = pluginId]() { + void* hwnd = (void*)arg1; +#ifdef _WIN32 + // Window PHAI thuoc MAIN thread (audio loop pump nay dispatch + // messages cua no moi vong lap). Tao/cap nhat window ngay tai day. + HWND nativeHwnd = nullptr; + { + std::lock_guard lock(g_guiMutex); + auto it = g_guiWindows.find(guiCh); + if (it != g_guiWindows.end()) nativeHwnd = (HWND)it->second; + } + if (hwnd == 0) { + if (nativeHwnd && IsWindow(nativeHwnd)) { + hwnd = (void*)nativeHwnd; + SetWindowTextA(nativeHwnd, pluginId.c_str()); + ShowWindow(nativeHwnd, SW_SHOW); + SetForegroundWindow(nativeHwnd); + // Reuse: cap nhat USERDATA (channel+1) - inst CU da bi thay + // the boi assign() -> WM_DESTROY sau nay lookup inst MOI. + SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1)); + } else { + nativeHwnd = (HWND)create_native_vst_window(pluginId.c_str()); + if (!nativeHwnd) { + std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << pluginId << std::endl; + return; + } + { + std::lock_guard lock(g_guiMutex); + g_guiWindows[guiCh] = nativeHwnd; // keep window alive + g_hwndToCh[nativeHwnd] = guiCh; // WM_DESTROY cleanup + } + SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1)); + hwnd = (void*)nativeHwnd; + } + } +#endif + workers[guiCh]->post([&instruments, guiCh, hwnd, arg2 = pluginId]() { if (!instruments.get(guiCh)) { - std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << (void*)arg1 + std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd << " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl; return; } @@ -894,44 +1005,6 @@ int main(int argc, char* argv[]) { return; } } - void* hwnd = (void*)arg1; -#ifdef _WIN32 - HWND nativeHwnd = nullptr; - { - std::lock_guard lock(g_guiMutex); - auto it = g_guiWindows.find(guiCh); - if (it != g_guiWindows.end()) nativeHwnd = (HWND)it->second; - } - if (hwnd == 0) { - if (nativeHwnd && IsWindow(nativeHwnd)) { - hwnd = (void*)nativeHwnd; - SetWindowTextA(nativeHwnd, arg2.c_str()); - ShowWindow(nativeHwnd, SW_SHOW); - SetForegroundWindow(nativeHwnd); - // Reuse: cap nhat USERDATA (channel+1) - inst CU da bi thay - // the boi assign() -> WM_DESTROY sau nay lookup inst MOI. - SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1)); - } else { - nativeHwnd = (HWND)create_native_vst_window(arg2.c_str()); - if (!nativeHwnd) { - std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl; - return; - } - { - std::lock_guard lock(g_guiMutex); - g_guiWindows[guiCh] = nativeHwnd; // keep window alive - g_hwndToCh[nativeHwnd] = guiCh; // WM_DESTROY cleanup - } - SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1)); - hwnd = (void*)nativeHwnd; - } - } -#endif - if (auto* inst0 = instruments.get(guiCh)) { - if (inst0->hasAttachedView()) { - return; - } - } #ifdef _WIN32 // Option B: chi 1 editor VST mo tai 1 thoi diem toan // bridge. Instance thu 2 cua CUNG plugin (Nexus) attach @@ -1063,10 +1136,7 @@ int main(int argc, char* argv[]) { // std::terminate the worker). Treat as attach failure; // the else branch below closes the empty window. try { - instruments.setBypassed(true); - sleep_ms(10); ok = inst->reloadForGUI(); - instruments.setBypassed(false); // IME recursion fix: strip the editor's IMC before // attachView creates the editor child — a live IMC on // a JUCE_ window + plugin wndproc (ImmIsUIMessageW -> @@ -1075,7 +1145,6 @@ int main(int argc, char* argv[]) { if (ok) disable_ime_contexts((HWND)hwnd); if (ok) ok = inst->attachView(hwnd); } catch (...) { - instruments.setBypassed(false); std::cerr << "[NativeBridge] GUI attach EXCEPTION ch=" << guiCh << " — plugin threw (createView/attached)" << std::endl; ok = false; @@ -1127,7 +1196,7 @@ int main(int argc, char* argv[]) { #endif std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd << " plugin=" << arg2 << " ch=" << guiCh - << " (channel active with editor open)" << std::endl; + << " (channel muted while editor open)" << std::endl; instruments.setReloading(guiCh, false); } else {