M2: GUI thread + host window trong juce_fx_bridge (Reaper-style — editor cua CHINH instance DSP)
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.
This commit is contained in:
@@ -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 "<path.vst3>"
|
||||
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()
|
||||
|
||||
@@ -196,6 +196,7 @@ struct JuceFxEngine::Impl {
|
||||
juce::AudioProcessor* proc = nullptr; // processor slot (không sở hữu)
|
||||
std::unique_ptr<juce::AudioProcessorEditor> 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<std::mutex> lk(impl_->editorMutex);
|
||||
if (slot >= impl_->editors.size()) return nullptr;
|
||||
return impl_->editors[slot].editor.get();
|
||||
}
|
||||
|
||||
uint32_t JuceFxEngine::slotCount() const {
|
||||
std::lock_guard<std::mutex> lk(impl_->editorMutex);
|
||||
return (uint32_t)impl_->editors.size();
|
||||
}
|
||||
|
||||
bool JuceFxEngine::hasOpenEditor() const {
|
||||
std::lock_guard<std::mutex> 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<uint32_t> open;
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 <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;
|
||||
}
|
||||
@@ -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 <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
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 <typename F>
|
||||
void runOnGui(F&& f) {
|
||||
enqueueGuiTask([f = std::forward<F>(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<void()> 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> impl_; // windows_ (PluginWindow chỉ thấy trong .cpp)
|
||||
JuceFxEngine& engine_;
|
||||
std::thread thread_;
|
||||
std::atomic<bool> started_{false};
|
||||
std::atomic<bool> quit_{false};
|
||||
std::atomic<bool> guiExited_{false};
|
||||
std::atomic<uint32_t> winCount_{0};
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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 "<path.vst3>"
|
||||
// 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 <chrono>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
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 <typename F>
|
||||
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::milliseconds>(
|
||||
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 <path.vst3>\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;
|
||||
}
|
||||
Reference in New Issue
Block a user