Files
SonicForgeStudio/native_bridge/juce_fx/JuceFxGuiHost.cpp
T
admin ef521d9981 M7-M16: VST FX GUI native embed hoàn thiện — fix click-2-lần chuyển slot, anchor lần mở đầu, taskbar icon
- app.jsx: masterGuiOpenRef sync truoc setState — guard chuyen slot doc ref, het race closure (click 1 lan chuyen slot)
- JuceFxGuiHost.cpp: M14 applyAnchor truoc setVisible (het flash giua man hinh); M15 clear WS_EX_APPWINDOW + set TOOLWINDOW (het taskbar icon — JUCE set APPWINDOW 0x40000 thang TOOLWINDOW); M9 owner DAW + M9d skip applyOwner khi an
- JuceFxLoop.cpp: M16 bo clearAnchor khi ed.open (retry ed.open ~700ms xoa rect fresh -> window moi khong anchor duoc lan dau mo panel); chi clear khi Close
- JuceFxEngine/app.jsx/plugins.py/fx_realtime.py: editor session realtime, anchor rect routing, retry ed.open
- Rebuild engine PyInstaller + juce_fx_bridge cmake; binaries dong bo 5 noi (bridge 7464a7ef)
2026-09-06 19:45:14 +07:00

618 lines
27 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>
#include <tlhelp32.h>
#endif
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstdint>
#include <functional>
#include <iostream>
#include <mutex>
#include <thread>
#include <string>
#include <tuple>
#include <cwchar>
#include <utility>
#include <vector>
namespace {
// Số slot tối đa — khớp FXRT_CTRL_SLOTS (FxRealtimeIPC.h); anchor per slot.
constexpr uint32_t kAnchorSlots = 8;
// 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);
editor_ = editor;
// M9b: WS_EX_TOOLWINDOW dặt NGAY khi native window tồn tại (DocumentWindow
// ctor đã addToDesktop) — cửa sổ GUI plugin không tạo taskbar icon phụ /
// không lọt Alt-Tab kể cả khi owner DAW chưa gắn được (unowned tạm thời).
// Đặt ở preShow (cũ) vẫn có thể show sớm trước khi style áp.
if (auto* peer = getPeer()) {
if (HWND hwnd = (HWND)peer->getNativeHandle()) {
// M15: JUCE addToDesktop set WS_EX_APPWINDOW (0x40000) — taskbar
// icon riêng THẮNG WS_EX_TOOLWINDOW. Clear APPWINDOW + set
// TOOLWINDOW trước show: window plugin không bao giờ vào taskbar.
LONG_PTR ex = GetWindowLongPtrA(hwnd, GWL_EXSTYLE);
if ((ex & WS_EX_APPWINDOW) || !(ex & WS_EX_TOOLWINDOW))
SetWindowLongPtrA(hwnd, GWL_EXSTYLE,
(ex & ~WS_EX_APPWINDOW) | WS_EX_TOOLWINDOW);
}
}
}
// 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); }
// M7: SetWindowPos window vào rect anchor (outer bounds == rect; caption
// native nằm trong rect như cửa sổ embedded). HWND_TOP (KHÔNG TOPMOST —
// fix "floating over other apps") + SWP_NOACTIVATE — mirror FxGuiServer
// /show. Re-anchor chỉ khi rect ĐỔI so với lần áp cuối (dedupe — tránh
// flicker/giật khi user thao tác DAW mà panel yên). Chạy trên GUI thread
// (reconcile tick). Window mới chưa áp lần nào → luôn áp.
// M13c (Bug 1): kich thuoc window = DUNG editor tu nhien + khung cua so —
// KHONG ep theo rect panel (M7/M12 lam window to hon editor cua plugin
// kich thuoc co dinh nhu Ozone 11 -> vung DocumentWindow trong quanh GUI,
// "GUI lot thom giua khung"). x,y van la goc tren-trai panel (neo dung o
// anchor trong DAW); w,h tu JS (rect panel) chi la doi chieu vi tri.
// Nhung doi GUI -> window doi theo qua resizeToFit content (JUCE).
void applyAnchor(int x, int y, int /*w*/, int /*h*/) {
auto* peer = getPeer();
if (!peer) return;
const HWND hwnd = (HWND)peer->getNativeHandle();
if (!hwnd) return;
const int ew = editor_ ? editor_->getWidth() : 0;
const int eh = editor_ ? editor_->getHeight() : 0;
if (ew < 32 || eh < 32) return; // editor chua co size — tick sau
RECT rc = { 0, 0, ew, eh };
AdjustWindowRectEx(&rc,
(DWORD)GetWindowLongPtrA(hwnd, GWL_STYLE),
FALSE,
(DWORD)GetWindowLongPtrA(hwnd, GWL_EXSTYLE));
const int cw = rc.right - rc.left;
const int chh = rc.bottom - rc.top;
if (cw <= 0 || chh <= 0) return;
if (applied_ && ax_ == x && ay_ == y && aw_ == cw && ah_ == chh) {
// M12 stick-to-anchor: rect khong doi nhung window bi keo di (native
// title bar cho drag) -> re-snap ve vi tri panel moi tick <=30ms (cam
// giac "dinh" nhu Reaper docked, khong floating). Bo qua khi an/thu
// nho — khong pha trang thai user. GetWindowRect re re — khong chi
// phi khi that su lech.
if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) return;
RECT r;
if (!GetWindowRect(hwnd, &r)) return;
if (r.left == x && r.top == y && (r.right - r.left) == cw &&
(r.bottom - r.top) == chh)
return;
SetWindowPos(hwnd, HWND_TOP, x, y, cw, chh,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
std::cerr << "[JuceFxGuiHost] re-snap slot " << (int)slot
<< " -> " << x << "," << y << " " << cw << "x" << chh
<< " (editor " << ew << "x" << eh << ") hwnd=0x"
<< std::hex << (uintptr_t)hwnd << std::dec << std::endl;
return;
}
SetWindowPos(hwnd, HWND_TOP, x, y, cw, chh,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
std::cerr << "[JuceFxGuiHost] anchor slot " << (int)slot
<< " -> " << x << "," << y << " " << cw << "x" << chh
<< " (editor " << ew << "x" << eh << ") hwnd=0x"
<< std::hex << (uintptr_t)hwnd << std::dec << std::endl;
ax_ = x; ay_ = y; aw_ = cw; ah_ = chh; applied_ = true;
}
const uint32_t slot;
private:
JuceFxGuiHost& host_;
juce::Component* editor_ = nullptr; // content non-owned (engine so huu)
int ax_ = 0, ay_ = 0, aw_ = 0, ah_ = 0;
bool applied_ = false;
};
} // 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)
// M7: anchor rect per slot — ctrl thread (JuceFxLoop) ghi từng field
// (ed.rx/ry/rw/rh), GUI thread reconcile đọc dưới mutex + áp SetWindowPos.
struct Anchor {
bool xSet = false, ySet = false, wSet = false, hSet = false;
bool valid = false; // đủ 4 field (frontend gửi trọn bộ mỗi 250ms)
int x = 0, y = 0, w = 0, h = 0;
};
std::mutex anchorMutex;
std::vector<Anchor> anchors_; // size = kAnchorSlots (ctor khởi tạo)
// M9 owner window: main window DAW lam OWNER cho PluginWindow (owned
// luon TREN owner trong z-order -> khong bi app che khi DAW foreground,
// an theo owner khi minimize, khong taskbar/Alt-Tab rieng). GUI thread
// duy nhat ghi/doc (reconcile). 0 = chua tim thay (headless/test).
uintptr_t ownerHwnd = 0;
uint64_t ownerFindLastMs = 0;
};
// ── M9 helpers (Win32): tim main window DAW de lam owner. Bridge spawn boi
// engine, engine la sidecar cua DAW (Tauri) -> parent chain
// bridge -> engine -> sonicforge-daw. Fallback: scan process ten DAW.
#ifdef _WIN32
namespace {
uint64_t m9NowMs() {
using namespace std::chrono;
return (uint64_t)duration_cast<milliseconds>(
steady_clock::now().time_since_epoch())
.count();
}
std::wstring m9ExeName(DWORD pid) {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return L"";
std::wstring name;
PROCESSENTRY32W pe;
pe.dwSize = sizeof(pe);
if (Process32FirstW(snap, &pe)) {
do {
if (pe.th32ProcessID == pid) {
name = pe.szExeFile;
break;
}
} while (Process32NextW(snap, &pe));
}
CloseHandle(snap);
return name;
}
bool m9ExeIsDaw(const std::wstring& exe) {
std::wstring n;
for (wchar_t c : exe) n += (c >= L'A' && c <= L'Z') ? (wchar_t)(c + 32) : c;
if (n.size() > 4 && n.compare(n.size() - 4, 4, L".exe") == 0)
n = n.substr(0, n.size() - 4);
return n == L"sonicforge-daw";
}
DWORD m9DawPid() {
static DWORD cached = 0;
if (cached) return cached;
DWORD cur = GetCurrentProcessId();
for (int hop = 0; hop < 3; ++hop) { // bridge -> engine -> daw
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) break;
DWORD parent = 0;
PROCESSENTRY32W pe;
pe.dwSize = sizeof(pe);
if (Process32FirstW(snap, &pe)) {
do {
if (pe.th32ProcessID == cur) {
parent = pe.th32ParentProcessID;
break;
}
} while (Process32NextW(snap, &pe));
}
CloseHandle(snap);
if (!parent) break;
if (m9ExeIsDaw(m9ExeName(parent))) {
cached = parent;
return cached;
}
cur = parent;
}
// Fallback: process dau tien ten sonicforge-daw.
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap != INVALID_HANDLE_VALUE) {
PROCESSENTRY32W pe;
pe.dwSize = sizeof(pe);
if (Process32FirstW(snap, &pe)) {
do {
if (m9ExeIsDaw(pe.szExeFile)) {
cached = pe.th32ProcessID;
break;
}
} while (Process32NextW(snap, &pe));
}
CloseHandle(snap);
}
return cached;
}
struct M9EnumCtx {
DWORD pid;
std::vector<HWND> wins;
};
BOOL CALLBACK m9CollectWnd(HWND h, LPARAM lp) {
auto* ctx = reinterpret_cast<M9EnumCtx*>(lp);
DWORD pid = 0;
GetWindowThreadProcessId(h, &pid);
if (pid == ctx->pid) ctx->wins.push_back(h);
return TRUE;
}
HWND m9FindDawMainWindow() {
const DWORD pid = m9DawPid();
if (!pid) return nullptr;
M9EnumCtx ctx;
ctx.pid = pid;
EnumWindows(m9CollectWnd, reinterpret_cast<LPARAM>(&ctx));
HWND best = nullptr;
std::tuple<int, int, int> bestKey; // (iconic, !tauri, -area)
bool haveBest = false;
for (HWND h : ctx.wins) {
// M9b (Bug 2): chỉ top-level THẬT của DAW pid — loại window ẩn / child /
// owned (WebView2 host, tooltip, helper owned): chọn nhầm → owner sai →
// PluginWindow không owned được → taskbar icon riêng (err=5 symptom).
if (!IsWindowVisible(h)) continue;
if (GetWindowLongPtrA(h, GWL_STYLE) & WS_CHILD) continue;
if (GetParent(h) != nullptr) continue; // owned top-level: GetParent trả owner
RECT r;
if (!GetWindowRect(h, &r)) continue;
const int w = r.right - r.left;
const int hgt = r.bottom - r.top;
if (w <= 0 || hgt <= 0) continue; // hidden/minimized rect 0
const int area = w * hgt;
wchar_t cls[64] = {0};
GetClassNameW(h, cls, 64);
const bool tauri = (wcscmp(cls, L"Tauri Window") == 0);
const bool iconic = IsIconic(h) != 0;
const auto key = std::make_tuple(iconic ? 1 : 0, tauri ? 0 : 1, -area);
if (!haveBest || key < bestKey) {
haveBest = true;
best = h;
bestKey = key;
}
}
return best;
}
} // namespace
#endif
JuceFxGuiHost::JuceFxGuiHost(JuceFxEngine& engine)
: impl_(std::make_unique<Impl>()), engine_(engine) {
impl_->anchors_.assign(kAnchorSlots, Impl::Anchor{});
}
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::setAnchorField(uint32_t slot, char field, int value) {
std::lock_guard<std::mutex> lk(impl_->anchorMutex);
if (slot >= impl_->anchors_.size()) return;
Impl::Anchor& a = impl_->anchors_[slot];
switch (field) {
case 'x': a.x = value; a.xSet = true; break;
case 'y': a.y = value; a.ySet = true; break;
case 'w': a.w = value; a.wSet = true; break;
case 'h': a.h = value; a.hSet = true; break;
default: return;
}
a.valid = a.xSet && a.ySet && a.wSet && a.hSet;
if (a.valid) {
std::cerr << "[JuceFxGuiHost] anchor fields slot " << slot << " rect "
<< a.x << "," << a.y << " " << a.w << "x" << a.h
<< std::endl;
}
}
void JuceFxGuiHost::clearAnchor(uint32_t slot) {
std::lock_guard<std::mutex> lk(impl_->anchorMutex);
if (slot < impl_->anchors_.size())
impl_->anchors_[slot] = Impl::Anchor{};
}
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_;
// M9: gan owner (main DAW) cho window plugin — OWNED window luon tren
// owner: fix bug "click VST FX khong thay GUI" (bridge sinh window duoi
// main DAW foreground-lock). Tim DAW toi da 1 lan/s (ownerHwnd=0 -> DAW
// chua xac dinh / headless). Set truoc khi show; retry moi reconcile.
// M9b: log định danh window (class/title/pid/vis) — chẩn đoán err=5
// (owner target sai / integrity / target chết).
auto logWnd = [](HWND hwnd, const char* tag) {
wchar_t cls[128] = {0}, title[256] = {0};
GetClassNameW(hwnd, cls, 128);
GetWindowTextW(hwnd, title, 256);
char clsA[256], titleA[512];
snprintf(clsA, sizeof(clsA), "%ls", cls);
snprintf(titleA, sizeof(titleA), "%ls", title);
DWORD pid = 0;
GetWindowThreadProcessId(hwnd, &pid);
std::cerr << "[JuceFxGuiHost] " << tag << " hwnd=0x" << std::hex
<< (uintptr_t)hwnd << std::dec << " pid=" << pid
<< " class=\"" << clsA << "\" title=\"" << titleA << "\""
<< " vis=" << (IsWindowVisible(hwnd) ? 1 : 0)
<< std::endl;
};
auto applyOwner = [&](HWND wnd) {
if (!impl_->ownerHwnd) {
const uint64_t now = m9NowMs();
if (now - impl_->ownerFindLastMs >= 1000) {
impl_->ownerFindLastMs = now;
const HWND daw = m9FindDawMainWindow();
if (daw) {
impl_->ownerHwnd = (uintptr_t)daw;
logWnd(daw, "M9 owner DAW main");
}
}
}
if (!impl_->ownerHwnd) return;
const HWND target = (HWND)impl_->ownerHwnd;
if ((HWND)GetWindowLongPtrA(wnd, GWLP_HWNDPARENT) == target) return;
// M9b bug cũ: SetWindowLongPtrA trả PREVIOUS owner — previous NULL → trả
// 0 → code cũ log "fail err=0" dù THÀNH CÔNG (owned OK). SetLastError(0)
// trước call: fail luôn set error → err != 0 mới là fail thật.
SetLastError(0);
const LONG_PTR prev = SetWindowLongPtrA(wnd, GWLP_HWNDPARENT,
(LONG_PTR)target);
const DWORD err = GetLastError();
if (err == 0) {
std::cerr << "[JuceFxGuiHost] owned hwnd=0x" << std::hex
<< (uintptr_t)wnd << std::dec << " prevOwner=0x"
<< std::hex << (uintptr_t)prev << std::dec
<< " owner=0x" << std::hex << (uintptr_t)target
<< std::dec << std::endl;
} else {
std::cerr << "[JuceFxGuiHost] owner SetWindowLongPtr fail err="
<< err << std::hex << " hwnd=0x" << (uintptr_t)wnd
<< std::dec << std::endl;
logWnd(target, " target");
logWnd(wnd, " wnd");
}
};
// 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);
if (auto* peer = w->getPeer()) {
// M9b: WS_EX_TOOLWINDOW đã đặt trong PluginWindow ctor (peer có ngay
// sau DocumentWindow ctor) — trước mọi show.
const HWND hwnd = (HWND)peer->getNativeHandle();
if (hwnd) {
std::cerr << "[JuceFxGuiHost] window slot " << (int)s
<< " preShow hwnd=0x" << std::hex
<< (uintptr_t)hwnd << std::dec
<< " exstyle=0x" << std::hex
<< GetWindowLongPtrA(hwnd, GWL_EXSTYLE)
<< std::dec << std::endl;
applyOwner(hwnd);
}
// M14: anchor NGAY TRUOC show — JS gui rect ngay sau openFxGui,
// thuong toi TRUOC reconcile tao window -> dat vi tri dung tick
// dau, khong flash vi tri mac dinh giua man hinh nhu window noi.
{
std::lock_guard<std::mutex> lk(impl_->anchorMutex);
if (s < impl_->anchors_.size()) {
const Impl::Anchor& a = impl_->anchors_[s];
if (a.valid) w->applyAnchor(a.x, a.y, a.w, a.h);
}
}
std::cerr << "[JuceFxGuiHost] window slot " << (int)s
<< " showing visible="
<< (engine_.isEditorVisible(s) ? 1 : 0) << std::endl;
w->setVisible(engine_.isEditorVisible(s));
std::cerr << "[JuceFxGuiHost] window slot " << (int)s
<< " hwnd=0x" << std::hex << (uintptr_t)hwnd << std::dec
<< " editor=" << ed->getWidth() << "x" << ed->getHeight()
<< std::endl;
} else {
w->setVisible(engine_.isEditorVisible(s));
}
windows.push_back(std::move(w));
}
// M7: áp anchor (rect panel embed) cho mọi window đang mở — snapshot dưới
// mutex, SetWindowPos trên GUI thread; dedupe trong applyAnchor. Window
// mới (chưa applied_) luôn áp; khi panel di chuyển rect đổi → re-anchor.
{
std::lock_guard<std::mutex> lk(impl_->anchorMutex);
for (const auto& wp : windows) {
if (wp->slot >= impl_->anchors_.size()) continue;
const Impl::Anchor& a = impl_->anchors_[wp->slot];
if (a.valid) wp->applyAnchor(a.x, a.y, a.w, a.h);
}
}
// M9 retry: window da mo truoc khi tim duoc DAW -> owned lai khi co.
for (const auto& wp : windows) {
if (auto* peer = wp->getPeer()) {
if (HWND hwnd = (HWND)peer->getNativeHandle()) {
// M9d: bo retry khi an (panel dong / DAW an) — owner khong set
// duoc luc ca hai an, spam err=5 moi tick vo nghia. Re-show ->
// ca hai visible -> retry tu nhien. Bo qua o retry LOOP nay
// thoi; applyOwner preShow (truoc show) van giu nguyen.
if (!IsWindowVisible(hwnd)) continue;
const HWND tgt = (HWND)impl_->ownerHwnd;
if (tgt && !IsWindowVisible(tgt)) continue;
applyOwner(hwnd);
}
}
}
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;
}