fix: SF2 bank select, midi multi-key cut, VSTi reopen hang, multi-track stuck

- NativeInstrumentEngine: track GM bank per channel (CC0/CC32), use bank in programChange
- app.jsx: send CC0/CC32+PROGRAM before notes via __ensureBridgeProgram, dedupe, clear dedupe after async LOAD
- audioRoutingEngine/bridgeAudioNode: idempotent connect (no disconnect-flush on re-connect), fixes note cut & multi-track stuck
- main.cpp: remove 10s poll in OPEN_GUI control job (blocked realtime loop, watchdog race), VstWindowProc stores channel not inst pointer, cleanup gui maps on WM_DESTROY
This commit is contained in:
2026-08-13 12:19:34 +07:00
parent d168328004
commit 3f59c2c4c2
7 changed files with 136 additions and 50 deletions
+49 -26
View File
@@ -49,13 +49,29 @@ static void sleep_ms(uint32_t ms) {
}
#ifdef _WIN32
// Native VST editor windows registry — global de WM_DESTROY (chay tren worker
// thread cua channel tao window) co the don map. USERDATA luu channel+1 (KHONG
// luu con tro inst truc tiep: assign() thay inst moi moi lan load — con tro cu
// bi huy → WM_DESTROY tren con tro dangling → crash/hang bridge).
static InstrumentEngineManager* g_engine = nullptr;
static std::mutex g_guiMutex;
static std::map<uint32_t, void*> g_guiWindows; // channel -> HWND (keep window alive)
static std::map<HWND, uint32_t> g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup)
static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
if (uMsg == WM_DESTROY) {
void* ptr = (void*)GetWindowLongPtrA(hwnd, GWLP_USERDATA);
if (ptr) {
INativeInstrument* inst = static_cast<INativeInstrument*>(ptr);
inst->closeGUI();
INativeInstrument* instToClose = nullptr;
{
std::lock_guard<std::mutex> lock(g_guiMutex);
auto it = g_hwndToCh.find(hwnd);
if (it != g_hwndToCh.end()) {
uint32_t ch = it->second;
g_hwndToCh.erase(it);
g_guiWindows.erase(ch);
if (g_engine) instToClose = g_engine->get(ch);
}
}
if (instToClose) instToClose->closeGUI();
}
return DefWindowProcA(hwnd, uMsg, wParam, lParam);
}
@@ -184,12 +200,15 @@ int main(int argc, char* argv[]) {
#endif
InstrumentEngineManager instruments;
#ifdef _WIN32
g_engine = &instruments;
#endif
// 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;
// Registry la global (g_guiWindows) — WM_DESTROY cleanup can tu VstWindowProc.
// 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.
@@ -310,50 +329,55 @@ int main(int argc, char* argv[]) {
// 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).
// LOAD va OPEN_GUI duoc drain trong cung vong lap: LOAD post job
// len worker (async, VST3 init co the mat giay) truoc khi type=4
// duoc xu ly — khong doi, instruments.get() con rong -> "no
// instrument loaded". Poll toi da 10s cho LOAD hoan tat.
uint32_t guiCh = c.channel;
if (guiCh >= 16) guiCh = 0;
for (int tries = 0; tries < 200 && !instruments.get(guiCh); ++tries) {
sleep_ms(50);
}
if (!instruments.get(guiCh)) {
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << c.arg1
<< " plugin=" << c.arg2 << " ch=" << guiCh << " (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)]() {
// Bo poll 10s tren real-time loop (writeIndex stall > 3s -> Rust
// tuong bridge chet va restart -> 2 bridge cung map SHM -> race).
// LOAD (type=2) post truoc OPEN_GUI tren CUNG ChannelWorker (FIFO)
// -> job openGUI chay sau khi LOAD xong -> kiem tra inst trong job.
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
workers[guiCh]->post([&instruments, guiCh, arg1 = c.arg1, arg2 = std::string(c.arg2)]() {
if (!instruments.get(guiCh)) {
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << arg1
<< " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl;
return;
}
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) {
HWND existingHwnd = nullptr;
auto it = guiWindows.find(guiCh);
if (it != guiWindows.end()) {
existingHwnd = (HWND)it->second;
{
std::lock_guard<std::mutex> lock(g_guiMutex);
auto it = g_guiWindows.find(guiCh);
if (it != g_guiWindows.end()) existingHwnd = (HWND)it->second;
}
if (existingHwnd && IsWindow(existingHwnd)) {
hwnd = existingHwnd;
SetWindowTextA((HWND)hwnd, arg2.c_str());
ShowWindow((HWND)hwnd, SW_SHOW);
SetForegroundWindow((HWND)hwnd);
// 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((HWND)hwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
} else {
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
if (auto* inst = instruments.get(guiCh)) {
SetWindowLongPtrA((HWND)hwnd, GWLP_USERDATA, (LONG_PTR)inst);
{
std::lock_guard<std::mutex> lock(g_guiMutex);
g_guiWindows[guiCh] = hwnd; // keep window alive
g_hwndToCh[(HWND)hwnd] = guiCh; // WM_DESTROY cleanup
}
SetWindowLongPtrA((HWND)hwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
}
}
#else
(void)guiWindows;
(void)0;
#endif
if (auto* inst = instruments.get(guiCh)) {
if (inst->openGUI(hwnd))
@@ -366,7 +390,6 @@ int main(int argc, char* argv[]) {
// Editor windows song tren thread nay — ChannelWorker
// pump message queue khi idle (xem class comment).
});
}
}
}
shmIPC->controlQueueCount = 0;