G1.3: bo pump gate cu (is_teardown_window/g_ownerTid/g_juceOwnerTids/g_tidToWorker/post_and_wait) — 1 UiThread khong con race teardown-pump; CBT hook chi tren UiThread; destroy_window_on_owner destroy truc tiep
This commit is contained in:
+43
-220
@@ -72,18 +72,11 @@ static void disable_ime_contexts(HWND w) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
// G1.3: bo pump gate cu (g_juceOwnerTids / g_ownerTid /
|
||||
// is_teardown_window): teardown (close/LOAD job) va message pump cung chay tren
|
||||
// 1 thread (UiThread) — job chay thi pump dung, job xong thi editor window da
|
||||
// destroy nen USER32 tu huy message toi window chet. Khong con "2 thread trong
|
||||
// 1 DLL" khi teardown (multi-worker cu).
|
||||
|
||||
// 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
|
||||
@@ -99,10 +92,7 @@ static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
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);
|
||||
@@ -117,15 +107,13 @@ static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
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;
|
||||
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 ChannelWorker* g_uiWorker = 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
|
||||
@@ -136,12 +124,7 @@ static std::map<DWORD, ChannelWorker*> g_tidToWorker; // worker tid -> worker (
|
||||
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
|
||||
@@ -271,10 +254,7 @@ public:
|
||||
#endif
|
||||
th_ = std::thread([this] {
|
||||
#ifdef _WIN32
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_tidMutex);
|
||||
g_tidToWorker[GetCurrentThreadId()] = this;
|
||||
}
|
||||
OleInitialize(nullptr);
|
||||
OleInitialize(nullptr);
|
||||
// Default IMC = none on this thread: new editor windows get
|
||||
// no IME context (see disable_ime_contexts).
|
||||
@@ -306,41 +286,11 @@ public:
|
||||
for (int pumped = 0; pumped < 128; ++pumped) {
|
||||
if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break;
|
||||
hadMessages = true;
|
||||
// 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> lock(g_juceTidsMutex);
|
||||
g_juceOwnerTids.insert(ot);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_teardown_window(msg.hwnd, pch)) {
|
||||
continue;
|
||||
}
|
||||
// G1.3: bo pump gate cu — teardown (close/LOAD job)
|
||||
// va pump cung 1 thread (UiThread): job chay thi pump dung;
|
||||
// het job thi editor window da destroy -> message chet bi
|
||||
// USER32 huy tu dong. Khong can drop theo close_in_flight.
|
||||
|
||||
TranslateMessage(&msg);
|
||||
// CRASH FIX (0xc000041d STATUS_FATAL_USER_CALLBACK_EXCEPTION):
|
||||
// a plugin window proc (Nexus throws nlohmann::json::out_of_range
|
||||
@@ -369,10 +319,7 @@ public:
|
||||
}
|
||||
}
|
||||
#ifdef _WIN32
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_tidMutex);
|
||||
g_tidToWorker.erase(GetCurrentThreadId());
|
||||
}
|
||||
OleUninitialize();
|
||||
OleUninitialize();
|
||||
#endif
|
||||
});
|
||||
@@ -408,34 +355,7 @@ public:
|
||||
#endif
|
||||
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();
|
||||
});
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (jobEvent_) {
|
||||
SetEvent(jobEvent_);
|
||||
}
|
||||
#endif
|
||||
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_;
|
||||
@@ -492,103 +412,24 @@ 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;
|
||||
}
|
||||
// G1.3: 1 UiThread — editor windows tao trong attach job (UiThread), destroy
|
||||
// chi goi tu UiThread job (close_editor_now / LOAD) -> ownerTid luon == current
|
||||
// tid -> destroy truc tiep. Neu owner khac thread (khong con xay ra) -> bo
|
||||
// (leak > crash): cross-thread DestroyWindow khong an toan.
|
||||
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;
|
||||
}
|
||||
std::cerr << "[NativeBridge] destroy_window_on_owner: owner tid=" << ownerTid
|
||||
<< " != current " << curTid << " - leaving window " << 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
|
||||
@@ -764,11 +605,9 @@ int main(int argc, char* argv[]) {
|
||||
// without an exe manifest. Ignore failure (already aware).
|
||||
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
// Default IMC = none on the main thread (IME recursion fix).
|
||||
// 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
|
||||
@@ -1189,23 +1028,7 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -1219,8 +1042,9 @@ int main(int argc, char* argv[]) {
|
||||
#ifdef _WIN32
|
||||
// Attach that — khong co view (VD: channel la SF2/SFZ hoac plugin
|
||||
// loi). Dong ngay cua so vo nghia de khong con window treo trong
|
||||
// registry; WM_CLOSE -> main pump destroy tren main thread (owner).
|
||||
// registry; WM_CLOSE -> UiThread pump destroy (owner = UiThread).
|
||||
// PostMessage an toan cross-thread (khong nhu DestroyWindow).
|
||||
|
||||
PostMessageA((HWND)hwnd, WM_CLOSE, 0, 0);
|
||||
#endif
|
||||
}
|
||||
@@ -1296,12 +1120,11 @@ int main(int argc, char* argv[]) {
|
||||
// thay the inst (VST3 -> SF2/inst khac) ma editor con song ->
|
||||
// old inst destructor goi view->removed() tren HWND con hoat
|
||||
// dong -> plugin block -> treo bridge.
|
||||
// QUAN TRONG: window duoc tao tren MAIN thread (handleOpenGui
|
||||
// goi create_native_vst_window o main loop) — DestroyWindow tu
|
||||
// worker thread la cross-thread (MSDN cam), gay crash USER32
|
||||
// 0xc000041d. Post WM_CLOSE -> main pump destroy window tren
|
||||
// DUNG thread so huu no. Registry da erase o tren nen WM_DESTROY
|
||||
// khong goi closeGUI tren inst cu (inst moi chua co view).
|
||||
// G1.2/G1.3: window tao tren UiThread (handleOpenGui post attach
|
||||
// job; create_native_vst_window chay trong job). LOAD job cung
|
||||
// chay tren UiThread -> khong cross-thread destroy. An window,
|
||||
// giu registry de reuse; editor detach + reload o duoi.
|
||||
|
||||
HWND hToHide = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||
|
||||
Reference in New Issue
Block a user