// 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 #endif #include #include #include #include #include #include #include #include #include #include namespace { // 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); } // 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); } const uint32_t slot; private: JuceFxGuiHost& host_; }; } // 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) }; JuceFxGuiHost::JuceFxGuiHost(JuceFxEngine& engine) : impl_(std::make_unique()), engine_(engine) {} 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::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_; // 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); w->setVisible(engine_.isEditorVisible(s)); if (auto* peer = w->getPeer()) { const auto hwnd = (uintptr_t)peer->getNativeHandle(); std::cerr << "[JuceFxGuiHost] window slot " << (int)s << " hwnd=0x" << std::hex << hwnd << std::dec << " editor=" << ed->getWidth() << "x" << ed->getHeight() << std::endl; } windows.push_back(std::move(w)); } 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 — 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 tickMs = std::chrono::milliseconds(30); 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(); if (now - lastTick >= tickMs) { 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; }