diff --git a/install/daw_vst_bridge.exe b/install/daw_vst_bridge.exe index 8e92906..046ca04 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 c404559..e845ae2 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -72,18 +72,11 @@ static void disable_ime_contexts(HWND w) { } } -// CRASH FIX (run 16, 0xc0000005): thread ids owning JUCE_* windows (JUCE -// message windows have NO parent under the native window -> the pump gate's -// per-channel cases miss them). Recorded by the CBT hook at window birth and -// by the pump. While a CLOSE JOB is in flight (close_in_flight), a message -// bound for a JUCE owner thread must not be dispatched into its plugin DLL — -// observed: worker pump dispatching a Nexus wndproc (heap free) while another -// worker was inside createView -> 2 threads in one DLL -> Nexus AV/heap -// corruption. Over-gates every channel's JUCE windows during a teardown -// window (brief; editors may glitch, bridge survives). NOT gated on plain -// reloading (attach-silence) — that starves view->attached() (run 17 hang). -static std::mutex g_juceTidsMutex; -static std::set g_juceOwnerTids; + // G1.3: bo pump gate cu (g_juceOwnerTids / g_ownerTid / + // is_teardown_window): teardown (close/LOAD job) va message pump cung chay tren + // 1 thread (UiThread) — job chay thi pump dung, job xong thi editor window da + // destroy nen USER32 tu huy message toi window chet. Khong con "2 thread trong + // 1 DLL" khi teardown (multi-worker cu). // CBT hook: strip the IMC the moment any JUCE_* window is born (JUCE message // window AND editor child) — the post-attach disable_ime_contexts runs too @@ -99,10 +92,7 @@ static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam) char cls[64] = {0}; if (GetClassNameA(w, cls, 63) > 0 && std::strncmp(cls, "JUCE_", 5) == 0) { ImmAssociateContext(w, nullptr); - { - std::lock_guard lk(g_juceTidsMutex); - g_juceOwnerTids.insert(GetCurrentThreadId()); - } + } } return CallNextHookEx(g_cbtHook, nCode, wParam, lParam); @@ -117,15 +107,13 @@ static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam) 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; 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 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) // crash or deadlock. g_attachPaths = lowercase plugin paths whose reload/ // createView is running on some worker — a same-path close job must not unmute @@ -136,12 +124,7 @@ static std::map g_tidToWorker; // worker tid -> worker ( static std::mutex g_attachMutex; static std::vector g_attachPaths; static bool g_closeInFlight[16] = { false }; -static DWORD g_ownerTid[16] = { 0 }; -// Per channel: thread id of the plugin instance's JUCE MessageManager owner -// (recorded by the worker pump when it first sees the channel's editor -// window, and at attach end). 0 = unknown. Used by the pump teardown gate to -// drop messages for windows the owner thread pumps (incl. the instance's -// JUCE message window, which has no parent under the native window). + // Editor-open tracking: while a channel's VST editor (native window) is // attached, EVERY channel assigned the same plugin DLL path must stay quiet — // the plugin's window proc runs on the editor channel's worker while the @@ -271,10 +254,7 @@ public: #endif th_ = std::thread([this] { #ifdef _WIN32 - { - std::lock_guard lk(g_tidMutex); - g_tidToWorker[GetCurrentThreadId()] = this; - } + OleInitialize(nullptr); OleInitialize(nullptr); // Default IMC = none on this thread: new editor windows get // no IME context (see disable_ime_contexts). @@ -306,41 +286,11 @@ public: for (int pumped = 0; pumped < 128; ++pumped) { if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break; hadMessages = true; - // CRASH FIX (0xc000041d): JUCE editor child windows of a - // plugin DLL are owned by the FIRST worker that ran the - // DLL's global JUCE MessageManager - not by the channel - // worker doing the teardown. While another worker's LOAD / - // close job tears an instance down (reloading / close in - // flight), dispatching a message (e.g. WM_PAINT) into the - // plugin wndproc reads instance state being destroyed -> - // AV in USER32 (observed: crash on the owner worker's - // pump, right after the teardown job closed the view). - // Drop the message instead (PeekMessageW already removed - // it): the editor may glitch, the bridge survives. - uint32_t pch = pump_window_channel(msg.hwnd); - if (pch != UINT32_MAX) { - // Plugin-owned window pumped on this thread: remember - // its JUCE owner thread for teardown gating. - std::lock_guard lock(g_guiMutex); - g_ownerTid[pch] = GetCurrentThreadId(); - } - // Belt & braces: record JUCE window owner threads here too - // (a window born before the CBT hook was installed on its - // thread would otherwise never enter g_juceOwnerTids). - { - char cls[64] = ""; - if (msg.hwnd) GetClassNameA(msg.hwnd, cls, sizeof(cls)); - if (std::strncmp(cls, "JUCE_", 5) == 0) { - DWORD ot = GetWindowThreadProcessId(msg.hwnd, nullptr); - if (ot) { - std::lock_guard lock(g_juceTidsMutex); - g_juceOwnerTids.insert(ot); - } - } - } - if (is_teardown_window(msg.hwnd, pch)) { - continue; - } + // G1.3: bo pump gate cu — teardown (close/LOAD job) + // va pump cung 1 thread (UiThread): job chay thi pump dung; + // het job thi editor window da destroy -> message chet bi + // USER32 huy tu dong. Khong can drop theo close_in_flight. + TranslateMessage(&msg); // CRASH FIX (0xc000041d STATUS_FATAL_USER_CALLBACK_EXCEPTION): // a plugin window proc (Nexus throws nlohmann::json::out_of_range @@ -369,10 +319,7 @@ public: } } #ifdef _WIN32 - { - std::lock_guard lk(g_tidMutex); - g_tidToWorker.erase(GetCurrentThreadId()); - } + OleUninitialize(); OleUninitialize(); #endif }); @@ -408,34 +355,7 @@ public: #endif cv_.notify_all(); } - // Run job on THIS worker and wait (5s cap) until it finished. Used to - // serialize a window destroy with the owner worker's pump. MUST NOT be - // called from a job running on this same worker (deadlock) - callers - // destroy directly when owner tid == current tid. - bool post_and_wait(std::function job) { - std::mutex doneMx; - std::condition_variable doneCv; - bool done = false; - { - std::lock_guard lk(mu_); - jobs_.push_back([&]() { - job(); - { - std::lock_guard dk(doneMx); - done = true; - } - doneCv.notify_all(); - }); - } -#ifdef _WIN32 - if (jobEvent_) { - SetEvent(jobEvent_); - } -#endif - cv_.notify_all(); - std::unique_lock dk(doneMx); - return doneCv.wait_for(dk, std::chrono::seconds(5), [&] { return done; }); - } + private: std::thread th_; @@ -492,103 +412,24 @@ static bool close_in_flight(uint32_t ch) { return g_closeInFlight[ch]; } -// Channel owning hwnd via its ancestor chain to a registered native VST -// window (UINT32_MAX if none). JUCE editor children hang under the native -// window, so their parent chain resolves to the channel. -static uint32_t pump_window_channel(HWND hwnd) { - for (HWND h = hwnd; h; h = GetParent(h)) { - uint32_t ch = UINT32_MAX; - { - std::lock_guard lock(g_guiMutex); - auto it = g_hwndToCh.find(h); - if (it != g_hwndToCh.end()) ch = it->second; - } - if (ch != UINT32_MAX) return ch; - } - return UINT32_MAX; -} -// CRASH FIX (0xc000041d): drop a pump message whose dispatch would enter a -// plugin instance that another worker is tearing down (reloading / close in -// flight). Two cases: -// (a) the window's ancestor chain reaches the native window of a tearing- -// down channel (JUCE editor children); -// (b) the window's OWNER THREAD is the JUCE owner of a tearing-down -// channel's instance - covers the instance's JUCE message window (the -// 0x47B/1147 flood window): it has NO parent under the native window, -// is not destroyed by closeGUI, and after terminate() frees the -// instance, dispatching to it reads freed state -> AV in USER32 -// (observed: LOAD worker freed the old Nexus instance while the owner -// thread's pump dispatched msg=1147 -> 0xfeeefeee read). -// g_ownerTid[ch] recorded by the pump (first plugin window seen for ch) and -// at attach end; 0 = unknown -> case (b) inactive for that channel. -static bool is_teardown_window(HWND hwnd, uint32_t pch) { - DWORD ownerTid = GetWindowThreadProcessId(hwnd, nullptr); - if (ownerTid == 0) return false; - // Over-gate: a message bound for any JUCE window owner thread is dropped - // while a CLOSE JOB tears a channel down (close_in_flight) — the JUCE - // message window has no parent under the native window (pch==UINT32_MAX) - // and its owner thread may differ from g_ownerTid[y] (owner changes - // between runs), so the per-channel cases below alone let it through into - // the plugin DLL while another worker is inside createInstance/reload (2 - // threads in one DLL -> Nexus AV, run 16). Deliberately does NOT fire on - // plain reloading flags: the attach path marks same-plugin channels - // reloading for AUDIO silence, and gating their (and the attaching - // channel's own) JUCE windows then starves view->attached() of the - // cross-thread messages it needs -> attach hangs (observed run 17). - { - std::lock_guard lk(g_juceTidsMutex); - if (g_juceOwnerTids.count(ownerTid)) { - for (uint32_t y = 0; y < 16; ++y) - if (close_in_flight(y)) return true; - } - } - for (uint32_t y = 0; y < 16; ++y) { - bool closing = close_in_flight(y); - bool reloading = g_engine && g_engine->isReloading(y); - if (!closing && !reloading) continue; - if (y == pch) return true; - if (g_ownerTid[y] == ownerTid) return true; - } - return false; -} -// CRASH FIX (0xc000041d): plugin editor child windows are owned by the -// thread that first ran the plugin's JUCE MessageManager (a single worker), -// NOT by the channel worker doing the teardown. Cross-thread DestroyWindow -// races the owner's message pump. Post the destroy to the OWNER worker -// (serialized with its pump - no wndproc entry can race) and wait. Fall -// back to direct destroy when the owner is the current thread or unknown -// (leak > crash). Non-trivial: destroy jobs left queued on timeout are -// harmless - IsWindow guards them, and the pump gate already stopped -// dispatching to teardown windows. -static void destroy_window_on_owner(HWND hwnd) { - if (!hwnd || !IsWindow(hwnd)) return; - DWORD ownerTid = GetWindowThreadProcessId(hwnd, nullptr); - DWORD curTid = GetCurrentThreadId(); - if (ownerTid == 0 || ownerTid == curTid) { - if (IsWindow(hwnd)) DestroyWindow(hwnd); - return; - } - ChannelWorker* owner = nullptr; - { - std::lock_guard lk(g_tidMutex); - auto it = g_tidToWorker.find(ownerTid); - if (it != g_tidToWorker.end()) owner = it->second; - } - if (!owner) { - // Window belongs to a thread we don't control (e.g. main thread). - // Cross-thread DestroyWindow is unsafe -> leave it (leak > crash). - std::cerr << "[NativeBridge] destroy_window_on_owner: owner tid=" << ownerTid - << " not a bridge worker - leaving window " << hwnd << std::endl; - return; - } - if (!owner->post_and_wait([hwnd]() { - if (IsWindow(hwnd)) DestroyWindow(hwnd); - })) - std::cerr << "[NativeBridge] destroy_window_on_owner: timeout waiting owner tid=" - << ownerTid << " to destroy " << hwnd << std::endl; -} + // G1.3: 1 UiThread — editor windows tao trong attach job (UiThread), destroy + // chi goi tu UiThread job (close_editor_now / LOAD) -> ownerTid luon == current + // tid -> destroy truc tiep. Neu owner khac thread (khong con xay ra) -> bo + // (leak > crash): cross-thread DestroyWindow khong an toan. + static void destroy_window_on_owner(HWND hwnd) { + if (!hwnd || !IsWindow(hwnd)) return; + DWORD ownerTid = GetWindowThreadProcessId(hwnd, nullptr); + DWORD curTid = GetCurrentThreadId(); + if (ownerTid == 0 || ownerTid == curTid) { + if (IsWindow(hwnd)) DestroyWindow(hwnd); + return; + } + std::cerr << "[NativeBridge] destroy_window_on_owner: owner tid=" << ownerTid + << " != current " << curTid << " - leaving window " << hwnd << std::endl; + } + // Unmute y unless its own close job is still inside createInstance — that job // performs the unmute once its fresh instance is loaded (or, if an attach for @@ -764,11 +605,9 @@ int main(int argc, char* argv[]) { // without an exe manifest. Ignore failure (already aware). SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); // Default IMC = none on the main thread (IME recursion fix). + // Default IMC = none on the main thread (IME recursion fix). ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT); - // Catch JUCE_* window creation and strip its IMC at birth. Global hooks - // (dwThreadId=0) fail from an exe module (lpfn must be in a DLL) — hooks - // must be installed per thread; workers install their own in ChannelWorker. - g_cbtHook = SetWindowsHookExW(WH_CBT, ImeCbtHookProc, GetModuleHandleW(nullptr), GetCurrentThreadId()); + #endif // 1. Shared memory name: argv --shm | env SF_SHM_NAME | default @@ -1189,23 +1028,7 @@ int main(int argc, char* argv[]) { } } disable_ime_contexts((HWND)hwnd); -#ifdef _WIN32 - { - // Record the plugin editor child's owner thread (JUCE - // MessageManager thread) so the pump gate can drop - // messages to the instance's windows during teardown. - std::lock_guard lock(g_guiMutex); - auto git = g_guiWindows.find(guiCh); - if (git != g_guiWindows.end()) { - HWND nh = (HWND)git->second; - for (HWND c = FindWindowExA(nh, nullptr, nullptr, nullptr); - c; c = FindWindowExA(nh, c, nullptr, nullptr)) { - DWORD ot = GetWindowThreadProcessId(c, nullptr); - if (ot) { g_ownerTid[guiCh] = ot; break; } - } - } - } -#endif + std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd << " plugin=" << arg2 << " ch=" << guiCh << " (channel muted while editor open)" << std::endl; @@ -1219,8 +1042,9 @@ int main(int argc, char* argv[]) { #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). + // registry; WM_CLOSE -> UiThread pump destroy (owner = UiThread). // PostMessage an toan cross-thread (khong nhu DestroyWindow). + PostMessageA((HWND)hwnd, WM_CLOSE, 0, 0); #endif } @@ -1296,12 +1120,11 @@ int main(int argc, char* argv[]) { // thay the inst (VST3 -> SF2/inst khac) ma editor con song -> // old inst destructor goi view->removed() tren HWND con hoat // dong -> plugin block -> treo bridge. - // QUAN TRONG: window duoc tao tren MAIN thread (handleOpenGui - // goi create_native_vst_window o main loop) — DestroyWindow tu - // 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). + // G1.2/G1.3: window tao tren UiThread (handleOpenGui post attach + // job; create_native_vst_window chay trong job). LOAD job cung + // chay tren UiThread -> khong cross-thread destroy. An window, + // giu registry de reuse; editor detach + reload o duoi. + HWND hToHide = nullptr; { std::lock_guard lock(g_guiMutex);