fix crash daw_vst_bridge khi đóng editor Nexus đang play: reload instance trong close job + silence same-plugin quanh createInstance/createView

This commit is contained in:
2026-08-14 16:52:53 +07:00
parent 0696545ae9
commit af59aaf257
5 changed files with 325 additions and 56 deletions
Binary file not shown.
@@ -41,6 +41,11 @@ public:
// inside processAudioBlock on the same instance). // inside processAudioBlock on the same instance).
virtual bool needsReload() const { return false; } virtual bool needsReload() const { return false; }
virtual void setReloading(bool /*on*/) {} virtual void setReloading(bool /*on*/) {}
// VST3: rebuild the plugin instance (terminate + loadPlugin) on the owning
// apartment thread. The bridge calls it after tearing an editor down so the
// audio loop never processes an instance whose editor was dropped without
// view->removed(). Non-VST engines: no-op.
virtual bool reload() { return true; }
// VST3: split openGUI so instance teardown/reload runs on the owning // VST3: split openGUI so instance teardown/reload runs on the owning
// apartment thread while view->attached() may run on a temporary thread // apartment thread while view->attached() may run on a temporary thread
// with the channel worker pumping messages (see bridge main.cpp). // with the channel worker pumping messages (see bridge main.cpp).
+1 -1
View File
@@ -29,6 +29,7 @@ public:
bool attachView(void* parentWindowHandle) override; bool attachView(void* parentWindowHandle) override;
bool hasAttachedView() const override; bool hasAttachedView() const override;
void closeGUI() override; void closeGUI() override;
bool reload() override;
void setChannel(uint32_t ch) { channel_ = ch; } void setChannel(uint32_t ch) { channel_ = ch; }
bool needsReload() const override { return hasAttachedOnce_ && !guiAttached_; } bool needsReload() const override { return hasAttachedOnce_ && !guiAttached_; }
void setReloading(bool on) override { reloading_ = on; } void setReloading(bool on) override { reloading_ = on; }
@@ -45,7 +46,6 @@ private:
bool hasAttachedOnce_; bool hasAttachedOnce_;
bool reloading_; // guarded by InstrumentEngineManager::mu_ (set via setReloading) bool reloading_; // guarded by InstrumentEngineManager::mu_ (set via setReloading)
uint32_t channel_ = 0; uint32_t channel_ = 0;
bool reload();
}; };
#endif // VST3_INSTRUMENT_H #endif // VST3_INSTRUMENT_H
+13 -17
View File
@@ -246,12 +246,7 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
std::cerr << "[dbg] loadPlugin: createInstance ..." << std::endl; std::cerr << "[dbg] loadPlugin: createInstance ..." << std::endl;
std::cerr << "[dbg] loadPlugin: chosen name=" << chosen.name() << " category=" << chosen.category() std::cerr << "[dbg] loadPlugin: chosen name=" << chosen.name() << " category=" << chosen.category()
<< " subcat=" << chosen.subCategoriesString() << " idbytes="; << " subcat=" << chosen.subCategoriesString() << std::endl;
{
const unsigned char* cid2 = (const unsigned char*)chosen.ID().data();
for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)cid2[b] << ' ';
std::cerr << std::dec << std::endl;
}
IPtr<IComponent> component = factory.createInstance<IComponent>(chosen.ID()); IPtr<IComponent> component = factory.createInstance<IComponent>(chosen.ID());
if (!component) { if (!component) {
std::cerr << "[Vst3Instrument] createInstance<IComponent> failed" << std::endl; std::cerr << "[Vst3Instrument] createInstance<IComponent> failed" << std::endl;
@@ -579,16 +574,6 @@ bool Vst3Instrument::attachView(void* parentWindowHandle) {
if (!s || !s->controller || !parentWindowHandle) return false; if (!s || !s->controller || !parentWindowHandle) return false;
IPlugView* rawView = nullptr; IPlugView* rawView = nullptr;
tresult qi = s->controller->queryInterface(IPlugView::iid, (void**)&rawView); tresult qi = s->controller->queryInterface(IPlugView::iid, (void**)&rawView);
{
const unsigned char* iidb = (const unsigned char*)&IPlugView::iid;
std::cerr << "[dbg] openGUI: IPlugView::iid bytes=";
for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)iidb[b] << ' ';
std::cerr << std::dec << std::endl;
const unsigned char* eidb = (const unsigned char*)&IEditController::iid;
std::cerr << "[dbg] openGUI: IEditController::iid bytes=";
for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)eidb[b] << ' ';
std::cerr << std::dec << std::endl;
}
std::cerr << "[dbg] openGUI: controller qi IPlugView=" << (int)qi << " raw=" << (void*)rawView << " isSingle=" << (s->controllerIsComponent ? 1 : 0) << std::endl; std::cerr << "[dbg] openGUI: controller qi IPlugView=" << (int)qi << " raw=" << (void*)rawView << " isSingle=" << (s->controllerIsComponent ? 1 : 0) << std::endl;
FUnknownPtr<IPlugView> view(rawView); FUnknownPtr<IPlugView> view(rawView);
if (!view) { if (!view) {
@@ -669,7 +654,18 @@ void Vst3Instrument::closeGUI() {
return; return;
#else #else
auto* s = static_cast<Vst3HostState*>(state_); auto* s = static_cast<Vst3HostState*>(state_);
if (s && s->view && guiAttached_) s->view->removed(); // Do NOT call s->view->removed(): Nexus's removed() enters an internal
// modal wait for a message only this thread's message pump can dispatch
// — but this thread is inside removed(), so it deadlocks forever
// (observed: closeGUI job never returns; wdisp flood from the editor).
// Nulling the view and letting the next terminate() (reload / destructor)
// tear the editor windows down on this same thread is the deterministic
// path out. Callers (post_close_gui / load job) additionally destroy the
// editor's child windows right here on this thread so the plugin stops
// pumping; the leaked editor object is inert once its windows are gone.
if (s && s->view && guiAttached_)
std::cerr << "[dbg] closeGUI: skip view->removed() (Nexus deadlock) view="
<< (void*)s->view << std::endl;
if (s) s->view = nullptr; if (s) s->view = nullptr;
guiAttached_ = false; guiAttached_ = false;
#endif #endif
+306 -38
View File
@@ -19,6 +19,7 @@
#include <chrono> #include <chrono>
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
#include <cctype>
#include <condition_variable> #include <condition_variable>
#include <cstring> #include <cstring>
#include <deque> #include <deque>
@@ -62,6 +63,16 @@ static std::mutex g_guiMutex;
static std::map<uint32_t, void*> g_guiWindows; // channel -> HWND (keep window alive) static std::map<uint32_t, void*> g_guiWindows; // channel -> HWND (keep window alive)
static std::map<HWND, uint32_t> g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup) static std::map<HWND, uint32_t> g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup)
static std::map<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr; static std::map<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr;
// Same-plugin-DLL reentrancy guards: two threads inside one VST3 DLL (Nexus)
// crash or deadlock. g_attachPaths = lowercase plugin paths whose reload/
// createView is running on some worker — a same-path close job must not unmute
// its channel mid-attach (the attach job owns the silence state until
// attachView finishes). g_closeInFlight = channels whose close job
// (closeGUI + destroy children + reload) is still inside createInstance —
// unmuting them then would race the plugin teardown.
static std::mutex g_attachMutex;
static std::vector<std::string> g_attachPaths;
static bool g_closeInFlight[16] = { false };
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_CLOSE) { if (uMsg == WM_CLOSE) {
@@ -172,7 +183,12 @@ public:
// dispatched here or the editor freezes after attach. // dispatched here or the editor freezes after attach.
lk.unlock(); lk.unlock();
MSG msg; MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { // Cap the idle pump: a busy editor (Nexus posts timer/paint
// messages continuously) otherwise starves this worker's job
// queue forever — post_close_gui never runs, Option B's 5s
// close wait times out, two editors stay alive (deadlock).
for (int pumped = 0; pumped < 16; ++pumped) {
if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break;
TranslateMessage(&msg); TranslateMessage(&msg);
DispatchMessageW(&msg); DispatchMessageW(&msg);
} }
@@ -207,6 +223,48 @@ private:
bool stop_ = false; bool stop_ = false;
}; };
// --- 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_plugin_path(uint32_t ch) {
std::string p = g_engine ? g_engine->pathOf(ch) : std::string();
std::transform(p.begin(), p.end(), p.begin(),
[](unsigned char c) { return (char)::tolower(c); });
return p;
}
// Channels (other than ch) currently assigned the same plugin DLL path.
static std::vector<uint32_t> same_plugin_channels(uint32_t ch) {
std::vector<uint32_t> 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<std::mutex> lk(g_attachMutex);
return g_closeInFlight[ch];
}
// 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) {
if (close_in_flight(y)) {
std::cerr << "[dbg] " << why << ": ch=" << y
<< " close job still in flight - close job unmutes" << std::endl;
return;
}
g_engine->setReloading(y, false);
std::cerr << "[dbg] " << why << ": restored ch=" << y << std::endl;
}
// closeGUI() MUST run on the channel worker thread (its COM STA apartment) — // closeGUI() MUST run on the channel worker thread (its COM STA apartment) —
// the view was attached there. Calling view->removed() from the main thread // the view was attached there. Calling view->removed() from the main thread
// (WM_DESTROY handler) is a cross-apartment COM call that corrupts the plugin; // (WM_DESTROY handler) is a cross-apartment COM call that corrupts the plugin;
@@ -231,9 +289,92 @@ static void post_close_gui(uint32_t ch, HWND hwnd) {
{ {
std::lock_guard<std::mutex> lock(g_guiMutex); std::lock_guard<std::mutex> lock(g_guiMutex);
auto it = g_guiWindows.find(ch); auto it = g_guiWindows.find(ch);
if (it == g_guiWindows.end() || it->second != hwnd) return; if (it == g_guiWindows.end() || it->second != hwnd) {
std::cerr << "[dbg] closeGUI ch=" << ch << " skipped (window replaced)" << std::endl;
return;
}
}
std::cerr << "[dbg] closeGUI ch=" << ch << " start" << std::endl;
// 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<std::mutex> 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()).
i->closeGUI();
// 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<uint32_t> sp = same_plugin_channels(ch);
for (uint32_t y : sp) {
g_engine->setReloading(y, true);
std::cerr << "[dbg] closeGUI: silenced same-plugin ch=" << y
<< " during reload ch=" << ch << std::endl;
}
bool reloadOk = false;
{
// destroy children + reload in ONE lock: destroying the
// editor windows runs the plugin's window proc (DLL
// entry) on this worker; it must not race another
// thread's createInstance/createView of the same DLL.
// Same-thread destroy is valid (attachView created them
// on this worker); without destroy the editor keeps
// pumping (0x47b flood) while the audio loop processes
// the instance (2 threads in one DLL) -> Nexus USER32 crash.
std::lock_guard<std::mutex> lg(g_loadMutex);
#ifdef _WIN32
if (hwnd && IsWindow(hwnd)) {
while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr))
DestroyWindow(c);
}
#endif
reloadOk = i->reload();
}
std::cerr << "[dbg] closeGUI ch=" << ch << " reload="
<< (reloadOk ? 1 : 0) << 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<std::mutex> lk(g_attachMutex);
g_closeInFlight[ch] = false;
}
bool pathBusy = false;
{
std::lock_guard<std::mutex> 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) {
std::cerr << "[dbg] closeGUI ch=" << ch
<< ": attach in flight for same plugin - leaving silenced (attach restore unmutes)"
<< std::endl;
} else {
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<std::mutex> lk(g_attachMutex);
g_closeInFlight[ch] = false;
} }
if (auto* i = g_engine->get(ch)) i->closeGUI();
}); });
} }
} }
@@ -455,28 +596,35 @@ 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
// channel khac TRUOC khi attach: WM_CLOSE -> main pump // channel khac TRUOC khi attach: WM_CLOSE -> main pump
// (audio loop) destroy window -> WM_DESTROY -> closeGUI() // destroy window -> WM_DESTROY -> closeGUI() + xoa registry.
// + xoa registry. Chay tren worker job de khong stall // Chay tren worker job de khong stall writeIndex cua real-time
// writeIndex cua real-time loop. // loop. CRASH FIX: silence moi channel TRUOC khi dong editor
// cua no (view->removed() tren worker cung luc process() tren
// audio loop = 2 thread trong 1 plugin -> crash USER32).
{ {
std::vector<uint32_t> others; std::vector<uint32_t> others;
bool abortAttach = false;
{ {
std::lock_guard<std::mutex> lock(g_guiMutex); std::lock_guard<std::mutex> lock(g_guiMutex);
for (const auto& kv : g_guiWindows) for (const auto& kv : g_guiWindows)
if (kv.first != guiCh) others.push_back(kv.first); if (kv.first != guiCh) others.push_back(kv.first);
} }
for (uint32_t y : others) { 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()) {
std::cerr << "[dbg] openGUI: ch=" << y
<< " editor already detached - skip"
<< std::endl;
continue;
}
}
HWND yHwnd = nullptr; HWND yHwnd = nullptr;
{ {
std::lock_guard<std::mutex> lock(g_guiMutex); std::lock_guard<std::mutex> lock(g_guiMutex);
@@ -484,6 +632,12 @@ int main(int argc, char* argv[]) {
if (it != g_guiWindows.end()) yHwnd = (HWND)it->second; if (it != g_guiWindows.end()) yHwnd = (HWND)it->second;
} }
if (!yHwnd || !IsWindow(yHwnd)) continue; if (!yHwnd || !IsWindow(yHwnd)) continue;
if (instruments.get(y)) {
instruments.setReloading(y, true);
std::cerr << "[dbg] openGUI: silenced ch=" << y
<< " while closing its editor (before attach ch="
<< guiCh << ")" << std::endl;
}
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;
@@ -491,27 +645,42 @@ int main(int argc, char* argv[]) {
// tren worker cua channel y. Doi cho view da detach (window // tren worker cua channel y. Doi cho view da detach (window
// duoc giu lai de reuse, khong doi registry erase nhu cu). // duoc giu lai de reuse, khong doi registry erase nhu cu).
bool closed = false; bool closed = false;
for (int i = 0; i < 500; ++i) { // The close job = closeGUI + destroy children + fresh
// reload (createInstance, ~2s for Nexus). hasAttachedView
// flips false after closeGUI only - ALSO wait for the
// close job to finish entirely (close_in_flight): the
// attach must not run createView while another thread is
// still inside this plugin DLL (2 threads in one DLL ->
// Nexus exits silently, observed in probes).
for (int i = 0; i < 500; ++i) { // 5s budget
auto* yi = instruments.get(y); auto* yi = instruments.get(y);
if (!yi || !yi->hasAttachedView()) { closed = true; break; } bool detached = !yi || !yi->hasAttachedView();
if (detached && !close_in_flight(y)) { closed = true; break; }
Sleep(10); Sleep(10);
} }
if (!closed) if (closed) {
// Editor detached and its close job finished; the
// close job unmutes the channel itself. Do NOT reset
// reloading here - the close job owns the silence state.
std::cerr << "[dbg] openGUI: restored ch=" << y
<< " after editor close" << std::endl;
} else {
std::cerr << "[dbg] openGUI: editor ch=" << y std::cerr << "[dbg] openGUI: editor ch=" << y
<< " not closed in 5s, proceeding" << std::endl; << " close job not finished in 5s, aborting attach ch=" << guiCh
// Same plugin DLL as guiCh? Silence its processAudioBlock << std::endl;
// until attach finishes (see STALL FIX above). abortAttach = true;
std::string yp = instruments.pathOf(y); break;
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;
} }
} }
if (abortAttach) {
// A close job is stuck; do NOT createView (would race the
// plugin DLL). Unmute what we silenced (skips channels
// whose close job is still in flight - that job unmutes
// them when it finishes).
for (uint32_t y : others) unmute_if_not_closing(y, "openGUI-abort");
std::cerr << "[dbg] openGUI: attach aborted ch=" << guiCh << std::endl;
return;
}
} }
#else #else
(void)0; (void)0;
@@ -524,25 +693,76 @@ int main(int argc, char* argv[]) {
// loop) dispatch messages cua no - khong can pump worker. // loop) dispatch messages cua no - khong can pump worker.
// Chan UAF bang flag reloading_ (set/clear duoi engine // Chan UAF bang flag reloading_ (set/clear duoi engine
// mutex; renderAll giu mutex khi process). // mutex; renderAll giu mutex khi process).
bool guard = inst->needsReload(); // CRASH FIX (0xc000041d USER32 while playing): keep THIS
if (guard) instruments.setReloading(guiCh, true); // channel silenced for the whole time its editor is attached.
// The worker pumps the plugin's editor windows -> the plugin's
// window proc runs on this worker thread while the audio loop
// calls processor->process() on the SAME instance (two threads
// inside one DLL) -> Nexus crashes ~1-2s after attach during
// playback. Muting the channel while the editor is open is the
// deterministic trade-off (track silent while GUI visible);
// restored when the editor closes (post_close_gui / LOAD).
// STALL/CRASH FIX (Fix C, restored): silence EVERY channel
// assigned the SAME plugin DLL path for the reload+attach
// duration — the audio loop must not process() another
// instance of this DLL while createInstance/createView runs
// on this worker (2 threads in one DLL -> Nexus exits
// silently, observed in probes). Iterate ALL 16 channels, not
// just window-owning ones: session-restore instances without
// a window are live and renderable.
std::vector<uint32_t> 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);
std::cerr << "[dbg] openGUI: silenced same-plugin ch=" << y
<< " during attach ch=" << guiCh << std::endl;
}
}
}
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<std::mutex> lk(g_attachMutex);
if (!gp.empty()) g_attachPaths.push_back(gp);
}
bool ok = false; bool ok = false;
{ {
// Serialize createInstance (reload) with other loads. // 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<std::mutex> lg(g_loadMutex); std::lock_guard<std::mutex> lg(g_loadMutex);
ok = inst->reloadForGUI(); ok = inst->reloadForGUI();
}
if (ok) ok = inst->attachView(hwnd); 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;
} }
// Attach done (ok or failed) — close jobs may unmute again.
{
std::lock_guard<std::mutex> 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) 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
<< " (channel muted while editor open)" << std::endl;
else { else {
// Attach failed -> no editor running -> safe to process again.
instruments.setReloading(guiCh, false);
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
<< " plugin=" << arg2 << std::endl; << " plugin=" << arg2 << std::endl;
#ifdef _WIN32 #ifdef _WIN32
@@ -566,9 +786,11 @@ int main(int argc, char* argv[]) {
// with gui_probe: worker-thread openGUI + concurrent pump → attached // with gui_probe: worker-thread openGUI + concurrent pump → attached
// returns kResultOk; without it → hangs forever). // returns kResultOk; without it → hangs forever).
MSG msg; MSG msg;
int pumpedMain = 0;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg); TranslateMessage(&msg);
DispatchMessageW(&msg); DispatchMessageW(&msg);
++pumpedMain;
} }
#endif #endif
// A. Control events — non-rt safe, drained first // A. Control events — non-rt safe, drained first
@@ -591,6 +813,22 @@ int main(int argc, char* argv[]) {
uint32_t ch = c.channel & 0xF; uint32_t ch = c.channel & 0xF;
if (!workers[ch]) workers[ch] = std::make_unique<ChannelWorker>(); if (!workers[ch]) workers[ch] = std::make_unique<ChannelWorker>();
workers[ch]->post([&instruments, t, ch, path, sampleRate, block]() { workers[ch]->post([&instruments, t, ch, path, sampleRate, block]() {
// CRASH FIX: this job enters the plugin DLL (createInstance
// in loadPlugin, old-instance terminate on replace) while
// the audio loop may process() another instance of the SAME
// DLL — 2 threads in one DLL crashes Nexus (observed with
// session-restore instances). Silence this channel (its old
// instance is being torn down / replaced) and every
// same-plugin channel for the whole job. Old path covers
// the old-instance teardown; the new path (after assign)
// covers the freshly created instance.
instruments.setReloading(ch, true);
std::vector<uint32_t> spOld = same_plugin_channels(ch);
for (uint32_t y : spOld) {
g_engine->setReloading(y, true);
std::cerr << "[dbg] load: silenced same-plugin ch=" << y
<< " during load ch=" << ch << std::endl;
}
#ifdef _WIN32 #ifdef _WIN32
// DONG cua so editor dang mo cua channel TRUOC khi assign(): // DONG cua so editor dang mo cua channel TRUOC khi assign():
// thay the inst (VST3 -> SF2/inst khac) ma editor con song -> // thay the inst (VST3 -> SF2/inst khac) ma editor con song ->
@@ -614,7 +852,18 @@ int main(int argc, char* argv[]) {
// -> DestroyWindow cross-thread -> USER32 0xc000041d. An // -> DestroyWindow cross-thread -> USER32 0xc000041d. An
// window, giu trong registry de reuse (reopen ShowWindow lai). // window, giu trong registry de reuse (reopen ShowWindow lai).
if (hToHide && IsWindow(hToHide)) { 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);
if (auto* i = instruments.get(ch)) i->closeGUI(); if (auto* i = instruments.get(ch)) i->closeGUI();
// Destroy the old editor's child windows (worker-owned,
// created by attachView here) so they stop pumping;
// 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))
DestroyWindow(c);
ShowWindow(hToHide, SW_HIDE); ShowWindow(hToHide, SW_HIDE);
} }
#endif #endif
@@ -630,6 +879,25 @@ int main(int argc, char* argv[]) {
std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch
<< " type=" << (int)t << " " << path << std::endl; << " type=" << (int)t << " " << path << std::endl;
} }
// New-path channels (the freshly created instance's DLL) —
// silence any not already covered by the old path.
std::vector<uint32_t> spNew;
if (ok) spNew = same_plugin_channels(ch);
for (uint32_t y : spNew) {
if (std::find(spOld.begin(), spOld.end(), y) == spOld.end()) {
g_engine->setReloading(y, true);
std::cerr << "[dbg] load: silenced same-plugin ch=" << y
<< " (new path) during load ch=" << ch << 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)
if (std::find(spOld.begin(), spOld.end(), y) == spOld.end())
unmute_if_not_closing(y, "load");
}); });
} else if (c.type == 1) { // PANIC } else if (c.type == 1) { // PANIC
instruments.allNotesOff(); instruments.allNotesOff();