#!/usr/bin/env python3 """Apply owner-thread-destroy + pump-gate crash fix to main.cpp (LF file).""" import sys, io p = r"C:/Users/locpham/SonicForgeStudio/native_bridge/src/main.cpp" s = open(p, "rb").read().decode("utf-8") orig = s def rep(old, new, tag): global s n = s.count(old) assert n == 1, f"[{tag}] count={n}" s = s.replace(old, new, 1) print(f"ok: {tag}") # 1. globals rep( """static std::map>* g_workers = nullptr;""", """static std::map>* g_workers = nullptr; static std::mutex g_tidMutex; static std::map g_tidToWorker; // worker tid -> worker (owner-thread destroy)""", "globals") # 2. fwd decl rep( """static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker""", """static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker static bool is_teardown_window(HWND hwnd); // fwd — defined after ChannelWorker""", "fwd") # 3. constructor register tid rep( """ th_ = std::thread([this] { #ifdef _WIN32 OleInitialize(nullptr);""", """ th_ = std::thread([this] { #ifdef _WIN32 { std::lock_guard lk(g_tidMutex); g_tidToWorker[GetCurrentThreadId()] = this; } OleInitialize(nullptr);""", "register tid") # 4. pump gate rep( """ for (int pumped = 0; pumped < 16; ++pumped) { if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break; TranslateMessage(&msg);""", """ for (int pumped = 0; pumped < 16; ++pumped) { if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break; // 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. if (is_teardown_window(msg.hwnd)) continue; TranslateMessage(&msg);""", "pump gate") # 5. unregister tid rep( """#ifdef _WIN32 OleUninitialize(); #endif });""", """#ifdef _WIN32 { std::lock_guard lk(g_tidMutex); g_tidToWorker.erase(GetCurrentThreadId()); } OleUninitialize(); #endif });""", "unregister tid") # 6. post_and_wait rep( """ void post(std::function job) { { std::lock_guard lk(mu_); jobs_.push_back(std::move(job)); } cv_.notify_all(); } private:""", """ void post(std::function job) { { std::lock_guard lk(mu_); jobs_.push_back(std::move(job)); } 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(); }); } cv_.notify_all(); std::unique_lock dk(doneMx); return doneCv.wait_for(dk, std::chrono::seconds(5), [&] { return done; }); } private:""", "post_and_wait") # 7. helpers after close_in_flight rep( """static bool close_in_flight(uint32_t ch) { std::lock_guard lk(g_attachMutex); return g_closeInFlight[ch]; }""", """static bool close_in_flight(uint32_t ch) { std::lock_guard lk(g_attachMutex); return g_closeInFlight[ch]; } // True if hwnd (or any ancestor) is a plugin editor window whose channel is // mid-teardown (reloading or close job in flight). Editor child windows // (JUCE) have the native VST window as parent; the native window is in // g_hwndToCh. JUCE's global message window has no such parent - walk stops // -> false (never dropped). static bool is_teardown_window(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) { if (close_in_flight(ch)) return true; return g_engine ? g_engine->isReloading(ch) : false; } } 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; }""", "helpers") # 8. post_close_gui: destroy outside g_loadMutex, via owner rep( """ bool reloadOk = false; { // destroy children + reload in ONE lock: destroying the // editor windows runs the plugin's window proc (DLL // entry) on this worker; it must not race another // thread's createInstance/createView of the same DLL. // Same-thread destroy is valid (attachView created them // on this worker); without destroy the editor keeps // pumping (0x47b flood) while the audio loop processes // the instance (2 threads in one DLL) -> Nexus USER32 crash. std::lock_guard lg(g_loadMutex); // CRASH FIX: reload() = terminate + loadPlugin // (createInstance) — 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). try { #ifdef _WIN32 disable_ime_contexts(hwnd); if (hwnd && IsWindow(hwnd)) { while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) { std::cerr << "[dbg] closeGUI destroy child=" << c << " owner_tid=" << GetWindowThreadProcessId(c, nullptr) << " cur_tid=" << GetCurrentThreadId() << std::endl; DestroyWindow(c); } } #endif reloadOk = i->reload(); } catch (...) { std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch << " — plugin threw (createInstance)" << std::endl; reloadOk = false; } }""", """ bool reloadOk = false; #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)) { std::cerr << "[dbg] closeGUI destroy child=" << c << " owner_tid=" << GetWindowThreadProcessId(c, nullptr) << " cur_tid=" << GetCurrentThreadId() << std::endl; destroy_window_on_owner(c); } } #endif { // 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 { reloadOk = i->reload(); } catch (...) { std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch << " — plugin threw (createInstance)" << std::endl; reloadOk = false; } }""", "post_close_gui destroy") # 9. LOAD close-editor destroy loop via owner rep( """ // Destroy the old editor's child windows (worker-owned, // created by attachView here) so they stop pumping; // the parent window is hidden and kept for reuse. // See post_close_gui for the same pattern. while (HWND c = FindWindowExA(hToHide, nullptr, nullptr, nullptr)) { std::cerr << "[dbg] load close-editor destroy child=" << c << " owner_tid=" << GetWindowThreadProcessId(c, nullptr) << " cur_tid=" << GetCurrentThreadId() << std::endl; DestroyWindow(c); }""", """ // CRASH FIX (0xc000041d): destroy the old // editor's child windows on the thread that OWNS // them (JUCE MessageManager worker), never // cross-thread - DestroyWindow from this LOAD // worker raced the owner's pump dispatching // WM_PAINT into the plugin wndproc -> AV. Wait // for each destroy before assign() below. The // parent window is hidden and kept for reuse. // See post_close_gui for the same pattern. while (HWND c = FindWindowExA(hToHide, nullptr, nullptr, nullptr)) { std::cerr << "[dbg] load close-editor destroy child=" << c << " owner_tid=" << GetWindowThreadProcessId(c, nullptr) << " cur_tid=" << GetCurrentThreadId() << std::endl; destroy_window_on_owner(c); }""", "load destroy loop") assert s != orig open(p, "wb").write(s.encode("utf-8")) print("written")