fix: GUI VST native window qua bridge + audio path bridge (SF2 preview/play câm)

- open_vst_gui: bo WebviewWindowBuilder/thread, chi push_control type=4 (hwnd=0 -> bridge tao window)
- main.cpp: create_native_vst_window (class SonicForge_Native_VST3_Class, 800x600, khong TOPMOST),
  tao trong ChannelWorker job, map guiWindows, capture arg2 by value (fix dangling)
- app.jsx: guard isBridgeActive() 8 cho -> bridge active thi moi note di router -> pushEvent -> bridge
  (truoc day SF2 cam vi HAS_PYFLUIDSYNTH=FALSE -> /soundfont-render 501; VST3 path cu dung nativeSf/Carla)
- E2E: SF2 NOTE_ON qua dispatchMidiEvent -> SHM peak 0.029745; Nexus GUI native hwnd OK (license/preset)
- docs: TASKS.md + TEST_NOTES.md ghi batch fix + ket qua; gitignore vendor/junk
This commit is contained in:
2026-08-12 22:12:43 +07:00
parent c3368d0a91
commit d8f8506077
37 changed files with 2170 additions and 419 deletions
+190 -10
View File
@@ -9,6 +9,7 @@
#include <windows.h>
#include <mmsystem.h>
#include <process.h>
#include <thread>
#else
#include <unistd.h>
#include <cstdlib>
@@ -17,9 +18,14 @@
#endif
#include <algorithm>
#include <condition_variable>
#include <cstring>
#include <deque>
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <string>
// --- platform helpers -------------------------------------------------------
@@ -42,6 +48,98 @@ static void sleep_ms(uint32_t ms) {
#endif
}
// 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.
static void* create_native_vst_window(const char* title) {
#ifdef _WIN32
static const char* kWndClass = "SonicForge_Native_VST3_Class";
static bool registered = false;
if (!registered) {
WNDCLASSA wc = {};
wc.lpfnWndProc = DefWindowProcA;
wc.hInstance = GetModuleHandleA(nullptr);
wc.lpszClassName = kWndClass;
RegisterClassA(&wc);
registered = true;
}
HWND hwnd = CreateWindowExA(0, kWndClass, title ? title : "VST",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
return (void*)hwnd;
#else
(void)title;
return nullptr;
#endif
}
// Per-channel persistent worker: ONE thread owns the COM STA apartment for that
// channel's instrument for its whole lifetime. loadPlugin and openGUI MUST run
// on the same thread — if the loading thread exits, its apartment dies and
// VST3 plugins that marshal internally (Nexus) hang forever in
// view->attached(). Verified with gui_probe: `bridge_like` (load thread exits,
// openGUI on another) hangs; `same_thread` (load+openGUI on one alive thread)
// returns attached=OK.
class ChannelWorker {
public:
ChannelWorker() {
th_ = std::thread([this] {
#ifdef _WIN32
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
#endif
std::unique_lock<std::mutex> lk(mu_);
for (;;) {
if (stop_ && jobs_.empty()) break;
if (!jobs_.empty()) {
auto job = std::move(jobs_.front());
jobs_.pop_front();
lk.unlock();
job();
lk.lock();
continue;
}
cv_.wait_for(lk, std::chrono::milliseconds(5));
// Pump THIS thread's message queue while idle — VST editor
// windows are created on this thread, their messages must be
// dispatched here or the editor freezes after attach.
lk.unlock();
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
lk.lock();
}
#ifdef _WIN32
CoUninitialize();
#endif
});
}
~ChannelWorker() {
{
std::lock_guard<std::mutex> lk(mu_);
stop_ = true;
}
cv_.notify_all();
if (th_.joinable()) th_.join();
}
void post(std::function<void()> job) {
{
std::lock_guard<std::mutex> lk(mu_);
jobs_.push_back(std::move(job));
}
cv_.notify_all();
}
private:
std::thread th_;
std::mutex mu_;
std::condition_variable cv_;
std::deque<std::function<void()>> jobs_;
bool stop_ = false;
};
int main(int argc, char* argv[]) {
std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl;
@@ -73,6 +171,12 @@ int main(int argc, char* argv[]) {
#endif
InstrumentEngineManager instruments;
// Per-channel persistent workers: loadPlugin + openGUI run on the SAME
// thread whose COM STA apartment stays alive for the channel's lifetime
// (see ChannelWorker comment — a dead apartment hangs Nexus attached()).
std::map<uint32_t, std::unique_ptr<ChannelWorker>> workers;
// B9: native editor windows per channel — keep alive (HWND outlives the job).
std::map<uint32_t, void*> guiWindows;
// B8: sample rate from the DAW (Rust spawns us with SF_SAMPLE_RATE).
// Block size is fixed by the SHM layout (AUDIO_BLOCK_SIZE) — SF_BLOCK_SIZE
// is accepted but must match, otherwise warned and ignored.
@@ -95,13 +199,16 @@ int main(int argc, char* argv[]) {
if (!inst) return; // channel chưa gán instrument → silent (A10)
switch (evt.command) {
case 0x9:
// sampleOffset LUON LUON = 0 khi den day: events duoc dispatch ngay
// truoc segment chua no (A11 splitting), nen offset tuong doi la 0.
// Truyen offset tuyet doi truoc day lam sfizz/VST3 trigger tre.
if (evt.velocity > 0)
inst->noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f, evt.sampleOffset);
inst->noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f, 0);
else
inst->noteOff(evt.channel, evt.pitch, evt.sampleOffset);
inst->noteOff(evt.channel, evt.pitch, 0);
break;
case 0x8:
inst->noteOff(evt.channel, evt.pitch, evt.sampleOffset);
inst->noteOff(evt.channel, evt.pitch, 0);
break;
case 0xB: // CC: controller number in pitch, value in data2
inst->controlChange(evt.channel, evt.pitch, evt.data2);
@@ -124,23 +231,50 @@ int main(int argc, char* argv[]) {
// 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
while (true) {
#ifdef _WIN32
// Message pump: VST editors (Nexus, JUCE-based...) block inside
// view->attached() until the host dispatches messages — openGUI runs
// on a worker thread, so THIS loop must pump concurrently (verified
// with gui_probe: worker-thread openGUI + concurrent pump → attached
// returns kResultOk; without it → hangs forever).
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
#endif
// A. Control events — non-rt safe, drained first
for (uint32_t i = 0; i < shmIPC->controlQueueCount; ++i) {
const auto& c = shmIPC->controlQueue[i];
if (c.type == 2) { // LOAD_INSTRUMENT (A10: assign per MIDI channel)
// Robust path length: do not trust arg1 (Rust passes 0 for LOAD).
// Chay tren persistent worker thread cua channel: VST3 init
// (loadPlugin) co the mat giay — chay dong bo tren audio loop
// lam writeIndex stall > 3s -> Rust tuong bridge chet va restart
// nham (2 bridge cung map SHM -> race control queue / double
// load). assign() chi giu mutex khi ghi map nen renderAll khong
// bao gio stall. openGUI sau nay chay tren CUNG thread nay
// (ChannelWorker) — thread khong bao gio exit nen COM STA
// apartment cua plugin con song (xem ChannelWorker comment).
size_t plen = 0;
while (plen < sizeof(c.arg2) && c.arg2[plen]) ++plen;
std::string path(c.arg2, plen);
InstrumentType t = (InstrumentType)c.arg0;
uint32_t ch = c.channel & 0xF;
if (instruments.assign(ch, t, path, sampleRate, block)) {
std::cout << "[NativeBridge] instrument loaded ch=" << ch
<< " type=" << (int)c.arg0 << " " << path << std::endl;
} else {
std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch
<< " type=" << (int)c.arg0 << " " << path << std::endl;
}
if (!workers[ch]) workers[ch] = std::make_unique<ChannelWorker>();
workers[ch]->post([&instruments, t, ch, path, sampleRate, block]() {
std::cerr << "[dbg] load thread start ch=" << ch
<< " type=" << (int)t << " path=" << path << std::endl;
bool ok = instruments.assign(ch, t, path, sampleRate, block);
std::cerr << "[dbg] assign returned ch=" << ch << " ok=" << (ok ? 1 : 0) << std::endl;
if (ok) {
std::cout << "[NativeBridge] instrument loaded ch=" << ch
<< " type=" << (int)t << " " << path << std::endl;
} else {
std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch
<< " type=" << (int)t << " " << path << std::endl;
}
});
} else if (c.type == 1) { // PANIC
instruments.allNotesOff();
std::cout << "[NativeBridge] PANIC — all notes off" << std::endl;
@@ -154,6 +288,52 @@ int main(int argc, char* argv[]) {
} else if (c.arg0 == 2) { // SET_POSITION (seek while stopped)
playheadSamples = c.arg1;
}
} else if (c.type == 4) { // OPEN_GUI (A7): arg1 = parent HWND (0 → bridge tự tạo native window), arg2 = plugin id
// ponytail: per-plugin channel mapping chua co — gan GUI cho
// instrument dau tien duoc load (smoke test = 1 instrument).
// Chay tren CUNG ChannelWorker da load instrument: thread rieng
// cho openGUI lai tao COM apartment moi, con plugin thi song o
// apartment cu da chet (load thread exit) -> Nexus attached()
// hang (gui_probe: bridge_like treo, same_thread OK).
uint32_t guiCh = 16;
for (uint32_t ch = 0; ch < 16; ++ch) {
if (instruments.get(ch)) { guiCh = ch; break; }
}
if (guiCh == 16) {
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << c.arg1
<< " plugin=" << c.arg2 << " (no instrument loaded)" << std::endl;
} else {
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
workers[guiCh]->post([&instruments, &guiWindows, guiCh, arg1 = c.arg1, arg2 = std::string(c.arg2)]() {
std::cerr << "[dbg] openGUI thread start hwnd=" << arg1
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
void* hwnd = (void*)(uintptr_t)arg1;
#ifdef _WIN32
if (arg1 == 0) {
// B9: bridge tự tạo native window — editor VST3 đính
// vào đây; message pump bởi ChannelWorker idle loop.
hwnd = create_native_vst_window(arg2.c_str());
if (!hwnd) {
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl;
return;
}
guiWindows[guiCh] = hwnd; // keep window alive
}
#else
(void)guiWindows;
#endif
if (auto* inst = instruments.get(guiCh)) {
if (inst->openGUI(hwnd))
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;
}
// Editor windows song tren thread nay — ChannelWorker
// pump message queue khi idle (xem class comment).
});
}
}
}
shmIPC->controlQueueCount = 0;