==== ProbeWorker ====
   34|         for (HWND c = GetWindow(w, GW_CHILD); c; c = GetWindow(c, GW_HWNDNEXT))
   35|             ImmAssociateContext(c, nullptr);
   36|     }
   37| }
   38| 
   39| static void pump_for(int secs) {
   40| #ifdef _WIN32
   41|     _set_invalid_parameter_handler(probe_invalid_param);
   42| #endif
   43|     auto t0 = std::chrono::steady_clock::now();
   44|     while (std::chrono::duration_cast<std::chrono::seconds>(
   45|                std::chrono::steady_clock::now() - t0).count() < secs) {
   46|         MSG msg;
   47|         while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
   48|             printf("[pump] msg=0x%X hwnd=%p\n", (unsigned)msg.message, (void*)msg.hwnd);
   49|             fflush(stdout);
   50|             TranslateMessage(&msg);
   51|             DispatchMessageW(&msg);
   52|         }
   53|         Sleep(5);
   54|     }
   55| }
   56| 
   57| static void sleep_for(int secs) { Sleep((DWORD)(secs * 1000)); }
   58| 
   59| // Per-channel persistent worker replicating the bridge (round-3 probe).
   60| class ProbeWorker {
   61| public:
   62|     void start() {
   63|         th = std::thread([this] {
   64| #ifdef _WIN32
   65|             _set_invalid_parameter_handler(probe_invalid_param);
   66|             CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
   67|             ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT);
   68| #endif
   69|             std::unique_lock<std::mutex> lk(mu);
   70|             for (;;) {
   71|                 if (stop && jobs.empty()) break;
   72|                 if (!jobs.empty()) {
   73|                     auto j = std::move(jobs.front());
   74|                     jobs.pop_front();
   75|                     lk.unlock();
   76|                     try { j(); }
   77|                     catch (const std::exception& e) { printf("[worker] JOB EXCEPTION: %s\n", e.what()); fflush(stdout); }
   78|                     catch (...) { printf("[worker] JOB UNKNOWN EXCEPTION\n"); fflush(stdout); }
   79|                     lk.lock();
   80|                     continue;
   81|                 }
   82|                 cv.wait_for(lk, std::chrono::milliseconds(5));
   83|                 lk.unlock();
   84|                 try {
   85|                     MSG msg;
   86|                     while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
   87|                         printf("[wpump] msg=0x%X hwnd=%p\n", (unsigned)msg.message, (void*)msg.hwnd);
   88|                         fflush(stdout);
   89|                         TranslateMessage(&msg);
   90|                         DispatchMessageW(&msg);
   91|                     }
   92|                 }
   93|                 catch (const std::exception& e) { printf("[worker] PUMP EXCEPTION: %s\n", e.what()); fflush(stdout); }
   94|                 catch (...) { printf("[worker] PUMP UNKNOWN EXCEPTION\n"); fflush(stdout); }
   95|                 lk.lock();
   96|             }
   97| #ifdef _WIN32
   98|             CoUninitialize();
   99| #endif
  100|         });
  101|     }
  102|     void post(std::function<void()> j) {
  103|         { std::lock_guard<std::mutex> lk(mu); jobs.push_back(std::move(j)); }
  104|         cv.notify_all();
  105|     }
  106|     std::thread th;
  107|     std::mutex mu;
  108|     std::condition_variable cv;
  109|     std::deque<std::function<void()>> jobs;
  110|     bool stop = false;
  111| };
  112| 
  113| int main(int argc, char* argv[]) {
  114|     if (argc < 2) {
  115|         printf("usage: gui_probe <plugin.vst3> [variant] [secs] [hwnd]\n"
==== shared_worker ====
  220|     } else if (variant == "shared_worker" || variant == "ui_thread") {
  221|         // Proposed round-3 fix: ALL VST3 instances live in ONE apartment —
  222|         // one persistent worker thread hosts load + openGUI for every channel.
  223|         ProbeWorker w;
  224|         w.start();
  225|         std::atomic<bool> done{false};
  226|         int aRes = 0, bRes = 0;
  227|         w.post([&]() {
  228|             bool r = inst.loadPlugin(path, 44100.0);
  229|             printf("[shared_worker] load inst1=%d\n", r ? 1 : 0);
  230|             fflush(stdout);
  231|             if (r) {
  232|                 HWND h = CreateWindowEx(0, "GuiProbeClass", "ProbeA", WS_OVERLAPPEDWINDOW,
  233|                                         0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
  234|                                 disable_ime_contexts(h);
  235|                 r = inst.openGUI(h);
  236|                 disable_ime_contexts(h);
  237|                 printf("[shared_worker] attach inst1=%d hwnd=%p\n", r ? 1 : 0, (void*)h);
  238|                 fflush(stdout);
  239|                 aRes = r ? 1 : 0;
  240|             }
  241|             r = inst2.loadPlugin(path, 44100.0);
  242|             printf("[shared_worker] load inst2=%d\n", r ? 1 : 0);
  243|             fflush(stdout);
  244|             if (r) {
  245|                 HWND h = CreateWindowEx(0, "GuiProbeClass", "ProbeB", WS_OVERLAPPEDWINDOW,
  246|                                         0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
  247|                                 disable_ime_contexts(h);
  248|                 r = inst2.openGUI(h);
  249|                 disable_ime_contexts(h);
  250|                 printf("[shared_worker] attach inst2=%d hwnd=%p\n", r ? 1 : 0, (void*)h);
  251|                 fflush(stdout);
  252|                 bRes = r ? 1 : 0;
  253|             }
  254|             done = true;
  255|         });
  256|         auto t0 = std::chrono::steady_clock::now();
  257|         try {
  258|         while (!done.load() && std::chrono::duration_cast<std::chrono::seconds>(
  259|                    std::chrono::steady_clock::now() - t0).count() < secs) {
  260|             MSG msg;
  261|             while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
  262|                 TranslateMessage(&msg);
  263|                 DispatchMessageW(&msg);
  264|             }
  265|             Sleep(2);
  266|         }
  267|         } catch (const std::exception& e) { printf("[main] PUMP1 EXCEPTION: %s\n", e.what()); fflush(stdout); }
  268|         catch (...) { printf("[main] PUMP1 UNKNOWN EXCEPTION\n"); fflush(stdout); }
  269|         // Stay alive 5s with BOTH editors attached and the worker pumping,
  270|         // while MAIN KEEPS PUMPING like the bridge audio loop: distinguishes a
  271|         // live crash (bad) from a stop-pumping probe artifact.
  272|         auto t1 = std::chrono::steady_clock::now();
  273|         try {
  274|         while (std::chrono::duration_cast<std::chrono::seconds>(
  275|                    std::chrono::steady_clock::now() - t1).count() < 5) {
  276|             MSG msg;
  277|             while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
  278|                 TranslateMessage(&msg);
  279|                 DispatchMessageW(&msg);
  280|             }
  281|             Sleep(2);
  282|         }
  283|         } catch (const std::exception& e) { printf("[main] PUMP2 EXCEPTION: %s\n", e.what()); fflush(stdout); }
  284|         catch (...) { printf("[main] PUMP2 UNKNOWN EXCEPTION\n"); fflush(stdout); }
  285|         printf("RESULT: done=%d aRes=%d bRes=%d\n", done.load() ? 1 : 0, aRes, bRes);
  286|         return (done.load() && aRes && bRes) ? 0 : 3;
  287|     } else if (variant == "bridge_two") {
  288|         // Exact bridge architecture: per-channel persistent worker threads with
  289|         // own COM STA apartments and idle message pumps; inst1 loaded+attached
  290|         // on worker A, then inst2 loaded+attached on worker B; main pumps.
==== tail ====
  540|         catch (...) { printf("[main] PUMP2 UNKNOWN EXCEPTION\n"); fflush(stdout); }
  541|         printf("RESULT: done=%d attached_ok=%d\n", 1, 1);
  542|         return 0;
  543|     } else {
  544|         printf("unknown variant %s\n", variant.c_str());
  545|         return 2;
  546|     }
  547| 
  548|     printf("RESULT: done=%d attached_ok=%d\n", done.load() ? 1 : 0, result.load() ? 1 : 0);
  549|     return 0;
  550| }
  551| 