// 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" #include "Vst2Instrument.h" #include "StateStore.h" #include "RenderJob.h" #include "RenderFxJob.h" #ifdef _WIN32 #include #include #include #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) { ImmAssociateContext(w, nullptr); } } 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 static InstrumentEngineManager* g_engine = nullptr; // declared unconditionally (Linux --render build needs the symbol) 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 SharedAudioBufferIPC* g_shmIPC = nullptr; // A14: midi-out hook writes SHM // VST2 (DUNE 3) thread contract: entry + init dispatcher + processReplacing // must share ONE thread. processReplacing runs on the audio (main) thread, // so VST2 loads (LoadLibrary + VSTPluginMain + init dispatcher) are deferred // to main too — loading on the uiWorker made DUNE write ~2M floats past n // into in/out buffers and corrupt the heap (0xC0000374). Other instrument // types (VST3 COM STA apartment) keep their worker. Drained each loop before // render; a VST2 load stalls the audio thread for its duration (DUNE loads // well under the 3s watchdog kill). static std::mutex g_mainDeferredMutex; static std::vector> g_mainDeferred; static void runOnMain(std::function job) { std::lock_guard lock(g_mainDeferredMutex); g_mainDeferred.push_back(std::move(job)); } // T2.4: realtime master FX chain (live audio through VST3 FX). Created in // main() once sampleRate is known; fed via control queue type 6. static std::unique_ptr g_fxChain; // 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 }; 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; // G2.1: state persistence. g_stateDirty is set by load/unload/transport/periodic // marks; the main loop debounces and posts one save job to the UiThread, so disk // IO never runs on the audio loop. Playing is tracked separately from // transportStopped because the audio loop must not write files. static std::atomic g_stateDirty{false}; static std::atomic g_transportPlaying{false}; static std::mutex g_lastSnapMutex; static BridgeStateSnapshot g_lastSnap; // Same instrument set and transport -> nothing to write. Playhead is excluded: // comparing it would rewrite the file every ~2s while playing. static bool snapshotsEqual(const BridgeStateSnapshot& a, const BridgeStateSnapshot& b) { if (a.playing != b.playing || a.instruments.size() != b.instruments.size()) return false; for (size_t i = 0; i < a.instruments.size(); ++i) { const auto& x = a.instruments[i]; const auto& y = b.instruments[i]; if (x.channel != y.channel || x.type != y.type || x.path != y.path || x.presetBase64 != y.presetBase64 || x.bank != y.bank || x.program != y.program) return false; } return true; } // 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; // 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; } } // VST2 editor idle tick: effEditIdle (repaints / size change // notifications). Editors were opened on this thread; the // engine's default guiIdle() is a no-op for other types. if (g_engine) { try { g_engine->guiIdleAll(); } catch (...) { std::cerr << "[NativeBridge] worker guiIdle EXCEPTION — plugin 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]; } // G1.3: 1 UiThread � editor windows tao trong attach job (UiThread), destroy // chi goi tu UiThread job (close_editor_now / LOAD) -> ownerTid luon == current // tid -> destroy truc tiep. Neu owner khac thread (khong con xay ra) -> bo // (leak > crash): cross-thread DestroyWindow khong an toan. static void destroy_window_on_owner(HWND hwnd) { if (!hwnd || !IsWindow(hwnd)) return; DWORD ownerTid = GetWindowThreadProcessId(hwnd, nullptr); DWORD curTid = GetCurrentThreadId(); if (ownerTid == 0 || ownerTid == curTid) { if (IsWindow(hwnd)) DestroyWindow(hwnd); return; } std::cerr << "[NativeBridge] destroy_window_on_owner: owner tid=" << ownerTid << " != current " << curTid << " - leaving window " << hwnd << std::endl; } // Unmute y unless its own close job is still inside createInstance — that job // performs the unmute once its fresh instance is loaded (or, if an attach for // 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); }); } } // --- crash minidump handler (temp diagnostics) ------------------------------- // WER LocalDumps produces NO dump for 0xC0000374 (heap corruption) — write our // own minidump via dbghelp.dll when an unhandled exception reaches this filter. #ifdef _WIN32 static LONG WINAPI CrashDumpHandler(EXCEPTION_POINTERS* ep) { static LONG once = 0; if (InterlockedCompareExchange(&once, 1, 0)) return EXCEPTION_CONTINUE_SEARCH; PEXCEPTION_RECORD er = ep ? ep->ExceptionRecord : nullptr; std::cerr << "[CRASH] exception code=0x" << std::hex << (er ? er->ExceptionCode : 0) << " addr=0x" << (er ? (void*)er->ExceptionAddress : nullptr) << std::dec << std::endl; if (er) { HMODULE hMod = nullptr; char modPath[MAX_PATH] = {0}; if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCSTR)er->ExceptionAddress, &hMod) && hMod) { GetModuleFileNameA(hMod, modPath, MAX_PATH); std::cerr << "[CRASH] faulting module: " << modPath << std::endl; } } const char* dumpDir = "C:/Users/locpham/AppData/Local/Temp/sf_dumps"; CreateDirectoryA(dumpDir, nullptr); char dumpPath[MAX_PATH]; snprintf(dumpPath, sizeof(dumpPath), "%s/bridge_crash_%lu.dmp", dumpDir, GetCurrentProcessId()); HMODULE hDbg = LoadLibraryA("dbghelp.dll"); if (hDbg) { typedef BOOL(WINAPI* MiniDumpWriteDumpFn)(HANDLE, DWORD, HANDLE, MINIDUMP_TYPE, PMINIDUMP_EXCEPTION_INFORMATION, PMINIDUMP_USER_STREAM_INFORMATION, PMINIDUMP_CALLBACK_INFORMATION); auto pMiniDumpWriteDump = (MiniDumpWriteDumpFn)GetProcAddress(hDbg, "MiniDumpWriteDump"); if (pMiniDumpWriteDump) { HANDLE hFile = CreateFileA(dumpPath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile != INVALID_HANDLE_VALUE) { MINIDUMP_EXCEPTION_INFORMATION mei = {}; mei.ThreadId = GetCurrentThreadId(); mei.ExceptionPointers = ep; mei.ClientPointers = TRUE; BOOL ok = pMiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, MiniDumpNormal, &mei, nullptr, nullptr); CloseHandle(hFile); std::cerr << "[CRASH] minidump " << (ok ? "written: " : "FAILED: ") << dumpPath << std::endl; } else { std::cerr << "[CRASH] cannot open dump file" << std::endl; } } else { std::cerr << "[CRASH] MiniDumpWriteDump not found in dbghelp.dll" << std::endl; } } else { std::cerr << "[CRASH] cannot load dbghelp.dll" << std::endl; } return EXCEPTION_CONTINUE_SEARCH; // let WER proceed, process dies normally } #endif // Vectored handler: catches first-chance exceptions INCLUDING heap-corruption // raises that never reach SetUnhandledExceptionFilter (0xC0000374 observed to // bypass it on this machine). Writes the same minidump then continues. #ifdef _WIN32 // Unwind the LIVE stack from the exception context (MiniDumpWriteDump captures // the context at dump time, not at raise time — by then the handler's own // frames replace the real ones). Print return-address candidates that land in // loaded modules, lowest stack address first. #ifdef _WIN32 static void DumpStackTraceFromContext(const CONTEXT* ctx) { if (!ctx) return; std::cerr << "[VCRASH] stack trace (raw scan):" << std::endl; const unsigned char* sp = (const unsigned char*)ctx->Rsp; for (int i = 0; i < 512; i++) { uintptr_t val = 0; memcpy(&val, sp + i * 8, sizeof(val)); // stack is readable in a handler HMODULE h = nullptr; char m[MAX_PATH] = {0}; if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCSTR)val, &h) && h && GetModuleFileNameA(h, m, MAX_PATH)) { const char* base = strrchr(m, '\\'); base = base ? base + 1 : m; std::cerr << " +" << i * 8 << " " << (void*)val << " " << base << "+0x" << std::hex << (val - (uintptr_t)h) << std::dec << std::endl; } } } #endif static LONG WINAPI VectoredCrashHandler(PEXCEPTION_POINTERS ep) { PEXCEPTION_RECORD er = ep ? ep->ExceptionRecord : nullptr; DWORD code = er ? er->ExceptionCode : 0; if (code != 0xC0000374 && code != 0xC0000005 && code != 0xC0000409) { return EXCEPTION_CONTINUE_SEARCH; // ignore benign first-chance noise } std::cerr << "[VCRASH] first-chance code=0x" << std::hex << code << " addr=0x" << (er ? (void*)er->ExceptionAddress : nullptr) << std::dec << " tid=" << GetCurrentThreadId() << std::endl; DumpStackTraceFromContext(ep ? ep->ContextRecord : nullptr); // route into the same dump writer return CrashDumpHandler(ep) == EXCEPTION_CONTINUE_SEARCH ? EXCEPTION_CONTINUE_SEARCH : EXCEPTION_CONTINUE_SEARCH; } #endif int main(int argc, char* argv[]) { SetUnhandledExceptionFilter(CrashDumpHandler); AddVectoredExceptionHandler(1, VectoredCrashHandler); #ifdef _WIN32 // Background daemon: detach the console window unless SF_KEEP_CONSOLE is // set. Console apps spawned from the GUI (tauri-shell, Popen) flash a // console window otherwise. Redirected stdio (pipe to bridge.log / the // fx-gui port reader) survives FreeConsole. if (!std::getenv("SF_KEEP_CONSOLE")) FreeConsole(); #endif // stderr, not stdout: stdout is reserved for machine-readable output in // one-shot modes (--scan JSON, --fx-gui "SF_FXGUI_PORT=..."). std::cerr << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl; // JBridge per-plugin settings bootstrap: Kong Audio Qin bridges all // instances through a SHARED auxhost.exe unless the per-plugin settings // file disables "run in existing auxhost"; the shared auxhost then caps // at 2 live instances (3rd floods ML_RELEASE_ERROR forever). Writing // RUN_IN_EXISTING_AUXHOST 0 gives every instance its own auxhost -> // unlimited Qin tracks. File is created if missing, never overwritten // (user tweaks are respected). try { const char* appdata = std::getenv("APPDATA"); if (appdata) { const char* dirs[] = {"SettingsFilesFor64BitPlugins", "SettingsFilesFor32BitPlugins"}; for (const char* d : dirs) { std::string dir = std::string(appdata) + "\\jBridge\\" + d; std::string file = dir + "\\Qin_RV.jBridge"; CreateDirectoryA(dir.c_str(), nullptr); HANDLE h = CreateFileA(file.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (h == INVALID_HANDLE_VALUE) { const char* content = "//Qin_RV.jBridge - jBridge per-plugin settings\r\n" "RUN_IN_EXISTING_AUXHOST 0\r\n" "RUN_IN_SAME_AUXHOST_GROUP 0\r\n" "AUXHOST_PRIORITY 0\r\n" "USE_SEPARATED_GUI 0\r\n" "USE_DEPRECATED_OPCODES 0\r\n"; HANDLE w = CreateFileA(file.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); if (w != INVALID_HANDLE_VALUE) { DWORD wr = 0; WriteFile(w, content, (DWORD)strlen(content), &wr, nullptr); CloseHandle(w); std::cerr << "[NativeBridge] jBridge settings created: " << file << std::endl; } } else { CloseHandle(h); } } } } catch (...) { std::cerr << "[NativeBridge] jBridge settings bootstrap failed (non-fatal)" << 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 // --render --out : one-shot offline render, no SHM/loop. std::string renderJob, renderOut; for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "--render") == 0 && i + 1 < argc) renderJob = argv[i + 1]; else if (std::strcmp(argv[i], "--out") == 0 && i + 1 < argc) renderOut = argv[i + 1]; } if (!renderJob.empty()) { if (renderOut.empty()) { std::cerr << "[RenderJob] --render requires --out " << std::endl; return 1; } return run_render_job(renderJob, renderOut); } // --render-fx --in --out : one-shot // offline FX render (VST3 effects + builtins), no SHM/loop. std::string renderFxJob, renderFxIn, renderFxOut; for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "--render-fx") == 0 && i + 1 < argc) renderFxJob = argv[i + 1]; else if (std::strcmp(argv[i], "--in") == 0 && i + 1 < argc) renderFxIn = argv[i + 1]; else if (std::strcmp(argv[i], "--out") == 0 && i + 1 < argc) renderFxOut = argv[i + 1]; } if (!renderFxJob.empty()) { if (renderFxIn.empty() || renderFxOut.empty()) { std::cerr << "[RenderFxJob] --render-fx requires --in --out " << std::endl; return 1; } return run_render_fx_job(renderFxJob, renderFxIn, renderFxOut); } // --scan : one-shot plugin scan, no SHM/loop. std::string scanDir; for (int i = 1; i + 1 < argc; ++i) { if (std::strcmp(argv[i], "--scan") == 0) scanDir = argv[i + 1]; } if (!scanDir.empty()) return run_scan_dir(scanDir); // --open-fx-gui : one-shot native editor window for a VST3 FX // (Mastering/FX Rack → "mở VST GUI"); runs until the user closes it. std::string fxGuiJob; for (int i = 1; i + 1 < argc; ++i) { if (std::strcmp(argv[i], "--open-fx-gui") == 0) fxGuiJob = argv[i + 1]; } if (!fxGuiJob.empty()) return run_open_fx_gui(fxGuiJob); // --fx-gui : embedded GUI server (frame capture + input relay). // See PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 0. std::string fxGuiServerJob; for (int i = 1; i + 1 < argc; ++i) { if (std::strcmp(argv[i], "--fx-gui") == 0) fxGuiServerJob = argv[i + 1]; } if (!fxGuiServerJob.empty()) return run_fx_gui_server(fxGuiServerJob); // 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; } g_shmIPC = shmIPC; // A14: midi-out hook (audio thread) writes SHM via this // VSTi crackle fix: 8-slot audio ring (see SharedMemoryIPC.h). Must be set // on Windows too — ringMask stays 0 otherwise and the ring collapses to // 1 slot (slot = writeIndex & 0), dropping whole blocks on any jank. shmIPC->ringMask = 7; #else (void)shmName; // POSIX shm mapping (shm_open) added when porting off Windows auto* shmIPC = (SharedAudioBufferIPC*)std::calloc(1, sizeof(SharedAudioBufferIPC)); // VSTi crackle fix: 8-slot audio ring (see SharedMemoryIPC.h). shmIPC->ringMask = 7; if (!shmIPC) return 1; #endif // G3.1: dedicated heartbeat thread, independent of the audio loop. // Increments every 100ms (~10Hz). Tauri watchdog keys liveness on this. // Started right after the SHM map so a slow instrument restore at // startup (or after watchdog respawn) never looks like a dead bridge. std::thread([shmIPC]() { for (;;) { Sleep(100); shmIPC->heartbeat++; } }).detach(); 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; // VST2 editor resize hook: plugin audioMaster(SizeWindow) -> resize the // host window on the UI worker (the window belongs to its pump). Vst2Instrument::setResizeHook([](void* hwnd, int w, int h) { if (!g_uiWorker) return; g_uiWorker->post([hwnd, w, h]() { HWND parent = (HWND)hwnd; if (!parent || !IsWindow(parent)) return; RECT rc; if (!GetClientRect(parent, &rc)) return; int cw = rc.right - rc.left; int chh = rc.bottom - rc.top; if (cw != w || chh != h) SetWindowPos(parent, nullptr, 0, 0, w, h, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); }); }); // A14: plugin MIDI output sink. audioMaster(kAudioMasterProcessEvents) // (VstEvents from Scaler2/Instacomposer/...) -> SHM midiOutQueue, // drained by the Tauri pump to `bridge-midi-out`, JS routes notes to // DAW tracks. Append-only, no mutex — same pattern as midiQueue (audio // thread writes, pump reads+resets). Cap 64; drop when full. Vst2Instrument::setMidiOutHook([](uint8_t cmd, uint8_t ch, uint8_t d1, uint8_t d2, uint32_t deltaFrames) { if (!g_shmIPC) return; if (g_shmIPC->midiOutCount >= 64) return; auto& ev = g_shmIPC->midiOutQueue[g_shmIPC->midiOutCount++]; ev.command = cmd; ev.channel = ch; ev.pitch = d1; ev.velocity = d2; ev.data2 = 0; ev.data3 = 0; ev.reserved[0] = 0; ev.reserved[1] = 0; ev.sampleOffset = deltaFrames; }); // 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; } // G2.1: state file. The Rust app sets SF_STATE_FILE; fall back to the default. std::string stateFile; if (const char* e = std::getenv("SF_STATE_FILE")) stateFile = e; if (stateFile.empty()) stateFile = StateStore::defaultPath(); const uint32_t block = AUDIO_BLOCK_SIZE; // T2.4: realtime master FX chain — created once, fed via control type 6. g_fxChain = std::make_unique(); std::atomic 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 only (G1.4: bo mute-when-editor-open). // 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 && !evt.reserved[0]) return; // V8 bug 3: drop note-on sau STOP, tru live input (reserved[0]) 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 && !evt.reserved[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. // VSTi crackle fix: target the multi-slot ring slot for the block being // built, so old blocks stay readable until the reader drains them. auto renderSegment = [&](uint32_t from, uint32_t to) { if (to <= from) return; uint32_t slot = shmIPC->bridgeWriteIndex & shmIPC->ringMask; instruments.renderAll(shmIPC->ringLeft[slot] + from, shmIPC->ringRight[slot] + 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.has(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; } // VST2 headless (no editor) hoac instrument load fail -> khong tao // window, khong reuse window cua channel khac (window trong voi title // sai plugin). Frontend hien "VST2 headless - no GUI" thay vi window. if (instruments.withInstrument(guiCh, [](INativeInstrument* i0) { return !i0 || !i0->canOpenGUI(); })) { std::cerr << "[NativeBridge] GUI skip ch=" << guiCh << " plugin=" << pluginId << " (headless - no editor)" << std::endl; 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 (instruments.withInstrument(guiCh, [](INativeInstrument* i0) { return i0 && i0->hasAttachedView() && !i0->ownsGuiWindow(); })) { return; } // G0.1 (historical): mo GUI khi dang PLAY bi defer toi khi STOP vi 2 // thread 1 plugin (audio loop process + attach worker) -> Nexus throw. // LEVEL-1 (2026-08-15): bo defer. G0.1b (setReloading tren MAIN thread // truoc job, o duoi) da silence same-path channels va G1.2 (1 UiThread, // audio loop khong pump window messages) chan race 2 thread 1 DLL. // GUI mo ngay khi play; mute ngan (~0.1-0.3s lan dau: hasAttachedOnce_ // false nen reloadForGUI khong reload, Vst3Instrument.cpp 688). Note // dang ngan khong mat. Reopen sau khi dong VAN reload (1-3s mute). // 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() && !inst0->ownsGuiWindow()) { // 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; } } // G4.6: child-own GUI (sandbox VST2 like Qin). The child process // opens its OWN top-level window on control 4 — no native parent // window to create, no g_guiWindows registry, no same-path // silencing, no closing of other editors (separate process; // nothing to serialize). Just forward the OPEN_GUI control. if (auto* inst0 = instruments.get(guiCh)) { if (inst0->ownsGuiWindow()) { std::cerr << "[NativeBridge] GUI via child-own ch=" << guiCh << " plugin=" << arg2 << std::endl; try { inst0->attachView(nullptr); } catch (...) { std::cerr << "[NativeBridge] GUI child-own attach EXCEPTION ch=" << guiCh << std::endl; } 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). // G1.4: bo mute-when-editor-open - channel process() binh // thuong khi editor mo trong PLAY (verified: writeIndex // advancing voi GUI open, Nexus khong crash). reloading_ giu // im channel chi trong luc createView/createInstance chay // (2 thread 1 DLL); clear ngay sau attach xong. // 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 { std::cerr << "[NativeBridge] GUI job step1 reloadForGUI ch=" << guiCh << std::endl; ok = inst->reloadForGUI(); std::cerr << "[NativeBridge] GUI job step2 reloadForGUI ok=" << ok << " ch=" << guiCh << std::endl; // 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). #ifdef _WIN32 if (ok) disable_ime_contexts((HWND)hwnd); #endif if (ok) ok = inst->attachView(hwnd); std::cerr << "[NativeBridge] GUI job step3 attachView ok=" << ok << " ch=" << guiCh << std::endl; } 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) { #ifdef _WIN32 disable_ime_contexts((HWND)hwnd); #endif 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 } } }); }; // G2.1: persist instrument state to disk. Runs on the UiThread (posted via // uiWorker.post when g_stateDirty flips) so file IO never blocks the audio // loop. Deferred while any channel is mid-reload (half-dead instance). auto saveStateJob = [&]() { std::cerr << "[NativeBridge] saveStateJob begin" << std::endl; for (uint32_t ch = 0; ch < 16; ++ch) { if (instruments.isReloading(ch)) { g_stateDirty = true; return; } } BridgeStateSnapshot snap; if (!instruments.snapshot(snap)) return; snap.playing = g_transportPlaying.load(); snap.playhead = (double)playheadSamples.load(); { std::lock_guard lock(g_lastSnapMutex); if (snapshotsEqual(snap, g_lastSnap)) return; g_lastSnap = snap; } if (StateStore::save(snap, stateFile)) std::cout << "[NativeBridge] state saved: " << snap.instruments.size() << " instruments, playing=" << (snap.playing ? "true" : "false") << std::endl; else std::cerr << "[NativeBridge] state save FAILED: " << stateFile << std::endl; }; // G2.3: auto-reload instruments from the state file after a respawn. // Runs before the audio loop: read the file once, post one LOAD job per // saved channel to the UiThread (assign + preset restore), then resume // transport if the session was PLAYing. Only restores channels that are // empty (the frontend may already have loaded some by the time the state // file becomes visible). All jobs serialize on g_loadMutex; file IO here // is startup-only, never in the audio loop. { BridgeStateSnapshot snap; if (StateStore::load(snap, stateFile)) { std::cout << "[NativeBridge] restoring " << snap.instruments.size() << " instruments from " << stateFile << std::endl; for (const auto& e : snap.instruments) { uint32_t ch = e.channel & 0xF; if (e.path.empty() || instruments.has(ch)) continue; // guard: empty channel only InstrumentType t = (InstrumentType)e.type; std::string path = e.path; std::string preset = e.presetBase64; uint32_t bank = e.bank, program = e.program; uiWorker.post([&instruments, t, ch, path, preset, bank, program, sampleRate, block]() { auto doRestore = std::function( [&instruments, t, ch, path, preset, bank, program, sampleRate, block]() { std::lock_guard lg(g_loadMutex); bool ok = false; try { ok = instruments.assign(ch, t, path, sampleRate, block); } catch (...) { std::cerr << "[NativeBridge] restore assign EXCEPTION ch=" << ch << " type=" << (int)t << " path=" << path << std::endl; ok = false; } if (ok) { if (auto* inst = instruments.get(ch)) { } if (ok) { auto* inst2 = instruments.get(ch); if (!inst2) { std::cerr << "[NativeBridge] restore lost inst ch=" << ch << std::endl; } else { if ((t == InstrumentType::VST3 || t == InstrumentType::VST2) && preset.size() >= 100) inst2->loadSerializedState(preset); else inst2->selectProgram(ch, bank, program); // Re-calibrate AFTER the preset exists: assign() // calibrated the empty instance (VST2 like Qin // probes silent -> makeup=1.0, quieter than the // other VSTi). Probe-render runs off mu_ so the // audio loop never stalls. if (t == InstrumentType::VST3 || t == InstrumentType::VST2) instruments.recalibrate(ch, sampleRate, block); } } std::cout << "[NativeBridge] restored ch=" << ch << " type=" << (int)t << " " << path << std::endl; } else { std::cerr << "[NativeBridge] restore FAILED ch=" << ch << " type=" << (int)t << " " << path << std::endl; } }); // DUNE 3: VST2 entry+init must run on the audio thread. if (t == InstrumentType::VST2) runOnMain(doRestore); else doRestore(); }); } // FIX auto-play: KHONG tu bat transport khi restore state -- bridge // khong duoc tu phat am. Frontend la chu transport: page dang play // that su se tu gui PLAY lai sau reconnect; page fresh thi im lang. if (snap.playing) { std::cout << "[NativeBridge] restore transport SKIPPED (snap.playing=true)" << std::endl; } } else { std::cout << "[NativeBridge] no state file to restore (" << stateFile << ")" << std::endl; } } // 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]() { // G2.4: double-load guard - sau respawn/bootstrap, JS // reconnect reload gui LOAD cung path (C++ da tu restore // tu state file); thay instance se mat preset. Skip khi // channel da co DUNG instrument nay. if (!path.empty() && !lower_plugin_path(ch).empty() && lower_plugin_path(ch) == lower_path_str(path)) { std::cout << "[NativeBridge] LOAD skip ch=" << ch << " (already loaded: " << path << ")" << std::endl; return; } // 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); } #endif // 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. g_loadMutex held INSIDE the lambda // so the deferred (main-thread) VST2 load still serializes // with worker loads. auto doAssign = std::function( [&instruments, t, ch, path, sampleRate, block, spOld, spNew]() { std::lock_guard lg(g_loadMutex); // G4.6: deferred double-load re-check. Restore's // doRestore (also deferred via runOnMain) can assign // the SAME path FIRST — a second assign here replaces // the restored instance with a seed-only one (doAssign // applies NO preset) and clobbers the real patch in // the state file (ch3 63ba3667 -> 7f07473c observed). // Skip like the job-level guard; undo the silence. if (!path.empty() && !lower_plugin_path(ch).empty() && lower_plugin_path(ch) == lower_path_str(path)) { std::cout << "[NativeBridge] LOAD skip (deferred re-check) ch=" << ch << " path=" << path << std::endl; g_engine->setReloading(ch, false); for (uint32_t y : spOld) unmute_if_not_closing(y, "load-skip"); for (uint32_t y : spNew) unmute_if_not_closing(y, "load-skip"); g_stateDirty = true; return; } 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"); g_stateDirty = true; // G2.1: instrument changed }); if (t == InstrumentType::VST2) { // DUNE 3: entry+init must share the processReplacing // thread → defer the whole load to the audio thread. runOnMain(doAssign); } else { doAssign(); } }); } 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) { instruments.withInstrument(ch, [&](INativeInstrument* inst) { if (inst) inst->controlChange(ch, 64, 0); }); } instruments.allNotesOff(); g_transportPlaying = false; g_stateDirty = true; // G2.1: transport changed std::cout << "[NativeBridge] transport STOP — all notes off" << std::endl; } else if (c.arg0 == 1) { // PLAY transportStopped = false; g_transportPlaying = true; g_stateDirty = true; // G2.1: transport changed 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; g_stateDirty = true; // G2.1: transport changed } } 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)); } else if (c.type == 5) { // DUMP_STATE (G2.2): Rust pre-kill flush std::cout << "[NativeBridge] DUMP_STATE - flushing state now" << std::endl; uiWorker.post([&]() { saveStateJob(); }); } else if (c.type == 6) { // SET_FX_CHAIN (T2.4): arg2 = JSON [{"path","bypass"},...] if (g_fxChain) g_fxChain->setChain(std::string(c.arg2), sampleRate, (int32_t)block); } } shmIPC->controlQueueCount = 0; // VST2 deferred loads (entry + init must run on THIS thread — DUNE 3 // thread contract). Drained after the control queue so a LOAD ctrl and // its deferred assign land in the same iteration, before any render. { std::vector> deferred; { std::lock_guard lock(g_mainDeferredMutex); deferred.swap(g_mainDeferred); } for (auto& job : deferred) { try { job(); } catch (...) { std::cerr << "[NativeBridge] main-deferred job EXCEPTION" << std::endl; } } } // 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: fulfill as soon as instrument is ready, even while playing if (instruments.has(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); } // G2.1: state persistence. The VST GUI can change the preset with no // control/MIDI traffic, so mark dirty every ~2s while instruments are // loaded; a 300ms debounce posts one save job to the UiThread. // TEMP diag: SF_NO_AUTOSAVE=1 disables the 2s mark (race test). if (!std::getenv("SF_NO_AUTOSAVE")) { { static auto lastMark = std::chrono::steady_clock::now(); auto nowMark = std::chrono::steady_clock::now(); if (std::chrono::duration_cast(nowMark - lastMark).count() >= 2000) { lastMark = nowMark; for (uint32_t ch = 0; ch < 16; ++ch) { if (instruments.has(ch)) { g_stateDirty = true; break; } } } } if (g_stateDirty.load()) { static auto lastStatePost = std::chrono::steady_clock::now(); auto nowPost = std::chrono::steady_clock::now(); if (std::chrono::duration_cast(nowPost - lastStatePost).count() >= 300) { lastStatePost = nowPost; g_stateDirty = false; uiWorker.post([&]() { saveStateJob(); }); } } } // SF_NO_AUTOSAVE // V8 bug 5 (stale-reload watchdog): a close/attach job can hang // inside createInstance (Nexus, observed) — reloading_ stays true // forever, the channel and its same-plugin siblings stay muted, // heartbeat keeps ticking so the Rust watchdog never respawns us: // permanent silence with no log. Scan once per second; a reload // older than 20s with no close/attach job in flight, or older than // 60s regardless, is stale — force-clear so the audio loop processes // the instance again. Risk (2 threads in one DLL) is bounded: SEH // SafeProcessChannel re-mutes on crash; a hard DLL deadlock stalls // writeIndex and Rust respawns the bridge (which recovers sound — // observed: restart fixed muted channels). { static bool reloadWasSet[16] = { false }; static std::chrono::steady_clock::time_point reloadSeen[16]; static auto lastScan = std::chrono::steady_clock::now(); auto nowScan = std::chrono::steady_clock::now(); if (std::chrono::duration_cast(nowScan - lastScan).count() >= 1000) { lastScan = nowScan; for (uint32_t ch = 0; ch < 16; ++ch) { bool rl = instruments.isReloading(ch); if (!rl) { reloadWasSet[ch] = false; continue; } if (!reloadWasSet[ch]) { reloadWasSet[ch] = true; reloadSeen[ch] = nowScan; continue; } double sec = std::chrono::duration(nowScan - reloadSeen[ch]).count(); bool jobInFlight = shmIPC->attachInFlight != 0 || close_in_flight(ch); if (sec > 60.0 || (sec > 20.0 && !jobInFlight)) { std::cerr << "[NativeBridge] STALE-RELOAD watchdog ch=" << ch << " reloading " << (int)sec << "s (attachInFlight=" << shmIPC->attachInFlight << " closeInFlight=" << close_in_flight(ch) << ") — force unmute" << std::endl; instruments.setReloading(ch, false); reloadWasSet[ch] = false; } } } } // 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); // T2.4: realtime master FX chain — apply to the just-rendered slot // (in-place, owned buffers) before publishing. if (g_fxChain) { uint32_t slot = shmIPC->bridgeWriteIndex & shmIPC->ringMask; g_fxChain->process(shmIPC->ringLeft[slot], shmIPC->ringRight[slot], 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); if (g_fxChain) g_fxChain->shutdown(); // destroy VST3 FX before COM teardown CoUninitialize(); #else std::free(shmIPC); #endif return 0; }