// native_bridge/juce_fx/JuceFxGuiHost.cpp // M2 implementation: GUI thread + PluginWindow host. Lịch sử fix: // - tick 30ms = pumpEditorQueue + reconcile (editor Component chỉ đụng trên // message thread — mọi JUCE GUI op chạy trong GUI thread này). // - KHÔNG dùng juce::Timer cho cadence: timer không fire trên GUI thread // riêng (0 tick trong selftest) — guiMain dùng inline steady_clock 30ms // quanh runDispatchLoopUntil(10), tick() chạy trên message thread. // - stop(): waitEditorsClosed(1s) → quit → join 2s → detach nếu kẹt // (plugin editor block message thread); teardown còn editor thì close // TRƯỚC shutdownJuce_GUI (JUCE assert editor phải xoá trước MM). // - reconcile erase window khi !isEditorOpen (sau re-prepare editors clear); // hide = setVisible(false) GIỮ window object (editor + instance sống). #include "JuceFxGuiHost.h" #include "JuceFxEngine.h" #include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { // Số slot tối đa — khớp FXRT_CTRL_SLOTS (FxRealtimeIPC.h); anchor per slot. constexpr uint32_t kAnchorSlots = 8; // Window host cho editor của slot (engine sở hữu editor — window không xoá). class PluginWindow final : public juce::DocumentWindow { public: PluginWindow(uint32_t slot, juce::Component* editor, JuceFxGuiHost& host) : juce::DocumentWindow("FX slot " + juce::String((int)slot), juce::Colours::darkgrey, juce::DocumentWindow::allButtons, true), // addToDesktop — native window thật slot(slot), host_(host) { setUsingNativeTitleBar(true); setResizable(true, false); // user resize được; resizeToFit co theo editor // resizeToFitWhenContentChangesSize=true: window duy trì kích thước ôm // editor (plugin đổi size → window theo). Engine vẫn sở hữu editor. setContentNonOwned(editor, true); editor_ = editor; // M9b: WS_EX_TOOLWINDOW dặt NGAY khi native window tồn tại (DocumentWindow // ctor đã addToDesktop) — cửa sổ GUI plugin không tạo taskbar icon phụ / // không lọt Alt-Tab kể cả khi owner DAW chưa gắn được (unowned tạm thời). // Đặt ở preShow (cũ) vẫn có thể show sớm trước khi style áp. if (auto* peer = getPeer()) { if (HWND hwnd = (HWND)peer->getNativeHandle()) { // M15: JUCE addToDesktop set WS_EX_APPWINDOW (0x40000) — taskbar // icon riêng THẮNG WS_EX_TOOLWINDOW. Clear APPWINDOW + set // TOOLWINDOW trước show: window plugin không bao giờ vào taskbar. LONG_PTR ex = GetWindowLongPtrA(hwnd, GWL_EXSTYLE); if ((ex & WS_EX_APPWINDOW) || !(ex & WS_EX_TOOLWINDOW)) SetWindowLongPtrA(hwnd, GWL_EXSTYLE, (ex & ~WS_EX_APPWINDOW) | WS_EX_TOOLWINDOW); } } } // X button → Close editor (giữ instance DSP + state — mở lại nhanh như // Reaper). Tick sau: pump Close → reconcile xoá window này. void closeButtonPressed() override { host_.onWindowClose(slot); } // M7: SetWindowPos window vào rect anchor (outer bounds == rect; caption // native nằm trong rect như cửa sổ embedded). HWND_TOP (KHÔNG TOPMOST — // fix "floating over other apps") + SWP_NOACTIVATE — mirror FxGuiServer // /show. Re-anchor chỉ khi rect ĐỔI so với lần áp cuối (dedupe — tránh // flicker/giật khi user thao tác DAW mà panel yên). Chạy trên GUI thread // (reconcile tick). Window mới chưa áp lần nào → luôn áp. // M13c (Bug 1): kich thuoc window = DUNG editor tu nhien + khung cua so — // KHONG ep theo rect panel (M7/M12 lam window to hon editor cua plugin // kich thuoc co dinh nhu Ozone 11 -> vung DocumentWindow trong quanh GUI, // "GUI lot thom giua khung"). x,y van la goc tren-trai panel (neo dung o // anchor trong DAW); w,h tu JS (rect panel) chi la doi chieu vi tri. // Nhung doi GUI -> window doi theo qua resizeToFit content (JUCE). void applyAnchor(int x, int y, int /*w*/, int /*h*/) { auto* peer = getPeer(); if (!peer) return; const HWND hwnd = (HWND)peer->getNativeHandle(); if (!hwnd) return; const int ew = editor_ ? editor_->getWidth() : 0; const int eh = editor_ ? editor_->getHeight() : 0; if (ew < 32 || eh < 32) return; // editor chua co size — tick sau RECT rc = { 0, 0, ew, eh }; AdjustWindowRectEx(&rc, (DWORD)GetWindowLongPtrA(hwnd, GWL_STYLE), FALSE, (DWORD)GetWindowLongPtrA(hwnd, GWL_EXSTYLE)); const int cw = rc.right - rc.left; const int chh = rc.bottom - rc.top; if (cw <= 0 || chh <= 0) return; if (applied_ && ax_ == x && ay_ == y && aw_ == cw && ah_ == chh) { // M12 stick-to-anchor: rect khong doi nhung window bi keo di (native // title bar cho drag) -> re-snap ve vi tri panel moi tick <=30ms (cam // giac "dinh" nhu Reaper docked, khong floating). Bo qua khi an/thu // nho — khong pha trang thai user. GetWindowRect re re — khong chi // phi khi that su lech. if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) return; RECT r; if (!GetWindowRect(hwnd, &r)) return; if (r.left == x && r.top == y && (r.right - r.left) == cw && (r.bottom - r.top) == chh) return; SetWindowPos(hwnd, HWND_TOP, x, y, cw, chh, SWP_NOACTIVATE | SWP_SHOWWINDOW); std::cerr << "[JuceFxGuiHost] re-snap slot " << (int)slot << " -> " << x << "," << y << " " << cw << "x" << chh << " (editor " << ew << "x" << eh << ") hwnd=0x" << std::hex << (uintptr_t)hwnd << std::dec << std::endl; return; } SetWindowPos(hwnd, HWND_TOP, x, y, cw, chh, SWP_NOACTIVATE | SWP_SHOWWINDOW); std::cerr << "[JuceFxGuiHost] anchor slot " << (int)slot << " -> " << x << "," << y << " " << cw << "x" << chh << " (editor " << ew << "x" << eh << ") hwnd=0x" << std::hex << (uintptr_t)hwnd << std::dec << std::endl; ax_ = x; ay_ = y; aw_ = cw; ah_ = chh; applied_ = true; } const uint32_t slot; private: JuceFxGuiHost& host_; juce::Component* editor_ = nullptr; // content non-owned (engine so huu) int ax_ = 0, ay_ = 0, aw_ = 0, ah_ = 0; bool applied_ = false; }; } // namespace struct JuceFxGuiHost::Impl { // Chỉ GUI thread chạm; stop() join GUI thread trước khi ~GuiHost. std::vector> windows_; // runOnGui task queue: main thread xếp (engine.prepare/shutdown phải chạy // trên GUI thread — thread tạo plugin instance); GUI thread drain mỗi vòng. std::mutex guiMutex; std::condition_variable guiCv; std::vector> guiTasks; uint64_t guiTaskDone = 0; // số task đã hoàn thành (runOnGui chờ tăng) // M7: anchor rect per slot — ctrl thread (JuceFxLoop) ghi từng field // (ed.rx/ry/rw/rh), GUI thread reconcile đọc dưới mutex + áp SetWindowPos. struct Anchor { bool xSet = false, ySet = false, wSet = false, hSet = false; bool valid = false; // đủ 4 field (frontend gửi trọn bộ mỗi 250ms) int x = 0, y = 0, w = 0, h = 0; }; std::mutex anchorMutex; std::vector anchors_; // size = kAnchorSlots (ctor khởi tạo) // M9 owner window: main window DAW lam OWNER cho PluginWindow (owned // luon TREN owner trong z-order -> khong bi app che khi DAW foreground, // an theo owner khi minimize, khong taskbar/Alt-Tab rieng). GUI thread // duy nhat ghi/doc (reconcile). 0 = chua tim thay (headless/test). uintptr_t ownerHwnd = 0; uint64_t ownerFindLastMs = 0; }; // ── M9 helpers (Win32): tim main window DAW de lam owner. Bridge spawn boi // engine, engine la sidecar cua DAW (Tauri) -> parent chain // bridge -> engine -> sonicforge-daw. Fallback: scan process ten DAW. #ifdef _WIN32 namespace { uint64_t m9NowMs() { using namespace std::chrono; return (uint64_t)duration_cast( steady_clock::now().time_since_epoch()) .count(); } std::wstring m9ExeName(DWORD pid) { HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snap == INVALID_HANDLE_VALUE) return L""; std::wstring name; PROCESSENTRY32W pe; pe.dwSize = sizeof(pe); if (Process32FirstW(snap, &pe)) { do { if (pe.th32ProcessID == pid) { name = pe.szExeFile; break; } } while (Process32NextW(snap, &pe)); } CloseHandle(snap); return name; } bool m9ExeIsDaw(const std::wstring& exe) { std::wstring n; for (wchar_t c : exe) n += (c >= L'A' && c <= L'Z') ? (wchar_t)(c + 32) : c; if (n.size() > 4 && n.compare(n.size() - 4, 4, L".exe") == 0) n = n.substr(0, n.size() - 4); return n == L"sonicforge-daw"; } DWORD m9DawPid() { static DWORD cached = 0; if (cached) return cached; DWORD cur = GetCurrentProcessId(); for (int hop = 0; hop < 3; ++hop) { // bridge -> engine -> daw HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snap == INVALID_HANDLE_VALUE) break; DWORD parent = 0; PROCESSENTRY32W pe; pe.dwSize = sizeof(pe); if (Process32FirstW(snap, &pe)) { do { if (pe.th32ProcessID == cur) { parent = pe.th32ParentProcessID; break; } } while (Process32NextW(snap, &pe)); } CloseHandle(snap); if (!parent) break; if (m9ExeIsDaw(m9ExeName(parent))) { cached = parent; return cached; } cur = parent; } // Fallback: process dau tien ten sonicforge-daw. HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snap != INVALID_HANDLE_VALUE) { PROCESSENTRY32W pe; pe.dwSize = sizeof(pe); if (Process32FirstW(snap, &pe)) { do { if (m9ExeIsDaw(pe.szExeFile)) { cached = pe.th32ProcessID; break; } } while (Process32NextW(snap, &pe)); } CloseHandle(snap); } return cached; } struct M9EnumCtx { DWORD pid; std::vector wins; }; BOOL CALLBACK m9CollectWnd(HWND h, LPARAM lp) { auto* ctx = reinterpret_cast(lp); DWORD pid = 0; GetWindowThreadProcessId(h, &pid); if (pid == ctx->pid) ctx->wins.push_back(h); return TRUE; } HWND m9FindDawMainWindow() { const DWORD pid = m9DawPid(); if (!pid) return nullptr; M9EnumCtx ctx; ctx.pid = pid; EnumWindows(m9CollectWnd, reinterpret_cast(&ctx)); HWND best = nullptr; std::tuple bestKey; // (iconic, !tauri, -area) bool haveBest = false; for (HWND h : ctx.wins) { // M9b (Bug 2): chỉ top-level THẬT của DAW pid — loại window ẩn / child / // owned (WebView2 host, tooltip, helper owned): chọn nhầm → owner sai → // PluginWindow không owned được → taskbar icon riêng (err=5 symptom). if (!IsWindowVisible(h)) continue; if (GetWindowLongPtrA(h, GWL_STYLE) & WS_CHILD) continue; if (GetParent(h) != nullptr) continue; // owned top-level: GetParent trả owner RECT r; if (!GetWindowRect(h, &r)) continue; const int w = r.right - r.left; const int hgt = r.bottom - r.top; if (w <= 0 || hgt <= 0) continue; // hidden/minimized rect 0 const int area = w * hgt; wchar_t cls[64] = {0}; GetClassNameW(h, cls, 64); const bool tauri = (wcscmp(cls, L"Tauri Window") == 0); const bool iconic = IsIconic(h) != 0; const auto key = std::make_tuple(iconic ? 1 : 0, tauri ? 0 : 1, -area); if (!haveBest || key < bestKey) { haveBest = true; best = h; bestKey = key; } } return best; } } // namespace #endif JuceFxGuiHost::JuceFxGuiHost(JuceFxEngine& engine) : impl_(std::make_unique()), engine_(engine) { impl_->anchors_.assign(kAnchorSlots, Impl::Anchor{}); } JuceFxGuiHost::~JuceFxGuiHost() { stop(); } void JuceFxGuiHost::start() { if (started_.exchange(true)) return; quit_ = false; guiExited_ = false; thread_ = std::thread([this] { guiMain(); }); } void JuceFxGuiHost::stop() { if (!started_.load()) return; std::cerr << "[JuceFxGuiHost] stop begin" << std::endl; waitEditorsClosed(1000); // started_ vẫn true → có pump từ GUI thread started_.store(false); quit_ = true; // Chờ guiMain thoát tối đa 2s (runDispatchLoopUntil trả sau <=50ms; kẹt // khi plugin editor block message thread). Kẹt → detach, không treo audio // loop; teardown lệch có thể xảy ra nhưng editor block là pathological. for (int i = 0; i < 200 && !guiExited_.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(10)); if (guiExited_.load() && thread_.joinable()) { thread_.join(); std::cerr << "[JuceFxGuiHost] stop joined" << std::endl; } else if (thread_.joinable()) { std::cerr << "[JuceFxGuiHost] GUI thread không thoát trong 2s — detach" << std::endl; thread_.detach(); } } void JuceFxGuiHost::enqueueGuiTask(std::function task) { // GUI không chạy / đang thoát → chạy inline (không có message thread). if (!started_.load() || quit_.load() || guiExited_.load()) { task(); return; } std::unique_lock lk(impl_->guiMutex); const uint64_t before = impl_->guiTaskDone; impl_->guiTasks.push_back(std::move(task)); impl_->guiCv.wait(lk, [&] { return impl_->guiTaskDone > before; }); } void JuceFxGuiHost::waitEditorsClosed(uint32_t timeoutMs) { if (!started_.load()) return; engine_.closeAllEditors(); // enqueue Close mọi editor mở const auto t0 = std::chrono::steady_clock::now(); while (engine_.hasOpenEditor()) { const auto el = std::chrono::duration_cast( std::chrono::steady_clock::now() - t0) .count(); if ((uint32_t)el >= timeoutMs) break; std::this_thread::sleep_for(std::chrono::milliseconds(5)); } } void JuceFxGuiHost::onWindowClose(uint32_t slot) { engine_.requestEditor(slot, JuceFxEngine::EditorAction::Close); } void JuceFxGuiHost::setAnchorField(uint32_t slot, char field, int value) { std::lock_guard lk(impl_->anchorMutex); if (slot >= impl_->anchors_.size()) return; Impl::Anchor& a = impl_->anchors_[slot]; switch (field) { case 'x': a.x = value; a.xSet = true; break; case 'y': a.y = value; a.ySet = true; break; case 'w': a.w = value; a.wSet = true; break; case 'h': a.h = value; a.hSet = true; break; default: return; } a.valid = a.xSet && a.ySet && a.wSet && a.hSet; if (a.valid) { std::cerr << "[JuceFxGuiHost] anchor fields slot " << slot << " rect " << a.x << "," << a.y << " " << a.w << "x" << a.h << std::endl; } } void JuceFxGuiHost::clearAnchor(uint32_t slot) { std::lock_guard lk(impl_->anchorMutex); if (slot < impl_->anchors_.size()) impl_->anchors_[slot] = Impl::Anchor{}; } void JuceFxGuiHost::tick() { static int n = 0; if (++n <= 3 || n % 100 == 0) std::cerr << "[JuceFxGuiHost] tick " << n << std::endl; engine_.pumpEditorQueue(); reconcile(); } void JuceFxGuiHost::reconcile() { const uint32_t n = engine_.slotCount(); auto& windows = impl_->windows_; // M9: gan owner (main DAW) cho window plugin — OWNED window luon tren // owner: fix bug "click VST FX khong thay GUI" (bridge sinh window duoi // main DAW foreground-lock). Tim DAW toi da 1 lan/s (ownerHwnd=0 -> DAW // chua xac dinh / headless). Set truoc khi show; retry moi reconcile. // M9b: log định danh window (class/title/pid/vis) — chẩn đoán err=5 // (owner target sai / integrity / target chết). auto logWnd = [](HWND hwnd, const char* tag) { wchar_t cls[128] = {0}, title[256] = {0}; GetClassNameW(hwnd, cls, 128); GetWindowTextW(hwnd, title, 256); char clsA[256], titleA[512]; snprintf(clsA, sizeof(clsA), "%ls", cls); snprintf(titleA, sizeof(titleA), "%ls", title); DWORD pid = 0; GetWindowThreadProcessId(hwnd, &pid); std::cerr << "[JuceFxGuiHost] " << tag << " hwnd=0x" << std::hex << (uintptr_t)hwnd << std::dec << " pid=" << pid << " class=\"" << clsA << "\" title=\"" << titleA << "\"" << " vis=" << (IsWindowVisible(hwnd) ? 1 : 0) << std::endl; }; auto applyOwner = [&](HWND wnd) { if (!impl_->ownerHwnd) { const uint64_t now = m9NowMs(); if (now - impl_->ownerFindLastMs >= 1000) { impl_->ownerFindLastMs = now; const HWND daw = m9FindDawMainWindow(); if (daw) { impl_->ownerHwnd = (uintptr_t)daw; logWnd(daw, "M9 owner DAW main"); } } } if (!impl_->ownerHwnd) return; const HWND target = (HWND)impl_->ownerHwnd; if ((HWND)GetWindowLongPtrA(wnd, GWLP_HWNDPARENT) == target) return; // M9b bug cũ: SetWindowLongPtrA trả PREVIOUS owner — previous NULL → trả // 0 → code cũ log "fail err=0" dù THÀNH CÔNG (owned OK). SetLastError(0) // trước call: fail luôn set error → err != 0 mới là fail thật. SetLastError(0); const LONG_PTR prev = SetWindowLongPtrA(wnd, GWLP_HWNDPARENT, (LONG_PTR)target); const DWORD err = GetLastError(); if (err == 0) { std::cerr << "[JuceFxGuiHost] owned hwnd=0x" << std::hex << (uintptr_t)wnd << std::dec << " prevOwner=0x" << std::hex << (uintptr_t)prev << std::dec << " owner=0x" << std::hex << (uintptr_t)target << std::dec << std::endl; } else { std::cerr << "[JuceFxGuiHost] owner SetWindowLongPtr fail err=" << err << std::hex << " hwnd=0x" << (uintptr_t)wnd << std::dec << std::endl; logWnd(target, " target"); logWnd(wnd, " wnd"); } }; // Xoá window: slot hết chain hoặc editor đã đóng (Close pump / re-prepare // clear editors). ~DocumentWindow trên GUI thread; content editor có thể // đã bị engine xoá — ResizableWindow giữ SafePointer nên an toàn. for (auto it = windows.begin(); it != windows.end();) { if ((*it)->slot >= n || !engine_.isEditorOpen((*it)->slot)) { it = windows.erase(it); } else { ++it; } } // Tạo window cho slot đang mở; ẩn/hiện theo flag visible (Hide giữ window). for (uint32_t s = 0; s < n; ++s) { if (!engine_.isEditorOpen(s)) continue; auto it = std::find_if(windows.begin(), windows.end(), [s](const std::unique_ptr& w) { return w->slot == s; }); if (it != windows.end()) { const bool vis = engine_.isEditorVisible(s); if ((*it)->isVisible() != vis) (*it)->setVisible(vis); continue; } auto* ed = static_cast(engine_.editorHandle(s)); if (!ed) continue; // open nhưng handle chưa sẵn — tick sau auto w = std::make_unique(s, ed, *this); if (auto* peer = w->getPeer()) { // M9b: WS_EX_TOOLWINDOW đã đặt trong PluginWindow ctor (peer có ngay // sau DocumentWindow ctor) — trước mọi show. const HWND hwnd = (HWND)peer->getNativeHandle(); if (hwnd) { std::cerr << "[JuceFxGuiHost] window slot " << (int)s << " preShow hwnd=0x" << std::hex << (uintptr_t)hwnd << std::dec << " exstyle=0x" << std::hex << GetWindowLongPtrA(hwnd, GWL_EXSTYLE) << std::dec << std::endl; applyOwner(hwnd); } // M14: anchor NGAY TRUOC show — JS gui rect ngay sau openFxGui, // thuong toi TRUOC reconcile tao window -> dat vi tri dung tick // dau, khong flash vi tri mac dinh giua man hinh nhu window noi. { std::lock_guard lk(impl_->anchorMutex); if (s < impl_->anchors_.size()) { const Impl::Anchor& a = impl_->anchors_[s]; if (a.valid) w->applyAnchor(a.x, a.y, a.w, a.h); } } std::cerr << "[JuceFxGuiHost] window slot " << (int)s << " showing visible=" << (engine_.isEditorVisible(s) ? 1 : 0) << std::endl; w->setVisible(engine_.isEditorVisible(s)); std::cerr << "[JuceFxGuiHost] window slot " << (int)s << " hwnd=0x" << std::hex << (uintptr_t)hwnd << std::dec << " editor=" << ed->getWidth() << "x" << ed->getHeight() << std::endl; } else { w->setVisible(engine_.isEditorVisible(s)); } windows.push_back(std::move(w)); } // M7: áp anchor (rect panel embed) cho mọi window đang mở — snapshot dưới // mutex, SetWindowPos trên GUI thread; dedupe trong applyAnchor. Window // mới (chưa applied_) luôn áp; khi panel di chuyển rect đổi → re-anchor. { std::lock_guard lk(impl_->anchorMutex); for (const auto& wp : windows) { if (wp->slot >= impl_->anchors_.size()) continue; const Impl::Anchor& a = impl_->anchors_[wp->slot]; if (a.valid) wp->applyAnchor(a.x, a.y, a.w, a.h); } } // M9 retry: window da mo truoc khi tim duoc DAW -> owned lai khi co. for (const auto& wp : windows) { if (auto* peer = wp->getPeer()) { if (HWND hwnd = (HWND)peer->getNativeHandle()) { // M9d: bo retry khi an (panel dong / DAW an) — owner khong set // duoc luc ca hai an, spam err=5 moi tick vo nghia. Re-show -> // ca hai visible -> retry tu nhien. Bo qua o retry LOOP nay // thoi; applyOwner preShow (truoc show) van giu nguyen. if (!IsWindowVisible(hwnd)) continue; const HWND tgt = (HWND)impl_->ownerHwnd; if (tgt && !IsWindowVisible(tgt)) continue; applyOwner(hwnd); } } } winCount_.store((uint32_t)windows.size()); } void JuceFxGuiHost::guiMain() { #ifdef _WIN32 // BELOW_NORMAL: GUI spike không preempt audio thread HIGHEST. SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); #endif // Thread này trở thành JUCE message thread. std::cerr << "[JuceFxGuiHost] guiMain start" << std::endl; juce::initialiseJuce_GUI(); std::cerr << "[JuceFxGuiHost] initialiseJuce_GUI ok" << std::endl; // Cadence inline (30ms busy / 200ms idle) — KHÔNG dùng juce::Timer: trên GUI thread riêng, // timer callback không bao giờ fire dù runDispatchLoopUntil pump liên tục // (test cho thấy 0 tick). tick() ở đây chạy trên chính message thread nên // pump + tạo/xoá Component an toàn. { using clock = std::chrono::steady_clock; const auto fastMs = std::chrono::milliseconds(30); const auto slowMs = std::chrono::milliseconds(200); auto lastTick = clock::now(); while (!quit_.load()) { // Drain runOnGui tasks trước mỗi pump: engine.prepare()/shutdown() // phải chạy trên GUI thread (= thread tạo plugin instance — M2b). for (;;) { std::function t; { std::lock_guard lk(impl_->guiMutex); if (impl_->guiTasks.empty()) break; t = std::move(impl_->guiTasks.front()); impl_->guiTasks.erase(impl_->guiTasks.begin()); } if (t) t(); { std::lock_guard lk(impl_->guiMutex); ++impl_->guiTaskDone; } impl_->guiCv.notify_all(); } juce::MessageManager::getInstance()->runDispatchLoopUntil(10); const auto now = clock::now(); // Cadence động: có việc editor (queue/open/retry) → 30ms giữ // anchor/reconcile phản hồi nhanh; idle (không editor) → 200ms — // giảm wakeup + churn GUI thread BELOW_NORMAL khi chỉ chạy audio. const auto curMs = engine_.guiBusy() ? fastMs : slowMs; if (now - lastTick >= curMs) { tick(); lastTick = now; } } std::cerr << "[JuceFxGuiHost] loop exit" << std::endl; } // Teardown order: editor là JUCE Component — xoá TRƯỚC shutdownJuce_GUI. std::cerr << "[JuceFxGuiHost] teardown begin" << std::endl; engine_.closeAllEditors(); std::cerr << "[JuceFxGuiHost] teardown closeAllEditors ok" << std::endl; engine_.pumpEditorQueue(); // xử lý Close đồng bộ (message thread) std::cerr << "[JuceFxGuiHost] teardown pump ok" << std::endl; impl_->windows_.clear(); // ~PluginWindow trên message thread std::cerr << "[JuceFxGuiHost] teardown windows clear ok" << std::endl; juce::shutdownJuce_GUI(); std::cerr << "[JuceFxGuiHost] teardown done" << std::endl; guiExited_ = true; }