diff --git a/install/daw_vst_bridge.exe b/install/daw_vst_bridge.exe index 046ca04..57fcc64 100644 Binary files a/install/daw_vst_bridge.exe and b/install/daw_vst_bridge.exe differ diff --git a/native_bridge/include/NativeInstrumentEngine.h b/native_bridge/include/NativeInstrumentEngine.h index e478af2..6aaec3a 100644 --- a/native_bridge/include/NativeInstrumentEngine.h +++ b/native_bridge/include/NativeInstrumentEngine.h @@ -110,7 +110,6 @@ public: // plugin DLL path has an open editor count as quiet (same-plugin channels // too -- the worker pumps the editor's window proc inside the DLL while the // audio loop process()es other instances of the same DLL). - void setEditorOpenPredicate(std::function fn); // Flush every sounding note on every assigned channel. void allNotesOff(); @@ -137,7 +136,6 @@ private: // is in progress): real-time code drops MIDI and skips rendering. Lowercased // compare inside -- Windows plugin paths are case-insensitive. bool channelQuiet(uint32_t ch) const; - std::function editorOpenFn_; std::atomic isBypassed_{false}; }; diff --git a/native_bridge/src/NativeInstrumentEngine.cpp b/native_bridge/src/NativeInstrumentEngine.cpp index 0e5e0ee..0300910 100644 --- a/native_bridge/src/NativeInstrumentEngine.cpp +++ b/native_bridge/src/NativeInstrumentEngine.cpp @@ -260,24 +260,12 @@ bool InstrumentEngineManager::isReloading(uint32_t channel) const { return channel < 16 && reloadingCh_[channel]; } -void InstrumentEngineManager::setEditorOpenPredicate(std::function fn) { - std::lock_guard lock(mu_); - editorOpenFn_ = std::move(fn); -} - -// Quiet = reloading OR an editor attached for the channel's plugin DLL path. -// Called with mu_ held by the real-time dispatch (the predicate locks -// g_editorMutex -- lock order mu_ -> g_editorMutex; g_editorMutex is never -// held while taking mu_). +// Quiet = reloading (mid-rebuild) only. G1.4: bo mute-when-editor-open β€” +// editor co the mo trong luc PLAY, audio loop van process() binh thuong. +// Called with mu_ held by the real-time dispatch. bool InstrumentEngineManager::channelQuiet(uint32_t ch) const { if (ch >= 16 || reloadingCh_[ch]) return true; - if (!editorOpenFn_) return false; - auto it = paths_.find(ch); - if (it == paths_.end() || it->second.empty()) return false; - std::string lp = it->second; - std::transform(lp.begin(), lp.end(), lp.begin(), - [](unsigned char c) { return (char)::tolower(c); }); - return editorOpenFn_(lp); + return false; } // Real-time MIDI dispatch: hold mu_ for the WHOLE call so assign()/unload() diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index e845ae2..a18d9cc 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -1,419 +1,406 @@ -// native_bridge/src/main.cpp -// Entry point of daw_vst_bridge.exe β€” opens shared memory created by the DAW -// (Rust/Tauri side), runs the real-time MIDI->audio loop, watches the parent. -#include "INativeInstrument.h" -#include "SharedMemoryIPC.h" -#include "NativeInstrumentEngine.h" - -#ifdef _WIN32 -#include -#include -#include -#include -#include -#else -#include -#include -#include -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// --- platform helpers ------------------------------------------------------- -static bool parent_alive(uint32_t pid) { -#ifdef _WIN32 - HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); - if (!h) return false; - CloseHandle(h); - return true; -#else - return pid == 0 || (kill(pid, 0) == 0); -#endif -} - -static void sleep_ms(uint32_t ms) { -#ifdef _WIN32 - Sleep(ms); -#else - std::this_thread::sleep_for(std::chrono::milliseconds(ms)); -#endif -} - -#ifdef _WIN32 -// IME recursion fix (Nexus 0xC00000FD -> USER32 0xC000041D): the plugin's -// wndproc calls ImmIsUIMessageW for every message and forwards WM_IME_* to the -// IME window; with a live IMC on the editor window that bounces back to the -// same hwnd -> infinite SendMessageW recursion -> stack overflow. Removing the -// IMC (ImmAssociateContext NULL) stops the forwarding. -static void disable_ime_contexts(HWND w) { - if (w && IsWindow(w)) { - char cls[64] = {0}; - GetClassNameA(w, cls, 63); - ImmAssociateContext(w, nullptr); - for (HWND c = GetWindow(w, GW_CHILD); c; c = GetWindow(c, GW_HWNDNEXT)) { - char cls2[64] = {0}; - GetClassNameA(c, cls2, 63); - ImmAssociateContext(c, nullptr); - } - } -} - +// native_bridge/src/main.cpp +// Entry point of daw_vst_bridge.exe β€” opens shared memory created by the DAW +// (Rust/Tauri side), runs the real-time MIDI->audio loop, watches the parent. +#include "INativeInstrument.h" +#include "SharedMemoryIPC.h" +#include "NativeInstrumentEngine.h" + +#ifdef _WIN32 +#include +#include +#include +#include +#include +#else +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// --- platform helpers ------------------------------------------------------- +static bool parent_alive(uint32_t pid) { +#ifdef _WIN32 + HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (!h) return false; + CloseHandle(h); + return true; +#else + return pid == 0 || (kill(pid, 0) == 0); +#endif +} + +static void sleep_ms(uint32_t ms) { +#ifdef _WIN32 + Sleep(ms); +#else + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); +#endif +} + +#ifdef _WIN32 +// IME recursion fix (Nexus 0xC00000FD -> USER32 0xC000041D): the plugin's +// wndproc calls ImmIsUIMessageW for every message and forwards WM_IME_* to the +// IME window; with a live IMC on the editor window that bounces back to the +// same hwnd -> infinite SendMessageW recursion -> stack overflow. Removing the +// IMC (ImmAssociateContext NULL) stops the forwarding. +static void disable_ime_contexts(HWND w) { + if (w && IsWindow(w)) { + char cls[64] = {0}; + GetClassNameA(w, cls, 63); + ImmAssociateContext(w, nullptr); + for (HWND c = GetWindow(w, GW_CHILD); c; c = GetWindow(c, GW_HWNDNEXT)) { + char cls2[64] = {0}; + GetClassNameA(c, cls2, 63); + ImmAssociateContext(c, nullptr); + } + } +} + // 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 -// late; the recursion can start inside attachView (0xC00000FD observed, run -// 9). Runs on the window's creating thread, so the association is legal. -static HHOOK g_cbtHook = nullptr; -static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam) { - if (nCode == HCBT_CREATEWND) { - // Class name via GetClassNameA, NOT lpszClass: JUCE creates windows - // with MAKEINTATOM class names (HIWORD(lpszClass)==0), and ANSI-created - // windows deliver a CREATESTRUCTA β€” lpszClass is unusable in both cases. - HWND w = (HWND)wParam; - char cls[64] = {0}; - if (GetClassNameA(w, cls, 63) > 0 && std::strncmp(cls, "JUCE_", 5) == 0) { + +// 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 +// late; the recursion can start inside attachView (0xC00000FD observed, run +// 9). Runs on the window's creating thread, so the association is legal. +static HHOOK g_cbtHook = nullptr; +static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam) { + if (nCode == HCBT_CREATEWND) { + // Class name via GetClassNameA, NOT lpszClass: JUCE creates windows + // with MAKEINTATOM class names (HIWORD(lpszClass)==0), and ANSI-created + // windows deliver a CREATESTRUCTA β€” lpszClass is unusable in both cases. + HWND w = (HWND)wParam; + char cls[64] = {0}; + if (GetClassNameA(w, cls, 63) > 0 && std::strncmp(cls, "JUCE_", 5) == 0) { ImmAssociateContext(w, nullptr); - } - } - return CallNextHookEx(g_cbtHook, nCode, wParam, lParam); -} -#endif + } + } + return CallNextHookEx(g_cbtHook, nCode, wParam, lParam); +} +#endif + +#ifdef _WIN32 +// Native VST editor windows registry β€” global de WM_DESTROY (chay tren worker +// thread cua channel tao window) co the don map. USERDATA luu channel+1 (KHONG +// luu con tro inst truc tiep: assign() thay inst moi moi lan load β€” con tro cu +// 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 -#ifdef _WIN32 -// Native VST editor windows registry β€” global de WM_DESTROY (chay tren worker -// thread cua channel tao window) co the don map. USERDATA luu channel+1 (KHONG -// luu con tro inst truc tiep: assign() thay inst moi moi lan load β€” con tro cu -// 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 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 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; +// 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 +// its channel mid-attach (the attach job owns the silence state until +// attachView finishes). g_closeInFlight = channels whose close job +// (closeGUI + destroy children + reload) is still inside createInstance β€” +// unmuting them then would race the plugin teardown. +static std::mutex g_attachMutex; +static std::vector g_attachPaths; +static bool g_closeInFlight[16] = { false }; -// 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 -// its channel mid-attach (the attach job owns the silence state until -// attachView finishes). g_closeInFlight = channels whose close job -// (closeGUI + destroy children + reload) is still inside createInstance β€” -// unmuting them then would race the plugin teardown. -static std::mutex g_attachMutex; -static std::vector g_attachPaths; -static bool g_closeInFlight[16] = { false }; - -// 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 -// audio loop calls process() on other instances of the same DLL (2 threads -// in one DLL -> Nexus USER32 crash). g_editorPathCount = refcount per -// lowercase plugin path; g_editorOpenPath/g_editorOpen per channel. -// Lock order: g_editorMutex is taken while holding the engine mutex -// (channelQuiet) or standalone in non-rt jobs β€” never the reverse. -static std::mutex g_editorMutex; -static std::map g_editorPathCount; -static std::string g_editorOpenPath[16]; -static bool g_editorOpen[16] = { false }; - -static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { - if (uMsg == WM_CLOSE) { - uint32_t ch = UINT32_MAX; - { - std::lock_guard lock(g_guiMutex); - auto it = g_hwndToCh.find(hwnd); - if (it != g_hwndToCh.end()) ch = it->second; - } - if (ch != UINT32_MAX) { - // CRASH FIX (0xc000041d STATUS_FATAL_USER_CALLBACK_EXCEPTION / - // 0xc0000005 trong USER32, Event Log 9:47/10:20/10:22): - // KHONG de DefWindowProc DestroyWindow o day. Plugin editor la - // child cua window nay, tao TREN worker thread (view->attached() - // chay trong attach job) - DestroyWindow tren main thread pha huy - // child cross-thread -> crash USER32. Detach view tren worker - // (closeGUI), an window, GIU window trong registry de reuse. - post_close_gui(ch, hwnd); - ShowWindow(hwnd, SW_HIDE); - return 0; - } - // Window khong thuoc registry (chua dang ky) - de DefWindowProc huy. - } else if (uMsg == WM_DESTROY) { - // Chi xay ra khi window thuc su bi huy (khong con path chu dong nao - // DestroyWindow khi view dang attached). Xoa registry + detach view. - uint32_t ch = UINT32_MAX; - { - std::lock_guard lock(g_guiMutex); - auto it = g_hwndToCh.find(hwnd); - if (it != g_hwndToCh.end()) ch = it->second; - g_hwndToCh.erase(hwnd); - for (auto it = g_guiWindows.begin(); it != g_guiWindows.end();) { - if (it->second == hwnd) it = g_guiWindows.erase(it); - else ++it; - } - } - if (ch != UINT32_MAX) post_close_gui(ch, hwnd); - } else if (uMsg == WM_ERASEBKGND) { - return 1; // Do not paint background to prevent white flashes - } else if (uMsg == WM_PAINT) { - PAINTSTRUCT ps; - HDC hdc = BeginPaint(hwnd, &ps); - EndPaint(hwnd, &ps); - return 0; - } else if (uMsg == WM_SIZE) { - int w = LOWORD(lParam); - int h = HIWORD(lParam); - HWND child = FindWindowExA(hwnd, nullptr, nullptr, nullptr); - if (child) { - MoveWindow(child, 0, 0, w, h, TRUE); - } - uint32_t ch = UINT32_MAX; - { - std::lock_guard lock(g_guiMutex); - auto it = g_hwndToCh.find(hwnd); - if (it != g_hwndToCh.end()) ch = it->second; - } - if (ch != UINT32_MAX) { - post_resize_view(ch, w, h); - } - return 0; - } - return DefWindowProcA(hwnd, uMsg, wParam, lParam); -} -#endif - -// Serialize plugin loads across channels: concurrent VST3 createInstance on -// separate worker threads (Nexus) deadlocks inside the SDK factory - all -// stuck forever (observed: 3 concurrent Nexus loads, zero assign returned). -// One load at a time; SF2/SFZ loads are fast, contention negligible. -static std::mutex g_loadMutex; -// Pending OPEN_GUI requests whose channel instrument was not loaded yet. -// The main loop fulfils them as soon as the load job completes (assign ok), -// so the GUI button works regardless of JS retry timing. -static std::mutex g_pendingGuiMutex; -static std::map> g_pendingGui; - -// B9: native Win32 window for the VST editor (replaces the WebView2 surface β€” -// the HTML window was drawn ON TOP of the plugin GUI). MUST be created on the -// ChannelWorker thread so the worker's idle message pump services its messages. -static std::once_flag registerWindowOnce; -static void* create_native_vst_window(const char* title) { -#ifdef _WIN32 - static const char* kWndClass = "SonicForge_Native_VST3_Class"; - std::call_once(registerWindowOnce, []() { - WNDCLASSA wc = {}; - wc.lpfnWndProc = VstWindowProc; - wc.hInstance = GetModuleHandleA(nullptr); - wc.lpszClassName = kWndClass; - RegisterClassA(&wc); - }); - HWND hwnd = CreateWindowExA(0, kWndClass, title ? title : "VST", - WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, - CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, - nullptr, nullptr, GetModuleHandleA(nullptr), nullptr); - return (void*)hwnd; -#else - (void)title; - return nullptr; -#endif -} - -// 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, -// openGUI on another) hangs; `same_thread` (load+openGUI on one alive thread) -// returns attached=OK. -class ChannelWorker { -public: - ChannelWorker() { -#ifdef _WIN32 - jobEvent_ = CreateEvent(nullptr, FALSE, FALSE, nullptr); -#endif - th_ = std::thread([this] { -#ifdef _WIN32 +static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + if (uMsg == WM_CLOSE) { + uint32_t ch = UINT32_MAX; + { + std::lock_guard lock(g_guiMutex); + auto it = g_hwndToCh.find(hwnd); + if (it != g_hwndToCh.end()) ch = it->second; + } + if (ch != UINT32_MAX) { + // CRASH FIX (0xc000041d STATUS_FATAL_USER_CALLBACK_EXCEPTION / + // 0xc0000005 trong USER32, Event Log 9:47/10:20/10:22): + // KHONG de DefWindowProc DestroyWindow o day. Plugin editor la + // child cua window nay, tao TREN worker thread (view->attached() + // chay trong attach job) - DestroyWindow tren main thread pha huy + // child cross-thread -> crash USER32. Detach view tren worker + // (closeGUI), an window, GIU window trong registry de reuse. + post_close_gui(ch, hwnd); + ShowWindow(hwnd, SW_HIDE); + return 0; + } + // Window khong thuoc registry (chua dang ky) - de DefWindowProc huy. + } else if (uMsg == WM_DESTROY) { + // Chi xay ra khi window thuc su bi huy (khong con path chu dong nao + // DestroyWindow khi view dang attached). Xoa registry + detach view. + uint32_t ch = UINT32_MAX; + { + std::lock_guard lock(g_guiMutex); + auto it = g_hwndToCh.find(hwnd); + if (it != g_hwndToCh.end()) ch = it->second; + g_hwndToCh.erase(hwnd); + for (auto it = g_guiWindows.begin(); it != g_guiWindows.end();) { + if (it->second == hwnd) it = g_guiWindows.erase(it); + else ++it; + } + } + if (ch != UINT32_MAX) post_close_gui(ch, hwnd); + } else if (uMsg == WM_ERASEBKGND) { + return 1; // Do not paint background to prevent white flashes + } else if (uMsg == WM_PAINT) { + PAINTSTRUCT ps; + HDC hdc = BeginPaint(hwnd, &ps); + EndPaint(hwnd, &ps); + return 0; + } else if (uMsg == WM_SIZE) { + int w = LOWORD(lParam); + int h = HIWORD(lParam); + HWND child = FindWindowExA(hwnd, nullptr, nullptr, nullptr); + if (child) { + MoveWindow(child, 0, 0, w, h, TRUE); + } + uint32_t ch = UINT32_MAX; + { + std::lock_guard lock(g_guiMutex); + auto it = g_hwndToCh.find(hwnd); + if (it != g_hwndToCh.end()) ch = it->second; + } + if (ch != UINT32_MAX) { + post_resize_view(ch, w, h); + } + return 0; + } + return DefWindowProcA(hwnd, uMsg, wParam, lParam); +} +#endif + +// Serialize plugin loads across channels: concurrent VST3 createInstance on +// separate worker threads (Nexus) deadlocks inside the SDK factory - all +// stuck forever (observed: 3 concurrent Nexus loads, zero assign returned). +// One load at a time; SF2/SFZ loads are fast, contention negligible. +static std::mutex g_loadMutex; +// Pending OPEN_GUI requests whose channel instrument was not loaded yet. +// The main loop fulfils them as soon as the load job completes (assign ok), +// so the GUI button works regardless of JS retry timing. +static std::mutex g_pendingGuiMutex; +static std::map> g_pendingGui; + +// B9: native Win32 window for the VST editor (replaces the WebView2 surface β€” +// the HTML window was drawn ON TOP of the plugin GUI). MUST be created on the +// ChannelWorker thread so the worker's idle message pump services its messages. +static std::once_flag registerWindowOnce; +static void* create_native_vst_window(const char* title) { +#ifdef _WIN32 + static const char* kWndClass = "SonicForge_Native_VST3_Class"; + std::call_once(registerWindowOnce, []() { + WNDCLASSA wc = {}; + wc.lpfnWndProc = VstWindowProc; + wc.hInstance = GetModuleHandleA(nullptr); + wc.lpszClassName = kWndClass; + RegisterClassA(&wc); + }); + HWND hwnd = CreateWindowExA(0, kWndClass, title ? title : "VST", + WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, + CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, + nullptr, nullptr, GetModuleHandleA(nullptr), nullptr); + return (void*)hwnd; +#else + (void)title; + return nullptr; +#endif +} + +// 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, +// openGUI on another) hangs; `same_thread` (load+openGUI on one alive thread) +// returns attached=OK. +class ChannelWorker { +public: + ChannelWorker() { +#ifdef _WIN32 + jobEvent_ = CreateEvent(nullptr, FALSE, FALSE, nullptr); +#endif + th_ = std::thread([this] { +#ifdef _WIN32 OleInitialize(nullptr); - OleInitialize(nullptr); - // Default IMC = none on this thread: new editor windows get - // no IME context (see disable_ime_contexts). - ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT); - // CBT hook on this worker: strip IMC on JUCE_* windows at birth. - // Global hooks fail from an exe module β€” install per thread, and - // this thread creates the plugin editor windows. - SetWindowsHookExW(WH_CBT, ImeCbtHookProc, GetModuleHandleW(nullptr), GetCurrentThreadId()); - - // Elevate VST GUI thread priority to prevent throttling - SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL); -#endif - std::unique_lock lk(mu_); - for (;;) { - if (stop_ && jobs_.empty()) break; - if (!jobs_.empty()) { - auto job = std::move(jobs_.front()); - jobs_.pop_front(); - lk.unlock(); - job(); - lk.lock(); - continue; - } - lk.unlock(); - MSG msg; - bool hadMessages = false; - // Cap the idle pump per iteration to avoid starving the job queue, - // but pump up to 128 messages for high GUI responsiveness. - for (int pumped = 0; pumped < 128; ++pumped) { - if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break; - hadMessages = true; + OleInitialize(nullptr); + // Default IMC = none on this thread: new editor windows get + // no IME context (see disable_ime_contexts). + ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT); + // CBT hook on this worker: strip IMC on JUCE_* windows at birth. + // Global hooks fail from an exe module β€” install per thread, and + // this thread creates the plugin editor windows. + SetWindowsHookExW(WH_CBT, ImeCbtHookProc, GetModuleHandleW(nullptr), GetCurrentThreadId()); + + // Elevate VST GUI thread priority to prevent throttling + SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL); +#endif + std::unique_lock lk(mu_); + for (;;) { + if (stop_ && jobs_.empty()) break; + if (!jobs_.empty()) { + auto job = std::move(jobs_.front()); + jobs_.pop_front(); + lk.unlock(); + job(); + lk.lock(); + continue; + } + lk.unlock(); + MSG msg; + bool hadMessages = false; + // Cap the idle pump per iteration to avoid starving the job queue, + // but pump up to 128 messages for high GUI responsiveness. + for (int pumped = 0; pumped < 128; ++pumped) { + if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break; + hadMessages = true; // 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 - // internally) that raises a C++ exception would otherwise escape - // DispatchMessageW and std::terminate this worker thread. Catch - // it here β€” the editor may glitch, but the bridge survives. - try { - DispatchMessageW(&msg); - } catch (...) { - std::cerr << "[NativeBridge] worker pump EXCEPTION β€” plugin window proc threw" << std::endl; - } - } - lk.lock(); - if (!hadMessages && jobs_.empty()) { -#ifdef _WIN32 - if (jobEvent_) { - lk.unlock(); - MsgWaitForMultipleObjects(1, &jobEvent_, FALSE, 10, QS_ALLINPUT); - lk.lock(); - } else { - cv_.wait_for(lk, std::chrono::milliseconds(2)); - } -#else - cv_.wait_for(lk, std::chrono::milliseconds(2)); -#endif - } - } -#ifdef _WIN32 + TranslateMessage(&msg); + // CRASH FIX (0xc000041d STATUS_FATAL_USER_CALLBACK_EXCEPTION): + // a plugin window proc (Nexus throws nlohmann::json::out_of_range + // internally) that raises a C++ exception would otherwise escape + // DispatchMessageW and std::terminate this worker thread. Catch + // it here β€” the editor may glitch, but the bridge survives. + try { + DispatchMessageW(&msg); + } catch (...) { + std::cerr << "[NativeBridge] worker pump EXCEPTION β€” plugin window proc threw" << std::endl; + } + } + lk.lock(); + if (!hadMessages && jobs_.empty()) { +#ifdef _WIN32 + if (jobEvent_) { + lk.unlock(); + MsgWaitForMultipleObjects(1, &jobEvent_, FALSE, 10, QS_ALLINPUT); + lk.lock(); + } else { + cv_.wait_for(lk, std::chrono::milliseconds(2)); + } +#else + cv_.wait_for(lk, std::chrono::milliseconds(2)); +#endif + } + } +#ifdef _WIN32 OleUninitialize(); - OleUninitialize(); -#endif - }); - } - ~ChannelWorker() { - { - std::lock_guard lk(mu_); - stop_ = true; - } -#ifdef _WIN32 - if (jobEvent_) { - SetEvent(jobEvent_); - } -#endif - cv_.notify_all(); - if (th_.joinable()) th_.join(); -#ifdef _WIN32 - if (jobEvent_) { - CloseHandle(jobEvent_); - jobEvent_ = nullptr; - } -#endif - } - void post(std::function job) { - { - std::lock_guard lk(mu_); - jobs_.push_back(std::move(job)); - } -#ifdef _WIN32 - if (jobEvent_) { - SetEvent(jobEvent_); - } -#endif - cv_.notify_all(); - } - - -private: - std::thread th_; - std::mutex mu_; - std::condition_variable cv_; - std::deque> jobs_; - bool stop_ = false; -#ifdef _WIN32 - HANDLE jobEvent_ = nullptr; -#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_uiWorker) { - g_uiWorker->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 -// on another. Windows plugin paths are case-insensitive β€” compare lowercased. -static std::string lower_path_str(std::string p) { - std::transform(p.begin(), p.end(), p.begin(), - [](unsigned char c) { return (char)::tolower(c); }); - return p; -} - -static std::string lower_plugin_path(uint32_t ch) { - return g_engine ? lower_path_str(g_engine->pathOf(ch)) : std::string(); -} - -// Channels (other than ch) currently assigned the same plugin DLL path. -static std::vector same_plugin_channels(uint32_t ch) { - std::vector out; - std::string p = lower_plugin_path(ch); - if (p.empty()) return out; - for (uint32_t y = 0; y < 16; ++y) { - if (y == ch) continue; - if (!g_engine->get(y)) continue; - if (lower_plugin_path(y) == p) out.push_back(y); - } - return out; -} - -static bool close_in_flight(uint32_t ch) { - std::lock_guard lk(g_attachMutex); - return g_closeInFlight[ch]; -} - + OleUninitialize(); +#endif + }); + } + ~ChannelWorker() { + { + std::lock_guard lk(mu_); + stop_ = true; + } +#ifdef _WIN32 + if (jobEvent_) { + SetEvent(jobEvent_); + } +#endif + cv_.notify_all(); + if (th_.joinable()) th_.join(); +#ifdef _WIN32 + if (jobEvent_) { + CloseHandle(jobEvent_); + jobEvent_ = nullptr; + } +#endif + } + void post(std::function job) { + { + std::lock_guard lk(mu_); + jobs_.push_back(std::move(job)); + } +#ifdef _WIN32 + if (jobEvent_) { + SetEvent(jobEvent_); + } +#endif + cv_.notify_all(); + } + +private: + std::thread th_; + std::mutex mu_; + std::condition_variable cv_; + std::deque> jobs_; + bool stop_ = false; +#ifdef _WIN32 + HANDLE jobEvent_ = nullptr; +#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_uiWorker) { + g_uiWorker->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 +// on another. Windows plugin paths are case-insensitive β€” compare lowercased. +static std::string lower_path_str(std::string p) { + std::transform(p.begin(), p.end(), p.begin(), + [](unsigned char c) { return (char)::tolower(c); }); + return p; +} + +static std::string lower_plugin_path(uint32_t ch) { + return g_engine ? lower_path_str(g_engine->pathOf(ch)) : std::string(); +} + +// Channels (other than ch) currently assigned the same plugin DLL path. +static std::vector same_plugin_channels(uint32_t ch) { + std::vector out; + std::string p = lower_plugin_path(ch); + if (p.empty()) return out; + for (uint32_t y = 0; y < 16; ++y) { + if (y == ch) continue; + if (!g_engine->get(y)) continue; + if (lower_plugin_path(y) == p) out.push_back(y); + } + return out; +} + +static bool close_in_flight(uint32_t ch) { + std::lock_guard lk(g_attachMutex); + return g_closeInFlight[ch]; +} + + // 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 @@ -430,881 +417,838 @@ static bool close_in_flight(uint32_t ch) { << " != 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 -// the same plugin is in flight, the attach job's restore does it). -static void unmute_if_not_closing(uint32_t y, const char* why) { - (void)why; - if (close_in_flight(y)) { - return; - } - g_engine->setReloading(y, false); -} - -// Editor closed / instrument replaced: drop the channel's editor-open state -// (refcounted per lowercase plugin path). Called from non-rt jobs only β€” -// never from the audio loop. -static void clear_editor_open(uint32_t ch) { - std::lock_guard lk(g_editorMutex); - if (!g_editorOpen[ch]) return; - g_editorOpen[ch] = false; - std::string pth = g_editorOpenPath[ch]; - g_editorOpenPath[ch].clear(); - if (!pth.empty()) { - auto it = g_editorPathCount.find(pth); - if (it != g_editorPathCount.end() && --it->second <= 0) - g_editorPathCount.erase(it); - } -} - -// 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); - if (it == g_guiWindows.end() || it->second != hwnd) { - return; - } - } - // 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)) { - 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 { - // G0.4: reload() terminates + recreates the instance; - // capture the preset first, restore it after, so the - // sound survives a GUI close (reopen keeps it too). - i->captureState(); - i->reload(); - i->restoreState(); - } 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; - } -} - -static void post_close_gui(uint32_t ch, HWND hwnd) { - if (g_uiWorker) { - g_uiWorker->post([ch, hwnd]() { close_editor_now(ch, hwnd); }); - } -} - -int main(int argc, char* argv[]) { - std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl; -#ifdef _WIN32 - // Disable Power Throttling (Efficiency Mode) for this background process -#ifndef PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION -#define PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION 0x00000004 -#endif -#ifndef PROCESS_POWER_THROTTLING_EXECUTION_SPEED -#define PROCESS_POWER_THROTTLING_EXECUTION_SPEED 0x00000001 -#endif - PROCESS_POWER_THROTTLING_STATE powerState = {}; - powerState.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION; - powerState.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION | PROCESS_POWER_THROTTLING_EXECUTION_SPEED; - powerState.StateMask = 0; // Turn off throttling - SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &powerState, sizeof(powerState)); - - // 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 - // strips (reported "GUI trαΊ―ng, khΓ΄ng load"). Runtime call works even - // 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). + +// 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 +// the same plugin is in flight, the attach job's restore does it). +static void unmute_if_not_closing(uint32_t y, const char* why) { + (void)why; + if (close_in_flight(y)) { + return; + } + g_engine->setReloading(y, false); +} + +// 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); + if (it == g_guiWindows.end() || it->second != hwnd) { + return; + } + } + // 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)) { + 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 { + // G0.4: reload() terminates + recreates the instance; + // capture the preset first, restore it after, so the + // sound survives a GUI close (reopen keeps it too). + i->captureState(); + i->reload(); + i->restoreState(); + } catch (...) { + std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch + << " β€” plugin threw (createInstance)" << std::endl; + } + } + // 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); + 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); }); + } +} + +int main(int argc, char* argv[]) { + std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl; +#ifdef _WIN32 + // Disable Power Throttling (Efficiency Mode) for this background process +#ifndef PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION +#define PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION 0x00000004 +#endif +#ifndef PROCESS_POWER_THROTTLING_EXECUTION_SPEED +#define PROCESS_POWER_THROTTLING_EXECUTION_SPEED 0x00000001 +#endif + PROCESS_POWER_THROTTLING_STATE powerState = {}; + powerState.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION; + powerState.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION | PROCESS_POWER_THROTTLING_EXECUTION_SPEED; + powerState.StateMask = 0; // Turn off throttling + SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &powerState, sizeof(powerState)); + + // 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 + // strips (reported "GUI trαΊ―ng, khΓ΄ng load"). Runtime call works even + // 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); -#endif - - // 1. Shared memory name: argv --shm | env SF_SHM_NAME | default - std::string shmName = "SonicForge_DAW_IPC"; - for (int i = 1; i + 1 < argc; ++i) { - if (std::strcmp(argv[i], "--shm") == 0) shmName = argv[i + 1]; - } - if (const char* e = std::getenv("SF_SHM_NAME")) shmName = e; - - // Parent watchdog PID (set by Tauri sidecar spawner) - uint32_t parentPid = 0; - if (const char* e = std::getenv("SF_PARENT_PID")) parentPid = (uint32_t)std::atoi(e); - - // V8 bug 2: watchdog THREAD rieng - main loop chi check parent_alive giua - // cac block; neu VST process() chan loop thi khong bao gio thoat -> orphan. - // Thread nay chay doc lap, parent chet -> TerminateProcess ngay. Kem - // start-time check chong PID reuse (OpenProcess tra handle cua process - // khac chiem lai PID -> tuong parent con song mai). - if (parentPid != 0) { - std::thread([parentPid]() { - auto proc_birth = [](HANDLE h) -> uint64_t { - FILETIME c, e, k, u; - if (GetProcessTimes(h, &c, &e, &k, &u)) - return (uint64_t(c.dwHighDateTime) << 32) | c.dwLowDateTime; - return 0; - }; - uint64_t birth = 0; - { - HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, parentPid); - if (h) { birth = proc_birth(h); CloseHandle(h); } - } - for (;;) { - Sleep(2000); - HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, parentPid); - if (!h) { TerminateProcess(GetCurrentProcess(), 0); return; } - uint64_t nowBirth = proc_birth(h); - CloseHandle(h); - if (birth != 0 && nowBirth != 0 && nowBirth != birth) { - TerminateProcess(GetCurrentProcess(), 0); return; - } - } - }).detach(); - } - -#ifdef _WIN32 - // Real-time-ish timing: 1ms scheduler resolution - timeBeginPeriod(1); - HANDLE hMapFile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName.c_str()); - if (!hMapFile) { - std::cerr << "[NativeBridge] Failed to open Shared Memory mapping: " << shmName << std::endl; - return 1; - } - auto* shmIPC = (SharedAudioBufferIPC*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC)); - if (!shmIPC) { CloseHandle(hMapFile); return 1; } -#else - (void)shmName; // POSIX shm mapping (shm_open) added when porting off Windows - auto* shmIPC = (SharedAudioBufferIPC*)std::calloc(1, sizeof(SharedAudioBufferIPC)); - if (!shmIPC) return 1; -#endif - - InstrumentEngineManager instruments; -#ifdef _WIN32 - g_engine = &instruments; -#endif - // 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) { - 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 - // (see ChannelWorker comment β€” a dead apartment hangs Nexus attached()). - // 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). - // Block size is fixed by the SHM layout (AUDIO_BLOCK_SIZE) β€” SF_BLOCK_SIZE - // is accepted but must match, otherwise warned and ignored. - double sampleRate = 44100.0; - if (const char* e = std::getenv("SF_SAMPLE_RATE")) { - double sr = (double)std::atoi(e); - if (sr > 0) sampleRate = sr; - } - if (const char* e = std::getenv("SF_BLOCK_SIZE")) { - uint32_t b = (uint32_t)std::atoi(e); - if (b != AUDIO_BLOCK_SIZE) - std::cerr << "[NativeBridge] SHM block size fixed at " << AUDIO_BLOCK_SIZE - << " (SF_BLOCK_SIZE=" << b << " ignored)" << std::endl; - } - const uint32_t block = AUDIO_BLOCK_SIZE; - uint64_t playheadSamples = 0; - // V8 bug 3: khi STOP da xu ly, bo qua NOTE_ON (velocity>0) den sau - JS - // note-on timer co the bay toi sau STOP (guardPlay tre do React re-render) - // -> retrigger note -> VST loop am. Chi PLAY moi nhan note-on lai. - bool transportStopped = false; - - auto dispatch = [&](const SharedAudioBufferIPC::MidiEventIPC& evt) { - // Manager-level dispatch: ONE engine-mutex hold per event (get + - // channelQuiet + instrument call) so assign()/unload() can never - // destroy the instance mid-call β€” use-after-free when loading a new - // VSTi while other channels keep playing. channelQuiet drops events - // for channels mid-reload or whose plugin DLL has an open editor. - // CRASH FIX: noteOn/noteOff/CC enter the plugin (DLL entry); a C++ - // exception from the plugin must not escape the audio loop. - try { - switch (evt.command) { - case 0x9: - // sampleOffset LUON LUON = 0 khi den day: events duoc dispatch ngay - // truoc segment chua no (A11 splitting), nen offset tuong doi la 0. - // Truyen offset tuyet doi truoc day lam sfizz/VST3 trigger tre. - if (evt.velocity > 0) { - if (transportStopped) return; // V8 bug 3: drop note-on sau STOP - instruments.noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f); - } else - instruments.noteOff(evt.channel, evt.pitch); - break; - case 0x8: - instruments.noteOff(evt.channel, evt.pitch); - break; - case 0xB: // CC: controller number in pitch, value in data2 - // V9 bug 4: sau STOP, drop sustain-down (CC64>0) β€” JS co the - // gui CC64 xuong sau STOP (note-on/CC timer tre); VSTi giu - // note khi pedal down -> am treo loop. CC64=0 (sustain-up) - // van cho qua. - if (transportStopped && evt.pitch == 64 && evt.data2 > 0) return; - instruments.controlChange(evt.channel, evt.pitch, evt.data2); - break; - case 0xC: // program change: program in data2 - instruments.programChange(evt.channel, evt.data2); - break; - case 0xE: // 14-bit pitch bend: data2 = LSB, data3 = MSB - instruments.pitchBend(evt.channel, evt.data2 | (uint32_t(evt.data3) << 7)); - break; - default: break; - } - } catch (...) { - std::cerr << "[NativeBridge] dispatch EXCEPTION ch=" << (int)evt.channel - << " cmd=" << std::hex << (int)evt.command << std::dec - << " β€” plugin threw (note/CC)" << std::endl; - } - }; - - // Render only [from, to) of the block β€” used by sample-accurate splitting. - auto renderSegment = [&](uint32_t from, uint32_t to) { - if (to <= from) return; - instruments.renderAll(shmIPC->masterLeft + from, shmIPC->masterRight + from, to - from); - }; - - double blockDurationMs = (double)block / sampleRate * 1000.0; - auto startTime = std::chrono::steady_clock::now(); - uint64_t blockCount = 0; - - // OPEN_GUI handling - shared by the control queue and the pending-GUI - // sweep. Gate on the MAIN thread before creating a window: instrument - // load is ASYNC (worker thread); OPEN_GUI too early would attach to an - // empty channel and leave a blank window. Not loaded yet => remember the - // request; the sweep retries once the load job completes. Window MUST - // belong to the MAIN thread (audio loop pump dispatches its messages); - // a window on a worker whose pump is idle during jobs hangs - // view->attached() (gui_probe: two_workers_close TIMEOUT). The attach - // job runs on the channel worker (COM STA apartment stays alive). - // G0.3: while an attach job is in flight, tell the Tauri watchdog via - // SHM so a long Nexus reload/attach (can exceed 3s) is not mistaken - // for a dead bridge and killed. RAII -> cleared on every exit path. - struct AttachInFlightGuard { - SharedAudioBufferIPC* ipc; - explicit AttachInFlightGuard(SharedAudioBufferIPC* p) : ipc(p) { if (ipc) ipc->attachInFlight = 1; } - ~AttachInFlightGuard() { if (ipc) ipc->attachInFlight = 0; } - }; - auto handleOpenGui = [&](uint32_t guiCh, uintptr_t arg1, const std::string& pluginId) { - if (!instruments.get(guiCh)) { - std::cerr << "[NativeBridge] GUI deferred ch=" << guiCh - << " plugin=" << pluginId << " (no instrument yet) - queued" << std::endl; - std::lock_guard lock(g_pendingGuiMutex); - g_pendingGui[guiCh] = { (void*)arg1, pluginId }; - 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()) { - return; - } - } - // G0.1 (BUG_REPORT): mo GUI khi dang PLAY -> attachView chay tren worker - // thread trong luc audio loop process() cung DLL (2 thread 1 plugin) -> - // Nexus throw (createView/attached) -> GUI khong load + retry spam -> - // stall 3s -> watchdog kill bridge -> mat het instrument -> cam. Defer - // OPEN_GUI toi khi transport STOP (sweep duoi fulfill khi loaded && - // transportStopped). Khong doi: tranh mat am; user STOP la GUI tu mo. - if (!transportStopped) { - std::cerr << "[NativeBridge] GUI deferred ch=" << guiCh - << " plugin=" << pluginId << " (transport PLAY) - queued" << std::endl; - std::lock_guard lock(g_pendingGuiMutex); - g_pendingGui[guiCh] = { (void*)arg1, pluginId }; - return; - } - // 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; - } - } - 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 - // the worker sets reloading_) would otherwise process() another Nexus - // while the job's reload() (createInstance/terminate) enters the DLL - // -> 2 threads in 1 DLL -> Nexus hang -> writeIndex stall -> watchdog - // kills the bridge (observed: defer OK, attach after STOP still - // restarted). setReloading is a flag under the engine mutex; renderAll - // skips reloading channels, so no process() can race the job. The job - // keeps its own setReloading + restore logic unchanged. Dat o day (sau - // window create) de window fail khong keo mute vinh vien. - { - std::string gp0 = lower_plugin_path(guiCh); - if (!gp0.empty()) { - for (uint32_t y = 0; y < 16; ++y) { - if (y == guiCh) continue; - if (!instruments.get(y)) continue; - if (lower_plugin_path(y) == gp0) - instruments.setReloading(y, true); - } - instruments.setReloading(guiCh, true); - } - } -#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 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; - { - std::lock_guard lock(g_guiMutex); - for (const auto& kv : g_guiWindows) - if (kv.first != guiCh) others.push_back(kv.first); - } - for (uint32_t y : others) { - // Skip channels whose editor already detached (close job - // finished): do not WM_CLOSE a hidden reused window, do - // not re-silence a channel that is already unmuted. - if (auto* yi0 = instruments.get(y)) { - if (!yi0->hasAttachedView()) { - continue; - } - } - HWND yHwnd = nullptr; - { - std::lock_guard lock(g_guiMutex); - auto it = g_guiWindows.find(y); - if (it != g_guiWindows.end()) yHwnd = (HWND)it->second; - } - if (!yHwnd || !IsWindow(yHwnd)) continue; - close_editor_now(y, yHwnd); - } - } -#else - (void)0; -#endif - if (auto* inst = instruments.get(guiCh)) { - // Reopen sau khi dong: reload() (terminate + loadPlugin) - // PHAI chay tren worker thread nay - COM STA apartment - // cua channel song o day. view->attached() cung chay o - // day; window thuoc main thread nen main pump (audio - // loop) dispatch messages cua no - khong can pump worker. - // Chan UAF bang flag reloading_ (set/clear duoi engine - // mutex; renderAll giu mutex khi process). - // CRASH FIX (0xc000041d USER32 while playing): keep THIS - // channel silenced for the whole time its editor is attached. - // The worker pumps the plugin's editor windows -> the plugin's - // window proc runs on this worker thread while the audio loop - // calls processor->process() on the SAME instance (two threads - // inside one DLL) -> Nexus crashes ~1-2s after attach during - // playback. Muting the channel while the editor is open is the - // deterministic trade-off (track silent while GUI visible); - // restored when the editor closes (post_close_gui / LOAD). - // STALL/CRASH FIX (Fix C, restored): silence EVERY channel - // assigned the SAME plugin DLL path for the reload+attach - // duration β€” the audio loop must not process() another - // instance of this DLL while createInstance/createView runs - // on this worker (2 threads in one DLL -> Nexus exits - // silently, observed in probes). Iterate ALL 16 channels, not - // just window-owning ones: session-restore instances without - // a window are live and renderable. - std::vector samePathSilenced; - std::string gp = lower_plugin_path(guiCh); - if (!gp.empty()) { - for (uint32_t y = 0; y < 16; ++y) { - if (y == guiCh) continue; - if (!instruments.get(y)) continue; - if (lower_plugin_path(y) == gp) { - instruments.setReloading(y, true); - samePathSilenced.push_back(y); - } - } - } - instruments.setReloading(guiCh, true); - // Register attach-in-flight: a same-path close job must not - // unmute its channel while createView runs here (it stays - // silenced; this job's restore unmutes it at the end). - { - std::lock_guard lk(g_attachMutex); - if (!gp.empty()) g_attachPaths.push_back(gp); - } - bool ok = false; - { - // Serialize ALL plugin-DLL entry (createInstance AND - // createView) with other loads: two threads inside one - // DLL (Nexus) crash. createView outside the lock raced a - // same-path close job's reload (createInstance) -> Nexus - // exited silently (observed in probes). - std::lock_guard lg(g_loadMutex); - // CRASH FIX (0xc000041d): attachView -> view->attached() - // and reloadForGUI both enter the plugin DLL β€” a C++ - // exception (Nexus) must not escape this job (would - // std::terminate the worker). Treat as attach failure; - // the else branch below closes the empty window. - try { - ok = inst->reloadForGUI(); - // IME recursion fix: strip the editor's IMC before - // attachView creates the editor child β€” a live IMC on - // a JUCE_ window + plugin wndproc (ImmIsUIMessageW -> - // SendMessageW WM_IME_SETCONTEXT same hwnd) = infinite - // recursion (0xC00000FD, observed inside attachView). - if (ok) disable_ime_contexts((HWND)hwnd); - if (ok) ok = inst->attachView(hwnd); - } catch (...) { - std::cerr << "[NativeBridge] GUI attach EXCEPTION ch=" << guiCh - << " β€” plugin threw (createView/attached)" << std::endl; - ok = false; - } - } - // Attach done (ok or failed) β€” close jobs may unmute again. - { - std::lock_guard lk(g_attachMutex); - if (!gp.empty()) { - auto it = std::find(g_attachPaths.begin(), g_attachPaths.end(), gp); - if (it != g_attachPaths.end()) g_attachPaths.erase(it); - } - } - // Restore same-path silence. Channels whose own close job is - // still inside createInstance stay muted β€” that job owns - // their unmute and performs it once reload finished. - for (uint32_t y : samePathSilenced) - unmute_if_not_closing(y, "openGUI"); - if (ok) { - // Editor attached: mark the plugin path editor-open so the - // audio loop stops processing EVERY channel assigned this - // DLL while the editor lives (its window proc runs on this - // worker; process() elsewhere on the same DLL -> Nexus crash). - { - std::lock_guard lk(g_editorMutex); - if (!gp.empty()) { - g_editorOpen[guiCh] = true; - g_editorOpenPath[guiCh] = gp; - ++g_editorPathCount[gp]; - } - } - disable_ime_contexts((HWND)hwnd); - - std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd - << " plugin=" << arg2 << " ch=" << guiCh - << " (channel muted while editor open)" << std::endl; - instruments.setReloading(guiCh, false); - } - else { - // Attach failed -> no editor running -> safe to process again. - instruments.setReloading(guiCh, false); - std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd - << " 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 +#endif + + // 1. Shared memory name: argv --shm | env SF_SHM_NAME | default + std::string shmName = "SonicForge_DAW_IPC"; + for (int i = 1; i + 1 < argc; ++i) { + if (std::strcmp(argv[i], "--shm") == 0) shmName = argv[i + 1]; + } + if (const char* e = std::getenv("SF_SHM_NAME")) shmName = e; + + // Parent watchdog PID (set by Tauri sidecar spawner) + uint32_t parentPid = 0; + if (const char* e = std::getenv("SF_PARENT_PID")) parentPid = (uint32_t)std::atoi(e); + + // V8 bug 2: watchdog THREAD rieng - main loop chi check parent_alive giua + // cac block; neu VST process() chan loop thi khong bao gio thoat -> orphan. + // Thread nay chay doc lap, parent chet -> TerminateProcess ngay. Kem + // start-time check chong PID reuse (OpenProcess tra handle cua process + // khac chiem lai PID -> tuong parent con song mai). + if (parentPid != 0) { + std::thread([parentPid]() { + auto proc_birth = [](HANDLE h) -> uint64_t { + FILETIME c, e, k, u; + if (GetProcessTimes(h, &c, &e, &k, &u)) + return (uint64_t(c.dwHighDateTime) << 32) | c.dwLowDateTime; + return 0; + }; + uint64_t birth = 0; + { + HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, parentPid); + if (h) { birth = proc_birth(h); CloseHandle(h); } + } + for (;;) { + Sleep(2000); + HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, parentPid); + if (!h) { TerminateProcess(GetCurrentProcess(), 0); return; } + uint64_t nowBirth = proc_birth(h); + CloseHandle(h); + if (birth != 0 && nowBirth != 0 && nowBirth != birth) { + TerminateProcess(GetCurrentProcess(), 0); return; + } + } + }).detach(); + } + +#ifdef _WIN32 + // Real-time-ish timing: 1ms scheduler resolution + timeBeginPeriod(1); + HANDLE hMapFile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName.c_str()); + if (!hMapFile) { + std::cerr << "[NativeBridge] Failed to open Shared Memory mapping: " << shmName << std::endl; + return 1; + } + auto* shmIPC = (SharedAudioBufferIPC*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC)); + if (!shmIPC) { CloseHandle(hMapFile); return 1; } +#else + (void)shmName; // POSIX shm mapping (shm_open) added when porting off Windows + auto* shmIPC = (SharedAudioBufferIPC*)std::calloc(1, sizeof(SharedAudioBufferIPC)); + if (!shmIPC) return 1; +#endif + + InstrumentEngineManager instruments; +#ifdef _WIN32 + g_engine = &instruments; +#endif + // 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()). + // 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). + // Block size is fixed by the SHM layout (AUDIO_BLOCK_SIZE) β€” SF_BLOCK_SIZE + // is accepted but must match, otherwise warned and ignored. + double sampleRate = 44100.0; + if (const char* e = std::getenv("SF_SAMPLE_RATE")) { + double sr = (double)std::atoi(e); + if (sr > 0) sampleRate = sr; + } + if (const char* e = std::getenv("SF_BLOCK_SIZE")) { + uint32_t b = (uint32_t)std::atoi(e); + if (b != AUDIO_BLOCK_SIZE) + std::cerr << "[NativeBridge] SHM block size fixed at " << AUDIO_BLOCK_SIZE + << " (SF_BLOCK_SIZE=" << b << " ignored)" << std::endl; + } + const uint32_t block = AUDIO_BLOCK_SIZE; + uint64_t playheadSamples = 0; + // V8 bug 3: khi STOP da xu ly, bo qua NOTE_ON (velocity>0) den sau - JS + // note-on timer co the bay toi sau STOP (guardPlay tre do React re-render) + // -> retrigger note -> VST loop am. Chi PLAY moi nhan note-on lai. + bool transportStopped = false; + + auto dispatch = [&](const SharedAudioBufferIPC::MidiEventIPC& evt) { + // Manager-level dispatch: ONE engine-mutex hold per event (get + + // channelQuiet + instrument call) so assign()/unload() can never + // destroy the instance mid-call β€” use-after-free when loading a new + // VSTi while other channels keep playing. channelQuiet drops events + // for channels mid-reload or whose plugin DLL has an open editor. + // CRASH FIX: noteOn/noteOff/CC enter the plugin (DLL entry); a C++ + // exception from the plugin must not escape the audio loop. + try { + switch (evt.command) { + case 0x9: + // sampleOffset LUON LUON = 0 khi den day: events duoc dispatch ngay + // truoc segment chua no (A11 splitting), nen offset tuong doi la 0. + // Truyen offset tuyet doi truoc day lam sfizz/VST3 trigger tre. + if (evt.velocity > 0) { + if (transportStopped) return; // V8 bug 3: drop note-on sau STOP + instruments.noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f); + } else + instruments.noteOff(evt.channel, evt.pitch); + break; + case 0x8: + instruments.noteOff(evt.channel, evt.pitch); + break; + case 0xB: // CC: controller number in pitch, value in data2 + // V9 bug 4: sau STOP, drop sustain-down (CC64>0) β€” JS co the + // gui CC64 xuong sau STOP (note-on/CC timer tre); VSTi giu + // note khi pedal down -> am treo loop. CC64=0 (sustain-up) + // van cho qua. + if (transportStopped && evt.pitch == 64 && evt.data2 > 0) return; + instruments.controlChange(evt.channel, evt.pitch, evt.data2); + break; + case 0xC: // program change: program in data2 + instruments.programChange(evt.channel, evt.data2); + break; + case 0xE: // 14-bit pitch bend: data2 = LSB, data3 = MSB + instruments.pitchBend(evt.channel, evt.data2 | (uint32_t(evt.data3) << 7)); + break; + default: break; + } + } catch (...) { + std::cerr << "[NativeBridge] dispatch EXCEPTION ch=" << (int)evt.channel + << " cmd=" << std::hex << (int)evt.command << std::dec + << " β€” plugin threw (note/CC)" << std::endl; + } + }; + + // Render only [from, to) of the block β€” used by sample-accurate splitting. + auto renderSegment = [&](uint32_t from, uint32_t to) { + if (to <= from) return; + instruments.renderAll(shmIPC->masterLeft + from, shmIPC->masterRight + from, to - from); + }; + + double blockDurationMs = (double)block / sampleRate * 1000.0; + auto startTime = std::chrono::steady_clock::now(); + uint64_t blockCount = 0; + + // OPEN_GUI handling - shared by the control queue and the pending-GUI + // sweep. Gate on the MAIN thread before creating a window: instrument + // load is ASYNC (worker thread); OPEN_GUI too early would attach to an + // empty channel and leave a blank window. Not loaded yet => remember the + // request; the sweep retries once the load job completes. Window MUST + // belong to the MAIN thread (audio loop pump dispatches its messages); + // a window on a worker whose pump is idle during jobs hangs + // view->attached() (gui_probe: two_workers_close TIMEOUT). The attach + // job runs on the channel worker (COM STA apartment stays alive). + // G0.3: while an attach job is in flight, tell the Tauri watchdog via + // SHM so a long Nexus reload/attach (can exceed 3s) is not mistaken + // for a dead bridge and killed. RAII -> cleared on every exit path. + struct AttachInFlightGuard { + SharedAudioBufferIPC* ipc; + explicit AttachInFlightGuard(SharedAudioBufferIPC* p) : ipc(p) { if (ipc) ipc->attachInFlight = 1; } + ~AttachInFlightGuard() { if (ipc) ipc->attachInFlight = 0; } + }; + auto handleOpenGui = [&](uint32_t guiCh, uintptr_t arg1, const std::string& pluginId) { + if (!instruments.get(guiCh)) { + std::cerr << "[NativeBridge] GUI deferred ch=" << guiCh + << " plugin=" << pluginId << " (no instrument yet) - queued" << std::endl; + std::lock_guard lock(g_pendingGuiMutex); + g_pendingGui[guiCh] = { (void*)arg1, pluginId }; + 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()) { + return; + } + } + // G0.1 (BUG_REPORT): mo GUI khi dang PLAY -> attachView chay tren worker + // thread trong luc audio loop process() cung DLL (2 thread 1 plugin) -> + // Nexus throw (createView/attached) -> GUI khong load + retry spam -> + // stall 3s -> watchdog kill bridge -> mat het instrument -> cam. Defer + // OPEN_GUI toi khi transport STOP (sweep duoi fulfill khi loaded && + // transportStopped). Khong doi: tranh mat am; user STOP la GUI tu mo. + if (!transportStopped) { + std::cerr << "[NativeBridge] GUI deferred ch=" << guiCh + << " plugin=" << pluginId << " (transport PLAY) - queued" << std::endl; + std::lock_guard lock(g_pendingGuiMutex); + g_pendingGui[guiCh] = { (void*)arg1, pluginId }; + return; + } + // 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; + } + } + 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 + // the worker sets reloading_) would otherwise process() another Nexus + // while the job's reload() (createInstance/terminate) enters the DLL + // -> 2 threads in 1 DLL -> Nexus hang -> writeIndex stall -> watchdog + // kills the bridge (observed: defer OK, attach after STOP still + // restarted). setReloading is a flag under the engine mutex; renderAll + // skips reloading channels, so no process() can race the job. The job + // keeps its own setReloading + restore logic unchanged. Dat o day (sau + // window create) de window fail khong keo mute vinh vien. + { + std::string gp0 = lower_plugin_path(guiCh); + if (!gp0.empty()) { + for (uint32_t y = 0; y < 16; ++y) { + if (y == guiCh) continue; + if (!instruments.get(y)) continue; + if (lower_plugin_path(y) == gp0) + instruments.setReloading(y, true); + } + instruments.setReloading(guiCh, true); + } + } +#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 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; + { + std::lock_guard lock(g_guiMutex); + for (const auto& kv : g_guiWindows) + if (kv.first != guiCh) others.push_back(kv.first); + } + for (uint32_t y : others) { + // Skip channels whose editor already detached (close job + // finished): do not WM_CLOSE a hidden reused window, do + // not re-silence a channel that is already unmuted. + if (auto* yi0 = instruments.get(y)) { + if (!yi0->hasAttachedView()) { + continue; + } + } + HWND yHwnd = nullptr; + { + std::lock_guard lock(g_guiMutex); + auto it = g_guiWindows.find(y); + if (it != g_guiWindows.end()) yHwnd = (HWND)it->second; + } + if (!yHwnd || !IsWindow(yHwnd)) continue; + close_editor_now(y, yHwnd); + } + } +#else + (void)0; +#endif + if (auto* inst = instruments.get(guiCh)) { + // Reopen sau khi dong: reload() (terminate + loadPlugin) + // PHAI chay tren worker thread nay - COM STA apartment + // cua channel song o day. view->attached() cung chay o + // day; window thuoc main thread nen main pump (audio + // loop) dispatch messages cua no - khong can pump worker. + // Chan UAF bang flag reloading_ (set/clear duoi engine + // mutex; renderAll giu mutex khi process). + // CRASH FIX (0xc000041d USER32 while playing): keep THIS + // channel silenced for the whole time its editor is attached. + // The worker pumps the plugin's editor windows -> the plugin's + // window proc runs on this worker thread while the audio loop + // calls processor->process() on the SAME instance (two threads + // inside one DLL) -> Nexus crashes ~1-2s after attach during + // playback. Muting the channel while the editor is open is the + // deterministic trade-off (track silent while GUI visible); + // restored when the editor closes (post_close_gui / LOAD). + // STALL/CRASH FIX (Fix C, restored): silence EVERY channel + // assigned the SAME plugin DLL path for the reload+attach + // duration β€” the audio loop must not process() another + // instance of this DLL while createInstance/createView runs + // on this worker (2 threads in one DLL -> Nexus exits + // silently, observed in probes). Iterate ALL 16 channels, not + // just window-owning ones: session-restore instances without + // a window are live and renderable. + std::vector samePathSilenced; + std::string gp = lower_plugin_path(guiCh); + if (!gp.empty()) { + for (uint32_t y = 0; y < 16; ++y) { + if (y == guiCh) continue; + if (!instruments.get(y)) continue; + if (lower_plugin_path(y) == gp) { + instruments.setReloading(y, true); + samePathSilenced.push_back(y); + } + } + } + instruments.setReloading(guiCh, true); + // Register attach-in-flight: a same-path close job must not + // unmute its channel while createView runs here (it stays + // silenced; this job's restore unmutes it at the end). + { + std::lock_guard lk(g_attachMutex); + if (!gp.empty()) g_attachPaths.push_back(gp); + } + bool ok = false; + { + // Serialize ALL plugin-DLL entry (createInstance AND + // createView) with other loads: two threads inside one + // DLL (Nexus) crash. createView outside the lock raced a + // same-path close job's reload (createInstance) -> Nexus + // exited silently (observed in probes). + std::lock_guard lg(g_loadMutex); + // CRASH FIX (0xc000041d): attachView -> view->attached() + // and reloadForGUI both enter the plugin DLL β€” a C++ + // exception (Nexus) must not escape this job (would + // std::terminate the worker). Treat as attach failure; + // the else branch below closes the empty window. + try { + ok = inst->reloadForGUI(); + // IME recursion fix: strip the editor's IMC before + // attachView creates the editor child β€” a live IMC on + // a JUCE_ window + plugin wndproc (ImmIsUIMessageW -> + // SendMessageW WM_IME_SETCONTEXT same hwnd) = infinite + // recursion (0xC00000FD, observed inside attachView). + if (ok) disable_ime_contexts((HWND)hwnd); + if (ok) ok = inst->attachView(hwnd); + } catch (...) { + std::cerr << "[NativeBridge] GUI attach EXCEPTION ch=" << guiCh + << " β€” plugin threw (createView/attached)" << std::endl; + ok = false; + } + } + // Attach done (ok or failed) β€” close jobs may unmute again. + { + std::lock_guard lk(g_attachMutex); + if (!gp.empty()) { + auto it = std::find(g_attachPaths.begin(), g_attachPaths.end(), gp); + if (it != g_attachPaths.end()) g_attachPaths.erase(it); + } + } + // Restore same-path silence. Channels whose own close job is + // still inside createInstance stay muted β€” that job owns + // their unmute and performs it once reload finished. + for (uint32_t y : samePathSilenced) + unmute_if_not_closing(y, "openGUI"); + if (ok) { + disable_ime_contexts((HWND)hwnd); + + std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd + << " plugin=" << arg2 << " ch=" << guiCh + << std::endl; + instruments.setReloading(guiCh, false); + } + else { + // Attach failed -> no editor running -> safe to process again. + instruments.setReloading(guiCh, false); + std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd + << " 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 -> UiThread pump destroy (owner = UiThread). // PostMessage an toan cross-thread (khong nhu DestroyWindow). - PostMessageA((HWND)hwnd, WM_CLOSE, 0, 0); -#endif - } - } - }); - }; - - // 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP - while (true) { - // CRASH FIX (0xc000041d): last-resort net β€” any C++ exception that - // escapes the targeted guards (e.g. inside renderAll -> process()) - // must not kill the bridge. Log and continue the loop. - try { - // 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]; - if (c.type == 2) { // LOAD_INSTRUMENT (A10: assign per MIDI channel) - // Robust path length: do not trust arg1 (Rust passes 0 for LOAD). - // Chay tren persistent worker thread cua channel: VST3 init - // (loadPlugin) co the mat giay β€” chay dong bo tren audio loop - // lam writeIndex stall > 3s -> Rust tuong bridge chet va restart - // nham (2 bridge cung map SHM -> race control queue / double - // load). assign() chi giu mutex khi ghi map nen renderAll khong - // bao gio stall. openGUI sau nay chay tren CUNG thread nay - // (ChannelWorker) β€” thread khong bao gio exit nen COM STA - // apartment cua plugin con song (xem ChannelWorker comment). - size_t plen = 0; - while (plen < sizeof(c.arg2) && c.arg2[plen]) ++plen; - std::string path(c.arg2, plen); - InstrumentType t = (InstrumentType)c.arg0; - uint32_t ch = c.channel & 0xF; - 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 - // DLL β€” 2 threads in one DLL crashes Nexus (observed with - // session-restore instances). Silence this channel (its old - // instance is being torn down / replaced) and every - // same-plugin channel for the whole job. Old path covers - // the old-instance teardown; the new path (computed BEFORE - // assign β€” see spNew below) covers the freshly created - // instance. CRASH FIX (SFβ†’VSTi while playing): the new - // path must be silenced BEFORE assign() β€” assign() itself - // runs createInstance (loadPlugin) on this worker, and a - // restored channel on ANOTHER track holding the SAME DLL - // kept process()ing on the audio loop during that window - // (old code silenced spNew only AFTER assign β†’ crash). - instruments.setReloading(ch, true); - std::vector spOld = same_plugin_channels(ch); - for (uint32_t y : spOld) { - g_engine->setReloading(y, true); - } - // Channels (other than ch) already assigned the NEW path β€” - // silence BEFORE createInstance enters that DLL. - std::vector spNew; - if (t == InstrumentType::VST3) { - std::string np = lower_path_str(path); - if (!np.empty()) { - for (uint32_t y = 0; y < 16; ++y) { - if (y == ch) continue; - if (!g_engine->get(y)) continue; - if (std::find(spOld.begin(), spOld.end(), y) != spOld.end()) continue; - if (lower_plugin_path(y) != np) continue; - g_engine->setReloading(y, true); - spNew.push_back(y); - } - } - } -#ifdef _WIN32 - // DONG cua so editor dang mo cua channel TRUOC khi assign(): - // 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. + PostMessageA((HWND)hwnd, WM_CLOSE, 0, 0); +#endif + } + } + }); + }; + + // 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP + while (true) { + // CRASH FIX (0xc000041d): last-resort net β€” any C++ exception that + // escapes the targeted guards (e.g. inside renderAll -> process()) + // must not kill the bridge. Log and continue the loop. + try { + // 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]; + if (c.type == 2) { // LOAD_INSTRUMENT (A10: assign per MIDI channel) + // Robust path length: do not trust arg1 (Rust passes 0 for LOAD). + // Chay tren persistent worker thread cua channel: VST3 init + // (loadPlugin) co the mat giay β€” chay dong bo tren audio loop + // lam writeIndex stall > 3s -> Rust tuong bridge chet va restart + // nham (2 bridge cung map SHM -> race control queue / double + // load). assign() chi giu mutex khi ghi map nen renderAll khong + // bao gio stall. openGUI sau nay chay tren CUNG thread nay + // (ChannelWorker) β€” thread khong bao gio exit nen COM STA + // apartment cua plugin con song (xem ChannelWorker comment). + size_t plen = 0; + while (plen < sizeof(c.arg2) && c.arg2[plen]) ++plen; + std::string path(c.arg2, plen); + InstrumentType t = (InstrumentType)c.arg0; + uint32_t ch = c.channel & 0xF; + 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 + // DLL β€” 2 threads in one DLL crashes Nexus (observed with + // session-restore instances). Silence this channel (its old + // instance is being torn down / replaced) and every + // same-plugin channel for the whole job. Old path covers + // the old-instance teardown; the new path (computed BEFORE + // assign β€” see spNew below) covers the freshly created + // instance. CRASH FIX (SFβ†’VSTi while playing): the new + // path must be silenced BEFORE assign() β€” assign() itself + // runs createInstance (loadPlugin) on this worker, and a + // restored channel on ANOTHER track holding the SAME DLL + // kept process()ing on the audio loop during that window + // (old code silenced spNew only AFTER assign β†’ crash). + instruments.setReloading(ch, true); + std::vector spOld = same_plugin_channels(ch); + for (uint32_t y : spOld) { + g_engine->setReloading(y, true); + } + // Channels (other than ch) already assigned the NEW path β€” + // silence BEFORE createInstance enters that DLL. + std::vector spNew; + if (t == InstrumentType::VST3) { + std::string np = lower_path_str(path); + if (!np.empty()) { + for (uint32_t y = 0; y < 16; ++y) { + if (y == ch) continue; + if (!g_engine->get(y)) continue; + if (std::find(spOld.begin(), spOld.end(), y) != spOld.end()) continue; + if (lower_plugin_path(y) != np) continue; + g_engine->setReloading(y, true); + spNew.push_back(y); + } + } + } +#ifdef _WIN32 + // DONG cua so editor dang mo cua channel TRUOC khi assign(): + // 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. // 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); - auto git = g_guiWindows.find(ch); - if (git != g_guiWindows.end()) hToHide = (HWND)git->second; - } - // CRASH FIX: detach view TRUOC khi thay inst. Plugin editor - // children thuoc worker thread nay (attachView chay o day); - // khong destroy parent window (main-owned) khi view con song - // -> DestroyWindow cross-thread -> USER32 0xc000041d. An - // window, giu trong registry de reuse (reopen ShowWindow lai). - if (hToHide && IsWindow(hToHide)) { - // CRASH FIX (0xc000041d USER32 while playing): silence - // the channel while the old editor detaches β€” view->removed() - // on this worker would race processor->process() on the - // audio loop (same plugin instance). - instruments.setReloading(ch, true); - // CRASH FIX: the plugin's editor window procs run on - // this worker; if one throws (Nexus) the exception must - // not escape the LOAD job (would std::terminate the - // worker). Detach best-effort and continue. - try { - disable_ime_contexts(hToHide); - if (auto* i = instruments.get(ch)) i->closeGUI(); - // 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)) { - destroy_window_on_owner(c); - } - } catch (...) { - std::cerr << "[NativeBridge] LOAD close-editor EXCEPTION ch=" << ch - << " β€” plugin window proc threw" << std::endl; - } - ShowWindow(hToHide, SW_HIDE); - // Editor detached: drop editor-open state before the - // instrument is replaced (assign). - clear_editor_open(ch); - } -#endif - std::lock_guard lg(g_loadMutex); - // CRASH FIX (0xc000041d): Nexus throws nlohmann::json:: - // out_of_range inside createInstance (loadPlugin) β€” the - // C++ exception must not escape the LOAD job (would - // std::terminate the worker). Catch it, treat as failed - // load, keep the old instance, and let the restore below - // unmute the channel. - bool ok = false; - try { - ok = instruments.assign(ch, t, path, sampleRate, block); - } catch (...) { - std::cerr << "[NativeBridge] assign EXCEPTION ch=" << ch - << " type=" << (int)t << " path=" << path - << " β€” plugin threw (createInstance)" << std::endl; - ok = false; - } - if (ok) { - std::cout << "[NativeBridge] instrument loaded ch=" << ch - << " type=" << (int)t << " " << path << std::endl; - } else { - std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch - << " type=" << (int)t << " " << path << std::endl; - } - // Restore. Failed load keeps the OLD instance β€” unmute it - // back (silenced at job start); a successful assign leaves - // the fresh instance unmuted by construction. - if (!ok) g_engine->setReloading(ch, false); - for (uint32_t y : spOld) unmute_if_not_closing(y, "load"); - for (uint32_t y : spNew) unmute_if_not_closing(y, "load"); - }); - } else if (c.type == 1) { // PANIC - instruments.allNotesOff(); - std::cout << "[NativeBridge] PANIC β€” all notes off" << std::endl; - } else if (c.type == 3) { // TRANSPORT (A13) - if (c.arg0 == 0) { // STOP β†’ flush every note immediately - transportStopped = true; - // Release sustain pedal TRUOC (CC64=0) β€” nhieu VSTi giu note - // khi pedal con down -> note-off cua allNotesOff bi bo qua - // -> am treo loop (V8 bug 3). - for (uint32_t ch = 0; ch < 16; ++ch) { - if (auto* inst = instruments.get(ch)) inst->controlChange(ch, 64, 0); - } - instruments.allNotesOff(); - std::cout << "[NativeBridge] transport STOP β€” all notes off" << std::endl; - } else if (c.arg0 == 1) { // PLAY - transportStopped = false; - playheadSamples = c.arg1; - std::cout << "[NativeBridge] transport PLAY playhead=" << playheadSamples << std::endl; - } else if (c.arg0 == 2) { // SET_POSITION (seek while stopped) - playheadSamples = c.arg1; - } - } else if (c.type == 4) { // OPEN_GUI (A7): arg1 = parent HWND (0 β†’ bridge tα»± tαΊ‘o native window), arg2 = plugin id - uint32_t guiCh = c.channel; - if (guiCh >= 16) guiCh = 0; - handleOpenGui(guiCh, (uintptr_t)c.arg1, std::string(c.arg2)); - } - } - shmIPC->controlQueueCount = 0; - - // Pending OPEN_GUI requests: fulfilled as soon as the channel's - // instrument becomes available (load job completed on its worker). - { - std::vector>> readyGui; - { - std::lock_guard lock(g_pendingGuiMutex); - for (auto it = g_pendingGui.begin(); it != g_pendingGui.end(); ) { - // G0.1: giu entry khi dang PLAY - handleOpenGui se defer lai - if (transportStopped && instruments.get(it->first)) { - readyGui.push_back({ it->first, it->second }); - it = g_pendingGui.erase(it); - } else ++it; - } - } - for (auto& g : readyGui) - handleOpenGui(g.first, (uintptr_t)g.second.first, g.second.second); - } - - - // B. Snapshot queued MIDI events (bounded copy, queue reset immediately) - uint32_t nEvents = shmIPC->midiQueueCount > 64 ? 64 : shmIPC->midiQueueCount; - SharedAudioBufferIPC::MidiEventIPC evts[64]; - for (uint32_t i = 0; i < nEvents; ++i) evts[i] = shmIPC->midiQueue[i]; - shmIPC->midiQueueCount = 0; - - // A11 sample-accurate: sort by sampleOffset, dispatch at each boundary, - // render the sub-block before the boundary. Events with offset 0 (live - // keyboard, current JS) are dispatched first and shape the whole block. - std::stable_sort(evts, evts + nEvents, - [](const SharedAudioBufferIPC::MidiEventIPC& a, - const SharedAudioBufferIPC::MidiEventIPC& b) { - return a.sampleOffset < b.sampleOffset; - }); - uint32_t cursor = 0; - uint32_t ei = 0; - while (ei < nEvents && evts[ei].sampleOffset <= cursor) { dispatch(evts[ei]); ++ei; } - for (; ei < nEvents; ++ei) { - uint32_t off = evts[ei].sampleOffset < block ? evts[ei].sampleOffset : block; - if (off > cursor) { renderSegment(cursor, off); cursor = off; } - dispatch(evts[ei]); - } - if (cursor < block) renderSegment(cursor, block); - shmIPC->bridgeWriteIndex++; - - // D. Parent died / window closed -> exit (no orphan process) - if (parentPid != 0 && !parent_alive(parentPid)) { - std::cout << "[NativeBridge] parent gone β€” exiting." << std::endl; - break; - } - - // E. Synchronize with real-time audio playback - blockCount++; - auto targetTime = startTime + std::chrono::microseconds(static_cast(blockCount * blockDurationMs * 1000.0)); - auto now = std::chrono::steady_clock::now(); - if (now < targetTime) { - auto diff = std::chrono::duration_cast(targetTime - now).count(); - if (diff > 1000) { - sleep_ms(diff / 1000); - } - while (std::chrono::steady_clock::now() < targetTime) { - std::this_thread::yield(); - } - } - } catch (...) { - std::cerr << "[NativeBridge] main loop EXCEPTION β€” plugin threw during render/control" << std::endl; - } - } - -#ifdef _WIN32 - UnmapViewOfFile(shmIPC); - CloseHandle(hMapFile); - timeEndPeriod(1); - CoUninitialize(); -#else - std::free(shmIPC); -#endif - return 0; -} + HWND hToHide = nullptr; + { + std::lock_guard lock(g_guiMutex); + auto git = g_guiWindows.find(ch); + if (git != g_guiWindows.end()) hToHide = (HWND)git->second; + } + // CRASH FIX: detach view TRUOC khi thay inst. Plugin editor + // children thuoc worker thread nay (attachView chay o day); + // khong destroy parent window (main-owned) khi view con song + // -> DestroyWindow cross-thread -> USER32 0xc000041d. An + // window, giu trong registry de reuse (reopen ShowWindow lai). + if (hToHide && IsWindow(hToHide)) { + // CRASH FIX (0xc000041d USER32 while playing): silence + // the channel while the old editor detaches β€” view->removed() + // on this worker would race processor->process() on the + // audio loop (same plugin instance). + instruments.setReloading(ch, true); + // CRASH FIX: the plugin's editor window procs run on + // this worker; if one throws (Nexus) the exception must + // not escape the LOAD job (would std::terminate the + // worker). Detach best-effort and continue. + try { + disable_ime_contexts(hToHide); + if (auto* i = instruments.get(ch)) i->closeGUI(); + // 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)) { + destroy_window_on_owner(c); + } + } catch (...) { + std::cerr << "[NativeBridge] LOAD close-editor EXCEPTION ch=" << ch + << " β€” plugin window proc threw" << std::endl; + } + ShowWindow(hToHide, SW_HIDE); + } +#endif + std::lock_guard lg(g_loadMutex); + // CRASH FIX (0xc000041d): Nexus throws nlohmann::json:: + // out_of_range inside createInstance (loadPlugin) β€” the + // C++ exception must not escape the LOAD job (would + // std::terminate the worker). Catch it, treat as failed + // load, keep the old instance, and let the restore below + // unmute the channel. + bool ok = false; + try { + ok = instruments.assign(ch, t, path, sampleRate, block); + } catch (...) { + std::cerr << "[NativeBridge] assign EXCEPTION ch=" << ch + << " type=" << (int)t << " path=" << path + << " β€” plugin threw (createInstance)" << std::endl; + ok = false; + } + if (ok) { + std::cout << "[NativeBridge] instrument loaded ch=" << ch + << " type=" << (int)t << " " << path << std::endl; + } else { + std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch + << " type=" << (int)t << " " << path << std::endl; + } + // Restore. Failed load keeps the OLD instance β€” unmute it + // back (silenced at job start); a successful assign leaves + // the fresh instance unmuted by construction. + if (!ok) g_engine->setReloading(ch, false); + for (uint32_t y : spOld) unmute_if_not_closing(y, "load"); + for (uint32_t y : spNew) unmute_if_not_closing(y, "load"); + }); + } else if (c.type == 1) { // PANIC + instruments.allNotesOff(); + std::cout << "[NativeBridge] PANIC β€” all notes off" << std::endl; + } else if (c.type == 3) { // TRANSPORT (A13) + if (c.arg0 == 0) { // STOP β†’ flush every note immediately + transportStopped = true; + // Release sustain pedal TRUOC (CC64=0) β€” nhieu VSTi giu note + // khi pedal con down -> note-off cua allNotesOff bi bo qua + // -> am treo loop (V8 bug 3). + for (uint32_t ch = 0; ch < 16; ++ch) { + if (auto* inst = instruments.get(ch)) inst->controlChange(ch, 64, 0); + } + instruments.allNotesOff(); + std::cout << "[NativeBridge] transport STOP β€” all notes off" << std::endl; + } else if (c.arg0 == 1) { // PLAY + transportStopped = false; + playheadSamples = c.arg1; + std::cout << "[NativeBridge] transport PLAY playhead=" << playheadSamples << std::endl; + } else if (c.arg0 == 2) { // SET_POSITION (seek while stopped) + playheadSamples = c.arg1; + } + } else if (c.type == 4) { // OPEN_GUI (A7): arg1 = parent HWND (0 β†’ bridge tα»± tαΊ‘o native window), arg2 = plugin id + uint32_t guiCh = c.channel; + if (guiCh >= 16) guiCh = 0; + handleOpenGui(guiCh, (uintptr_t)c.arg1, std::string(c.arg2)); + } + } + shmIPC->controlQueueCount = 0; + + // Pending OPEN_GUI requests: fulfilled as soon as the channel's + // instrument becomes available (load job completed on its worker). + { + std::vector>> readyGui; + { + std::lock_guard lock(g_pendingGuiMutex); + for (auto it = g_pendingGui.begin(); it != g_pendingGui.end(); ) { + // G0.1: giu entry khi dang PLAY - handleOpenGui se defer lai + if (transportStopped && instruments.get(it->first)) { + readyGui.push_back({ it->first, it->second }); + it = g_pendingGui.erase(it); + } else ++it; + } + } + for (auto& g : readyGui) + handleOpenGui(g.first, (uintptr_t)g.second.first, g.second.second); + } + + + // B. Snapshot queued MIDI events (bounded copy, queue reset immediately) + uint32_t nEvents = shmIPC->midiQueueCount > 64 ? 64 : shmIPC->midiQueueCount; + SharedAudioBufferIPC::MidiEventIPC evts[64]; + for (uint32_t i = 0; i < nEvents; ++i) evts[i] = shmIPC->midiQueue[i]; + shmIPC->midiQueueCount = 0; + + // A11 sample-accurate: sort by sampleOffset, dispatch at each boundary, + // render the sub-block before the boundary. Events with offset 0 (live + // keyboard, current JS) are dispatched first and shape the whole block. + std::stable_sort(evts, evts + nEvents, + [](const SharedAudioBufferIPC::MidiEventIPC& a, + const SharedAudioBufferIPC::MidiEventIPC& b) { + return a.sampleOffset < b.sampleOffset; + }); + uint32_t cursor = 0; + uint32_t ei = 0; + while (ei < nEvents && evts[ei].sampleOffset <= cursor) { dispatch(evts[ei]); ++ei; } + for (; ei < nEvents; ++ei) { + uint32_t off = evts[ei].sampleOffset < block ? evts[ei].sampleOffset : block; + if (off > cursor) { renderSegment(cursor, off); cursor = off; } + dispatch(evts[ei]); + } + if (cursor < block) renderSegment(cursor, block); + shmIPC->bridgeWriteIndex++; + + // D. Parent died / window closed -> exit (no orphan process) + if (parentPid != 0 && !parent_alive(parentPid)) { + std::cout << "[NativeBridge] parent gone β€” exiting." << std::endl; + break; + } + + // E. Synchronize with real-time audio playback + blockCount++; + auto targetTime = startTime + std::chrono::microseconds(static_cast(blockCount * blockDurationMs * 1000.0)); + auto now = std::chrono::steady_clock::now(); + if (now < targetTime) { + auto diff = std::chrono::duration_cast(targetTime - now).count(); + if (diff > 1000) { + sleep_ms(diff / 1000); + } + while (std::chrono::steady_clock::now() < targetTime) { + std::this_thread::yield(); + } + } + } catch (...) { + std::cerr << "[NativeBridge] main loop EXCEPTION β€” plugin threw during render/control" << std::endl; + } + } + +#ifdef _WIN32 + UnmapViewOfFile(shmIPC); + CloseHandle(hMapFile); + timeEndPeriod(1); + CoUninitialize(); +#else + std::free(shmIPC); +#endif + return 0; +}