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:
+468
-88
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user