fix GUI VSTi mute/reopen/not-load: revert walkthrough 2 ve hanh vi 0cec3d3 (reloadForGUI reload-on-reopen, closeGUI khong goi view->removed(), post_close_gui reload-on-close + same-plugin silence + destroy children + pathBusy, editorOpenPredicate refcount mute, CoInitializeEx MTA, window main-thread), giu cai tien an toan (WM_ERASEBKGND/PAINT/SIZE chong trang, resizeView, call_once, jobEvent_/MsgWaitForMultipleObjects pump 128, ABOVE_NORMAL + PowerThrottling worker, setBypassed quanh assign/unload); queryInterface giu kNoInterface nhu 0cec3d3 — VERIFIED: probe_fix_flow PASS (mo GUI Nexus -> close -> reopen khong mute, load Ample khi GUI dang mo OK, screenshot khong trang); warning: driver stress test 6 Nexus cung luc crash xac suat CA tren exe stable 0cec3d3 (khong phai regression)

This commit is contained in:
2026-08-15 17:50:18 +07:00
parent f94b2fa757
commit 67e73a2a85
2 changed files with 154 additions and 85 deletions
+24 -24
View File
@@ -97,13 +97,7 @@ constexpr ParamID kHostMidiProgramChange = 0x3000;
// Minimal IComponentHandler so the plugin can inform the host of param edits.
class HostComponentHandler : public IComponentHandler {
public:
tresult queryInterface(const char* iid, void** v) override {
if (std::memcmp(iid, Steinberg::Vst::IComponentHandler::iid, 16) == 0 ||
std::memcmp(iid, Steinberg::FUnknown::iid, 16) == 0) {
*v = static_cast<Steinberg::Vst::IComponentHandler*>(this);
addRef();
return kResultOk;
}
tresult queryInterface(const char*, void** v) override {
*v = nullptr;
return kNoInterface;
}
@@ -118,13 +112,7 @@ public:
// Minimal IPlugFrame so plugins can resize their editor view.
class HostPlugFrame : public Steinberg::IPlugFrame {
public:
tresult queryInterface(const char* iid, void** v) override {
if (std::memcmp(iid, Steinberg::IPlugFrame::iid, 16) == 0 ||
std::memcmp(iid, Steinberg::FUnknown::iid, 16) == 0) {
*v = static_cast<Steinberg::IPlugFrame*>(this);
addRef();
return kResultOk;
}
tresult queryInterface(const char*, void** v) override {
*v = nullptr;
return kNoInterface;
}
@@ -543,6 +531,18 @@ bool Vst3Instrument::reloadForGUI() {
#else
auto* s = static_cast<Vst3HostState*>(state_);
if (!s || !s->controller) return false;
if (s->view && guiAttached_) return true;
if (hasAttachedOnce_ && !guiAttached_) {
// Reopen GUI sau khi dong: mot so plugin (Nexus...) hang o
// view->attached() LAN 2 tren cung component instance. Tao lai inst
// moi tren CUNG worker thread (COM STA con song) roi attach lai.
// PHAN NAY PHAI chay tren worker thread — apartment cua channel song
// o day; chay tren thread tam thi apartment moi chet ngay sau do
// (reopen lan sau -> treo nhu bridge_like).
if (!reload()) return false;
s = static_cast<Vst3HostState*>(state_);
if (!s || !s->controller) return false;
}
return true;
#endif
}
@@ -642,16 +642,16 @@ void Vst3Instrument::closeGUI() {
return;
#else
auto* s = static_cast<Vst3HostState*>(state_);
if (s && s->view) {
if (guiAttached_) {
try {
s->view->removed();
} catch (...) {
std::cerr << "[Vst3Instrument] Exception inside view->removed()" << std::endl;
}
}
s->view = nullptr;
}
// 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 = nullptr;
guiAttached_ = false;
#endif
}
+130 -61
View File
@@ -116,6 +116,7 @@ static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam)
// 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 bool is_teardown_window(HWND hwnd, uint32_t pch); // fwd — defined after ChannelWorker
static uint32_t pump_window_channel(HWND hwnd); // fwd — defined after ChannelWorker
static InstrumentEngineManager* g_engine = nullptr;
@@ -210,11 +211,8 @@ static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARA
auto it = g_hwndToCh.find(hwnd);
if (it != g_hwndToCh.end()) ch = it->second;
}
if (ch != UINT32_MAX && g_engine) {
auto* inst = g_engine->get(ch);
if (inst) {
inst->resizeView(w, h);
}
if (ch != UINT32_MAX) {
post_resize_view(ch, w, h);
}
return 0;
}
@@ -450,6 +448,21 @@ private:
#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_workers) {
auto wit = g_workers->find(ch);
if (wit != g_workers->end()) {
wit->second->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
@@ -638,18 +651,46 @@ static void post_close_gui(uint32_t ch, HWND hwnd) {
// 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()).
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<uint32_t> 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)) {
@@ -657,13 +698,51 @@ static void post_close_gui(uint32_t ch, HWND hwnd) {
}
}
#endif
}
clear_editor_open(ch);
{
{
// 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<std::mutex> lg(g_loadMutex);
try {
i->reload();
} catch (...) {
std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch
<< " — plugin threw (createInstance)" << std::endl;
}
}
// Editor detached + fresh instance loaded: drop editor-open
// state (channelQuiet falls back to reloading flags only).
clear_editor_open(ch);
// 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) {
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);
clear_editor_open(ch);
std::lock_guard<std::mutex> lk(g_attachMutex);
g_closeInFlight[ch] = false;
}
g_engine->setReloading(ch, false);
});
}
}
@@ -671,8 +750,6 @@ static void post_close_gui(uint32_t ch, HWND hwnd) {
int main(int argc, char* argv[]) {
std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl;
#ifdef _WIN32
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
// Disable Power Throttling (Efficiency Mode) for this background process
#ifndef PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION
#define PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION 0x00000004
@@ -686,9 +763,6 @@ int main(int argc, char* argv[]) {
powerState.StateMask = 0; // Turn off throttling
SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &powerState, sizeof(powerState));
// Elevate main audio processing thread to time-critical priority
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);
// 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
@@ -768,8 +842,10 @@ int main(int argc, char* argv[]) {
// Editor-open predicate: channelQuiet() checks whether ANY editor is
// attached for the channel's plugin DLL path (refcounted in
// g_editorPathCount, updated by the open/close GUI jobs).
instruments.setEditorOpenPredicate([](const std::string& /*lp*/) {
return false;
instruments.setEditorOpenPredicate([](const std::string& lp) {
std::lock_guard<std::mutex> lk(g_editorMutex);
auto it = g_editorPathCount.find(lp);
return it != g_editorPathCount.end() && it->second > 0;
});
// Per-channel persistent workers: loadPlugin + openGUI run on the SAME
// thread whose COM STA apartment stays alive for the channel's lifetime
@@ -883,9 +959,44 @@ int main(int argc, char* argv[]) {
if (!workers[guiCh]) {
workers[guiCh] = std::make_unique<ChannelWorker>();
}
workers[guiCh]->post([&instruments, guiCh, arg1, arg2 = pluginId]() {
void* hwnd = (void*)arg1;
#ifdef _WIN32
// Window PHAI thuoc MAIN thread (audio loop pump nay dispatch
// messages cua no moi vong lap). Tao/cap nhat window ngay tai day.
HWND nativeHwnd = nullptr;
{
std::lock_guard<std::mutex> 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, pluginId.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(pluginId.c_str());
if (!nativeHwnd) {
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << pluginId << std::endl;
return;
}
{
std::lock_guard<std::mutex> 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
workers[guiCh]->post([&instruments, guiCh, hwnd, arg2 = pluginId]() {
if (!instruments.get(guiCh)) {
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << (void*)arg1
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl;
return;
}
@@ -894,44 +1005,6 @@ int main(int argc, char* argv[]) {
return;
}
}
void* hwnd = (void*)arg1;
#ifdef _WIN32
HWND nativeHwnd = nullptr;
{
std::lock_guard<std::mutex> 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;
return;
}
{
std::lock_guard<std::mutex> 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
if (auto* inst0 = instruments.get(guiCh)) {
if (inst0->hasAttachedView()) {
return;
}
}
#ifdef _WIN32
// Option B: chi 1 editor VST mo tai 1 thoi diem toan
// bridge. Instance thu 2 cua CUNG plugin (Nexus) attach
@@ -1063,10 +1136,7 @@ int main(int argc, char* argv[]) {
// std::terminate the worker). Treat as attach failure;
// the else branch below closes the empty window.
try {
instruments.setBypassed(true);
sleep_ms(10);
ok = inst->reloadForGUI();
instruments.setBypassed(false);
// IME recursion fix: strip the editor's IMC before
// attachView creates the editor child — a live IMC on
// a JUCE_ window + plugin wndproc (ImmIsUIMessageW ->
@@ -1075,7 +1145,6 @@ int main(int argc, char* argv[]) {
if (ok) disable_ime_contexts((HWND)hwnd);
if (ok) ok = inst->attachView(hwnd);
} catch (...) {
instruments.setBypassed(false);
std::cerr << "[NativeBridge] GUI attach EXCEPTION ch=" << guiCh
<< " — plugin threw (createView/attached)" << std::endl;
ok = false;
@@ -1127,7 +1196,7 @@ int main(int argc, char* argv[]) {
#endif
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh
<< " (channel active with editor open)" << std::endl;
<< " (channel muted while editor open)" << std::endl;
instruments.setReloading(guiCh, false);
}
else {