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:
2026-09-04 10:40:46 +07:00
parent 0d22e8b4d3
commit df4a22b9b4
7 changed files with 553 additions and 18 deletions
+26
View File
@@ -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()
+63 -13
View File
@@ -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;
+23 -2
View File
@@ -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:
+245
View File
@@ -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;
}
+71
View File
@@ -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};
};
+20 -3
View File
@@ -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);
+105
View File
@@ -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;
}