diff --git a/install/daw_vst_bridge.exe b/install/daw_vst_bridge.exe index d855a20..8e92906 100644 Binary files a/install/daw_vst_bridge.exe and b/install/daw_vst_bridge.exe differ diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index 53f2487..c404559 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -123,7 +123,7 @@ static InstrumentEngineManager* g_engine = nullptr; static std::mutex g_guiMutex; static std::map g_guiWindows; // channel -> HWND (keep window alive) static std::map g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup) -static std::map>* g_workers = nullptr; +static ChannelWorker* g_uiWorker = nullptr; static std::mutex g_tidMutex; static std::map g_tidToWorker; // worker tid -> worker (owner-thread destroy) // Same-plugin-DLL reentrancy guards: two threads inside one VST3 DLL (Nexus) @@ -256,8 +256,8 @@ static void* create_native_vst_window(const char* title) { #endif } -// Per-channel persistent worker: ONE thread owns the COM STA apartment for that -// channel's instrument for its whole lifetime. loadPlugin and openGUI MUST run +// G1.2: single persistent UiThread — ONE thread owns the COM STA apartment for +// ALL instruments for the whole bridge lifetime. loadPlugin and openGUI MUST run // on the same thread — if the loading thread exits, its apartment dies and // VST3 plugins that marshal internally (Nexus) hang forever in // view->attached(). Verified with gui_probe: `bridge_like` (load thread exits, @@ -451,15 +451,12 @@ private: // 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); - }); - } + if (g_uiWorker) { + g_uiWorker->post([ch, w, h]() { + if (!g_engine) return; + auto* i = g_engine->get(ch); + if (i) i->resizeView(w, h); + }); } } @@ -620,27 +617,15 @@ static void clear_editor_open(uint32_t ch) { } } -// closeGUI() MUST run on the channel worker thread (its COM STA apartment) — -// the view was attached there. Calling view->removed() from the main thread -// (WM_DESTROY handler) is a cross-apartment COM call that corrupts the plugin; -// Nexus then hangs on the NEXT view->attached(). Erase the registry inside the -// job so Option B (closing another editor before attach) waits for closeGUI to -// actually finish. -static void post_close_gui(uint32_t ch, HWND hwnd) { - ChannelWorker* w = nullptr; - if (g_workers) { - auto wit = g_workers->find(ch); - if (wit != g_workers->end()) w = wit->second.get(); - } - if (w) { - // closeGUI() (view->removed()) PHAI chay tren channel worker - COM STA - // apartment cua plugin song o do. Window KHONG bi destroy: VstWindowProc - // giu lai (an) de reuse nen plugin editor children (worker-owned) khong - // bao gio bi huy cross-thread. Registry (g_guiWindows/g_hwndToCh) chi - // xoa trong WM_DESTROY khi window thuc su bi huy. - w->post([ch, hwnd]() { - // Guard window-replaced race: neu OPEN_GUI moi da dung window khac - // (hoac da xoa), khong detach view cua window moi. +// closeGUI() MUST run on the UiThread (its COM STA apartment) — the view was +// attached there. Calling view->removed() from another thread is a +// cross-apartment COM call that corrupts the plugin; Nexus then hangs on the +// NEXT view->attached(). G1.2: 1 UiThread chung — close chay synchronous tren +// job; goi truc tiep (Option B trong attach job) hoac qua post_close_gui +// (WM_CLOSE/WM_DESTROY). +static void close_editor_now(uint32_t ch, HWND hwnd) { + // Guard window-replaced race: neu OPEN_GUI moi da dung window khac + // (hoac da xoa), khong detach view cua window moi. { std::lock_guard lock(g_guiMutex); auto it = g_guiWindows.find(ch); @@ -748,7 +733,11 @@ static void post_close_gui(uint32_t ch, HWND hwnd) { std::lock_guard lk(g_attachMutex); g_closeInFlight[ch] = false; } - }); +} + +static void post_close_gui(uint32_t ch, HWND hwnd) { + if (g_uiWorker) { + g_uiWorker->post([ch, hwnd]() { close_editor_now(ch, hwnd); }); } } @@ -855,8 +844,11 @@ int main(int argc, char* argv[]) { // Per-channel persistent workers: loadPlugin + openGUI run on the SAME // thread whose COM STA apartment stays alive for the channel's lifetime // (see ChannelWorker comment — a dead apartment hangs Nexus attached()). - std::map> workers; - g_workers = &workers; + // G1.2: MOT UiThread chung (COM STA + message pump + job queue) cho moi + // channel — thay 16 ChannelWorker. Verify gui_probe `shared_worker` (G1.1): + // load + openGUI 2 instance tren 1 worker OK, khong crash/hang. + ChannelWorker uiWorker; + g_uiWorker = &uiWorker; // B9: native editor windows per channel — keep alive (HWND outlives the job). // Registry la global (g_guiWindows) — WM_DESTROY cleanup can tu VstWindowProc. // B8: sample rate from the DAW (Rust spawns us with SF_SAMPLE_RATE). @@ -982,43 +974,62 @@ int main(int argc, char* argv[]) { g_pendingGui[guiCh] = { (void*)arg1, pluginId }; return; } - if (!workers[guiCh]) { - workers[guiCh] = std::make_unique(); - } - 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; + // G1.2: window + attach chay tren UiThread (1 thread chung, COM STA + + // message pump + job queue). Window messages do UiThread pump dispatch — + // audio loop khong con PeekMessageW (xoa block pump o main loop). + // G1.1 verify: gui_probe shared_worker = 1 worker load+openGUI 2 inst OK. + uiWorker.post([&instruments, shmIPC, guiCh, arg1, arg2 = pluginId]() { + AttachInFlightGuard _aifGuard{shmIPC}; + if (!instruments.get(guiCh)) { + std::cerr << "[NativeBridge] GUI attach FAILED hwnd=0" + << " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl; + return; + } + if (auto* inst0 = instruments.get(guiCh)) { + if (inst0->hasAttachedView()) { + // Dedupe race: main da silence (setReloading) truoc khi post — + // restore de channel khong cam vinh vien. + std::vector sp = same_plugin_channels(guiCh); + for (uint32_t y : sp) unmute_if_not_closing(y, "openGUI-dedupe"); + unmute_if_not_closing(guiCh, "openGUI-dedupe"); 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; } - } + 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; + std::vector sp = same_plugin_channels(guiCh); + for (uint32_t y : sp) unmute_if_not_closing(y, "openGUI-createfail"); + unmute_if_not_closing(guiCh, "openGUI-createfail"); + 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 // G0.1b: silence same-path instances on the MAIN thread BEFORE the // attach job runs - renderAll of this loop iteration (and any before @@ -1042,31 +1053,15 @@ int main(int argc, char* argv[]) { instruments.setReloading(guiCh, true); } } - workers[guiCh]->post([&instruments, shmIPC, guiCh, hwnd, arg2 = pluginId]() { - AttachInFlightGuard _aifGuard{shmIPC}; - if (!instruments.get(guiCh)) { - std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd - << " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl; - return; - } - 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 - // view o apartment/worker khac -> treo. Dong editor cua - // channel khac TRUOC khi attach: WM_CLOSE -> main pump - // destroy window -> WM_DESTROY -> closeGUI() + xoa registry. - // Chay tren worker job de khong stall writeIndex cua real-time - // loop. CRASH FIX: silence moi channel TRUOC khi dong editor - // cua no (view->removed() tren worker cung luc process() tren - // audio loop = 2 thread trong 1 plugin -> crash USER32). + // Option B: chi 1 editor VST mo tai 1 thoi diem toan bridge. Instance + // thu 2 cua CUNG plugin (Nexus) attach view o thread khac -> treo. + // G1.2: 1 UiThread -> dong editor cua channel khac DONG BO (goi + // close_editor_now truc tiep). Khong the PostMessage(WM_CLOSE) cho + // job khac nhu cu — job close nam sau job hien tai trong queue cua + // CUNG thread -> khong bao gio chay -> 5s timeout -> attach abort. { std::vector others; - bool abortAttach = false; { std::lock_guard lock(g_guiMutex); for (const auto& kv : g_guiWindows) @@ -1088,39 +1083,7 @@ int main(int argc, char* argv[]) { if (it != g_guiWindows.end()) yHwnd = (HWND)it->second; } if (!yHwnd || !IsWindow(yHwnd)) continue; - if (instruments.get(y)) { - instruments.setReloading(y, true); - } - PostMessage(yHwnd, WM_CLOSE, 0, 0); - // WM_CLOSE -> VstWindowProc -> post_close_gui -> closeGUI() - // tren worker cua channel y. Doi cho view da detach (window - // duoc giu lai de reuse, khong doi registry erase nhu cu). - bool closed = false; - // The close job = closeGUI + destroy children + fresh - // reload (createInstance, ~2s for Nexus). hasAttachedView - // flips false after closeGUI only - ALSO wait for the - // close job to finish entirely (close_in_flight): the - // attach must not run createView while another thread is - // still inside this plugin DLL (2 threads in one DLL -> - // Nexus exits silently, observed in probes). - for (int i = 0; i < 500; ++i) { // 5s budget - auto* yi = instruments.get(y); - bool detached = !yi || !yi->hasAttachedView(); - if (detached && !close_in_flight(y)) { closed = true; break; } - Sleep(10); - } - if (!closed) { - abortAttach = true; - break; - } - } - if (abortAttach) { - // A close job is stuck; do NOT createView (would race the - // plugin DLL). Unmute what we silenced (skips channels - // whose close job is still in flight - that job unmutes - // them when it finishes). - for (uint32_t y : others) unmute_if_not_closing(y, "openGUI-abort"); - return; + close_editor_now(y, yHwnd); } } #else @@ -1271,27 +1234,8 @@ int main(int argc, char* argv[]) { // escapes the targeted guards (e.g. inside renderAll -> process()) // must not kill the bridge. Log and continue the loop. try { -#ifdef _WIN32 - // Message pump: VST editors (Nexus, JUCE-based...) block inside - // view->attached() until the host dispatches messages — openGUI runs - // on a worker thread, so THIS loop must pump concurrently (verified - // with gui_probe: worker-thread openGUI + concurrent pump → attached - // returns kResultOk; without it → hangs forever). - MSG msg; - int pumpedMain = 0; - while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { - TranslateMessage(&msg); - // CRASH FIX (0xc000041d): same as the worker pump — a C++ exception - // raised inside a plugin window proc (Nexus) must never escape - // DispatchMessageW as an unhandled fatal callback exception. - try { - DispatchMessageW(&msg); - } catch (...) { - std::cerr << "[NativeBridge] main pump EXCEPTION — plugin window proc threw" << std::endl; - } - ++pumpedMain; - } -#endif + // G1.2: audio loop KHONG pump messages nua — moi window (native VST + + // editor children) thuoc UiThread; UiThread pump dispatch chung. // A. Control events — non-rt safe, drained first for (uint32_t i = 0; i < shmIPC->controlQueueCount; ++i) { const auto& c = shmIPC->controlQueue[i]; @@ -1310,10 +1254,7 @@ int main(int argc, char* argv[]) { std::string path(c.arg2, plen); InstrumentType t = (InstrumentType)c.arg0; uint32_t ch = c.channel & 0xF; - if (!workers[ch]) { - workers[ch] = std::make_unique(); - } - workers[ch]->post([&instruments, t, ch, path, sampleRate, block]() { + uiWorker.post([&instruments, t, ch, path, sampleRate, block]() { // CRASH FIX: this job enters the plugin DLL (createInstance // in loadPlugin, old-instance terminate on replace) while // the audio loop may process() another instance of the SAME