fix(bridge): one editor at a time + guard reload race

- Close other channels' editor windows before attaching a new one (Option B)
- needsReload()/setReloading() guard: skip processAudioBlock during
  terminate+reload teardown to avoid UAF/hang when reopening GUI
- Wait for editor registry cleanup (5s timeout) before creating window
This commit is contained in:
2026-08-13 15:32:35 +07:00
parent c3eb0c9d16
commit 05e4244cc8
7 changed files with 349 additions and 4 deletions
+267 -2
View File
@@ -9,8 +9,12 @@
#endif
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstdlib>
#include <deque>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
@@ -29,10 +33,54 @@ static void pump_for(int secs) {
static void sleep_for(int secs) { Sleep((DWORD)(secs * 1000)); }
// Per-channel persistent worker replicating the bridge (round-3 probe).
class ProbeWorker {
public:
void start() {
th = std::thread([this] {
#ifdef _WIN32
OleInitialize(nullptr);
#endif
std::unique_lock<std::mutex> lk(mu);
for (;;) {
if (stop && jobs.empty()) break;
if (!jobs.empty()) {
auto j = std::move(jobs.front());
jobs.pop_front();
lk.unlock();
j();
lk.lock();
continue;
}
cv.wait_for(lk, std::chrono::milliseconds(5));
lk.unlock();
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
lk.lock();
}
#ifdef _WIN32
OleUninitialize();
#endif
});
}
void post(std::function<void()> j) {
{ std::lock_guard<std::mutex> lk(mu); jobs.push_back(std::move(j)); }
cv.notify_all();
}
std::thread th;
std::mutex mu;
std::condition_variable cv;
std::deque<std::function<void()>> jobs;
bool stop = false;
};
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("usage: gui_probe <plugin.vst3> [variant] [secs] [hwnd]\n"
"variants: main_own | worker_own_nopump | worker_own_pump | worker_foreign_nopump | worker_foreign_pump | bridge_like | same_thread\n");
"variants: main_own | worker_own_nopump | worker_own_pump | worker_foreign_nopump | worker_foreign_pump | bridge_like | same_thread | two_instances | two_instances_close | close_reopen | bridge_two | shared_worker\n");
return 2;
}
std::string path = argv[1];
@@ -68,7 +116,11 @@ int main(int argc, char* argv[]) {
printf("loading %s\n", path.c_str());
fflush(stdout);
Vst3Instrument inst;
bool isDeferred = (variant == "bridge_like" || variant == "same_thread");
Vst3Instrument inst2;
bool isDeferred = (variant == "bridge_like" || variant == "same_thread" ||
variant == "two_instances" || variant == "two_instances_close" ||
variant == "close_reopen" || variant == "bridge_two" ||
variant == "shared_worker");
bool ok = true;
if (!isDeferred) {
ok = inst.loadPlugin(path, 44100.0);
@@ -131,6 +183,219 @@ int main(int argc, char* argv[]) {
});
pump_for(secs);
if (!done.load()) { printf("RESULT: TIMEOUT (same_thread)\n"); return 3; }
} else if (variant == "shared_worker") {
// Proposed round-3 fix: ALL VST3 instances live in ONE apartment —
// one persistent worker thread hosts load + openGUI for every channel.
ProbeWorker w;
w.start();
std::atomic<bool> done{false};
int aRes = 0, bRes = 0;
w.post([&]() {
bool r = inst.loadPlugin(path, 44100.0);
printf("[shared_worker] load inst1=%d\n", r ? 1 : 0);
fflush(stdout);
if (r) {
HWND h = CreateWindowEx(0, "GuiProbeClass", "ProbeA", WS_OVERLAPPEDWINDOW,
0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
r = inst.openGUI(h);
printf("[shared_worker] attach inst1=%d hwnd=%p\n", r ? 1 : 0, (void*)h);
fflush(stdout);
aRes = r ? 1 : 0;
}
r = inst2.loadPlugin(path, 44100.0);
printf("[shared_worker] load inst2=%d\n", r ? 1 : 0);
fflush(stdout);
if (r) {
HWND h = CreateWindowEx(0, "GuiProbeClass", "ProbeB", WS_OVERLAPPEDWINDOW,
0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
r = inst2.openGUI(h);
printf("[shared_worker] attach inst2=%d hwnd=%p\n", r ? 1 : 0, (void*)h);
fflush(stdout);
bRes = r ? 1 : 0;
}
done = true;
});
auto t0 = std::chrono::steady_clock::now();
while (!done.load() && std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::steady_clock::now() - t0).count() < secs) {
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
Sleep(2);
}
// Stay alive 5s with BOTH editors attached and the worker pumping,
// while MAIN KEEPS PUMPING like the bridge audio loop: distinguishes a
// live crash (bad) from a stop-pumping probe artifact.
auto t1 = std::chrono::steady_clock::now();
while (std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::steady_clock::now() - t1).count() < 5) {
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
Sleep(2);
}
printf("RESULT: done=%d aRes=%d bRes=%d\n", done.load() ? 1 : 0, aRes, bRes);
return (done.load() && aRes && bRes) ? 0 : 3;
} else if (variant == "bridge_two") {
// Exact bridge architecture: per-channel persistent worker threads with
// own COM STA apartments and idle message pumps; inst1 loaded+attached
// on worker A, then inst2 loaded+attached on worker B; main pumps.
ProbeWorker wa, wb;
wa.start();
wb.start();
std::atomic<bool> aDone{false}, aOk{false}, bDone{false}, bOk{false};
wa.post([&]() {
printf("[bridge_two] workerA load inst1\n");
fflush(stdout);
bool r = inst.loadPlugin(path, 44100.0);
printf("[bridge_two] workerA load inst1=%d\n", r ? 1 : 0);
fflush(stdout);
if (r) {
HWND h = CreateWindowEx(0, "GuiProbeClass", "GuiProbeA", WS_OVERLAPPEDWINDOW,
0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
printf("[bridge_two] workerA hwnd=%p\n", (void*)h);
fflush(stdout);
printf("[bridge_two] workerA openGUI inst1\n");
fflush(stdout);
r = inst.openGUI(h);
printf("[bridge_two] workerA openGUI inst1=%d\n", r ? 1 : 0);
fflush(stdout);
aOk = r;
}
aDone = true;
});
auto t0 = std::chrono::steady_clock::now();
auto elapsed = [&]() {
return std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::steady_clock::now() - t0).count();
};
// main pump like the bridge audio loop
std::thread pump([&]() {
while (elapsed() < secs) {
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
Sleep(2);
}
});
while (!aDone.load() && elapsed() < secs) Sleep(50);
if (!aDone.load()) {
printf("RESULT: TIMEOUT inst1 attach (aDone=0)\n");
return 3;
}
wb.post([&]() {
printf("[bridge_two] workerB load inst2\n");
fflush(stdout);
bool r = inst2.loadPlugin(path, 44100.0);
printf("[bridge_two] workerB load inst2=%d\n", r ? 1 : 0);
fflush(stdout);
if (r) {
HWND h = CreateWindowEx(0, "GuiProbeClass", "GuiProbeB", WS_OVERLAPPEDWINDOW,
0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
printf("[bridge_two] workerB hwnd=%p\n", (void*)h);
fflush(stdout);
printf("[bridge_two] workerB openGUI inst2\n");
fflush(stdout);
r = inst2.openGUI(h);
printf("[bridge_two] workerB openGUI inst2=%d\n", r ? 1 : 0);
fflush(stdout);
bOk = r;
}
bDone = true;
});
while ((!aDone.load() || !bDone.load()) && elapsed() < secs) Sleep(50);
pump.join();
printf("RESULT: aDone=%d aOk=%d bDone=%d bOk=%d\n",
aDone.load() ? 1 : 0, aOk.load() ? 1 : 0,
bDone.load() ? 1 : 0, bOk.load() ? 1 : 0);
return (aDone.load() && aOk.load() && bDone.load() && bOk.load()) ? 0 : 3;
} else if (variant == "two_instances" || variant == "two_instances_close" || variant == "close_reopen") {
// Bridge round-3 probes: does a SECOND instance of the same plugin hang
// in view->attached() while the first instance's view is attached?
// Does closing the first view first (two_instances_close) or the
// close-then-reopen reload path (close_reopen) avoid the hang?
auto attachOnWorker = [&](Vst3Instrument* i, void* hwnd, const char* tag) -> bool {
std::atomic<bool> d{false};
std::atomic<bool> r{false};
std::thread t([&]() {
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
printf("[%s] openGUI(%s) start hwnd=%p\n", variant.c_str(), tag, hwnd);
fflush(stdout);
bool rr = i->openGUI(hwnd);
printf("[%s] openGUI(%s) returned=%d\n", variant.c_str(), tag, rr ? 1 : 0);
fflush(stdout);
r = rr;
d = true;
});
auto t0 = std::chrono::steady_clock::now();
while (!d.load() && std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::steady_clock::now() - t0).count() < secs) {
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
Sleep(2);
}
t.detach();
if (!d.load()) {
printf("[%s] TIMEOUT openGUI(%s)\n", variant.c_str(), tag);
return false;
}
return r.load();
};
auto loadOnMain = [&](Vst3Instrument* i, const char* tag) -> bool {
printf("[%s] loadPlugin(%s) on main\n", variant.c_str(), tag);
fflush(stdout);
bool rr = i->loadPlugin(path, 44100.0);
printf("[%s] load(%s)=%d\n", variant.c_str(), tag, rr ? 1 : 0);
fflush(stdout);
return rr;
};
// second parent window for inst2
HWND hwnd2 = CreateWindowEx(0, "GuiProbeClass", "GuiProbe2", WS_OVERLAPPEDWINDOW,
0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
printf("hwnd2=%p\n", (void*)hwnd2);
if (!loadOnMain(&inst, "inst1")) return 1;
if (!attachOnWorker(&inst, targetHwnd, "inst1")) return 3;
if (variant == "two_instances_close") {
printf("[%s] closeGUI(inst1) before inst2 attach\n", variant.c_str());
inst.closeGUI();
}
if (variant == "close_reopen") {
printf("[%s] closeGUI(inst1) then reopen same instance (reload path)\n", variant.c_str());
inst.closeGUI();
if (!attachOnWorker(&inst, targetHwnd, "inst1-reopen")) {
printf("RESULT: done=%d attached_ok=%d\n", 0, 0);
return 3;
}
printf("RESULT: done=%d attached_ok=%d\n", 1, 1);
return 0;
}
if (!loadOnMain(&inst2, "inst2")) return 1;
if (!attachOnWorker(&inst2, hwnd2, "inst2")) {
printf("RESULT: done=%d attached_ok=%d\n", 0, 0);
return 3;
}
// 5s survival with both editors attached (main keeps pumping).
auto t1 = std::chrono::steady_clock::now();
while (std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::steady_clock::now() - t1).count() < 5) {
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
Sleep(2);
}
printf("RESULT: done=%d attached_ok=%d\n", 1, 1);
return 0;
} else {
printf("unknown variant %s\n", variant.c_str());
return 2;