chore: commit G3.2+G3.3 deploy binaries + debug scripts; ignore debug run artifacts
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply owner-thread-destroy + pump-gate crash fix to main.cpp (LF file)."""
|
||||
import sys, io
|
||||
|
||||
p = r"C:/Users/locpham/SonicForgeStudio/native_bridge/src/main.cpp"
|
||||
s = open(p, "rb").read().decode("utf-8")
|
||||
orig = s
|
||||
|
||||
def rep(old, new, tag):
|
||||
global s
|
||||
n = s.count(old)
|
||||
assert n == 1, f"[{tag}] count={n}"
|
||||
s = s.replace(old, new, 1)
|
||||
print(f"ok: {tag}")
|
||||
|
||||
# 1. globals
|
||||
rep(
|
||||
"""static std::map<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr;""",
|
||||
"""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)""",
|
||||
"globals")
|
||||
|
||||
# 2. fwd decl
|
||||
rep(
|
||||
"""static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker""",
|
||||
"""static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker
|
||||
static bool is_teardown_window(HWND hwnd); // fwd — defined after ChannelWorker""",
|
||||
"fwd")
|
||||
|
||||
# 3. constructor register tid
|
||||
rep(
|
||||
""" th_ = std::thread([this] {
|
||||
#ifdef _WIN32
|
||||
OleInitialize(nullptr);""",
|
||||
""" th_ = std::thread([this] {
|
||||
#ifdef _WIN32
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_tidMutex);
|
||||
g_tidToWorker[GetCurrentThreadId()] = this;
|
||||
}
|
||||
OleInitialize(nullptr);""",
|
||||
"register tid")
|
||||
|
||||
# 4. pump gate
|
||||
rep(
|
||||
""" for (int pumped = 0; pumped < 16; ++pumped) {
|
||||
if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break;
|
||||
TranslateMessage(&msg);""",
|
||||
""" 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.
|
||||
if (is_teardown_window(msg.hwnd)) continue;
|
||||
TranslateMessage(&msg);""",
|
||||
"pump gate")
|
||||
|
||||
# 5. unregister tid
|
||||
rep(
|
||||
"""#ifdef _WIN32
|
||||
OleUninitialize();
|
||||
#endif
|
||||
});""",
|
||||
"""#ifdef _WIN32
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_tidMutex);
|
||||
g_tidToWorker.erase(GetCurrentThreadId());
|
||||
}
|
||||
OleUninitialize();
|
||||
#endif
|
||||
});""",
|
||||
"unregister tid")
|
||||
|
||||
# 6. post_and_wait
|
||||
rep(
|
||||
""" void post(std::function<void()> job) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
jobs_.push_back(std::move(job));
|
||||
}
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
private:""",
|
||||
""" void post(std::function<void()> job) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
jobs_.push_back(std::move(job));
|
||||
}
|
||||
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:""",
|
||||
"post_and_wait")
|
||||
|
||||
# 7. helpers after close_in_flight
|
||||
rep(
|
||||
"""static bool close_in_flight(uint32_t ch) {
|
||||
std::lock_guard<std::mutex> lk(g_attachMutex);
|
||||
return g_closeInFlight[ch];
|
||||
}""",
|
||||
"""static bool close_in_flight(uint32_t ch) {
|
||||
std::lock_guard<std::mutex> lk(g_attachMutex);
|
||||
return g_closeInFlight[ch];
|
||||
}
|
||||
|
||||
// True if hwnd (or any ancestor) is a plugin editor window whose channel is
|
||||
// mid-teardown (reloading or close job in flight). Editor child windows
|
||||
// (JUCE) have the native VST window as parent; the native window is in
|
||||
// g_hwndToCh. JUCE's global message window has no such parent - walk stops
|
||||
// -> false (never dropped).
|
||||
static bool is_teardown_window(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) {
|
||||
if (close_in_flight(ch)) return true;
|
||||
return g_engine ? g_engine->isReloading(ch) : false;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}""",
|
||||
"helpers")
|
||||
|
||||
# 8. post_close_gui: destroy outside g_loadMutex, via owner
|
||||
rep(
|
||||
""" 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);
|
||||
// CRASH FIX: reload() = terminate + loadPlugin
|
||||
// (createInstance) — 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).
|
||||
try {
|
||||
#ifdef _WIN32
|
||||
disable_ime_contexts(hwnd);
|
||||
if (hwnd && IsWindow(hwnd)) {
|
||||
while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) {
|
||||
std::cerr << "[dbg] closeGUI destroy child=" << c
|
||||
<< " owner_tid=" << GetWindowThreadProcessId(c, nullptr)
|
||||
<< " cur_tid=" << GetCurrentThreadId() << std::endl;
|
||||
DestroyWindow(c);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
reloadOk = i->reload();
|
||||
} catch (...) {
|
||||
std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch
|
||||
<< " — plugin threw (createInstance)" << std::endl;
|
||||
reloadOk = false;
|
||||
}
|
||||
}""",
|
||||
""" bool reloadOk = false;
|
||||
#ifdef _WIN32
|
||||
// CRASH FIX (0xc000041d): destroy the editor's child windows
|
||||
// on the thread that OWNS them (the plugin's JUCE
|
||||
// MessageManager thread - a single worker), never
|
||||
// cross-thread. Wait for each destroy to finish (owner worker
|
||||
// serializes it with its pump) BEFORE reload() - otherwise
|
||||
// reload's createInstance enters the DLL while the owner
|
||||
// thread is still inside the plugin wndproc (2 threads in one
|
||||
// DLL -> Nexus crash). Kept OUTSIDE g_loadMutex: the wait
|
||||
// must not block other loads (the owner worker may itself be
|
||||
// waiting on that lock).
|
||||
disable_ime_contexts(hwnd);
|
||||
if (hwnd && IsWindow(hwnd)) {
|
||||
while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) {
|
||||
std::cerr << "[dbg] closeGUI destroy child=" << c
|
||||
<< " owner_tid=" << GetWindowThreadProcessId(c, nullptr)
|
||||
<< " cur_tid=" << GetCurrentThreadId() << std::endl;
|
||||
destroy_window_on_owner(c);
|
||||
}
|
||||
}
|
||||
#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 {
|
||||
reloadOk = i->reload();
|
||||
} catch (...) {
|
||||
std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch
|
||||
<< " — plugin threw (createInstance)" << std::endl;
|
||||
reloadOk = false;
|
||||
}
|
||||
}""",
|
||||
"post_close_gui destroy")
|
||||
|
||||
# 9. LOAD close-editor destroy loop via owner
|
||||
rep(
|
||||
""" // 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)) {
|
||||
std::cerr << "[dbg] load close-editor destroy child=" << c
|
||||
<< " owner_tid=" << GetWindowThreadProcessId(c, nullptr)
|
||||
<< " cur_tid=" << GetCurrentThreadId() << std::endl;
|
||||
DestroyWindow(c);
|
||||
}""",
|
||||
""" // 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)) {
|
||||
std::cerr << "[dbg] load close-editor destroy child=" << c
|
||||
<< " owner_tid=" << GetWindowThreadProcessId(c, nullptr)
|
||||
<< " cur_tid=" << GetCurrentThreadId() << std::endl;
|
||||
destroy_window_on_owner(c);
|
||||
}""",
|
||||
"load destroy loop")
|
||||
|
||||
assert s != orig
|
||||
open(p, "wb").write(s.encode("utf-8"))
|
||||
print("written")
|
||||
Reference in New Issue
Block a user