fix(bridge): VSTi GUI crash on instrument switch — PostMessage WM_CLOSE instead of cross-thread DestroyWindow

- LOAD job posts WM_CLOSE to window created on main thread (DestroyWindow from worker crashed 0xc000041d, muting all tracks)
- handleOpenGui + attach jobs skip when Vst3Instrument::hasAttachedView() (state_ && guiAttached_)
- attach failure posts WM_CLOSE; post_close_gui erases entry only when it points at the same hwnd
- cap openVstGuiRetry at 5 to stop frontend retry spam
This commit is contained in:
2026-08-14 10:18:22 +07:00
parent f7f20e3a0a
commit 35499b614c
5 changed files with 54 additions and 12 deletions
+4 -4
View File
@@ -6213,11 +6213,11 @@ const ensureAndOpenVstGui = async (trackId, instrumentId) => {
try { await openVstGuiRetry(instrumentId, bch, 20); } catch (e) { console.warn('[Bridge] openNativeGUI fail:', e); } try { await openVstGuiRetry(instrumentId, bch, 20); } catch (e) { console.warn('[Bridge] openNativeGUI fail:', e); }
}; };
// V8 bug 4 + V9 bug 3/6/7: C++ load instrument ASYNC (worker thread) OPEN_GUI // V8 bug 4 + V9 bug 3/6/7: C++ load instrument ASYNC (worker thread) OPEN_GUI
// som C++ defer (chi log stderr, invoke van tra Ok) retry PHAI chay DU so // som C++ defer va tu fulfil khi assign() xong (pending-GUI sweep) khong
// lan, khong `return true` som. Mo GUI co the can cho den khi assign() xong // can spam. 20 lan x 500ms tao deferred storm (50+ event/moi restart) va
// (Nexus ~1-2s) 20 lan x 500ms, window reuse khi load xong. // re-attach view dang attached (crash USER32 0xc000041d). Cap 5.
const openVstGuiRetry = async (instrumentId, bch, attempts) => { const openVstGuiRetry = async (instrumentId, bch, attempts) => {
const n = (attempts && attempts > 0) ? attempts : 20; const n = Math.min((attempts && attempts > 0) ? attempts : 5, 5);
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
try { await window.NativeBridgeService.openNativeGUI(instrumentId, bch); } try { await window.NativeBridgeService.openNativeGUI(instrumentId, bch); }
catch (e) { console.warn('[Bridge] openNativeGUI retry', i + 1, e); } catch (e) { console.warn('[Bridge] openNativeGUI retry', i + 1, e); }
@@ -46,6 +46,10 @@ public:
// with the channel worker pumping messages (see bridge main.cpp). // with the channel worker pumping messages (see bridge main.cpp).
virtual bool reloadForGUI() { return true; } virtual bool reloadForGUI() { return true; }
virtual bool attachView(void* /*parentWindowHandle*/) { return false; } virtual bool attachView(void* /*parentWindowHandle*/) { return false; }
// True while the editor view is attached (GUI window open). The bridge uses
// it to dedupe repeated OPEN_GUI (frontend retry spam) — re-attaching a
// view that is already attached corrupts plugins (Nexus createView→null).
virtual bool hasAttachedView() const { return false; }
// Real-time Audio PCM Float32 rendering loop // Real-time Audio PCM Float32 rendering loop
virtual void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) = 0; virtual void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) = 0;
+1
View File
@@ -27,6 +27,7 @@ public:
bool openGUI(void* parentWindowHandle) override; bool openGUI(void* parentWindowHandle) override;
bool reloadForGUI() override; bool reloadForGUI() override;
bool attachView(void* parentWindowHandle) override; bool attachView(void* parentWindowHandle) override;
bool hasAttachedView() const override;
void closeGUI() override; void closeGUI() override;
void setChannel(uint32_t ch) { channel_ = ch; } void setChannel(uint32_t ch) { channel_ = ch; }
bool needsReload() const override { return hasAttachedOnce_ && !guiAttached_; } bool needsReload() const override { return hasAttachedOnce_ && !guiAttached_; }
+4
View File
@@ -635,6 +635,10 @@ bool Vst3Instrument::attachView(void* parentWindowHandle) {
#endif #endif
} }
bool Vst3Instrument::hasAttachedView() const {
return state_ != nullptr && guiAttached_;
}
bool Vst3Instrument::reload() { bool Vst3Instrument::reload() {
#ifndef HAVE_VST3SDK #ifndef HAVE_VST3SDK
return false; return false;
+41 -8
View File
@@ -197,12 +197,17 @@ static void post_close_gui(uint32_t ch, HWND hwnd) {
if (auto* i = g_engine->get(ch)) i->closeGUI(); if (auto* i = g_engine->get(ch)) i->closeGUI();
std::lock_guard<std::mutex> lock(g_guiMutex); std::lock_guard<std::mutex> lock(g_guiMutex);
g_hwndToCh.erase(hwnd); g_hwndToCh.erase(hwnd);
g_guiWindows.erase(ch); // Chi erase neu map van tro den HWND NAY — neu reopen da tao
// window moi (race: close cu + OPEN_GUI moi), khong duoc xoa
// entry cua window moi (con tro treo -> leak window moi).
auto wit = g_guiWindows.find(ch);
if (wit != g_guiWindows.end() && wit->second == hwnd) g_guiWindows.erase(wit);
}); });
} else { } else {
std::lock_guard<std::mutex> lock(g_guiMutex); std::lock_guard<std::mutex> lock(g_guiMutex);
g_hwndToCh.erase(hwnd); g_hwndToCh.erase(hwnd);
g_guiWindows.erase(ch); auto wit = g_guiWindows.find(ch);
if (wit != g_guiWindows.end() && wit->second == hwnd) g_guiWindows.erase(wit);
} }
} }
@@ -361,6 +366,16 @@ int main(int argc, char* argv[]) {
g_pendingGui[guiCh] = { (void*)arg1, pluginId }; g_pendingGui[guiCh] = { (void*)arg1, pluginId };
return; return;
} }
// Dedupe OPEN_GUI spam (frontend openVstGuiRetry): view dang
// attached -> GUI da mo, khong post attach job lan nua. Re-attach
// tren view dang attached lam plugin loi (Nexus createView null).
if (auto* i0 = instruments.get(guiCh)) {
if (i0->hasAttachedView()) {
std::cerr << "[dbg] openGUI: view already attached ch=" << guiCh
<< " — skip" << std::endl;
return;
}
}
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>(); if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
void* hwnd = (void*)arg1; void* hwnd = (void*)arg1;
#ifdef _WIN32 #ifdef _WIN32
@@ -405,6 +420,13 @@ int main(int argc, char* argv[]) {
} }
std::cerr << "[dbg] openGUI thread start hwnd=" << hwnd std::cerr << "[dbg] openGUI thread start hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl; << " plugin=" << arg2 << " ch=" << guiCh << std::endl;
if (auto* inst0 = instruments.get(guiCh)) {
if (inst0->hasAttachedView()) {
std::cerr << "[dbg] openGUI: view already attached ch=" << guiCh
<< " — skip" << std::endl;
return;
}
}
#ifdef _WIN32 #ifdef _WIN32
// Option B: chi 1 editor VST mo tai 1 thoi diem toan // Option B: chi 1 editor VST mo tai 1 thoi diem toan
// bridge. Instance thu 2 cua CUNG plugin (Nexus) attach // bridge. Instance thu 2 cua CUNG plugin (Nexus) attach
@@ -468,9 +490,17 @@ int main(int argc, char* argv[]) {
if (ok) if (ok)
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl; << " plugin=" << arg2 << " ch=" << guiCh << std::endl;
else else {
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
<< " plugin=" << arg2 << std::endl; << " plugin=" << arg2 << std::endl;
#ifdef _WIN32
// Attach that — khong co view (VD: channel la SF2/SFZ hoac plugin
// loi). Dong ngay cua so vo nghia de khong con window treo trong
// registry; WM_CLOSE -> main pump destroy tren main thread (owner).
// PostMessage an toan cross-thread (khong nhu DestroyWindow).
PostMessageA((HWND)hwnd, WM_CLOSE, 0, 0);
#endif
}
} }
}); });
}; };
@@ -513,10 +543,13 @@ int main(int argc, char* argv[]) {
// DONG cua so editor dang mo cua channel TRUOC khi assign(): // DONG cua so editor dang mo cua channel TRUOC khi assign():
// thay the inst (VST3 -> SF2/inst khac) ma editor con song -> // thay the inst (VST3 -> SF2/inst khac) ma editor con song ->
// old inst destructor goi view->removed() tren HWND con hoat // old inst destructor goi view->removed() tren HWND con hoat
// dong -> plugin block -> treo bridge. DestroyWindow chay tren // dong -> plugin block -> treo bridge.
// CUNG worker thread so huu window; WM_DESTROY goi closeGUI() // QUAN TRONG: window duoc tao tren MAIN thread (handleOpenGui
// dung thu tu. Phai erase map TRUOC DestroyWindow (WM_DESTROY // goi create_native_vst_window o main loop) — DestroyWindow tu
// handler lay g_guiMutex lai — khong duoc giu lock khi destroy). // worker thread la cross-thread (MSDN cam), gay crash USER32
// 0xc000041d. Post WM_CLOSE -> main pump destroy window tren
// DUNG thread so huu no. Registry da erase o tren nen WM_DESTROY
// khong goi closeGUI tren inst cu (inst moi chua co view).
HWND hToDestroy = nullptr; HWND hToDestroy = nullptr;
{ {
std::lock_guard<std::mutex> lock(g_guiMutex); std::lock_guard<std::mutex> lock(g_guiMutex);
@@ -527,7 +560,7 @@ int main(int argc, char* argv[]) {
g_hwndToCh.erase(hToDestroy); g_hwndToCh.erase(hToDestroy);
} }
} }
if (hToDestroy && IsWindow(hToDestroy)) DestroyWindow(hToDestroy); if (hToDestroy && IsWindow(hToDestroy)) PostMessageA(hToDestroy, WM_CLOSE, 0, 0);
#endif #endif
std::cerr << "[dbg] load thread start ch=" << ch std::cerr << "[dbg] load thread start ch=" << ch
<< " type=" << (int)t << " path=" << path << std::endl; << " type=" << (int)t << " path=" << path << std::endl;