diff --git a/install/daw_vst_bridge.exe b/install/daw_vst_bridge.exe index cb54b63..95c32d0 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 00172f5..f5b716e 100644 --- a/native_bridge/include/NativeInstrumentEngine.h +++ b/native_bridge/include/NativeInstrumentEngine.h @@ -72,6 +72,16 @@ public: INativeInstrument* get(uint32_t channel); + // Remove and destroy the instrument on `channel` (its destructor may call + // VST terminate — MUST run on the channel worker thread, caller's duty). + // Used by Option B: two live instances of the same plugin DLL (Nexus) + // hang createView on the second — unload the other channel's copy. + void unload(uint32_t channel); + + // Path last assigned to a channel (empty if none). Windows paths are + // case-insensitive — caller must lowercase before comparing. + std::string pathOf(uint32_t channel); + // Mark a channel as rebuilding its VST instance (GUI reopen): the // real-time loop then skips processAudioBlock for that channel so VST // teardown never races the audio thread. @@ -90,6 +100,7 @@ private: mutable std::mutex mu_; std::map> channels_; + std::map paths_; // last assigned path per channel // Per-instrument scratch so engines that overwrite (not mix) stay additive. std::vector scratchL_, scratchR_; }; diff --git a/native_bridge/src/NativeInstrumentEngine.cpp b/native_bridge/src/NativeInstrumentEngine.cpp index d8fe6c2..b2b6a11 100644 --- a/native_bridge/src/NativeInstrumentEngine.cpp +++ b/native_bridge/src/NativeInstrumentEngine.cpp @@ -186,11 +186,33 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type, auto it = channels_.find(channel); if (it != channels_.end()) oldInst = std::move(it->second); channels_[channel] = std::move(inst); + paths_[channel] = path; } oldInst.reset(); return true; } +void InstrumentEngineManager::unload(uint32_t channel) { + if (channel >= 16) return; + // Destructor runs on the CALLER thread (VST3 terminate must run on the + // channel worker). The map write is under mu_ so renderAll never stalls. + std::unique_ptr oldInst; + { + std::lock_guard lock(mu_); + auto it = channels_.find(channel); + if (it != channels_.end()) oldInst = std::move(it->second); + channels_.erase(channel); + paths_.erase(channel); + } + oldInst.reset(); +} + +std::string InstrumentEngineManager::pathOf(uint32_t channel) { + std::lock_guard lock(mu_); + auto it = paths_.find(channel); + return it == paths_.end() ? std::string() : it->second; +} + INativeInstrument* InstrumentEngineManager::get(uint32_t channel) { std::lock_guard lock(mu_); auto it = channels_.find(channel); diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index fd2df19..920c7f3 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -64,13 +64,40 @@ static std::map g_hwndToCh; // HWND -> channel (WM_DESTROY c static std::map>* g_workers = nullptr; static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { - if (uMsg == WM_DESTROY) { + 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); } return DefWindowProcA(hwnd, uMsg, wParam, lParam); @@ -193,21 +220,21 @@ static void post_close_gui(uint32_t ch, HWND hwnd) { if (wit != g_workers->end()) w = wit->second.get(); } if (w) { + // closeGUI() (view->removed()) PHAI chay tren channel worker - COM STA + // apartment cua plugin song o do. Window KHONG bi destroy: VstWindowProc + // giu lai (an) de reuse nen plugin editor children (worker-owned) khong + // bao gio bi huy cross-thread. Registry (g_guiWindows/g_hwndToCh) chi + // xoa trong WM_DESTROY khi window thuc su bi huy. w->post([ch, hwnd]() { + // Guard window-replaced race: neu OPEN_GUI moi da dung window khac + // (hoac da xoa), khong detach view cua window moi. + { + std::lock_guard lock(g_guiMutex); + auto it = g_guiWindows.find(ch); + if (it == g_guiWindows.end() || it->second != hwnd) return; + } if (auto* i = g_engine->get(ch)) i->closeGUI(); - std::lock_guard lock(g_guiMutex); - g_hwndToCh.erase(hwnd); - // Chi erase neu map van tro den HWND NAY — neu reopen da tao - // window moi (race: close cu + OPEN_GUI moi), khong duoc xoa - // entry cua window moi (con tro treo -> leak window moi). - auto wit = g_guiWindows.find(ch); - if (wit != g_guiWindows.end() && wit->second == hwnd) g_guiWindows.erase(wit); }); - } else { - std::lock_guard lock(g_guiMutex); - g_hwndToCh.erase(hwnd); - auto wit = g_guiWindows.find(ch); - if (wit != g_guiWindows.end() && wit->second == hwnd) g_guiWindows.erase(wit); } } @@ -428,6 +455,13 @@ int main(int argc, char* argv[]) { } } #ifdef _WIN32 + // STALL FIX (spawn.log "bridge stalled 3s restart"): audio loop + // processes ANOTHER live instance of the SAME plugin DLL (Nexus) + // while reload()+createView runs on THIS worker thread -> two + // threads inside one DLL -> deadlock, writeIndex stops, Tauri + // restarts the bridge. Silence same-path channels for the + // reload/attach duration, then restore (audio returns). + std::vector samePathSilenced; // Option B: chi 1 editor VST mo tai 1 thoi diem toan // bridge. Instance thu 2 cua CUNG plugin (Nexus) attach // view o apartment/worker khac -> treo. Dong editor cua @@ -453,17 +487,30 @@ int main(int argc, char* argv[]) { PostMessage(yHwnd, WM_CLOSE, 0, 0); std::cerr << "[dbg] openGUI: closing editor ch=" << y << " before attach ch=" << guiCh << std::endl; + // WM_CLOSE -> VstWindowProc -> post_close_gui -> closeGUI() + // tren worker cua channel y. Doi cho view da detach (window + // duoc giu lai de reuse, khong doi registry erase nhu cu). bool closed = false; for (int i = 0; i < 500; ++i) { - { - std::lock_guard lock(g_guiMutex); - if (g_guiWindows.find(y) == g_guiWindows.end()) { closed = true; break; } - } + auto* yi = instruments.get(y); + if (!yi || !yi->hasAttachedView()) { closed = true; break; } Sleep(10); } if (!closed) std::cerr << "[dbg] openGUI: editor ch=" << y << " not closed in 5s, proceeding" << std::endl; + // Same plugin DLL as guiCh? Silence its processAudioBlock + // until attach finishes (see STALL FIX above). + std::string yp = instruments.pathOf(y); + std::string gp = instruments.pathOf(guiCh); + std::transform(yp.begin(), yp.end(), yp.begin(), ::tolower); + std::transform(gp.begin(), gp.end(), gp.begin(), ::tolower); + if (!gp.empty() && gp == yp && instruments.get(y)) { + instruments.setReloading(y, true); + samePathSilenced.push_back(y); + std::cerr << "[dbg] openGUI: silenced same-plugin ch=" << y + << " during attach ch=" << guiCh << std::endl; + } } } #else @@ -487,6 +534,11 @@ int main(int argc, char* argv[]) { } if (ok) ok = inst->attachView(hwnd); if (guard) instruments.setReloading(guiCh, false); + for (uint32_t y : samePathSilenced) { + instruments.setReloading(y, false); + std::cerr << "[dbg] openGUI: restored ch=" << y + << " after attach ch=" << guiCh << std::endl; + } if (ok) std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd << " plugin=" << arg2 << " ch=" << guiCh << std::endl; @@ -550,17 +602,21 @@ int main(int argc, char* argv[]) { // 0xc000041d. Post WM_CLOSE -> main pump destroy window tren // DUNG thread so huu no. Registry da erase o tren nen WM_DESTROY // khong goi closeGUI tren inst cu (inst moi chua co view). - HWND hToDestroy = nullptr; + HWND hToHide = nullptr; { std::lock_guard lock(g_guiMutex); auto git = g_guiWindows.find(ch); - if (git != g_guiWindows.end()) { - hToDestroy = (HWND)git->second; - g_guiWindows.erase(git); - g_hwndToCh.erase(hToDestroy); - } + 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)) { + if (auto* i = instruments.get(ch)) i->closeGUI(); + ShowWindow(hToHide, SW_HIDE); } - if (hToDestroy && IsWindow(hToDestroy)) PostMessageA(hToDestroy, WM_CLOSE, 0, 0); #endif std::cerr << "[dbg] load thread start ch=" << ch << " type=" << (int)t << " path=" << path << std::endl; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5278630..cd02f88 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -169,7 +169,7 @@ fn spawn_bridge( match app .shell() .command(&bridge_exe) - .env("SF_SHM_NAME", shm::SHM_NAME) + .env("SF_SHM_NAME", shm::shm_name()) .env("SF_PARENT_PID", std::process::id().to_string()) .env("SF_SAMPLE_RATE", sample_rate.to_string()) .env("SF_BLOCK_SIZE", "256") @@ -342,7 +342,7 @@ pub fn run() { let shm_created = Shm::create(); println!( "SharedMemory {}: {}", - shm::SHM_NAME, + shm::shm_name(), if shm_created.is_some() { "created" } else { "unavailable (non-windows or error)" } ); app.manage(ShmState(Mutex::new(shm_created))); @@ -588,7 +588,7 @@ fn open_vst_gui(app: AppHandle, plugin_id: String, channel: u8) -> Result<(), St #[derive(serde::Serialize)] struct BridgeStatus { connected: bool, - shm_name: &'static str, + shm_name: String, write_index: u32, block_timestamp: u64, shm_size_bytes: usize, @@ -610,7 +610,7 @@ fn bridge_status(app: AppHandle) -> Result { let status = match guard.as_ref() { Some(shm) => BridgeStatus { connected: true, - shm_name: shm::SHM_NAME, + shm_name: shm::shm_name(), write_index: shm.write_index(), block_timestamp: shm.block_timestamp(), shm_size_bytes: std::mem::size_of::(), @@ -623,7 +623,7 @@ fn bridge_status(app: AppHandle) -> Result { }, None => BridgeStatus { connected: false, - shm_name: shm::SHM_NAME, + shm_name: shm::shm_name(), write_index: 0, block_timestamp: 0, shm_size_bytes: 0, diff --git a/src-tauri/src/shm.rs b/src-tauri/src/shm.rs index 11c999c..64da131 100644 --- a/src-tauri/src/shm.rs +++ b/src-tauri/src/shm.rs @@ -12,7 +12,14 @@ use windows_sys::Win32::System::Memory::{ CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS, PAGE_READWRITE, }; -pub const SHM_NAME: &str = "SonicForge_DAW_IPC"; +// SHM name RIENG cho tung process app: 2 instance (vi du bản portable install\ +// + bản MSI cài song song) dung chung ten -> 2 bridge cung map 1 SHM -> ca 2 +// drain control/midi queue (double instrument load, double GUI attach) -> race +// -> crash USER32 (Event Log 12:07:06 + 12:07:51: bridge MSI crash trong luc +// bridge portable dang chay). App PID duy nhat moi instance. +pub fn shm_name() -> String { + format!("SonicForge_DAW_IPC_{}", std::process::id()) +} pub const AUDIO_BLOCK_SIZE: usize = 256; pub const MIDI_QUEUE_CAP: usize = 64; pub const CONTROL_QUEUE_CAP: usize = 8; @@ -67,7 +74,7 @@ impl Shm { if !cfg!(windows) { return None; } - let name_wide: Vec = SHM_NAME.encode_utf16().chain(std::iter::once(0)).collect(); + let name_wide: Vec = shm_name().encode_utf16().chain(std::iter::once(0)).collect(); let size = std::mem::size_of::() as u64; let handle = unsafe { CreateFileMappingW(