fix(ui): GUI button inline same row + same height as Synth/File/FX in TCP

This commit is contained in:
2026-08-14 08:04:33 +07:00
parent 7a8122f99d
commit 29f7fecb35
6 changed files with 221 additions and 144 deletions
+162 -122
View File
@@ -77,6 +77,17 @@ static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARA
}
#endif
// Serialize plugin loads across channels: concurrent VST3 createInstance on
// separate worker threads (Nexus) deadlocks inside the SDK factory - all
// stuck forever (observed: 3 concurrent Nexus loads, zero assign returned).
// One load at a time; SF2/SFZ loads are fast, contention negligible.
static std::mutex g_loadMutex;
// Pending OPEN_GUI requests whose channel instrument was not loaded yet.
// The main loop fulfils them as soon as the load job completes (assign ok),
// so the GUI button works regardless of JS retry timing.
static std::mutex g_pendingGuiMutex;
static std::map<uint32_t, std::pair<void*, std::string>> g_pendingGui;
// B9: native Win32 window for the VST editor (replaces the WebView2 surface —
// the HTML window was drawn ON TOP of the plugin GUI). MUST be created on the
// ChannelWorker thread so the worker's idle message pump services its messages.
@@ -333,6 +344,137 @@ int main(int argc, char* argv[]) {
auto startTime = std::chrono::steady_clock::now();
uint64_t blockCount = 0;
// OPEN_GUI handling - shared by the control queue and the pending-GUI
// sweep. Gate on the MAIN thread before creating a window: instrument
// load is ASYNC (worker thread); OPEN_GUI too early would attach to an
// empty channel and leave a blank window. Not loaded yet => remember the
// request; the sweep retries once the load job completes. Window MUST
// belong to the MAIN thread (audio loop pump dispatches its messages);
// a window on a worker whose pump is idle during jobs hangs
// view->attached() (gui_probe: two_workers_close TIMEOUT). The attach
// job runs on the channel worker (COM STA apartment stays alive).
auto handleOpenGui = [&](uint32_t guiCh, uintptr_t arg1, const std::string& pluginId) {
if (!instruments.get(guiCh)) {
std::cerr << "[NativeBridge] GUI deferred ch=" << guiCh
<< " plugin=" << pluginId << " (no instrument yet) - queued" << std::endl;
std::lock_guard<std::mutex> lock(g_pendingGuiMutex);
g_pendingGui[guiCh] = { (void*)arg1, pluginId };
return;
}
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
void* hwnd = (void*)arg1;
#ifdef _WIN32
// Window PHAI thuoc MAIN thread (audio loop pump nay dispatch
// messages cua no moi vong lap). Tao/cap nhat window ngay tai day.
HWND nativeHwnd = nullptr;
{
std::lock_guard<std::mutex> lock(g_guiMutex);
auto it = g_guiWindows.find(guiCh);
if (it != g_guiWindows.end()) nativeHwnd = (HWND)it->second;
}
if (hwnd == 0) {
if (nativeHwnd && IsWindow(nativeHwnd)) {
hwnd = (void*)nativeHwnd;
SetWindowTextA(nativeHwnd, pluginId.c_str());
ShowWindow(nativeHwnd, SW_SHOW);
SetForegroundWindow(nativeHwnd);
// Reuse: cap nhat USERDATA (channel+1) - inst CU da bi thay
// the boi assign() -> WM_DESTROY sau nay lookup inst MOI.
SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
} else {
nativeHwnd = (HWND)create_native_vst_window(pluginId.c_str());
if (!nativeHwnd) {
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << pluginId << std::endl;
return;
}
{
std::lock_guard<std::mutex> lock(g_guiMutex);
g_guiWindows[guiCh] = nativeHwnd; // keep window alive
g_hwndToCh[nativeHwnd] = guiCh; // WM_DESTROY cleanup
}
SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
hwnd = (void*)nativeHwnd;
}
}
#endif
workers[guiCh]->post([&instruments, guiCh, hwnd, arg2 = pluginId]() {
if (!instruments.get(guiCh)) {
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl;
return;
}
std::cerr << "[dbg] openGUI thread start hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
#ifdef _WIN32
// Option B: chi 1 editor VST mo tai 1 thoi diem toan
// bridge. Instance thu 2 cua CUNG plugin (Nexus) attach
// view o apartment/worker khac -> treo. Dong editor cua
// channel khac TRUOC khi attach: WM_CLOSE -> main pump
// (audio loop) destroy window -> WM_DESTROY -> closeGUI()
// + xoa registry. Chay tren worker job de khong stall
// writeIndex cua real-time loop.
{
std::vector<uint32_t> others;
{
std::lock_guard<std::mutex> lock(g_guiMutex);
for (const auto& kv : g_guiWindows)
if (kv.first != guiCh) others.push_back(kv.first);
}
for (uint32_t y : others) {
HWND yHwnd = nullptr;
{
std::lock_guard<std::mutex> lock(g_guiMutex);
auto it = g_guiWindows.find(y);
if (it != g_guiWindows.end()) yHwnd = (HWND)it->second;
}
if (!yHwnd || !IsWindow(yHwnd)) continue;
PostMessage(yHwnd, WM_CLOSE, 0, 0);
std::cerr << "[dbg] openGUI: closing editor ch=" << y
<< " before attach ch=" << guiCh << std::endl;
bool closed = false;
for (int i = 0; i < 500; ++i) {
{
std::lock_guard<std::mutex> lock(g_guiMutex);
if (g_guiWindows.find(y) == g_guiWindows.end()) { closed = true; break; }
}
Sleep(10);
}
if (!closed)
std::cerr << "[dbg] openGUI: editor ch=" << y
<< " not closed in 5s, proceeding" << std::endl;
}
}
#else
(void)0;
#endif
if (auto* inst = instruments.get(guiCh)) {
// Reopen sau khi dong: reload() (terminate + loadPlugin)
// PHAI chay tren worker thread nay - COM STA apartment
// cua channel song o day. view->attached() cung chay o
// day; window thuoc main thread nen main pump (audio
// loop) dispatch messages cua no - khong can pump worker.
// Chan UAF bang flag reloading_ (set/clear duoi engine
// mutex; renderAll giu mutex khi process).
bool guard = inst->needsReload();
if (guard) instruments.setReloading(guiCh, true);
bool ok = false;
{
// Serialize createInstance (reload) with other loads.
std::lock_guard<std::mutex> lg(g_loadMutex);
ok = inst->reloadForGUI();
}
if (ok) ok = inst->attachView(hwnd);
if (guard) instruments.setReloading(guiCh, false);
if (ok)
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
else
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
<< " plugin=" << arg2 << std::endl;
}
});
};
// 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
while (true) {
#ifdef _WIN32
@@ -389,6 +531,7 @@ int main(int argc, char* argv[]) {
#endif
std::cerr << "[dbg] load thread start ch=" << ch
<< " type=" << (int)t << " path=" << path << std::endl;
std::lock_guard<std::mutex> lg(g_loadMutex);
bool ok = instruments.assign(ch, t, path, sampleRate, block);
std::cerr << "[dbg] assign returned ch=" << ch << " ok=" << (ok ? 1 : 0) << std::endl;
if (ok) {
@@ -423,132 +566,29 @@ int main(int argc, char* argv[]) {
} else if (c.type == 4) { // OPEN_GUI (A7): arg1 = parent HWND (0 → bridge tự tạo native window), arg2 = plugin id
uint32_t guiCh = c.channel;
if (guiCh >= 16) guiCh = 0;
// V9 bug 3/6/7: gate tren MAIN thread TRUOC khi tao window —
// instrument load ASYNC (worker thread); OPEN_GUI som -> attach
// job fail -> cua so trang van con song. Chua load xong =>
// defer: KHONG tao window, KHONG post job. JS retry
// openNativeGUI 8x500ms; khi assign xong nhay lai day, tao
// window 1 lan va attach OK.
if (!instruments.get(guiCh)) {
std::cerr << "[NativeBridge] GUI deferred ch=" << guiCh
<< " plugin=" << c.arg2 << " (no instrument yet)" << std::endl;
continue;
}
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
void* hwnd = (void*)(uintptr_t)c.arg1;
#ifdef _WIN32
// Window PHAI thuoc MAIN thread (audio loop pump nay dispatch
// messages cua no moi vong lap). Window tren worker + worker
// khong pump trong luc job chay -> view->attached() treo
// (gui_probe: two_workers_close TIMEOUT; same_thread /
// two_instances_close — window tren main — OK). Tao/cap nhat
// window ngay tai day tren main thread.
HWND nativeHwnd = nullptr;
{
std::lock_guard<std::mutex> lock(g_guiMutex);
auto it = g_guiWindows.find(guiCh);
if (it != g_guiWindows.end()) nativeHwnd = (HWND)it->second;
}
if (hwnd == 0) {
if (nativeHwnd && IsWindow(nativeHwnd)) {
hwnd = (void*)nativeHwnd;
SetWindowTextA(nativeHwnd, std::string(c.arg2).c_str());
ShowWindow(nativeHwnd, SW_SHOW);
SetForegroundWindow(nativeHwnd);
// Reuse: cap nhat USERDATA (channel+1) — inst CU da bi
// thay the boi assign() -> WM_DESTROY sau nay lookup
// inst MOI, khong dung con tro dangling.
SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
} else {
nativeHwnd = (HWND)create_native_vst_window(std::string(c.arg2).c_str());
if (!nativeHwnd) {
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << c.arg2 << std::endl;
continue;
}
{
std::lock_guard<std::mutex> lock(g_guiMutex);
g_guiWindows[guiCh] = nativeHwnd; // keep window alive
g_hwndToCh[nativeHwnd] = guiCh; // WM_DESTROY cleanup
}
SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
hwnd = (void*)nativeHwnd;
}
}
#endif
workers[guiCh]->post([&instruments, guiCh, hwnd, arg2 = std::string(c.arg2)]() {
if (!instruments.get(guiCh)) {
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl;
return;
}
std::cerr << "[dbg] openGUI thread start hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
#ifdef _WIN32
// Option B: chi 1 editor VST mo tai 1 thoi diem toan
// bridge. Instance thu 2 cua CUNG plugin (Nexus) attach
// view o apartment/worker khac -> treo. Dong editor cua
// channel khac TRUOC khi attach: WM_CLOSE -> main pump
// (audio loop) destroy window -> WM_DESTROY -> closeGUI()
// + xoa registry. Chay tren worker job de khong stall
// writeIndex cua real-time loop.
{
std::vector<uint32_t> others;
{
std::lock_guard<std::mutex> lock(g_guiMutex);
for (const auto& kv : g_guiWindows)
if (kv.first != guiCh) others.push_back(kv.first);
}
for (uint32_t y : others) {
HWND yHwnd = nullptr;
{
std::lock_guard<std::mutex> lock(g_guiMutex);
auto it = g_guiWindows.find(y);
if (it != g_guiWindows.end()) yHwnd = (HWND)it->second;
}
if (!yHwnd || !IsWindow(yHwnd)) continue;
PostMessage(yHwnd, WM_CLOSE, 0, 0);
std::cerr << "[dbg] openGUI: closing editor ch=" << y
<< " before attach ch=" << guiCh << std::endl;
bool closed = false;
for (int i = 0; i < 500; ++i) {
{
std::lock_guard<std::mutex> lock(g_guiMutex);
if (g_guiWindows.find(y) == g_guiWindows.end()) { closed = true; break; }
}
Sleep(10);
}
if (!closed)
std::cerr << "[dbg] openGUI: editor ch=" << y
<< " not closed in 5s, proceeding" << std::endl;
}
}
#else
(void)0;
#endif
if (auto* inst = instruments.get(guiCh)) {
// Reopen sau khi dong: reload() (terminate + loadPlugin)
// PHAI chay tren worker thread nay — COM STA apartment
// cua channel song o day. view->attached() cung chay o
// day; window thuoc main thread nen main pump (audio
// loop) dispatch messages cua no — khong can pump worker.
// Chan UAF bang flag reloading_ (set/clear duoi engine
// mutex; renderAll giu mutex khi process).
bool guard = inst->needsReload();
if (guard) instruments.setReloading(guiCh, true);
bool ok = inst->reloadForGUI() && inst->attachView(hwnd);
if (guard) instruments.setReloading(guiCh, false);
if (ok)
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
else
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
<< " plugin=" << arg2 << std::endl;
}
});
handleOpenGui(guiCh, (uintptr_t)c.arg1, std::string(c.arg2));
}
}
shmIPC->controlQueueCount = 0;
// Pending OPEN_GUI requests: fulfilled as soon as the channel's
// instrument becomes available (load job completed on its worker).
{
std::vector<std::pair<uint32_t, std::pair<void*, std::string>>> readyGui;
{
std::lock_guard<std::mutex> lock(g_pendingGuiMutex);
for (auto it = g_pendingGui.begin(); it != g_pendingGui.end(); ) {
if (instruments.get(it->first)) {
readyGui.push_back({ it->first, it->second });
it = g_pendingGui.erase(it);
} else ++it;
}
}
for (auto& g : readyGui)
handleOpenGui(g.first, (uintptr_t)g.second.first, g.second.second);
}
// B. Snapshot queued MIDI events (bounded copy, queue reset immediately)
uint32_t nEvents = shmIPC->midiQueueCount > 64 ? 64 : shmIPC->midiQueueCount;
SharedAudioBufferIPC::MidiEventIPC evts[64];