fix crash daw_vst_bridge 0xc000041d khi mở GUI Nexus rồi switch sang Ample: over-gate JUCE message window (g_juceOwnerTids, chỉ chạy khi close_in_flight) + gỡ VEH tạm và mọi log [dbg] chẩn đoán, giữ [NativeBridge] production; deploy exe FINAL (đã verify 3 run driver)

This commit is contained in:
2026-08-15 11:35:50 +07:00
parent 2596372ab1
commit 90241f626f
4 changed files with 472 additions and 137 deletions
Binary file not shown.
+4 -1
View File
@@ -3,6 +3,9 @@ project(daw_vst_bridge LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(MSVC)
string(REPLACE "/EHsc" "/EHa" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
endif()
# ── 1. VST3 SDK (Steinberg, vendored as git submodule — NOT in vcpkg) ──
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/CMakeLists.txt")
@@ -71,7 +74,7 @@ endif()
if(WIN32)
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY})
# Required Windows libs
target_link_libraries(daw_vst_bridge PRIVATE winmm)
target_link_libraries(daw_vst_bridge PRIVATE winmm imm32)
else()
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARIES} ${SFIZZ_LIBRARIES})
endif()
-48
View File
@@ -213,14 +213,12 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
using namespace VST3::Hosting;
std::string err;
std::cerr << "[dbg] loadPlugin: Module::create ..." << std::endl;
Module::Ptr module = Module::create(path, err);
if (!module) {
std::cerr << "[Vst3Instrument] Module::create failed: " << err << std::endl;
return false;
}
const PluginFactory& factory = module->getFactory();
std::cerr << "[dbg] loadPlugin: Module::create OK" << std::endl;
// Pick the first Audio Module class; prefer an Instrument subcategory.
// classInfos() returns a TEMPORARY vector (by value) — never keep a
@@ -244,16 +242,12 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
return false;
}
std::cerr << "[dbg] loadPlugin: createInstance ..." << std::endl;
std::cerr << "[dbg] loadPlugin: chosen name=" << chosen.name() << " category=" << chosen.category()
<< " subcat=" << chosen.subCategoriesString() << std::endl;
IPtr<IComponent> component = factory.createInstance<IComponent>(chosen.ID());
if (!component) {
std::cerr << "[Vst3Instrument] createInstance<IComponent> failed" << std::endl;
return false;
}
IPtr<HostApplication> hostApp = owned(new HostApplication());
std::cerr << "[dbg] loadPlugin: initialize ..." << std::endl;
FUnknownPtr<IPluginBase> plugBase(component.get());
if (!plugBase || plugBase->initialize(hostApp) != kResultOk) {
std::cerr << "[Vst3Instrument] component initialize failed" << std::endl;
@@ -269,13 +263,8 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
} else {
Steinberg::TUID cid = {};
tresult cidRes = component->getControllerClassId(cid);
std::cerr << "[dbg] loadPlugin: getControllerClassId=" << (int)cidRes
<< " cid=";
for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)(unsigned char)cid[b] << ' ';
std::cerr << std::dec << std::endl;
if (cidRes == kResultTrue || cidRes == kResultOk) {
controller = factory.createInstance<IEditController>(VST3::UID(cid));
std::cerr << "[dbg] loadPlugin: controller from factory=" << (controller ? 1 : 0) << std::endl;
if (controller) {
FUnknownPtr<IPluginBase> ctrlBase(controller.get());
if (!ctrlBase || ctrlBase->initialize(hostApp) != kResultOk) controller = nullptr;
@@ -326,13 +315,11 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
std::cerr << "[Vst3Instrument] no IAudioProcessor" << std::endl;
return false;
}
std::cerr << "[dbg] loadPlugin: setupProcessing ..." << std::endl;
ProcessSetup setup{kRealtime, kSample32, (int32)maxBlockSize_, sampleRate};
if (processor->setupProcessing(setup) != kResultOk) {
std::cerr << "[Vst3Instrument] setupProcessing failed" << std::endl;
return false;
}
std::cerr << "[dbg] loadPlugin: setActive ..." << std::endl;
if (component->setActive(true) != kResultOk) {
std::cerr << "[Vst3Instrument] setActive failed" << std::endl;
return false;
@@ -342,7 +329,6 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
// Build per-bus buffers sized maxBlockSize_ (HostProcessData owns them;
// we must NOT override channelBuffers with external pointers — unprepare
// would delete[] them).
std::cerr << "[dbg] loadPlugin: processData.prepare ..." << std::endl;
if (!s->processData.prepare(*component, (int32)maxBlockSize_, kSample32)) {
std::cerr << "[Vst3Instrument] processData.prepare failed" << std::endl;
return false;
@@ -356,7 +342,6 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
outChannels = bi.channelCount;
}
std::cerr << "[dbg] loadPlugin: prepare OK, wiring state" << std::endl;
// SDK EventList default maxSize=50 is far too small for the STOP
// all-notes-off sweep (128 note-offs per channel). addEvent beyond the
// cap fails SILENTLY → notes never released → stuck sound loop.
@@ -554,7 +539,6 @@ bool Vst3Instrument::reloadForGUI() {
// 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).
std::cerr << "[dbg] openGUI: re-attach - reloading fresh plugin instance" << std::endl;
if (!reload()) return false;
s = static_cast<Vst3HostState*>(state_);
if (!s || !s->controller) return false;
@@ -569,35 +553,21 @@ bool Vst3Instrument::attachView(void* parentWindowHandle) {
return false;
#else
auto* s = static_cast<Vst3HostState*>(state_);
std::cerr << "[dbg] openGUI hwnd=" << (void*)(uintptr_t)parentWindowHandle
<< " controller=" << (s ? (s->controller ? 1 : 0) : -1) << std::endl;
if (!s || !s->controller || !parentWindowHandle) return false;
IPlugView* rawView = nullptr;
tresult qi = s->controller->queryInterface(IPlugView::iid, (void**)&rawView);
std::cerr << "[dbg] openGUI: controller qi IPlugView=" << (int)qi << " raw=" << (void*)rawView << " isSingle=" << (s->controllerIsComponent ? 1 : 0) << std::endl;
FUnknownPtr<IPlugView> view(rawView);
if (!view) {
// Mot so plugin khong expose IPlugView tren edit controller; thu component.
std::cerr << "[dbg] openGUI: controller no IPlugView, trying component" << std::endl;
IPlugView* rawViewC = nullptr;
tresult qic = s->component->queryInterface(IPlugView::iid, (void**)&rawViewC);
std::cerr << "[dbg] openGUI: component qi IPlugView=" << (int)qic << " raw=" << (void*)rawViewC << std::endl;
view = FUnknownPtr<IPlugView>(rawViewC);
}
if (!view) {
// Official editorhost.cpp pattern: IEditController::createView(kEditor).
// JUCE-based plugins (Scaler2) expose the editor only this way.
std::cerr << "[dbg] openGUI: qi failed, trying controller->createView(kEditor)" << std::endl;
view = owned(s->controller->createView(Steinberg::Vst::ViewType::kEditor));
std::cerr << "[dbg] openGUI: createView view=" << (view ? "ok" : "null") << std::endl;
}
if (!view) { std::cerr << "[dbg] openGUI: no IPlugView" << std::endl; return false; }
view->setFrame(&s->plugFrame);
tresult ts = view->isPlatformTypeSupported(kPlatformTypeHWND);
std::cerr << "[dbg] openGUI: isPlatformTypeSupported=" << (int)ts << std::endl;
// Log only — proceed to attach anyway to support non-compliant plugins.
tresult ta = view->attached(parentWindowHandle, kPlatformTypeHWND);
std::cerr << "[dbg] openGUI: attached=" << (int)ta << std::endl;
if (ta != kResultOk) return false;
#ifdef _WIN32
HWND hwnd = (HWND)parentWindowHandle;
@@ -663,9 +633,6 @@ void Vst3Instrument::closeGUI() {
// 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;
guiAttached_ = false;
#endif
@@ -696,8 +663,6 @@ void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
(double)s->processContext.projectTimeSamples / s->processContext.sampleRate *
(s->processContext.tempo / 60.0);
static uint32_t dbgN = 0;
static int dbgMaxShown = 0; // ev>0 blocks printed (raised: sweep must be visible)
// VST3: host buffers must be zeroed (silence) before process — plugins
// that skip output leave garbage otherwise (EZkeys 2 -> huge noise).
if (s->processData.numOutputs > 0) {
@@ -727,19 +692,6 @@ void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
std::memset(outputL, 0, numSamples * sizeof(float));
std::memset(outputR, 0, numSamples * sizeof(float));
}
uint32_t evc = s->eventList.getEventCount();
int evt = -1;
if (evc > 0) {
const Event* e0 = s->eventList.getEventByIndex(0);
evt = e0 ? (int)e0->type : -2;
}
if ((++dbgN % 250) == 0 || (evc > 0 && dbgMaxShown < 5000)) {
if (evc > 0) ++dbgMaxShown;
std::cerr << "[dbg] pid=" << (int)GetCurrentProcessId() << " ch=" << channel_ << " n=" << numSamples << " ev=" << evc << " evt=" << evt
<< " pr=" << (int)pr << " nOut=" << s->processData.numOutputs
<< " mx=" << mx << " ts=" << s->processContext.projectTimeSamples
<< " nch=" << (s->processData.numOutputs > 0 ? s->processData.outputs[0].numChannels : -1) << std::endl;
}
s->eventList.clear();
s->paramChanges.clearQueue();
#endif
+468 -88
View File
@@ -7,6 +7,7 @@
#ifdef _WIN32
#include <windows.h>
#include <imm.h>
#include <mmsystem.h>
#include <process.h>
#include <thread>
@@ -28,6 +29,7 @@
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <vector>
@@ -51,6 +53,62 @@ static void sleep_ms(uint32_t ms) {
#endif
}
#ifdef _WIN32
// IME recursion fix (Nexus 0xC00000FD -> USER32 0xC000041D): the plugin's
// wndproc calls ImmIsUIMessageW for every message and forwards WM_IME_* to the
// IME window; with a live IMC on the editor window that bounces back to the
// same hwnd -> infinite SendMessageW recursion -> stack overflow. Removing the
// IMC (ImmAssociateContext NULL) stops the forwarding.
static void disable_ime_contexts(HWND w) {
if (w && IsWindow(w)) {
char cls[64] = {0};
GetClassNameA(w, cls, 63);
ImmAssociateContext(w, nullptr);
for (HWND c = GetWindow(w, GW_CHILD); c; c = GetWindow(c, GW_HWNDNEXT)) {
char cls2[64] = {0};
GetClassNameA(c, cls2, 63);
ImmAssociateContext(c, nullptr);
}
}
}
// CRASH FIX (run 16, 0xc0000005): thread ids owning JUCE_* windows (JUCE
// message windows have NO parent under the native window -> the pump gate's
// per-channel cases miss them). Recorded by the CBT hook at window birth and
// by the pump. While a CLOSE JOB is in flight (close_in_flight), a message
// bound for a JUCE owner thread must not be dispatched into its plugin DLL —
// observed: worker pump dispatching a Nexus wndproc (heap free) while another
// worker was inside createView -> 2 threads in one DLL -> Nexus AV/heap
// corruption. Over-gates every channel's JUCE windows during a teardown
// window (brief; editors may glitch, bridge survives). NOT gated on plain
// reloading (attach-silence) — that starves view->attached() (run 17 hang).
static std::mutex g_juceTidsMutex;
static std::set<DWORD> g_juceOwnerTids;
// CBT hook: strip the IMC the moment any JUCE_* window is born (JUCE message
// window AND editor child) — the post-attach disable_ime_contexts runs too
// late; the recursion can start inside attachView (0xC00000FD observed, run
// 9). Runs on the window's creating thread, so the association is legal.
static HHOOK g_cbtHook = nullptr;
static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HCBT_CREATEWND) {
// Class name via GetClassNameA, NOT lpszClass: JUCE creates windows
// with MAKEINTATOM class names (HIWORD(lpszClass)==0), and ANSI-created
// windows deliver a CREATESTRUCTA — lpszClass is unusable in both cases.
HWND w = (HWND)wParam;
char cls[64] = {0};
if (GetClassNameA(w, cls, 63) > 0 && std::strncmp(cls, "JUCE_", 5) == 0) {
ImmAssociateContext(w, nullptr);
{
std::lock_guard<std::mutex> lk(g_juceTidsMutex);
g_juceOwnerTids.insert(GetCurrentThreadId());
}
}
}
return CallNextHookEx(g_cbtHook, nCode, wParam, lParam);
}
#endif
#ifdef _WIN32
// Native VST editor windows registry — global de WM_DESTROY (chay tren worker
// thread cua channel tao window) co the don map. USERDATA luu channel+1 (KHONG
@@ -58,11 +116,15 @@ static void sleep_ms(uint32_t ms) {
// 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 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;
static std::mutex g_guiMutex;
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<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr;
static std::mutex g_tidMutex;
static std::map<DWORD, ChannelWorker*> g_tidToWorker; // worker tid -> worker (owner-thread destroy)
// 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
@@ -73,6 +135,24 @@ static std::map<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr;
static std::mutex g_attachMutex;
static std::vector<std::string> g_attachPaths;
static bool g_closeInFlight[16] = { false };
static DWORD g_ownerTid[16] = { 0 };
// Per channel: thread id of the plugin instance's JUCE MessageManager owner
// (recorded by the worker pump when it first sees the channel's editor
// window, and at attach end). 0 = unknown. Used by the pump teardown gate to
// drop messages for windows the owner thread pumps (incl. the instance's
// JUCE message window, which has no parent under the native window).
// Editor-open tracking: while a channel's VST editor (native window) is
// attached, EVERY channel assigned the same plugin DLL path must stay quiet —
// the plugin's window proc runs on the editor channel's worker while the
// audio loop calls process() on other instances of the same DLL (2 threads
// in one DLL -> Nexus USER32 crash). g_editorPathCount = refcount per
// lowercase plugin path; g_editorOpenPath/g_editorOpen per channel.
// Lock order: g_editorMutex is taken while holding the engine mutex
// (channelQuiet) or standalone in non-rt jobs — never the reverse.
static std::mutex g_editorMutex;
static std::map<std::string, int> g_editorPathCount;
static std::string g_editorOpenPath[16];
static bool g_editorOpen[16] = { false };
static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
if (uMsg == WM_CLOSE) {
@@ -164,7 +244,18 @@ public:
ChannelWorker() {
th_ = std::thread([this] {
#ifdef _WIN32
{
std::lock_guard<std::mutex> lk(g_tidMutex);
g_tidToWorker[GetCurrentThreadId()] = this;
}
OleInitialize(nullptr);
// Default IMC = none on this thread: new editor windows get
// no IME context (see disable_ime_contexts).
ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT);
// CBT hook on this worker: strip IMC on JUCE_* windows at birth.
// Global hooks fail from an exe module — install per thread, and
// this thread creates the plugin editor windows.
SetWindowsHookExW(WH_CBT, ImeCbtHookProc, GetModuleHandleW(nullptr), GetCurrentThreadId());
#endif
std::unique_lock<std::mutex> lk(mu_);
for (;;) {
@@ -189,12 +280,60 @@ public:
// 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;
// CRASH FIX (0xc000041d): JUCE editor child windows of a
// plugin DLL are owned by the FIRST worker that ran the
// DLL's global JUCE MessageManager - not by the channel
// worker doing the teardown. While another worker's LOAD /
// close job tears an instance down (reloading / close in
// flight), dispatching a message (e.g. WM_PAINT) into the
// plugin wndproc reads instance state being destroyed ->
// AV in USER32 (observed: crash on the owner worker's
// pump, right after the teardown job closed the view).
// Drop the message instead (PeekMessageW already removed
// it): the editor may glitch, the bridge survives.
uint32_t pch = pump_window_channel(msg.hwnd);
if (pch != UINT32_MAX) {
// Plugin-owned window pumped on this thread: remember
// its JUCE owner thread for teardown gating.
std::lock_guard<std::mutex> lock(g_guiMutex);
g_ownerTid[pch] = GetCurrentThreadId();
}
// Belt & braces: record JUCE window owner threads here too
// (a window born before the CBT hook was installed on its
// thread would otherwise never enter g_juceOwnerTids).
{
char cls[64] = "";
if (msg.hwnd) GetClassNameA(msg.hwnd, cls, sizeof(cls));
if (std::strncmp(cls, "JUCE_", 5) == 0) {
DWORD ot = GetWindowThreadProcessId(msg.hwnd, nullptr);
if (ot) {
std::lock_guard<std::mutex> lk(g_juceTidsMutex);
g_juceOwnerTids.insert(ot);
}
}
}
if (is_teardown_window(msg.hwnd, pch)) {
continue;
}
TranslateMessage(&msg);
DispatchMessageW(&msg);
// CRASH FIX (0xc000041d STATUS_FATAL_USER_CALLBACK_EXCEPTION):
// a plugin window proc (Nexus throws nlohmann::json::out_of_range
// internally) that raises a C++ exception would otherwise escape
// DispatchMessageW and std::terminate this worker thread. Catch
// it here — the editor may glitch, but the bridge survives.
try {
DispatchMessageW(&msg);
} catch (...) {
std::cerr << "[NativeBridge] worker pump EXCEPTION — plugin window proc threw" << std::endl;
}
}
lk.lock();
}
#ifdef _WIN32
{
std::lock_guard<std::mutex> lk(g_tidMutex);
g_tidToWorker.erase(GetCurrentThreadId());
}
OleUninitialize();
#endif
});
@@ -214,6 +353,29 @@ public:
}
cv_.notify_all();
}
// Run job on THIS worker and wait (5s cap) until it finished. Used to
// serialize a window destroy with the owner worker's pump. MUST NOT be
// called from a job running on this same worker (deadlock) - callers
// destroy directly when owner tid == current tid.
bool post_and_wait(std::function<void()> job) {
std::mutex doneMx;
std::condition_variable doneCv;
bool done = false;
{
std::lock_guard<std::mutex> lk(mu_);
jobs_.push_back([&]() {
job();
{
std::lock_guard<std::mutex> dk(doneMx);
done = true;
}
doneCv.notify_all();
});
}
cv_.notify_all();
std::unique_lock<std::mutex> dk(doneMx);
return doneCv.wait_for(dk, std::chrono::seconds(5), [&] { return done; });
}
private:
std::thread th_;
@@ -255,17 +417,129 @@ static bool close_in_flight(uint32_t ch) {
return g_closeInFlight[ch];
}
// Channel owning hwnd via its ancestor chain to a registered native VST
// window (UINT32_MAX if none). JUCE editor children hang under the native
// window, so their parent chain resolves to the channel.
static uint32_t pump_window_channel(HWND hwnd) {
for (HWND h = hwnd; h; h = GetParent(h)) {
uint32_t ch = UINT32_MAX;
{
std::lock_guard<std::mutex> lock(g_guiMutex);
auto it = g_hwndToCh.find(h);
if (it != g_hwndToCh.end()) ch = it->second;
}
if (ch != UINT32_MAX) return ch;
}
return UINT32_MAX;
}
// CRASH FIX (0xc000041d): drop a pump message whose dispatch would enter a
// plugin instance that another worker is tearing down (reloading / close in
// flight). Two cases:
// (a) the window's ancestor chain reaches the native window of a tearing-
// down channel (JUCE editor children);
// (b) the window's OWNER THREAD is the JUCE owner of a tearing-down
// channel's instance - covers the instance's JUCE message window (the
// 0x47B/1147 flood window): it has NO parent under the native window,
// is not destroyed by closeGUI, and after terminate() frees the
// instance, dispatching to it reads freed state -> AV in USER32
// (observed: LOAD worker freed the old Nexus instance while the owner
// thread's pump dispatched msg=1147 -> 0xfeeefeee read).
// g_ownerTid[ch] recorded by the pump (first plugin window seen for ch) and
// at attach end; 0 = unknown -> case (b) inactive for that channel.
static bool is_teardown_window(HWND hwnd, uint32_t pch) {
DWORD ownerTid = GetWindowThreadProcessId(hwnd, nullptr);
if (ownerTid == 0) return false;
// Over-gate: a message bound for any JUCE window owner thread is dropped
// while a CLOSE JOB tears a channel down (close_in_flight) — the JUCE
// message window has no parent under the native window (pch==UINT32_MAX)
// and its owner thread may differ from g_ownerTid[y] (owner changes
// between runs), so the per-channel cases below alone let it through into
// the plugin DLL while another worker is inside createInstance/reload (2
// threads in one DLL -> Nexus AV, run 16). Deliberately does NOT fire on
// plain reloading flags: the attach path marks same-plugin channels
// reloading for AUDIO silence, and gating their (and the attaching
// channel's own) JUCE windows then starves view->attached() of the
// cross-thread messages it needs -> attach hangs (observed run 17).
{
std::lock_guard<std::mutex> lk(g_juceTidsMutex);
if (g_juceOwnerTids.count(ownerTid)) {
for (uint32_t y = 0; y < 16; ++y)
if (close_in_flight(y)) return true;
}
}
for (uint32_t y = 0; y < 16; ++y) {
bool closing = close_in_flight(y);
bool reloading = g_engine && g_engine->isReloading(y);
if (!closing && !reloading) continue;
if (y == pch) return true;
if (g_ownerTid[y] == ownerTid) return true;
}
return false;
}
// CRASH FIX (0xc000041d): plugin editor child windows are owned by the
// thread that first ran the plugin's JUCE MessageManager (a single worker),
// NOT by the channel worker doing the teardown. Cross-thread DestroyWindow
// races the owner's message pump. Post the destroy to the OWNER worker
// (serialized with its pump - no wndproc entry can race) and wait. Fall
// back to direct destroy when the owner is the current thread or unknown
// (leak > crash). Non-trivial: destroy jobs left queued on timeout are
// harmless - IsWindow guards them, and the pump gate already stopped
// dispatching to teardown windows.
static void destroy_window_on_owner(HWND hwnd) {
if (!hwnd || !IsWindow(hwnd)) return;
DWORD ownerTid = GetWindowThreadProcessId(hwnd, nullptr);
DWORD curTid = GetCurrentThreadId();
if (ownerTid == 0 || ownerTid == curTid) {
if (IsWindow(hwnd)) DestroyWindow(hwnd);
return;
}
ChannelWorker* owner = nullptr;
{
std::lock_guard<std::mutex> lk(g_tidMutex);
auto it = g_tidToWorker.find(ownerTid);
if (it != g_tidToWorker.end()) owner = it->second;
}
if (!owner) {
// Window belongs to a thread we don't control (e.g. main thread).
// Cross-thread DestroyWindow is unsafe -> leave it (leak > crash).
std::cerr << "[NativeBridge] destroy_window_on_owner: owner tid=" << ownerTid
<< " not a bridge worker - leaving window " << hwnd << std::endl;
return;
}
if (!owner->post_and_wait([hwnd]() {
if (IsWindow(hwnd)) DestroyWindow(hwnd);
}))
std::cerr << "[NativeBridge] destroy_window_on_owner: timeout waiting owner tid="
<< ownerTid << " to destroy " << hwnd << std::endl;
}
// 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) {
(void)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;
}
// Editor closed / instrument replaced: drop the channel's editor-open state
// (refcounted per lowercase plugin path). Called from non-rt jobs only —
// never from the audio loop.
static void clear_editor_open(uint32_t ch) {
std::lock_guard<std::mutex> lk(g_editorMutex);
if (!g_editorOpen[ch]) return;
g_editorOpen[ch] = false;
std::string pth = g_editorOpenPath[ch];
g_editorOpenPath[ch].clear();
if (!pth.empty()) {
auto it = g_editorPathCount.find(pth);
if (it != g_editorPathCount.end() && --it->second <= 0)
g_editorPathCount.erase(it);
}
}
// closeGUI() MUST run on the channel worker thread (its COM STA apartment) —
@@ -293,11 +567,9 @@ static void post_close_gui(uint32_t ch, HWND hwnd) {
std::lock_guard<std::mutex> lock(g_guiMutex);
auto it = g_guiWindows.find(ch);
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);
@@ -311,7 +583,12 @@ static void post_close_gui(uint32_t ch, HWND hwnd) {
// 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();
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
@@ -324,30 +601,43 @@ static void post_close_gui(uint32_t ch, HWND hwnd) {
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);
// 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)) {
destroy_window_on_owner(c);
}
#endif
reloadOk = i->reload();
}
std::cerr << "[dbg] closeGUI ch=" << ch << " reload="
<< (reloadOk ? 1 : 0) << std::endl;
#endif
{
// 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
@@ -364,17 +654,14 @@ static void post_close_gui(uint32_t ch, HWND hwnd) {
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 {
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;
}
@@ -384,6 +671,14 @@ 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
// Default IMC = none on the main thread (IME recursion fix).
ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT);
// Catch JUCE_* window creation and strip its IMC at birth. Global hooks
// (dwThreadId=0) fail from an exe module (lpfn must be in a DLL) — hooks
// must be installed per thread; workers install their own in ChannelWorker.
g_cbtHook = SetWindowsHookExW(WH_CBT, ImeCbtHookProc, GetModuleHandleW(nullptr), GetCurrentThreadId());
#endif
// 1. Shared memory name: argv --shm <name> | env SF_SHM_NAME | default
std::string shmName = "SonicForge_DAW_IPC";
@@ -447,6 +742,14 @@ int main(int argc, char* argv[]) {
#ifdef _WIN32
g_engine = &instruments;
#endif
// 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) {
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
// (see ChannelWorker comment — a dead apartment hangs Nexus attached()).
@@ -476,8 +779,14 @@ int main(int argc, char* argv[]) {
bool transportStopped = false;
auto dispatch = [&](const SharedAudioBufferIPC::MidiEventIPC& evt) {
auto* inst = instruments.get(evt.channel);
if (!inst) return; // channel chưa gán instrument → silent (A10)
// Manager-level dispatch: ONE engine-mutex hold per event (get +
// channelQuiet + instrument call) so assign()/unload() can never
// destroy the instance mid-call — use-after-free when loading a new
// VSTi while other channels keep playing. channelQuiet drops events
// for channels mid-reload or whose plugin DLL has an open editor.
// CRASH FIX: noteOn/noteOff/CC enter the plugin (DLL entry); a C++
// exception from the plugin must not escape the audio loop.
try {
switch (evt.command) {
case 0x9:
// sampleOffset LUON LUON = 0 khi den day: events duoc dispatch ngay
@@ -485,12 +794,12 @@ int main(int argc, char* argv[]) {
// Truyen offset tuyet doi truoc day lam sfizz/VST3 trigger tre.
if (evt.velocity > 0) {
if (transportStopped) return; // V8 bug 3: drop note-on sau STOP
inst->noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f, 0);
instruments.noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f);
} else
inst->noteOff(evt.channel, evt.pitch, 0);
instruments.noteOff(evt.channel, evt.pitch);
break;
case 0x8:
inst->noteOff(evt.channel, evt.pitch, 0);
instruments.noteOff(evt.channel, evt.pitch);
break;
case 0xB: // CC: controller number in pitch, value in data2
// V9 bug 4: sau STOP, drop sustain-down (CC64>0) — JS co the
@@ -498,16 +807,21 @@ int main(int argc, char* argv[]) {
// note khi pedal down -> am treo loop. CC64=0 (sustain-up)
// van cho qua.
if (transportStopped && evt.pitch == 64 && evt.data2 > 0) return;
inst->controlChange(evt.channel, evt.pitch, evt.data2);
instruments.controlChange(evt.channel, evt.pitch, evt.data2);
break;
case 0xC: // program change: program in data2
inst->programChange(evt.channel, evt.data2);
instruments.programChange(evt.channel, evt.data2);
break;
case 0xE: // 14-bit pitch bend: data2 = LSB, data3 = MSB
inst->pitchBend(evt.channel, evt.data2 | (uint32_t(evt.data3) << 7));
instruments.pitchBend(evt.channel, evt.data2 | (uint32_t(evt.data3) << 7));
break;
default: break;
}
} catch (...) {
std::cerr << "[NativeBridge] dispatch EXCEPTION ch=" << (int)evt.channel
<< " cmd=" << std::hex << (int)evt.command << std::dec
<< " — plugin threw (note/CC)" << std::endl;
}
};
// Render only [from, to) of the block — used by sample-accurate splitting.
@@ -542,12 +856,12 @@ int main(int argc, char* argv[]) {
// tren view dang attached lam plugin loi (Nexus createView null).
if (auto* i0 = instruments.get(guiCh)) {
if (i0->hasAttachedView()) {
std::cerr << "[dbg] openGUI: view already attached ch=" << guiCh
<< " — skip" << std::endl;
return;
}
}
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
if (!workers[guiCh]) {
workers[guiCh] = std::make_unique<ChannelWorker>();
}
void* hwnd = (void*)arg1;
#ifdef _WIN32
// Window PHAI thuoc MAIN thread (audio loop pump nay dispatch
@@ -589,12 +903,8 @@ int main(int argc, char* argv[]) {
<< " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl;
return;
}
std::cerr << "[dbg] openGUI thread start hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
if (auto* inst0 = instruments.get(guiCh)) {
if (inst0->hasAttachedView()) {
std::cerr << "[dbg] openGUI: view already attached ch=" << guiCh
<< " — skip" << std::endl;
return;
}
}
@@ -622,9 +932,6 @@ int main(int argc, char* argv[]) {
// 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;
}
}
@@ -637,13 +944,8 @@ int main(int argc, char* argv[]) {
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);
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).
@@ -661,16 +963,7 @@ int main(int argc, char* argv[]) {
if (detached && !close_in_flight(y)) { closed = true; break; }
Sleep(10);
}
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
<< " close job not finished in 5s, aborting attach ch=" << guiCh
<< std::endl;
if (!closed) {
abortAttach = true;
break;
}
@@ -681,7 +974,6 @@ int main(int argc, char* argv[]) {
// 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;
}
}
@@ -722,8 +1014,6 @@ int main(int argc, char* argv[]) {
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;
}
}
}
@@ -743,8 +1033,25 @@ int main(int argc, char* argv[]) {
// same-path close job's reload (createInstance) -> Nexus
// exited silently (observed in probes).
std::lock_guard<std::mutex> lg(g_loadMutex);
ok = inst->reloadForGUI();
if (ok) ok = inst->attachView(hwnd);
// CRASH FIX (0xc000041d): attachView -> view->attached()
// and reloadForGUI both enter the plugin DLL — a C++
// exception (Nexus) must not escape this job (would
// std::terminate the worker). Treat as attach failure;
// the else branch below closes the empty window.
try {
ok = inst->reloadForGUI();
// IME recursion fix: strip the editor's IMC before
// attachView creates the editor child — a live IMC on
// a JUCE_ window + plugin wndproc (ImmIsUIMessageW ->
// SendMessageW WM_IME_SETCONTEXT same hwnd) = infinite
// recursion (0xC00000FD, observed inside attachView).
if (ok) disable_ime_contexts((HWND)hwnd);
if (ok) ok = inst->attachView(hwnd);
} catch (...) {
std::cerr << "[NativeBridge] GUI attach EXCEPTION ch=" << guiCh
<< " — plugin threw (createView/attached)" << std::endl;
ok = false;
}
}
// Attach done (ok or failed) — close jobs may unmute again.
{
@@ -759,10 +1066,41 @@ int main(int argc, char* argv[]) {
// their unmute and performs it once reload finished.
for (uint32_t y : samePathSilenced)
unmute_if_not_closing(y, "openGUI");
if (ok)
if (ok) {
// Editor attached: mark the plugin path editor-open so the
// audio loop stops processing EVERY channel assigned this
// DLL while the editor lives (its window proc runs on this
// worker; process() elsewhere on the same DLL -> Nexus crash).
{
std::lock_guard<std::mutex> lk(g_editorMutex);
if (!gp.empty()) {
g_editorOpen[guiCh] = true;
g_editorOpenPath[guiCh] = gp;
++g_editorPathCount[gp];
}
}
disable_ime_contexts((HWND)hwnd);
#ifdef _WIN32
{
// Record the plugin editor child's owner thread (JUCE
// MessageManager thread) so the pump gate can drop
// messages to the instance's windows during teardown.
std::lock_guard<std::mutex> lock(g_guiMutex);
auto git = g_guiWindows.find(guiCh);
if (git != g_guiWindows.end()) {
HWND nh = (HWND)git->second;
for (HWND c = FindWindowExA(nh, nullptr, nullptr, nullptr);
c; c = FindWindowExA(nh, c, nullptr, nullptr)) {
DWORD ot = GetWindowThreadProcessId(c, nullptr);
if (ot) { g_ownerTid[guiCh] = ot; break; }
}
}
}
#endif
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh
<< " (channel muted while editor open)" << std::endl;
}
else {
// Attach failed -> no editor running -> safe to process again.
instruments.setReloading(guiCh, false);
@@ -782,6 +1120,10 @@ int main(int argc, char* argv[]) {
// 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
while (true) {
// CRASH FIX (0xc000041d): last-resort net — any C++ exception that
// escapes the targeted guards (e.g. inside renderAll -> process())
// must not kill the bridge. Log and continue the loop.
try {
#ifdef _WIN32
// Message pump: VST editors (Nexus, JUCE-based...) block inside
// view->attached() until the host dispatches messages — openGUI runs
@@ -792,7 +1134,14 @@ int main(int argc, char* argv[]) {
int pumpedMain = 0;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
// CRASH FIX (0xc000041d): same as the worker pump — a C++ exception
// raised inside a plugin window proc (Nexus) must never escape
// DispatchMessageW as an unhandled fatal callback exception.
try {
DispatchMessageW(&msg);
} catch (...) {
std::cerr << "[NativeBridge] main pump EXCEPTION — plugin window proc threw" << std::endl;
}
++pumpedMain;
}
#endif
@@ -814,7 +1163,9 @@ int main(int argc, char* argv[]) {
std::string path(c.arg2, plen);
InstrumentType t = (InstrumentType)c.arg0;
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]() {
// CRASH FIX: this job enters the plugin DLL (createInstance
// in loadPlugin, old-instance terminate on replace) while
@@ -835,8 +1186,6 @@ int main(int argc, char* argv[]) {
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;
}
// Channels (other than ch) already assigned the NEW path —
// silence BEFORE createInstance enters that DLL.
@@ -851,8 +1200,6 @@ int main(int argc, char* argv[]) {
if (lower_plugin_path(y) != np) continue;
g_engine->setReloading(y, true);
spNew.push_back(y);
std::cerr << "[dbg] load: silenced same-plugin (new path) ch=" << y
<< " during load ch=" << ch << std::endl;
}
}
}
@@ -884,21 +1231,51 @@ int main(int argc, char* argv[]) {
// 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();
// 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);
// CRASH FIX: the plugin's editor window procs run on
// this worker; if one throws (Nexus) the exception must
// not escape the LOAD job (would std::terminate the
// worker). Detach best-effort and continue.
try {
disable_ime_contexts(hToHide);
if (auto* i = instruments.get(ch)) i->closeGUI();
// CRASH FIX (0xc000041d): destroy the old
// editor's child windows on the thread that OWNS
// them (JUCE MessageManager worker), never
// cross-thread - DestroyWindow from this LOAD
// worker raced the owner's pump dispatching
// WM_PAINT into the plugin wndproc -> AV. Wait
// for each destroy before assign() below. 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)) {
destroy_window_on_owner(c);
}
} catch (...) {
std::cerr << "[NativeBridge] LOAD close-editor EXCEPTION ch=" << ch
<< " — plugin window proc threw" << std::endl;
}
ShowWindow(hToHide, SW_HIDE);
// Editor detached: drop editor-open state before the
// instrument is replaced (assign).
clear_editor_open(ch);
}
#endif
std::cerr << "[dbg] load thread start ch=" << ch
<< " type=" << (int)t << " path=" << path << std::endl;
std::lock_guard<std::mutex> lg(g_loadMutex);
bool ok = instruments.assign(ch, t, path, sampleRate, block);
std::cerr << "[dbg] assign returned ch=" << ch << " ok=" << (ok ? 1 : 0) << std::endl;
// CRASH FIX (0xc000041d): Nexus throws nlohmann::json::
// out_of_range inside createInstance (loadPlugin) — the
// C++ exception must not escape the LOAD job (would
// std::terminate the worker). Catch it, treat as failed
// load, keep the old instance, and let the restore below
// unmute the channel.
bool ok = false;
try {
ok = instruments.assign(ch, t, path, sampleRate, block);
} catch (...) {
std::cerr << "[NativeBridge] assign EXCEPTION ch=" << ch
<< " type=" << (int)t << " path=" << path
<< " — plugin threw (createInstance)" << std::endl;
ok = false;
}
if (ok) {
std::cout << "[NativeBridge] instrument loaded ch=" << ch
<< " type=" << (int)t << " " << path << std::endl;
@@ -1004,6 +1381,9 @@ int main(int argc, char* argv[]) {
std::this_thread::yield();
}
}
} catch (...) {
std::cerr << "[NativeBridge] main loop EXCEPTION — plugin threw during render/control" << std::endl;
}
}
#ifdef _WIN32