// native_bridge/src/gui_probe.cpp — standalone VST3 GUI attach probe. // Debug tool (NOT shipped with the DAW): loads a VST3 with Vst3Instrument and // tests openGUI under different thread/pump/parent-window setups so we can // find why view->attached() hangs inside the bridge. #include "Vst3Instrument.h" #ifdef _WIN32 #include #endif #include #include #include #include #include #include #include #include #include #include static void pump_for(int secs) { auto t0 = std::chrono::steady_clock::now(); while (std::chrono::duration_cast( std::chrono::steady_clock::now() - t0).count() < secs) { MSG msg; while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); } Sleep(5); } } 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 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 j) { { std::lock_guard lk(mu); jobs.push_back(std::move(j)); } cv.notify_all(); } std::thread th; std::mutex mu; std::condition_variable cv; std::deque> jobs; bool stop = false; }; int main(int argc, char* argv[]) { if (argc < 2) { printf("usage: gui_probe [variant] [secs] [hwnd]\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 | two_workers_close\n"); return 2; } std::string path = argv[1]; std::string variant = argc > 2 ? argv[2] : "main_own"; int secs = argc > 3 ? atoi(argv[3]) : 20; CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); // Watchdog: never let a stuck plugin hang this probe forever. std::thread([secs]() { Sleep((DWORD)(secs + 10) * 1000); printf("WATCHDOG-KILL\n"); fflush(stdout); TerminateProcess(GetCurrentProcess(), 0); }).detach(); HWND ownHwnd = 0; { WNDCLASSEX wc = {}; wc.cbSize = sizeof(wc); wc.lpfnWndProc = DefWindowProc; wc.hInstance = GetModuleHandle(nullptr); wc.lpszClassName = "GuiProbeClass"; RegisterClassEx(&wc); ownHwnd = CreateWindowEx(0, "GuiProbeClass", "GuiProbe", WS_OVERLAPPEDWINDOW, 0, 0, 800, 600, nullptr, nullptr, wc.hInstance, nullptr); printf("ownHwnd=%p\n", (void*)ownHwnd); } void* targetHwnd = (void*)ownHwnd; if (argc > 4) targetHwnd = (void*)(uintptr_t)strtoull(argv[4], nullptr, 0); printf("loading %s\n", path.c_str()); fflush(stdout); Vst3Instrument inst; 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); printf("load=%d\n", ok ? 1 : 0); fflush(stdout); if (!ok) return 1; } std::atomic done{false}; std::atomic result{false}; auto attach_job = [&]() { CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); printf("[%s] openGUI start hwnd=%p\n", variant.c_str(), targetHwnd); fflush(stdout); bool r = inst.openGUI(targetHwnd); printf("[%s] openGUI returned=%d\n", variant.c_str(), r ? 1 : 0); fflush(stdout); result = r; done = true; }; if (variant == "main_own") { attach_job(); // editorhost pattern: attached on main thread, pump after pump_for(secs); } else if (variant == "worker_own_nopump" || variant == "worker_foreign_nopump") { std::thread t(attach_job); sleep_for(secs); if (!done.load()) { printf("RESULT: TIMEOUT (no pump)\n"); return 3; } } else if (variant == "worker_own_pump" || variant == "worker_foreign_pump") { std::thread t(attach_job); pump_for(secs); if (!done.load()) { printf("RESULT: TIMEOUT (pump concurrent)\n"); return 3; } } else if (variant == "bridge_like") { // Bridge pattern: plugin loaded on thread A which then EXITS (COM // apartment dies), openGUI on separate thread B, main pumps. std::thread loadA([&]() { CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); printf("[bridge_like] loadPlugin on thread A\n"); fflush(stdout); bool r = inst.loadPlugin(path, 44100.0); printf("[bridge_like] load=%d (thread A exiting)\n", r ? 1 : 0); fflush(stdout); CoUninitialize(); }); loadA.join(); std::thread t(attach_job); // thread B, after A exited pump_for(secs); if (!done.load()) { printf("RESULT: TIMEOUT (apartment dead)\n"); return 3; } } else if (variant == "same_thread") { // Load and openGUI on the SAME worker thread, kept alive; main pumps. std::thread t([&]() { CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); printf("[same_thread] loadPlugin on worker\n"); fflush(stdout); bool r = inst.loadPlugin(path, 44100.0); printf("[same_thread] load=%d\n", r ? 1 : 0); fflush(stdout); if (r) attach_job(); }); 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 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::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::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 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::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_workers_close") { // Round-4 exact bridge pattern: per-channel workers (own STA // apartments); attach via temp thread + WORKER nested pump (the bridge // main.cpp job pumps the worker queue while attachView runs on a // temporary thread); close editor inst1 before attaching inst2 on its // own worker. ProbeWorker wa, wb; wa.start(); wb.start(); std::atomic aDone{false}, aOk{false}, bDone{false}, bOk{false}; wa.post([&]() { printf("[two_workers_close] workerA load inst1\n"); fflush(stdout); bool r = inst.loadPlugin(path, 44100.0); printf("[two_workers_close] workerA 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); printf("[two_workers_close] workerA hwnd=%p\n", (void*)h); fflush(stdout); bool r2 = false; std::atomic d{false}; std::thread t([&]() { CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); r2 = inst.attachView(h); CoUninitialize(); d = true; }); while (!d.load()) { MSG msg; while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); } Sleep(1); } t.join(); printf("[two_workers_close] workerA attach inst1=%d\n", r2 ? 1 : 0); fflush(stdout); aOk = r2; inst.closeGUI(); // view->removed() on worker A printf("[two_workers_close] workerA closeGUI inst1\n"); fflush(stdout); } aDone = true; }); auto t0 = std::chrono::steady_clock::now(); auto elapsed = [&]() { return std::chrono::duration_cast( std::chrono::steady_clock::now() - t0).count(); }; while (!aDone.load() && elapsed() < secs) Sleep(50); if (!aDone.load()) { printf("RESULT: TIMEOUT inst1 (aDone=0)\n"); return 3; } wb.post([&]() { printf("[two_workers_close] workerB load inst2\n"); fflush(stdout); bool r = inst2.loadPlugin(path, 44100.0); printf("[two_workers_close] workerB 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); printf("[two_workers_close] workerB hwnd=%p\n", (void*)h); fflush(stdout); bool r2 = false; std::atomic d{false}; std::thread t([&]() { CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); r2 = inst2.attachView(h); CoUninitialize(); d = true; }); while (!d.load()) { MSG msg; while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); } Sleep(1); } t.join(); printf("[two_workers_close] workerB attach inst2=%d\n", r2 ? 1 : 0); fflush(stdout); bOk = r2; } bDone = true; }); while ((!aDone.load() || !bDone.load()) && elapsed() < secs) Sleep(50); 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 d{false}; std::atomic 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::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::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; } printf("RESULT: done=%d attached_ok=%d\n", done.load() ? 1 : 0, result.load() ? 1 : 0); return 0; }