fix: bridge crash/stall when opening native VST GUI while playing

- Fix A (USER32 crash): WM_CLOSE no longer destroys the native VST
  window from the main thread (plugin editor children live on the
  channel worker thread). WM_CLOSE -> hide + closeGUI on worker;
  WM_DESTROY erases registry. LOAD_INSTRUMENT close uses closeGUI +
  SW_HIDE, waits on hasAttachedView() instead of registry erase.
- Fix B: per-process SHM name SonicForge_DAW_IPC_{pid} so stale
  bridges from old builds can never attach to a new app's ring buffer.
- Fix C (stall): opening editor for a plugin whose other channel still
  has a live instance of the same DLL (Nexus) deadlocked the audio
  loop (reload+createView on worker while audio thread processed the
  other instance). Silence same-path channels via setReloading during
  reload/attach, restore afterwards; audio resumes.
- Rebuild bridge + update install/ portable package.
This commit is contained in:
2026-08-14 14:36:34 +07:00
parent a1bbf549b7
commit 0696545ae9
6 changed files with 127 additions and 31 deletions
Binary file not shown.
@@ -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<uint32_t, std::unique_ptr<INativeInstrument>> channels_;
std::map<uint32_t, std::string> paths_; // last assigned path per channel
// Per-instrument scratch so engines that overwrite (not mix) stay additive.
std::vector<float> scratchL_, scratchR_;
};
@@ -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<INativeInstrument> oldInst;
{
std::lock_guard<std::mutex> 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<std::mutex> lock(mu_);
auto it = paths_.find(channel);
return it == paths_.end() ? std::string() : it->second;
}
INativeInstrument* InstrumentEngineManager::get(uint32_t channel) {
std::lock_guard<std::mutex> lock(mu_);
auto it = channels_.find(channel);
+80 -24
View File
@@ -64,13 +64,40 @@ static std::map<HWND, uint32_t> g_hwndToCh; // HWND -> channel (WM_DESTROY c
static std::map<uint32_t, std::unique_ptr<ChannelWorker>>* 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<uint32_t> 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<std::mutex> 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<std::mutex> 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;
+5 -5
View File
@@ -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<BridgeStatus, String> {
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::<shm::SharedAudioBufferIPC>(),
@@ -623,7 +623,7 @@ fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
},
None => BridgeStatus {
connected: false,
shm_name: shm::SHM_NAME,
shm_name: shm::shm_name(),
write_index: 0,
block_timestamp: 0,
shm_size_bytes: 0,
+9 -2
View File
@@ -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<u16> = SHM_NAME.encode_utf16().chain(std::iter::once(0)).collect();
let name_wide: Vec<u16> = shm_name().encode_utf16().chain(std::iter::once(0)).collect();
let size = std::mem::size_of::<SharedAudioBufferIPC>() as u64;
let handle = unsafe {
CreateFileMappingW(