df4a22b9b4
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.
246 lines
10 KiB
C++
246 lines
10 KiB
C++
// 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 <juce_gui_basics/juce_gui_basics.h>
|
|
|
|
#ifdef _WIN32
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <mutex>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
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<std::unique_ptr<PluginWindow>> 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<std::function<void()>> guiTasks;
|
|
uint64_t guiTaskDone = 0; // số task đã hoàn thành (runOnGui chờ tăng)
|
|
};
|
|
|
|
JuceFxGuiHost::JuceFxGuiHost(JuceFxEngine& engine)
|
|
: impl_(std::make_unique<Impl>()), 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<void()> 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<std::mutex> 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::milliseconds>(
|
|
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<PluginWindow>& 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<juce::Component*>(engine_.editorHandle(s));
|
|
if (!ed) continue; // open nhưng handle chưa sẵn — tick sau
|
|
auto w = std::make_unique<PluginWindow>(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<void()> t;
|
|
{
|
|
std::lock_guard<std::mutex> 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<std::mutex> 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;
|
|
}
|