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:
Binary file not shown.
@@ -72,6 +72,16 @@ public:
|
|||||||
|
|
||||||
INativeInstrument* get(uint32_t channel);
|
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
|
// Mark a channel as rebuilding its VST instance (GUI reopen): the
|
||||||
// real-time loop then skips processAudioBlock for that channel so VST
|
// real-time loop then skips processAudioBlock for that channel so VST
|
||||||
// teardown never races the audio thread.
|
// teardown never races the audio thread.
|
||||||
@@ -90,6 +100,7 @@ private:
|
|||||||
|
|
||||||
mutable std::mutex mu_;
|
mutable std::mutex mu_;
|
||||||
std::map<uint32_t, std::unique_ptr<INativeInstrument>> channels_;
|
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.
|
// Per-instrument scratch so engines that overwrite (not mix) stay additive.
|
||||||
std::vector<float> scratchL_, scratchR_;
|
std::vector<float> scratchL_, scratchR_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -186,11 +186,33 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
|
|||||||
auto it = channels_.find(channel);
|
auto it = channels_.find(channel);
|
||||||
if (it != channels_.end()) oldInst = std::move(it->second);
|
if (it != channels_.end()) oldInst = std::move(it->second);
|
||||||
channels_[channel] = std::move(inst);
|
channels_[channel] = std::move(inst);
|
||||||
|
paths_[channel] = path;
|
||||||
}
|
}
|
||||||
oldInst.reset();
|
oldInst.reset();
|
||||||
return true;
|
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) {
|
INativeInstrument* InstrumentEngineManager::get(uint32_t channel) {
|
||||||
std::lock_guard<std::mutex> lock(mu_);
|
std::lock_guard<std::mutex> lock(mu_);
|
||||||
auto it = channels_.find(channel);
|
auto it = channels_.find(channel);
|
||||||
|
|||||||
+80
-24
@@ -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 std::map<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr;
|
||||||
|
|
||||||
static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
|
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;
|
uint32_t ch = UINT32_MAX;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||||
auto it = g_hwndToCh.find(hwnd);
|
auto it = g_hwndToCh.find(hwnd);
|
||||||
if (it != g_hwndToCh.end()) ch = it->second;
|
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);
|
if (ch != UINT32_MAX) post_close_gui(ch, hwnd);
|
||||||
}
|
}
|
||||||
return DefWindowProcA(hwnd, uMsg, wParam, lParam);
|
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 (wit != g_workers->end()) w = wit->second.get();
|
||||||
}
|
}
|
||||||
if (w) {
|
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]() {
|
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();
|
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
|
#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
|
// Option B: chi 1 editor VST mo tai 1 thoi diem toan
|
||||||
// bridge. Instance thu 2 cua CUNG plugin (Nexus) attach
|
// bridge. Instance thu 2 cua CUNG plugin (Nexus) attach
|
||||||
// view o apartment/worker khac -> treo. Dong editor cua
|
// 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);
|
PostMessage(yHwnd, WM_CLOSE, 0, 0);
|
||||||
std::cerr << "[dbg] openGUI: closing editor ch=" << y
|
std::cerr << "[dbg] openGUI: closing editor ch=" << y
|
||||||
<< " before attach ch=" << guiCh << std::endl;
|
<< " 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;
|
bool closed = false;
|
||||||
for (int i = 0; i < 500; ++i) {
|
for (int i = 0; i < 500; ++i) {
|
||||||
{
|
auto* yi = instruments.get(y);
|
||||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
if (!yi || !yi->hasAttachedView()) { closed = true; break; }
|
||||||
if (g_guiWindows.find(y) == g_guiWindows.end()) { closed = true; break; }
|
|
||||||
}
|
|
||||||
Sleep(10);
|
Sleep(10);
|
||||||
}
|
}
|
||||||
if (!closed)
|
if (!closed)
|
||||||
std::cerr << "[dbg] openGUI: editor ch=" << y
|
std::cerr << "[dbg] openGUI: editor ch=" << y
|
||||||
<< " not closed in 5s, proceeding" << std::endl;
|
<< " 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
|
#else
|
||||||
@@ -487,6 +534,11 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
if (ok) ok = inst->attachView(hwnd);
|
if (ok) ok = inst->attachView(hwnd);
|
||||||
if (guard) instruments.setReloading(guiCh, false);
|
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)
|
if (ok)
|
||||||
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
|
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
|
||||||
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
|
<< " 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
|
// 0xc000041d. Post WM_CLOSE -> main pump destroy window tren
|
||||||
// DUNG thread so huu no. Registry da erase o tren nen WM_DESTROY
|
// DUNG thread so huu no. Registry da erase o tren nen WM_DESTROY
|
||||||
// khong goi closeGUI tren inst cu (inst moi chua co view).
|
// 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);
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||||
auto git = g_guiWindows.find(ch);
|
auto git = g_guiWindows.find(ch);
|
||||||
if (git != g_guiWindows.end()) {
|
if (git != g_guiWindows.end()) hToHide = (HWND)git->second;
|
||||||
hToDestroy = (HWND)git->second;
|
}
|
||||||
g_guiWindows.erase(git);
|
// CRASH FIX: detach view TRUOC khi thay inst. Plugin editor
|
||||||
g_hwndToCh.erase(hToDestroy);
|
// 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
|
#endif
|
||||||
std::cerr << "[dbg] load thread start ch=" << ch
|
std::cerr << "[dbg] load thread start ch=" << ch
|
||||||
<< " type=" << (int)t << " path=" << path << std::endl;
|
<< " type=" << (int)t << " path=" << path << std::endl;
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ fn spawn_bridge(
|
|||||||
match app
|
match app
|
||||||
.shell()
|
.shell()
|
||||||
.command(&bridge_exe)
|
.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_PARENT_PID", std::process::id().to_string())
|
||||||
.env("SF_SAMPLE_RATE", sample_rate.to_string())
|
.env("SF_SAMPLE_RATE", sample_rate.to_string())
|
||||||
.env("SF_BLOCK_SIZE", "256")
|
.env("SF_BLOCK_SIZE", "256")
|
||||||
@@ -342,7 +342,7 @@ pub fn run() {
|
|||||||
let shm_created = Shm::create();
|
let shm_created = Shm::create();
|
||||||
println!(
|
println!(
|
||||||
"SharedMemory {}: {}",
|
"SharedMemory {}: {}",
|
||||||
shm::SHM_NAME,
|
shm::shm_name(),
|
||||||
if shm_created.is_some() { "created" } else { "unavailable (non-windows or error)" }
|
if shm_created.is_some() { "created" } else { "unavailable (non-windows or error)" }
|
||||||
);
|
);
|
||||||
app.manage(ShmState(Mutex::new(shm_created)));
|
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)]
|
#[derive(serde::Serialize)]
|
||||||
struct BridgeStatus {
|
struct BridgeStatus {
|
||||||
connected: bool,
|
connected: bool,
|
||||||
shm_name: &'static str,
|
shm_name: String,
|
||||||
write_index: u32,
|
write_index: u32,
|
||||||
block_timestamp: u64,
|
block_timestamp: u64,
|
||||||
shm_size_bytes: usize,
|
shm_size_bytes: usize,
|
||||||
@@ -610,7 +610,7 @@ fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
|
|||||||
let status = match guard.as_ref() {
|
let status = match guard.as_ref() {
|
||||||
Some(shm) => BridgeStatus {
|
Some(shm) => BridgeStatus {
|
||||||
connected: true,
|
connected: true,
|
||||||
shm_name: shm::SHM_NAME,
|
shm_name: shm::shm_name(),
|
||||||
write_index: shm.write_index(),
|
write_index: shm.write_index(),
|
||||||
block_timestamp: shm.block_timestamp(),
|
block_timestamp: shm.block_timestamp(),
|
||||||
shm_size_bytes: std::mem::size_of::<shm::SharedAudioBufferIPC>(),
|
shm_size_bytes: std::mem::size_of::<shm::SharedAudioBufferIPC>(),
|
||||||
@@ -623,7 +623,7 @@ fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
|
|||||||
},
|
},
|
||||||
None => BridgeStatus {
|
None => BridgeStatus {
|
||||||
connected: false,
|
connected: false,
|
||||||
shm_name: shm::SHM_NAME,
|
shm_name: shm::shm_name(),
|
||||||
write_index: 0,
|
write_index: 0,
|
||||||
block_timestamp: 0,
|
block_timestamp: 0,
|
||||||
shm_size_bytes: 0,
|
shm_size_bytes: 0,
|
||||||
|
|||||||
@@ -12,7 +12,14 @@ use windows_sys::Win32::System::Memory::{
|
|||||||
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS, PAGE_READWRITE,
|
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 AUDIO_BLOCK_SIZE: usize = 256;
|
||||||
pub const MIDI_QUEUE_CAP: usize = 64;
|
pub const MIDI_QUEUE_CAP: usize = 64;
|
||||||
pub const CONTROL_QUEUE_CAP: usize = 8;
|
pub const CONTROL_QUEUE_CAP: usize = 8;
|
||||||
@@ -67,7 +74,7 @@ impl Shm {
|
|||||||
if !cfg!(windows) {
|
if !cfg!(windows) {
|
||||||
return None;
|
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 size = std::mem::size_of::<SharedAudioBufferIPC>() as u64;
|
||||||
let handle = unsafe {
|
let handle = unsafe {
|
||||||
CreateFileMappingW(
|
CreateFileMappingW(
|
||||||
|
|||||||
Reference in New Issue
Block a user