From df4a22b9b44472ef6a62a93c351eba50c3956dae Mon Sep 17 00:00:00 2001 From: locphamtran Date: Fri, 4 Sep 2026 10:40:46 +0700 Subject: [PATCH] =?UTF-8?q?M2:=20GUI=20thread=20+=20host=20window=20trong?= =?UTF-8?q?=20juce=5Ffx=5Fbridge=20(Reaper-style=20=E2=80=94=20editor=20cu?= =?UTF-8?q?a=20CHINH=20instance=20DSP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JuceFxGuiHost: GUI thread rieng chay juce MessageManager runDispatchLoopUntil(10) + tick 30ms inline cadence (JUCE Timer KHONG fire tren GUI thread rieng — da xac minh selftest), PluginWindow (DocumentWindow native 'FX slot N') attach editor, reconcile dong editor+ window, teardown thu tu. runOnGui(): plugin instance phai tao tren GUI thread — JUCE plugin gan MM vao thread tao instance, editor tao thread khac -> treo (M1 chi chay vi main thread vua tao instance vua la message thread); prepare/shutdown bao qua runOnGui. JuceFxLoop: guiHost.start() TRUOC engine.prepare; re-prepare sau waitEditorsClosed(1500); teardown runOnGui(shutdown) truoc stop(). Idle ~0 CPU (khong feeder, khong instance 2). juce_fx_gui_selftest (khong ship) PASS tren purecomp + fast-compressor: open/hide/show/ resize/close, teardown sach; window native chup duoc (PrintWindow). Regression M1 selftest OK. --- native_bridge/juce_fx/CMakeLists.txt | 26 +++ native_bridge/juce_fx/JuceFxEngine.cpp | 76 ++++-- native_bridge/juce_fx/JuceFxEngine.h | 25 +- native_bridge/juce_fx/JuceFxGuiHost.cpp | 245 ++++++++++++++++++++ native_bridge/juce_fx/JuceFxGuiHost.h | 71 ++++++ native_bridge/juce_fx/JuceFxLoop.cpp | 23 +- native_bridge/juce_fx/gui_host_selftest.cpp | 105 +++++++++ 7 files changed, 553 insertions(+), 18 deletions(-) create mode 100644 native_bridge/juce_fx/JuceFxGuiHost.cpp create mode 100644 native_bridge/juce_fx/JuceFxGuiHost.h create mode 100644 native_bridge/juce_fx/gui_host_selftest.cpp diff --git a/native_bridge/juce_fx/CMakeLists.txt b/native_bridge/juce_fx/CMakeLists.txt index a531c11..b17caa7 100644 --- a/native_bridge/juce_fx/CMakeLists.txt +++ b/native_bridge/juce_fx/CMakeLists.txt @@ -20,6 +20,7 @@ add_executable(juce_fx_bridge main_juce_fx.cpp JuceFxLoop.cpp JuceFxEngine.cpp + JuceFxGuiHost.cpp ) target_include_directories(juce_fx_bridge PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include @@ -35,6 +36,8 @@ target_link_libraries(juce_fx_bridge PRIVATE # VST3PluginFormat (findAllTypesForFile/createInstanceFromDescription/...) # được compile (juce_audio_processors tự bundle VST3_SDK). target_compile_definitions(juce_fx_bridge PRIVATE JUCE_PLUGINHOST_VST3=1) +# M2 GUI thread: runDispatchLoopUntil cần modal loops được phép +target_compile_definitions(juce_fx_bridge PRIVATE JUCE_MODAL_LOOPS_PERMITTED=1) if(WIN32) # timeBeginPeriod (winmm) — như fx_vst_bridge target_link_libraries(juce_fx_bridge PRIVATE winmm) @@ -61,3 +64,26 @@ target_compile_definitions(juce_fx_selftest PRIVATE JUCE_MODAL_LOOPS_PERMITTED=1 if(WIN32) target_link_libraries(juce_fx_selftest PRIVATE winmm) endif() + +# ── M2 gui selftest (GUI thread + host window — KHÔNG ship): verify host +# window trên instance DSP thật, không SHM: +# build_juce/Release/juce_fx_gui_selftest.exe "" +add_executable(juce_fx_gui_selftest + gui_host_selftest.cpp + JuceFxEngine.cpp + JuceFxGuiHost.cpp +) +target_include_directories(juce_fx_gui_selftest PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../include +) +target_link_libraries(juce_fx_gui_selftest PRIVATE + juce::juce_core + juce::juce_audio_basics + juce::juce_audio_processors + juce::juce_gui_basics +) +target_compile_definitions(juce_fx_gui_selftest PRIVATE JUCE_PLUGINHOST_VST3=1) +target_compile_definitions(juce_fx_gui_selftest PRIVATE JUCE_MODAL_LOOPS_PERMITTED=1) +if(WIN32) + target_link_libraries(juce_fx_gui_selftest PRIVATE winmm) +endif() diff --git a/native_bridge/juce_fx/JuceFxEngine.cpp b/native_bridge/juce_fx/JuceFxEngine.cpp index 079e4f0..0be74ff 100644 --- a/native_bridge/juce_fx/JuceFxEngine.cpp +++ b/native_bridge/juce_fx/JuceFxEngine.cpp @@ -196,6 +196,7 @@ struct JuceFxEngine::Impl { juce::AudioProcessor* proc = nullptr; // processor slot (không sở hữu) std::unique_ptr editor; // sở hữu editor (GUI thread) bool open = false; + bool visible = true; // M2: trạng thái Show/Hide (GuiHost sync window) std::string captured; // state capture gần nhất (raw) }; struct EditorCmd { @@ -256,12 +257,16 @@ bool JuceFxEngine::pumpEditorQueue() { } se.editor.reset(ed); // engine sở hữu editor (M1) se.open = true; + se.visible = true; // M2: editor mở = hiển thị (GuiHost mở window) if (c.w > 0 && c.h > 0) se.editor->setSize(c.w, c.h); std::cerr << "[JuceFxEngine] slot " << c.slot << " editor open " << se.editor->getWidth() << "x" << se.editor->getHeight() << std::endl; - } else if (c.w > 0 && c.h > 0) { - se.editor->setSize(c.w, c.h); + } else { + // Đã mở — Show lại (Reaper: editor còn sống, mở window lại). + se.visible = true; + se.editor->setVisible(true); + if (c.w > 0 && c.h > 0) se.editor->setSize(c.w, c.h); } any = true; break; @@ -278,12 +283,12 @@ bool JuceFxEngine::pumpEditorQueue() { } case EditorAction::Show: case EditorAction::Hide: { - // M1: editor chưa attach host window — setVisible chưa đủ. M2 - // (GUI thread + host window) sẽ xử lý show/hide thật. - if (se.editor) { - se.editor->setVisible(c.action == EditorAction::Show); - any = true; - } + // M2: GuiHost đồng bộ window theo flag visible (editor setVisible + // cho nội dung). Show = hiện window, Hide = ẩn (giữ editor+instance). + const bool vis = (c.action == EditorAction::Show); + se.visible = vis; + if (se.editor) se.editor->setVisible(vis); + any = true; break; } case EditorAction::Resize: { @@ -321,6 +326,46 @@ std::string JuceFxEngine::takeCapturedState(uint32_t slot) { return s; } +void* JuceFxEngine::editorHandle(uint32_t slot) { + std::lock_guard lk(impl_->editorMutex); + if (slot >= impl_->editors.size()) return nullptr; + return impl_->editors[slot].editor.get(); +} + +uint32_t JuceFxEngine::slotCount() const { + std::lock_guard lk(impl_->editorMutex); + return (uint32_t)impl_->editors.size(); +} + +bool JuceFxEngine::hasOpenEditor() const { + std::lock_guard lk(impl_->editorMutex); + for (const auto& se : impl_->editors) + if (se.open) return true; + return false; +} + +void JuceFxEngine::closeAllEditors() { + // Gom slot đang mở (editorMutex) rồi enqueue Close (queueMutex) — gọi từ + // main thread trước khi prepare()/shutdown() khi GUI thread đang chạy. + std::vector open; + { + std::lock_guard lk(impl_->editorMutex); + for (size_t i = 0; i < impl_->editors.size(); ++i) + if (impl_->editors[i].open) open.push_back((uint32_t)i); + } + { + std::lock_guard lk(impl_->queueMutex); + for (uint32_t s : open) + impl_->editorQueue.push_back({s, EditorAction::Close, 0, 0}); + } +} + +bool JuceFxEngine::isEditorVisible(uint32_t slot) const { + std::lock_guard lk(impl_->editorMutex); + if (slot >= impl_->editors.size()) return false; + return impl_->editors[slot].open && impl_->editors[slot].visible; +} + void JuceFxEngine::shutdown() { // Đóng mọi editor TRƯỚC khi clear graph — JUCE assert editor phải xoá // trước processor. Editor ops ở đây chạy trên thread gọi shutdown; M1 @@ -332,6 +377,7 @@ void JuceFxEngine::shutdown() { if (se.editor) se.editor.reset(); // ~editor -> processor->editorBeingDeleted se.proc = nullptr; se.open = false; + se.visible = false; se.captured.clear(); } impl_->editors.clear(); @@ -468,11 +514,15 @@ void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) { // Editor mapping: slot -> processor (Reaper-style — editor dùng ĐÚNG // instance trong graph). Editors cũ đã đóng ở shutdown() khi graph đổi; // chain không đổi (early-return trên) thì editors giữ nguyên + proc sống. - impl_->editors.clear(); - impl_->editors.resize(impl_->slots.size()); - for (size_t i = 0; i < impl_->pluginNodes.size(); ++i) - if (impl_->pluginNodes[i]) - impl_->editors[i].proc = impl_->pluginNodes[i]->getProcessor(); + // Lock editorMutex: GUI thread (GuiHost tick reconcile) có thể đang đọc. + { + std::lock_guard lk(impl_->editorMutex); + impl_->editors.clear(); + impl_->editors.resize(impl_->slots.size()); + for (size_t i = 0; i < impl_->pluginNodes.size(); ++i) + if (impl_->pluginNodes[i]) + impl_->editors[i].proc = impl_->pluginNodes[i]->getProcessor(); + } for (int ch = 0; ch < 2; ++ch) { NCh src; src.nodeID = prev->nodeID; src.channelIndex = ch; diff --git a/native_bridge/juce_fx/JuceFxEngine.h b/native_bridge/juce_fx/JuceFxEngine.h index 7321081..9285aea 100644 --- a/native_bridge/juce_fx/JuceFxEngine.h +++ b/native_bridge/juce_fx/JuceFxEngine.h @@ -36,8 +36,8 @@ public: enum class EditorAction { Open, // tạo editor (createEditorIfNeeded) cho slot — giữ instance DSP Close, // huỷ editor, GIỮ instance DSP + state (mở lại nhanh) - Show, // hiện editor (M2: host window) — chưa tác dụng khi chưa attach - Hide, // ẩn editor (M2: host window) + Show, // hiện editor + host window (M2) + Hide, // ẩn editor + host window, GIỮ editor+instance (mở lại nhanh) Resize, // đổi kích thước editor (M2: host window) Capture // đọc state processor (getStateInformation) -> captured[slot] }; @@ -57,6 +57,27 @@ public: // Raw bytes (chưa base64) — M3 encode khi đẩy về engine Python. std::string takeCapturedState(uint32_t slot); + // ── GUI host support (M2): GuiHost chạy trên GUI thread dùng các API này + // để attach editor vào host window + teardown an toàn. Header giữ JUCE-free + // (editorHandle trả void* = juce::Component*). + // GUI thread: con trỏ editor Component của slot (null nếu chưa mở). Chỉ + // gọi trên message thread, cùng lúc pumpEditorQueue(). + void* editorHandle(uint32_t slot); + + // Thread-safe: số slot chain hiện tại (sau prepare; trước = 0). + uint32_t slotCount() const; + + // Thread-safe: có editor nào đang mở không (teardown đồng bộ). + bool hasOpenEditor() const; + + // Thread-safe: enqueue Close cho MỌI slot đang mở — main thread gọi trước + // engine.prepare()/shutdown() khi GUI thread đang chạy (editor là JUCE + // Component, phải đóng trên message thread; GuiHost chờ + pump). + void closeAllEditors(); + + // Thread-safe đọc: editor slot đang hiển thị (Show/Hide) không. + bool isEditorVisible(uint32_t slot) const; + void shutdown(); private: diff --git a/native_bridge/juce_fx/JuceFxGuiHost.cpp b/native_bridge/juce_fx/JuceFxGuiHost.cpp new file mode 100644 index 0000000..6ff4bcb --- /dev/null +++ b/native_bridge/juce_fx/JuceFxGuiHost.cpp @@ -0,0 +1,245 @@ +// 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; +} diff --git a/native_bridge/juce_fx/JuceFxGuiHost.h b/native_bridge/juce_fx/JuceFxGuiHost.h new file mode 100644 index 0000000..c2bb50f --- /dev/null +++ b/native_bridge/juce_fx/JuceFxGuiHost.h @@ -0,0 +1,71 @@ +// native_bridge/juce_fx/JuceFxGuiHost.h +// M2: GUI thread + window host (Reaper-style) — host editor của ĐÚNG instance +// DSP trong graph ngay trong fxrt_bridge (juce_fx_bridge.exe), không instance +// 2 / feeder (nguyên nhân CPU 100% .24). +// +// GUI thread riêng (BELOW_NORMAL) chạy juce::MessageManager + tick 30ms: +// engine.pumpEditorQueue() → reconcile window theo editor state. +// PluginWindow (DocumentWindow native) bọc juce::AudioProcessorEditor mà +// ENGINE sở hữu — window KHÔNG sở hữu content (setContentNonOwned). +// Idle (không Open, không play) ≈ 0 CPU như Reaper. +#pragma once +#include +#include +#include +#include +#include + +class JuceFxEngine; + +class JuceFxGuiHost { +public: + explicit JuceFxGuiHost(JuceFxEngine& engine); + ~JuceFxGuiHost(); // .cpp (Impl incomplete ở header) + JuceFxGuiHost(const JuceFxGuiHost&) = delete; + JuceFxGuiHost& operator=(const JuceFxGuiHost&) = delete; + + // Start GUI thread (thread này thành JUCE message thread). Gọi TRƯỚC mọi + // engine.prepare() — plugin instance phải tạo trên GUI thread (plugin JUCE + // MM gắn thread tạo instance; editor tạo thread khác → treo — M1 chỉ chạy + // vì main thread vừa tạo instance vừa là message thread). + void start(); + // Chạy task đồng bộ trên GUI thread (chờ xong). Main/audio thread dùng để + // bọc engine.prepare()/shutdown() — instance + editor sống trên cùng thread + // như plugin yêu cầu. KHÔNG gọi từ GUI thread; không started → chạy inline. + template + void runOnGui(F&& f) { + enqueueGuiTask([f = std::forward(f)]() mutable { f(); }); + } + // Đóng editor + dừng GUI thread + join (tối đa 2s, detach nếu GUI thread + // kẹt trong plugin editor). Gọi TRƯỚC engine.shutdown(). + void stop(); + // Main thread: enqueue Close cho mọi editor đang mở + chờ GUI thread pump + // xong (poll hasOpenEditor). Bắt buộc trước engine.prepare()/shutdown() + // khi GUI thread đang chạy (editor là JUCE Component — xoá trên message + // thread). Không started → return ngay. + void waitEditorsClosed(uint32_t timeoutMs); + // Thread-safe: số host window đang tồn tại (selftest). + uint32_t windowCount() const { return winCount_.load(); } + // GUI thread: X button của PluginWindow slot → engine Close (queue) — tick + // sau pump + reconcile xoá window. + void onWindowClose(uint32_t slot); + // GUI thread tick 30ms (GuiTickTimer trong .cpp gọi): pump + reconcile. + void tick(); + +private: + // runOnGui: xếp task cho GUI thread + chờ hoàn thành (count-based). + void enqueueGuiTask(std::function task); + // GUI thread: tạo/xoá/ẩn-hiện PluginWindow theo engine editor state. + void reconcile(); + // GUI thread entry. + void guiMain(); + + struct Impl; + std::unique_ptr impl_; // windows_ (PluginWindow chỉ thấy trong .cpp) + JuceFxEngine& engine_; + std::thread thread_; + std::atomic started_{false}; + std::atomic quit_{false}; + std::atomic guiExited_{false}; + std::atomic winCount_{0}; +}; diff --git a/native_bridge/juce_fx/JuceFxLoop.cpp b/native_bridge/juce_fx/JuceFxLoop.cpp index c4048a7..d62249c 100644 --- a/native_bridge/juce_fx/JuceFxLoop.cpp +++ b/native_bridge/juce_fx/JuceFxLoop.cpp @@ -6,6 +6,7 @@ // Validate header mỗi iteration: sampleRate/blockSize đổi giữa chừng → // teardown + prepareToPlay lại + report latency mới qua FxLatReport. #include "JuceFxEngine.h" +#include "JuceFxGuiHost.h" #include "FxRealtimeIPC.h" #include "FxShm.h" @@ -132,7 +133,16 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName, engine.setChain(chainJson); if (ipc->h.sampleRate) curSr = ipc->h.sampleRate; if (ipc->h.blockSize) curBlock = ipc->h.blockSize; - engine.prepare(curSr, curBlock); + // M2: GUI thread + host window (Reaper-style — editor của CHÍNH instance + // DSP trong graph, không instance 2 / feeder). GUI thread cũng là thread + // tạo plugin instance: JUCE plugin gắn MM vào thread tạo instance — editor + // tạo trên thread khác sẽ treo (selftest: createEditorIfNeeded hang khi + // instance tạo trên main). Vì vậy start() TRƯỚC prepare, prepare chạy qua + // runOnGui; main thread sau này chỉ engine.process (JUCE cho phép + // cross-thread process như host chuẩn). + JuceFxGuiHost guiHost(engine); + guiHost.start(); + guiHost.runOnGui([&] { engine.prepare(curSr, curBlock); }); std::cerr << "[JuceFxLoop] prepared sr=" << curSr << " block=" << curBlock << " latency=" << engine.latencySamples() << std::endl; @@ -160,6 +170,8 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName, ipc->h.state = FXRT_STATE_READY; std::cerr << "[JuceFxLoop] ready" << std::endl; + // (guiHost đã start TRƯỚC engine.prepare ở trên — instance phải tạo trên + // GUI thread.) Editor chỉ mở khi có lệnh (M3 ctrl ring); idle ≈ 0 CPU. std::thread hb([&]() { while (ipc->h.running && ipc->h.state == FXRT_STATE_READY) { ipc->h.heartbeat++; @@ -192,7 +204,11 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName, ipc->h.blockSize != curBlock)) { curSr = ipc->h.sampleRate; curBlock = ipc->h.blockSize; - engine.prepare(curSr, curBlock); + // M2: editor là JUCE Component — đóng trên GUI thread TRƯỚC khi + // re-prepare; prepare() chạy trên GUI thread (thread tạo instance — + // xem startup, M2b). + guiHost.waitEditorsClosed(1500); + guiHost.runOnGui([&] { engine.prepare(curSr, curBlock); }); reportLatencies(); std::cerr << "[JuceFxLoop] re-prepared sr=" << curSr << " block=" << curBlock @@ -270,7 +286,8 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName, } ipc->h.state = FXRT_STATE_STARTING; hb.join(); - engine.shutdown(); + guiHost.runOnGui([&] { engine.shutdown(); }); // graph clear trên GUI thread + guiHost.stop(); // M2: đóng editor (GUI thread) + dừng GUI thread + join fxshm::closeShm(v); #ifdef _WIN32 timeEndPeriod(1); diff --git a/native_bridge/juce_fx/gui_host_selftest.cpp b/native_bridge/juce_fx/gui_host_selftest.cpp new file mode 100644 index 0000000..4ed5dae --- /dev/null +++ b/native_bridge/juce_fx/gui_host_selftest.cpp @@ -0,0 +1,105 @@ +// native_bridge/juce_fx/gui_host_selftest.cpp +// M2 selftest (KHÔNG ship): GUI thread + host window trên instance DSP thật. +// juce_fx_gui_selftest.exe "" +// Main thread = engine + GuiHost điều khiển; GUI thread riêng (BELOW_NORMAL) +// chạy MessageManager + tick 30ms (pump + reconcile). Main thread KHÔNG gọi +// JUCE GUI — mọi thao tác qua requestEditor queue (thread-safe) + poll. +// Plugin không có editor (createEditorIfNeeded null) → [FAIL] open — thử khác. +#include "JuceFxEngine.h" +#include "JuceFxGuiHost.h" + +#include +#include +#include +#include + +static int failures = 0; + +static void check(bool ok, const char* what) { + std::printf("%s %s\n", ok ? "[PASS]" : "[FAIL]", what); + if (!ok) ++failures; +} + +static void sleepMs(int ms) { + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); +} + +// Poll predicate tối đa timeoutMs (bước 10ms). +template +static bool waitFor(F f, int timeoutMs) { + const auto t0 = std::chrono::steady_clock::now(); + while (!f()) { + const auto el = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0) + .count(); + if (el > timeoutMs) return false; + sleepMs(10); + } + return true; +} + +int main(int argc, char** argv) { + if (argc < 2) { + std::fprintf(stderr, "usage: juce_fx_gui_selftest \n"); + return 2; + } + // Escape backslash + quote cho JSON path. + std::string path(argv[1]), jpath; + jpath.reserve(path.size() + 8); + for (char c : path) { + if (c == '\\' || c == '"') jpath += '\\'; + jpath += c; + } + + JuceFxEngine engine; + const std::string chain = + "[{\"type\":\"vst3\",\"path\":\"" + jpath + "\",\"bypass\":false}]"; + engine.setChain(chain); + + JuceFxGuiHost host(engine); + host.start(); + // M2b: plugin instance phải tạo trên GUI thread — plugin JUCE MM gắn thread + // tạo instance, editor tạo thread khác → treo (M1 chỉ chạy vì main thread + // vừa tạo instance vừa là message thread). Prepare chạy trên GUI thread; + // audio thread chỉ gọi engine.process (JUCE cho phép cross-thread). + host.runOnGui([&] { engine.prepare(48000, 256); }); + + int rc = 1; + // Open editor 640x480 — GUI thread tạo editor + PluginWindow (host native). + engine.requestEditor(0, JuceFxEngine::EditorAction::Open, 640, 480); + check(waitFor([&] { return engine.isEditorOpen(0) && host.windowCount() == 1; }, 5000), + "open: isEditorOpen(0) && windowCount==1 (editor + host window)"); + + sleepMs(3000); // chừa thời gian render — chụp màn hình bên ngoài + + // Hide: editor + instance GIỮ (mở lại nhanh), window ẩn. + engine.requestEditor(0, JuceFxEngine::EditorAction::Hide); + check(waitFor([&] { return !engine.isEditorVisible(0) && engine.isEditorOpen(0); }, 3000), + "hide: !isEditorVisible(0) nhưng editor vẫn open"); + check(host.windowCount() == 1, "hide: windowCount vẫn 1 (giữ editor+instance)"); + + // Show lại. + engine.requestEditor(0, JuceFxEngine::EditorAction::Show); + check(waitFor([&] { return engine.isEditorVisible(0); }, 3000), + "show: isEditorVisible(0) true lại"); + + // Resize 700x500 — setSize editor; window resizeToFit co theo. + engine.requestEditor(0, JuceFxEngine::EditorAction::Resize, 700, 500); + sleepMs(1500); + check(host.windowCount() == 1 && engine.isEditorOpen(0), + "resize: editor vẫn mở sau Resize 700x500"); + + sleepMs(2000); // chừa render sau resize — chụp màn hình bên ngoài + + // Close: window bị reconcile xoá → windowCount 0. + engine.requestEditor(0, JuceFxEngine::EditorAction::Close); + check(waitFor([&] { return !engine.isEditorOpen(0) && host.windowCount() == 0; }, 3000), + "close: !isEditorOpen(0) && windowCount==0 (window xoá)"); + + host.runOnGui([&] { engine.shutdown(); }); // graph clear trên GUI thread + host.stop(); // đóng editor + dừng GUI thread + join + + rc = (failures == 0) ? 0 : 1; + std::printf(failures == 0 ? "GUI SELFTEST OK\n" : "GUI SELFTEST FAILED\n"); + return rc; +}