1287 lines
62 KiB
Plaintext
1287 lines
62 KiB
Plaintext
// native_bridge/src/main.cpp
|
|
// Entry point of daw_vst_bridge.exe — opens shared memory created by the DAW
|
|
// (Rust/Tauri side), runs the real-time MIDI->audio loop, watches the parent.
|
|
#include "INativeInstrument.h"
|
|
#include "SharedMemoryIPC.h"
|
|
#include "NativeInstrumentEngine.h"
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#include <mmsystem.h>
|
|
#include <process.h>
|
|
#include <thread>
|
|
#else
|
|
#include <unistd.h>
|
|
#include <cstdlib>
|
|
#include <thread>
|
|
#endif
|
|
|
|
#include <chrono>
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cctype>
|
|
#include <condition_variable>
|
|
#include <cstring>
|
|
#include <deque>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// --- platform helpers -------------------------------------------------------
|
|
static bool parent_alive(uint32_t pid) {
|
|
#ifdef _WIN32
|
|
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
|
if (!h) return false;
|
|
CloseHandle(h);
|
|
return true;
|
|
#else
|
|
return pid == 0 || (kill(pid, 0) == 0);
|
|
#endif
|
|
}
|
|
|
|
static void sleep_ms(uint32_t ms) {
|
|
#ifdef _WIN32
|
|
Sleep(ms);
|
|
#else
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
|
|
#endif
|
|
}
|
|
|
|
#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).
|
|
class ChannelWorker; // fwd — WM_DESTROY posts closeGUI() to the channel worker
|
|
static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker
|
|
static std::string lower_plugin_path(uint32_t ch); // defined after ChannelWorker
|
|
static void unmute_if_not_closing(uint32_t y, const char* why); // defined after ChannelWorker
|
|
|
|
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 std::map<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr;
|
|
// Same-plugin-DLL reentrancy guards: two threads inside one VST3 DLL (Nexus)
|
|
// crash or deadlock. g_attachPaths = lowercase plugin paths whose reload/
|
|
// createView is running on some worker — a same-path close job must not unmute
|
|
// its channel mid-attach (the attach job owns the silence state until
|
|
// attachView finishes). g_closeInFlight = channels whose close job
|
|
// (closeGUI + destroy children + reload) is still inside createInstance —
|
|
// unmuting them then would race the plugin teardown.
|
|
static std::mutex g_attachMutex;
|
|
static std::vector<std::string> g_attachPaths;
|
|
static bool g_closeInFlight[16] = { false };
|
|
// Editor-open predicate state: while a channel's VST editor is attached, EVERY
|
|
// channel assigned the same plugin DLL path stays quiet (the worker pumps the
|
|
// editor's window proc inside the DLL while the audio loop process()es other
|
|
// instances of the same DLL - 2 threads in one DLL crashes Nexus on GUI clicks).
|
|
// Count keyed by lowercased plugin path. The predicate runs on the audio thread
|
|
// under the engine's mu_ - lock order mu_ -> g_editorMutex (never inverted).
|
|
static std::mutex g_editorMutex;
|
|
static std::map<std::string, int> g_editorPathCount;
|
|
static std::string g_editorOpenPath[16];
|
|
static bool g_editorOpen[16] = { false };
|
|
|
|
// GUI attach state (CRASH FIX 0xc000041d): attachView MUST run on the MAIN
|
|
// thread so the editor children created inside view->attached() are owned by
|
|
// the same thread as the parent window (main pump). g_guiAttachPending = one
|
|
// attach in flight per channel (dedupe OPEN_GUI); g_attachSilenced = the
|
|
// same-path channels silenced by the worker job, restored by the kMsgAttach
|
|
// handler after attach; g_editorDestroyAck = kMsgDestroy handshake for the
|
|
// LOAD job (children are main-owned -> DestroyWindow must run on main).
|
|
static bool g_guiAttachPending[16] = { false };
|
|
static std::map<uint32_t, std::vector<uint32_t>> g_attachSilenced;
|
|
static bool g_editorDestroyAck[16] = { false };
|
|
static const UINT kMsgAttach = WM_APP + 1;
|
|
static const UINT kMsgDestroy = WM_APP + 2;
|
|
static std::mutex g_loadMutex; // moved up: VstWindowProc handlers need it
|
|
|
|
|
|
static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
|
|
if (uMsg == WM_CLOSE) {
|
|
uint32_t ch = UINT32_MAX;
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
auto it = g_hwndToCh.find(hwnd);
|
|
if (it != g_hwndToCh.end()) ch = it->second;
|
|
}
|
|
if (ch != UINT32_MAX) {
|
|
// CRASH FIX (0xc000041d STATUS_FATAL_USER_CALLBACK_EXCEPTION /
|
|
// 0xc0000005 trong USER32, Event Log 9:47/10:20/10:22):
|
|
// KHONG de DefWindowProc DestroyWindow o day. Plugin editor la
|
|
// child cua window nay, tao TREN worker thread (view->attached()
|
|
// chay trong attach job) - DestroyWindow tren main thread pha huy
|
|
// child cross-thread -> crash USER32. Detach view tren worker
|
|
// (closeGUI), an window, GIU window trong registry de reuse.
|
|
post_close_gui(ch, hwnd);
|
|
ShowWindow(hwnd, SW_HIDE);
|
|
return 0;
|
|
}
|
|
// Window khong thuoc registry (chua dang ky) - de DefWindowProc huy.
|
|
} else if (uMsg == WM_DESTROY) {
|
|
// Chi xay ra khi window thuc su bi huy (khong con path chu dong nao
|
|
// DestroyWindow khi view dang attached). Xoa registry + detach view.
|
|
uint32_t ch = UINT32_MAX;
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
auto it = g_hwndToCh.find(hwnd);
|
|
if (it != g_hwndToCh.end()) ch = it->second;
|
|
g_hwndToCh.erase(hwnd);
|
|
for (auto it = g_guiWindows.begin(); it != g_guiWindows.end();) {
|
|
if (it->second == hwnd) it = g_guiWindows.erase(it);
|
|
else ++it;
|
|
}
|
|
}
|
|
if (ch != UINT32_MAX) post_close_gui(ch, hwnd);
|
|
} else if (uMsg == kMsgAttach) {
|
|
// Attach the editor view on the MAIN thread: view->attached() creates
|
|
// the editor children windows, and they must be owned by the SAME
|
|
// thread as the parent (this main pump). Attaching on the worker made
|
|
// children worker-owned while the parent was main-owned -> cross-
|
|
// thread parent/child -> USER32 0xc000041d (verified with gui_probe).
|
|
uint32_t ch = (uint32_t)wParam;
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
auto it = g_guiWindows.find(ch);
|
|
if (it != g_guiWindows.end() && it->second != hwnd) {
|
|
// Window replaced by a newer OPEN_GUI - stale attach, drop.
|
|
std::cerr << "[dbg] kMsgAttach ch=" << ch << " skipped (window replaced)" << std::endl;
|
|
g_guiAttachPending[ch] = false;
|
|
return 0;
|
|
}
|
|
}
|
|
auto* inst = g_engine ? g_engine->get(ch) : nullptr;
|
|
if (!inst || inst->hasAttachedView()) {
|
|
std::cerr << "[dbg] kMsgAttach ch=" << ch << " skipped (no instrument / view attached)" << std::endl;
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
g_guiAttachPending[ch] = false;
|
|
return 0;
|
|
}
|
|
std::vector<uint32_t> samePathSilenced;
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
auto it = g_attachSilenced.find(ch);
|
|
if (it != g_attachSilenced.end()) {
|
|
samePathSilenced = it->second;
|
|
g_attachSilenced.erase(it);
|
|
}
|
|
}
|
|
std::string gp = lower_plugin_path(ch);
|
|
bool ok = false;
|
|
{
|
|
// Serialize ALL plugin-DLL entry (createInstance AND createView)
|
|
// with other loads: two threads inside one DLL (Nexus) crash.
|
|
std::lock_guard<std::mutex> lg(g_loadMutex);
|
|
ok = inst->attachView(hwnd);
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
g_guiAttachPending[ch] = false;
|
|
}
|
|
if (!gp.empty()) {
|
|
std::lock_guard<std::mutex> lk(g_attachMutex);
|
|
auto it = std::find(g_attachPaths.begin(), g_attachPaths.end(), gp);
|
|
if (it != g_attachPaths.end()) g_attachPaths.erase(it);
|
|
}
|
|
if (ok) {
|
|
// Editor is attached on this channel: register it globally so
|
|
// EVERY channel assigned the same plugin DLL path stays quiet
|
|
// for the whole editor-open duration (g_editorPathCount).
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_editorMutex);
|
|
if (!gp.empty()) {
|
|
g_editorOpen[ch] = true;
|
|
g_editorOpenPath[ch] = gp;
|
|
++g_editorPathCount[gp];
|
|
}
|
|
}
|
|
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
|
|
<< " plugin=" << gp << " ch=" << ch
|
|
<< " (channel muted while editor open)" << std::endl;
|
|
} else {
|
|
// Attach failed -> no editor running -> safe to process again.
|
|
g_engine->setReloading(ch, false);
|
|
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
|
|
<< " plugin=" << gp << std::endl;
|
|
PostMessageA(hwnd, WM_CLOSE, 0, 0);
|
|
}
|
|
// Restore same-path silence. Channels whose own close job is still
|
|
// inside createInstance stay muted - that job owns their unmute.
|
|
for (uint32_t y : samePathSilenced) unmute_if_not_closing(y, "openGUI");
|
|
return 0;
|
|
} else if (uMsg == kMsgDestroy) {
|
|
// LOAD job handshake: the editor children are main-owned - destroy
|
|
// them on THIS (main) thread, then acknowledge so the worker can
|
|
// assign() the new instrument.
|
|
uint32_t ch = (uint32_t)wParam;
|
|
if (auto* i = g_engine ? g_engine->get(ch) : nullptr) {
|
|
std::lock_guard<std::mutex> lg(g_loadMutex);
|
|
i->closeGUI();
|
|
while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr))
|
|
DestroyWindow(c);
|
|
ShowWindow(hwnd, SW_HIDE);
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
g_editorDestroyAck[ch] = true;
|
|
}
|
|
return 0;
|
|
}
|
|
return DefWindowProcA(hwnd, uMsg, wParam, lParam);
|
|
}
|
|
#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.
|
|
#ifndef _WIN32
|
|
static std::mutex g_loadMutex;
|
|
#endif
|
|
// 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.
|
|
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 = VstWindowProc;
|
|
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
|
|
OleInitialize(nullptr);
|
|
#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;
|
|
// Cap the idle pump: a busy editor (Nexus posts timer/paint
|
|
// messages continuously) otherwise starves this worker's job
|
|
// queue forever — post_close_gui never runs, Option B's 5s
|
|
// close wait times out, two editors stay alive (deadlock).
|
|
for (int pumped = 0; pumped < 16; ++pumped) {
|
|
if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break;
|
|
TranslateMessage(&msg);
|
|
DispatchMessageW(&msg);
|
|
}
|
|
lk.lock();
|
|
}
|
|
#ifdef _WIN32
|
|
OleUninitialize();
|
|
#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;
|
|
};
|
|
|
|
// --- same-plugin-DLL silence helpers ----------------------------------------
|
|
// Two threads inside one VST3 DLL (Nexus) crash/deadlock: the audio loop's
|
|
// process() on one instance must not race createInstance/createView/terminate
|
|
// on another. Windows plugin paths are case-insensitive — compare lowercased.
|
|
static std::string lower_path_str(std::string p) {
|
|
std::transform(p.begin(), p.end(), p.begin(),
|
|
[](unsigned char c) { return (char)::tolower(c); });
|
|
return p;
|
|
}
|
|
|
|
static std::string lower_plugin_path(uint32_t ch) {
|
|
return g_engine ? lower_path_str(g_engine->pathOf(ch)) : std::string();
|
|
}
|
|
|
|
// Channels (other than ch) currently assigned the same plugin DLL path.
|
|
static std::vector<uint32_t> same_plugin_channels(uint32_t ch) {
|
|
std::vector<uint32_t> out;
|
|
std::string p = lower_plugin_path(ch);
|
|
if (p.empty()) return out;
|
|
for (uint32_t y = 0; y < 16; ++y) {
|
|
if (y == ch) continue;
|
|
if (!g_engine->get(y)) continue;
|
|
if (lower_plugin_path(y) == p) out.push_back(y);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static bool close_in_flight(uint32_t ch) {
|
|
std::lock_guard<std::mutex> lk(g_attachMutex);
|
|
return g_closeInFlight[ch];
|
|
}
|
|
|
|
// Unmute y unless its own close job is still inside createInstance — that job
|
|
// performs the unmute once its fresh instance is loaded (or, if an attach for
|
|
// the same plugin is in flight, the attach job's restore does it).
|
|
static void unmute_if_not_closing(uint32_t y, const char* why) {
|
|
if (close_in_flight(y)) {
|
|
std::cerr << "[dbg] " << why << ": ch=" << y
|
|
<< " close job still in flight - close job unmutes" << std::endl;
|
|
return;
|
|
}
|
|
g_engine->setReloading(y, false);
|
|
std::cerr << "[dbg] " << why << ": restored ch=" << y << std::endl;
|
|
}
|
|
|
|
// closeGUI() MUST run on the channel worker thread (its COM STA apartment) —
|
|
// the view was attached there. Calling view->removed() from the main thread
|
|
// (WM_DESTROY handler) is a cross-apartment COM call that corrupts the plugin;
|
|
// Nexus then hangs on the NEXT view->attached(). Erase the registry inside the
|
|
// job so Option B (closing another editor before attach) waits for closeGUI to
|
|
// actually finish.
|
|
static void post_close_gui(uint32_t ch, HWND hwnd) {
|
|
// Runs on the MAIN thread (VstWindowProc WM_CLOSE/WM_DESTROY). The
|
|
// editor children are main-owned now (attachView runs on the main pump),
|
|
// so closeGUI + child destroy + hide all happen here on the owning
|
|
// thread; only the fresh-instance reload (createInstance ~2s, must live
|
|
// in the channel COM STA apartment) is posted to the channel worker.
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
auto it = g_guiWindows.find(ch);
|
|
if (it != g_guiWindows.end() && it->second != hwnd) {
|
|
std::cerr << "[dbg] closeGUI ch=" << ch << " skipped (window replaced)" << std::endl;
|
|
return;
|
|
}
|
|
}
|
|
std::cerr << "[dbg] closeGUI ch=" << ch << " start" << std::endl;
|
|
g_engine->setReloading(ch, true);
|
|
// Close-in-flight: until the fresh instance is loaded, no other job may
|
|
// unmute this channel (its plugin is being torn down).
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_attachMutex);
|
|
g_closeInFlight[ch] = true;
|
|
}
|
|
// Silence same-plugin channels: no second thread may enter this DLL
|
|
// while closeGUI/createInstance runs (2 threads in one DLL -> Nexus
|
|
// exits silently, observed in probes).
|
|
std::vector<uint32_t> sp = same_plugin_channels(ch);
|
|
for (uint32_t y : sp) {
|
|
g_engine->setReloading(y, true);
|
|
std::cerr << "[dbg] closeGUI: silenced same-plugin ch=" << y
|
|
<< " during reload ch=" << ch << std::endl;
|
|
}
|
|
if (auto* i = g_engine->get(ch)) {
|
|
{
|
|
// closeGUI nulls the view WITHOUT view->removed() (Nexus removed()
|
|
// deadlocks). Destroying the editor windows runs the plugin
|
|
// window proc on this (main) thread; it must not race another
|
|
// thread createInstance/createView of the same DLL.
|
|
std::lock_guard<std::mutex> lg(g_loadMutex);
|
|
i->closeGUI();
|
|
#ifdef _WIN32
|
|
if (hwnd && IsWindow(hwnd)) {
|
|
while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr))
|
|
DestroyWindow(c);
|
|
}
|
|
ShowWindow(hwnd, SW_HIDE);
|
|
#endif
|
|
}
|
|
// Rebuild a fresh instance on the channel worker (its COM STA
|
|
// apartment is alive); reload() == terminate + loadPlugin
|
|
// (createInstance). Serialized with other loads by g_loadMutex.
|
|
ChannelWorker* w = nullptr;
|
|
if (g_workers) {
|
|
auto wit = g_workers->find(ch);
|
|
if (wit != g_workers->end()) w = wit->second.get();
|
|
}
|
|
if (w) {
|
|
w->post([ch, sp]() {
|
|
bool reloadOk = false;
|
|
{
|
|
std::lock_guard<std::mutex> lg(g_loadMutex);
|
|
if (auto* i = g_engine->get(ch)) reloadOk = i->reload();
|
|
}
|
|
std::cerr << "[dbg] closeGUI ch=" << ch << " reload="
|
|
<< (reloadOk ? 1 : 0) << std::endl;
|
|
// Editor is closed: drop the global editor-open state so the
|
|
// same-plugin channels render again. (This channel is still
|
|
// silenced by its reloading flag until the restore below.)
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_editorMutex);
|
|
if (g_editorOpen[ch]) {
|
|
g_editorOpen[ch] = false;
|
|
std::string pth = g_editorOpenPath[ch];
|
|
g_editorOpenPath[ch].clear();
|
|
if (!pth.empty()) {
|
|
auto it = g_editorPathCount.find(pth);
|
|
if (it != g_editorPathCount.end() && --it->second <= 0)
|
|
g_editorPathCount.erase(it);
|
|
}
|
|
}
|
|
}
|
|
// createInstance is done: unmuting is safe again. But if an
|
|
// attach for the SAME plugin path is still running (its
|
|
// createView must not race process() on any same-path
|
|
// channel), leave the silence in place - the attach job
|
|
// restore unmutes everything once attachView finished.
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_attachMutex);
|
|
g_closeInFlight[ch] = false;
|
|
}
|
|
bool pathBusy = false;
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_attachMutex);
|
|
std::string p = lower_plugin_path(ch);
|
|
pathBusy = !p.empty() &&
|
|
std::find(g_attachPaths.begin(), g_attachPaths.end(), p) != g_attachPaths.end();
|
|
}
|
|
if (pathBusy) {
|
|
std::cerr << "[dbg] closeGUI ch=" << ch
|
|
<< ": attach in flight for same plugin - leaving silenced (attach restore unmutes)"
|
|
<< std::endl;
|
|
} else {
|
|
for (uint32_t y : sp) unmute_if_not_closing(y, "closeGUI");
|
|
g_engine->setReloading(ch, false);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
// Instrument already gone / no worker - nothing to rebuild: drop the
|
|
// editor-open state and unmute.
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_editorMutex);
|
|
if (g_editorOpen[ch]) {
|
|
g_editorOpen[ch] = false;
|
|
std::string pth = g_editorOpenPath[ch];
|
|
g_editorOpenPath[ch].clear();
|
|
if (!pth.empty()) {
|
|
auto it = g_editorPathCount.find(pth);
|
|
if (it != g_editorPathCount.end() && --it->second <= 0)
|
|
g_editorPathCount.erase(it);
|
|
}
|
|
}
|
|
}
|
|
for (uint32_t y : sp) unmute_if_not_closing(y, "closeGUI");
|
|
g_engine->setReloading(ch, false);
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_attachMutex);
|
|
g_closeInFlight[ch] = false;
|
|
}
|
|
}
|
|
|
|
int main(int argc, char* argv[]) {
|
|
std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl;
|
|
|
|
// 1. Shared memory name: argv --shm <name> | env SF_SHM_NAME | default
|
|
std::string shmName = "SonicForge_DAW_IPC";
|
|
for (int i = 1; i + 1 < argc; ++i) {
|
|
if (std::strcmp(argv[i], "--shm") == 0) shmName = argv[i + 1];
|
|
}
|
|
if (const char* e = std::getenv("SF_SHM_NAME")) shmName = e;
|
|
|
|
// Parent watchdog PID (set by Tauri sidecar spawner)
|
|
uint32_t parentPid = 0;
|
|
if (const char* e = std::getenv("SF_PARENT_PID")) parentPid = (uint32_t)std::atoi(e);
|
|
|
|
// V8 bug 2: watchdog THREAD rieng - main loop chi check parent_alive giua
|
|
// cac block; neu VST process() chan loop thi khong bao gio thoat -> orphan.
|
|
// Thread nay chay doc lap, parent chet -> TerminateProcess ngay. Kem
|
|
// start-time check chong PID reuse (OpenProcess tra handle cua process
|
|
// khac chiem lai PID -> tuong parent con song mai).
|
|
if (parentPid != 0) {
|
|
std::thread([parentPid]() {
|
|
auto proc_birth = [](HANDLE h) -> uint64_t {
|
|
FILETIME c, e, k, u;
|
|
if (GetProcessTimes(h, &c, &e, &k, &u))
|
|
return (uint64_t(c.dwHighDateTime) << 32) | c.dwLowDateTime;
|
|
return 0;
|
|
};
|
|
uint64_t birth = 0;
|
|
{
|
|
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, parentPid);
|
|
if (h) { birth = proc_birth(h); CloseHandle(h); }
|
|
}
|
|
for (;;) {
|
|
Sleep(2000);
|
|
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, parentPid);
|
|
if (!h) { TerminateProcess(GetCurrentProcess(), 0); return; }
|
|
uint64_t nowBirth = proc_birth(h);
|
|
CloseHandle(h);
|
|
if (birth != 0 && nowBirth != 0 && nowBirth != birth) {
|
|
TerminateProcess(GetCurrentProcess(), 0); return;
|
|
}
|
|
}
|
|
}).detach();
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
// Real-time-ish timing: 1ms scheduler resolution
|
|
timeBeginPeriod(1);
|
|
HANDLE hMapFile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName.c_str());
|
|
if (!hMapFile) {
|
|
std::cerr << "[NativeBridge] Failed to open Shared Memory mapping: " << shmName << std::endl;
|
|
return 1;
|
|
}
|
|
auto* shmIPC = (SharedAudioBufferIPC*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC));
|
|
if (!shmIPC) { CloseHandle(hMapFile); return 1; }
|
|
#else
|
|
(void)shmName; // POSIX shm mapping (shm_open) added when porting off Windows
|
|
auto* shmIPC = (SharedAudioBufferIPC*)std::calloc(1, sizeof(SharedAudioBufferIPC));
|
|
if (!shmIPC) return 1;
|
|
#endif
|
|
|
|
InstrumentEngineManager instruments;
|
|
// Editor-open predicate: channels whose plugin DLL has an attached editor
|
|
// stay quiet (channelQuiet in the engine) for the whole editor-open
|
|
// duration -- derived from the global editor count, so the restore loops
|
|
// below that unmute after attach/load become harmless no-ops.
|
|
instruments.setEditorOpenPredicate([](const std::string& lp) {
|
|
std::lock_guard<std::mutex> lk(g_editorMutex);
|
|
auto it = g_editorPathCount.find(lp);
|
|
return it != g_editorPathCount.end() && it->second > 0;
|
|
});
|
|
#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;
|
|
g_workers = &workers;
|
|
// B9: native editor windows per channel — keep alive (HWND outlives the job).
|
|
// 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.
|
|
double sampleRate = 44100.0;
|
|
if (const char* e = std::getenv("SF_SAMPLE_RATE")) {
|
|
double sr = (double)std::atoi(e);
|
|
if (sr > 0) sampleRate = sr;
|
|
}
|
|
if (const char* e = std::getenv("SF_BLOCK_SIZE")) {
|
|
uint32_t b = (uint32_t)std::atoi(e);
|
|
if (b != AUDIO_BLOCK_SIZE)
|
|
std::cerr << "[NativeBridge] SHM block size fixed at " << AUDIO_BLOCK_SIZE
|
|
<< " (SF_BLOCK_SIZE=" << b << " ignored)" << std::endl;
|
|
}
|
|
const uint32_t block = AUDIO_BLOCK_SIZE;
|
|
uint64_t playheadSamples = 0;
|
|
// V8 bug 3: khi STOP da xu ly, bo qua NOTE_ON (velocity>0) den sau - JS
|
|
// note-on timer co the bay toi sau STOP (guardPlay tre do React re-render)
|
|
// -> retrigger note -> VST loop am. Chi PLAY moi nhan note-on lai.
|
|
bool transportStopped = false;
|
|
|
|
auto dispatch = [&](const SharedAudioBufferIPC::MidiEventIPC& evt) {
|
|
// CRASH FIX: go through the manager, which holds mu_ for the WHOLE call
|
|
// (lookup + reloading check + instrument call). Direct get() then
|
|
// lock-free method calls raced assign() — the worker swapped the map and
|
|
// destroyed the old instance right after releasing mu_, so a playing
|
|
// channel loading another VSTi crashed the bridge (use-after-free).
|
|
// Unassigned and reloading channels are dropped inside the manager.
|
|
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) {
|
|
if (transportStopped) return; // V8 bug 3: drop note-on sau STOP
|
|
instruments.noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f);
|
|
} else
|
|
instruments.noteOff(evt.channel, evt.pitch);
|
|
break;
|
|
case 0x8:
|
|
instruments.noteOff(evt.channel, evt.pitch);
|
|
break;
|
|
case 0xB: // CC: controller number in pitch, value in data2
|
|
// V9 bug 4: sau STOP, drop sustain-down (CC64>0) — JS co the
|
|
// gui CC64 xuong sau STOP (note-on/CC timer tre); VSTi giu
|
|
// note khi pedal down -> am treo loop. CC64=0 (sustain-up)
|
|
// van cho qua.
|
|
if (transportStopped && evt.pitch == 64 && evt.data2 > 0) return;
|
|
instruments.controlChange(evt.channel, evt.pitch, evt.data2);
|
|
break;
|
|
case 0xC: // program change: program in data2
|
|
instruments.programChange(evt.channel, evt.data2);
|
|
break;
|
|
case 0xE: // 14-bit pitch bend: data2 = LSB, data3 = MSB
|
|
instruments.pitchBend(evt.channel, evt.data2 | (uint32_t(evt.data3) << 7));
|
|
break;
|
|
default: break;
|
|
}
|
|
};
|
|
|
|
// Render only [from, to) of the block — used by sample-accurate splitting.
|
|
auto renderSegment = [&](uint32_t from, uint32_t to) {
|
|
if (to <= from) return;
|
|
instruments.renderAll(shmIPC->masterLeft + from, shmIPC->masterRight + from, to - from);
|
|
};
|
|
|
|
double blockDurationMs = (double)block / sampleRate * 1000.0;
|
|
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;
|
|
}
|
|
// Dedupe OPEN_GUI spam (frontend openVstGuiRetry): view dang
|
|
// attached -> GUI da mo, khong post attach job lan nua. Re-attach
|
|
// tren view dang attached lam plugin loi (Nexus createView null).
|
|
if (auto* i0 = instruments.get(guiCh)) {
|
|
if (i0->hasAttachedView()) {
|
|
std::cerr << "[dbg] openGUI: view already attached ch=" << guiCh
|
|
<< " — skip" << std::endl;
|
|
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
|
|
#ifdef _WIN32
|
|
// Dedupe: one attach in flight per channel (frontend openVstGuiRetry
|
|
// spam). Cleared by the worker job early returns and by the
|
|
// main-thread kMsgAttach handler.
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
if (g_guiAttachPending[guiCh]) {
|
|
std::cerr << "[dbg] openGUI: attach already pending ch=" << guiCh
|
|
<< " - skip" << std::endl;
|
|
return;
|
|
}
|
|
g_guiAttachPending[guiCh] = true;
|
|
}
|
|
#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;
|
|
#ifdef _WIN32
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
g_guiAttachPending[guiCh] = false;
|
|
}
|
|
#endif
|
|
return;
|
|
|
|
|
|
}
|
|
std::cerr << "[dbg] openGUI thread start hwnd=" << hwnd
|
|
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
|
|
if (auto* inst0 = instruments.get(guiCh)) {
|
|
if (inst0->hasAttachedView()) {
|
|
std::cerr << "[dbg] openGUI: view already attached ch=" << guiCh
|
|
<< " â skip" << std::endl;
|
|
#ifdef _WIN32
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
g_guiAttachPending[guiCh] = false;
|
|
}
|
|
#endif
|
|
return;
|
|
|
|
|
|
}
|
|
}
|
|
#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
|
|
// destroy window -> WM_DESTROY -> closeGUI() + xoa registry.
|
|
// Chay tren worker job de khong stall writeIndex cua real-time
|
|
// loop. CRASH FIX: silence moi channel TRUOC khi dong editor
|
|
// cua no (view->removed() tren worker cung luc process() tren
|
|
// audio loop = 2 thread trong 1 plugin -> crash USER32).
|
|
{
|
|
std::vector<uint32_t> others;
|
|
bool abortAttach = false;
|
|
{
|
|
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) {
|
|
// Skip channels whose editor already detached (close job
|
|
// finished): do not WM_CLOSE a hidden reused window, do
|
|
// not re-silence a channel that is already unmuted.
|
|
if (auto* yi0 = instruments.get(y)) {
|
|
if (!yi0->hasAttachedView()) {
|
|
std::cerr << "[dbg] openGUI: ch=" << y
|
|
<< " editor already detached - skip"
|
|
<< std::endl;
|
|
continue;
|
|
}
|
|
}
|
|
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;
|
|
if (instruments.get(y)) {
|
|
instruments.setReloading(y, true);
|
|
std::cerr << "[dbg] openGUI: silenced ch=" << y
|
|
<< " while closing its editor (before attach ch="
|
|
<< guiCh << ")" << std::endl;
|
|
}
|
|
PostMessage(yHwnd, WM_CLOSE, 0, 0);
|
|
std::cerr << "[dbg] openGUI: closing editor ch=" << y
|
|
<< " before attach ch=" << guiCh << std::endl;
|
|
// WM_CLOSE -> VstWindowProc -> post_close_gui -> closeGUI()
|
|
// tren worker cua channel y. Doi cho view da detach (window
|
|
// duoc giu lai de reuse, khong doi registry erase nhu cu).
|
|
bool closed = false;
|
|
// The close job = closeGUI + destroy children + fresh
|
|
// reload (createInstance, ~2s for Nexus). hasAttachedView
|
|
// flips false after closeGUI only - ALSO wait for the
|
|
// close job to finish entirely (close_in_flight): the
|
|
// attach must not run createView while another thread is
|
|
// still inside this plugin DLL (2 threads in one DLL ->
|
|
// Nexus exits silently, observed in probes).
|
|
for (int i = 0; i < 500; ++i) { // 5s budget
|
|
auto* yi = instruments.get(y);
|
|
bool detached = !yi || !yi->hasAttachedView();
|
|
if (detached && !close_in_flight(y)) { closed = true; break; }
|
|
Sleep(10);
|
|
}
|
|
if (closed) {
|
|
// Editor detached and its close job finished; the
|
|
// close job unmutes the channel itself. Do NOT reset
|
|
// reloading here - the close job owns the silence state.
|
|
std::cerr << "[dbg] openGUI: restored ch=" << y
|
|
<< " after editor close" << std::endl;
|
|
} else {
|
|
std::cerr << "[dbg] openGUI: editor ch=" << y
|
|
<< " close job not finished in 5s, aborting attach ch=" << guiCh
|
|
<< std::endl;
|
|
abortAttach = true;
|
|
break;
|
|
}
|
|
}
|
|
if (abortAttach) {
|
|
// A close job is stuck; do NOT createView (would race the
|
|
// plugin DLL). Unmute what we silenced (skips channels
|
|
// whose close job is still in flight - that job unmutes
|
|
// them when it finishes).
|
|
for (uint32_t y : others) unmute_if_not_closing(y, "openGUI-abort");
|
|
std::cerr << "[dbg] openGUI: attach aborted ch=" << guiCh << std::endl;
|
|
#ifdef _WIN32
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
g_guiAttachPending[guiCh] = false;
|
|
}
|
|
#endif
|
|
return;
|
|
|
|
|
|
}
|
|
}
|
|
#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).
|
|
// CRASH FIX (0xc000041d USER32 while playing): keep THIS
|
|
// channel silenced for the whole time its editor is attached.
|
|
// The worker pumps the plugin's editor windows -> the plugin's
|
|
// window proc runs on this worker thread while the audio loop
|
|
// calls processor->process() on the SAME instance (two threads
|
|
// inside one DLL) -> Nexus crashes ~1-2s after attach during
|
|
// playback. Muting the channel while the editor is open is the
|
|
// deterministic trade-off (track silent while GUI visible);
|
|
// restored when the editor closes (post_close_gui / LOAD).
|
|
// STALL/CRASH FIX (Fix C, restored): silence EVERY channel
|
|
// assigned the SAME plugin DLL path for the reload+attach
|
|
// duration — the audio loop must not process() another
|
|
// instance of this DLL while createInstance/createView runs
|
|
// on this worker (2 threads in one DLL -> Nexus exits
|
|
// silently, observed in probes). Iterate ALL 16 channels, not
|
|
// just window-owning ones: session-restore instances without
|
|
// a window are live and renderable.
|
|
std::vector<uint32_t> samePathSilenced;
|
|
std::string gp = lower_plugin_path(guiCh);
|
|
if (!gp.empty()) {
|
|
// Flush held notes BEFORE muting: these channels stay muted
|
|
// for the whole editor-open duration and the manager drops
|
|
// MIDI for quiet channels -- notes held at mute time would
|
|
// otherwise resume stuck after the editor closes. The
|
|
// manager's dispatch holds mu_ (serialized vs the audio
|
|
// loop); g_loadMutex serializes vs other workers' DLL entry.
|
|
std::lock_guard<std::mutex> lg(g_loadMutex);
|
|
for (uint32_t y = 0; y < 16; ++y) {
|
|
if (y == guiCh) continue;
|
|
if (!instruments.get(y)) continue;
|
|
if (lower_plugin_path(y) == gp) {
|
|
for (uint32_t n = 0; n < 128; ++n)
|
|
instruments.noteOff(y, n);
|
|
instruments.setReloading(y, true);
|
|
samePathSilenced.push_back(y);
|
|
std::cerr << "[dbg] openGUI: silenced same-plugin ch=" << y
|
|
<< " during attach ch=" << guiCh << std::endl;
|
|
}
|
|
}
|
|
}
|
|
instruments.setReloading(guiCh, true);
|
|
// Register attach-in-flight: a same-path close job must not
|
|
// unmute its channel while createView runs here (it stays
|
|
// silenced; this job's restore unmutes it at the end).
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_attachMutex);
|
|
if (!gp.empty()) g_attachPaths.push_back(gp);
|
|
}
|
|
// Attach now happens on the MAIN thread (kMsgAttach): the
|
|
// editor children created inside view->attached() must be
|
|
// owned by the same thread as the parent window (main pump).
|
|
// Attaching on the worker made children worker-owned while
|
|
// the parent was main-owned -> cross-thread parent/child ->
|
|
// USER32 0xc000041d (verified with gui_probe). Only
|
|
// reloadForGUI stays here: it may createInstance (~2s on the
|
|
// hasAttachedOnce reopen path) and must run on this worker
|
|
// COM STA apartment.
|
|
bool reloadOk = false;
|
|
{
|
|
// Serialize ALL plugin-DLL entry (createInstance AND
|
|
// createView) with other loads: two threads inside one
|
|
// DLL (Nexus) crash.
|
|
std::lock_guard<std::mutex> lg(g_loadMutex);
|
|
reloadOk = inst->reloadForGUI();
|
|
}
|
|
#ifdef _WIN32
|
|
if (!reloadOk || !IsWindow((HWND)hwnd)) {
|
|
#else
|
|
if (!reloadOk) {
|
|
#endif
|
|
// No editor running -> safe to process again.
|
|
for (uint32_t y : samePathSilenced) unmute_if_not_closing(y, "openGUI");
|
|
instruments.setReloading(guiCh, false);
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_attachMutex);
|
|
if (!gp.empty()) {
|
|
auto it = std::find(g_attachPaths.begin(), g_attachPaths.end(), gp);
|
|
if (it != g_attachPaths.end()) g_attachPaths.erase(it);
|
|
}
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
g_guiAttachPending[guiCh] = false;
|
|
}
|
|
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
|
|
<< " plugin=" << arg2 << std::endl;
|
|
#ifdef _WIN32
|
|
// Attach khong co view (VD: SF2/SFZ hoac plugin loi).
|
|
// Dong ngay cua so vo nghia de khong con window treo.
|
|
PostMessageA((HWND)hwnd, WM_CLOSE, 0, 0);
|
|
#endif
|
|
} else {
|
|
#ifdef _WIN32
|
|
// Stash the same-path silenced set for the main-thread
|
|
// kMsgAttach handler, then ask it to attach (message, not
|
|
// direct call: the window proc runs on the main pump).
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
g_attachSilenced[guiCh] = samePathSilenced;
|
|
}
|
|
PostMessageW((HWND)hwnd, kMsgAttach, (WPARAM)guiCh, 0);
|
|
#else
|
|
// No native window on non-Windows: direct attach (null
|
|
// parent) fails -> restore the silenced channels.
|
|
if (!inst->attachView(hwnd)) {
|
|
for (uint32_t y : samePathSilenced) unmute_if_not_closing(y, "openGUI");
|
|
instruments.setReloading(guiCh, false);
|
|
std::lock_guard<std::mutex> lk(g_attachMutex);
|
|
if (!gp.empty()) {
|
|
auto it = std::find(g_attachPaths.begin(), g_attachPaths.end(), gp);
|
|
if (it != g_attachPaths.end()) g_attachPaths.erase(it);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
});
|
|
};
|
|
|
|
// 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;
|
|
int pumpedMain = 0;
|
|
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessageW(&msg);
|
|
++pumpedMain;
|
|
}
|
|
#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 (!workers[ch]) workers[ch] = std::make_unique<ChannelWorker>();
|
|
workers[ch]->post([&instruments, t, ch, path, sampleRate, block]() {
|
|
// CRASH FIX: this job enters the plugin DLL (createInstance
|
|
// in loadPlugin, old-instance terminate on replace) while
|
|
// the audio loop may process() another instance of the SAME
|
|
// DLL — 2 threads in one DLL crashes Nexus (observed with
|
|
// session-restore instances). Silence this channel (its old
|
|
// instance is being torn down / replaced) and every
|
|
// same-plugin channel for the whole job. Old path covers
|
|
// the old-instance teardown; the new path (computed BEFORE
|
|
// assign — see spNew below) covers the freshly created
|
|
// instance. CRASH FIX (SF→VSTi while playing): the new
|
|
// path must be silenced BEFORE assign() — assign() itself
|
|
// runs createInstance (loadPlugin) on this worker, and a
|
|
// restored channel on ANOTHER track holding the SAME DLL
|
|
// kept process()ing on the audio loop during that window
|
|
// (old code silenced spNew only AFTER assign → crash).
|
|
instruments.setReloading(ch, true);
|
|
std::vector<uint32_t> spOld = same_plugin_channels(ch);
|
|
for (uint32_t y : spOld) {
|
|
g_engine->setReloading(y, true);
|
|
std::cerr << "[dbg] load: silenced same-plugin ch=" << y
|
|
<< " during load ch=" << ch << std::endl;
|
|
}
|
|
// Channels (other than ch) already assigned the NEW path —
|
|
// silence BEFORE createInstance enters that DLL.
|
|
std::vector<uint32_t> spNew;
|
|
if (t == InstrumentType::VST3) {
|
|
std::string np = lower_path_str(path);
|
|
if (!np.empty()) {
|
|
for (uint32_t y = 0; y < 16; ++y) {
|
|
if (y == ch) continue;
|
|
if (!g_engine->get(y)) continue;
|
|
if (std::find(spOld.begin(), spOld.end(), y) != spOld.end()) continue;
|
|
if (lower_plugin_path(y) != np) continue;
|
|
g_engine->setReloading(y, true);
|
|
spNew.push_back(y);
|
|
std::cerr << "[dbg] load: silenced same-plugin (new path) ch=" << y
|
|
<< " during load ch=" << ch << std::endl;
|
|
}
|
|
}
|
|
}
|
|
#ifdef _WIN32
|
|
// DONG cua so editor dang mo cua channel TRUOC khi assign():
|
|
// thay the inst (VST3 -> SF2/inst khac) ma editor con song ->
|
|
// old inst destructor goi view->removed() tren HWND con hoat
|
|
// dong -> plugin block -> treo bridge.
|
|
// QUAN TRONG: window duoc tao tren MAIN thread (handleOpenGui
|
|
// goi create_native_vst_window o main loop) — DestroyWindow tu
|
|
// worker thread la cross-thread (MSDN cam), gay crash USER32
|
|
// 0xc000041d. Post WM_CLOSE -> main pump destroy window tren
|
|
// DUNG thread so huu no. Registry da erase o tren nen WM_DESTROY
|
|
// khong goi closeGUI tren inst cu (inst moi chua co view).
|
|
HWND hToHide = nullptr;
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
auto git = g_guiWindows.find(ch);
|
|
if (git != g_guiWindows.end()) hToHide = (HWND)git->second;
|
|
}
|
|
// CRASH FIX: detach view TRUOC khi thay inst. Plugin editor
|
|
// children thuoc worker thread nay (attachView chay o day);
|
|
// khong destroy parent window (main-owned) khi view con song
|
|
// -> DestroyWindow cross-thread -> USER32 0xc000041d. An
|
|
// window, giu trong registry de reuse (reopen ShowWindow lai).
|
|
if (hToHide && IsWindow(hToHide)) {
|
|
// CRASH FIX (0xc000041d USER32 while playing): silence
|
|
// the channel while the old editor detaches — view->removed()
|
|
// on this worker would race processor->process() on the
|
|
// audio loop (same plugin instance).
|
|
instruments.setReloading(ch, true);
|
|
if (auto* i = instruments.get(ch)) i->closeGUI();
|
|
// The editor goes away with this load: drop the global
|
|
// editor-open state (path stored at attach -- still the
|
|
// old path before assign() below).
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_editorMutex);
|
|
if (g_editorOpen[ch]) {
|
|
g_editorOpen[ch] = false;
|
|
std::string pth = g_editorOpenPath[ch];
|
|
g_editorOpenPath[ch].clear();
|
|
if (!pth.empty()) {
|
|
auto it = g_editorPathCount.find(pth);
|
|
if (it != g_editorPathCount.end() && --it->second <= 0)
|
|
g_editorPathCount.erase(it);
|
|
}
|
|
}
|
|
}
|
|
// Destroy the old editor's child windows (worker-owned,
|
|
// created by attachView here) so they stop pumping;
|
|
// the parent window is hidden and kept for reuse.
|
|
// See post_close_gui for the same pattern.
|
|
// CRASH FIX (0xc000041d): the editor children are
|
|
// MAIN-thread-owned now (attachView runs on the main
|
|
// pump) - DestroyWindow from this worker is cross-
|
|
// thread and crashes USER32. Ask the main thread to
|
|
// destroy them (kMsgDestroy) and wait for the ack.
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
g_editorDestroyAck[ch] = false;
|
|
}
|
|
PostMessageW(hToHide, kMsgDestroy, (WPARAM)ch, 0);
|
|
bool destroyAcked = false;
|
|
for (int i = 0; i < 500; ++i) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
if (g_editorDestroyAck[ch]) { destroyAcked = true; break; }
|
|
}
|
|
Sleep(10);
|
|
}
|
|
if (!destroyAcked)
|
|
std::cerr << "[dbg] load: editor destroy not acked ch=" << ch << std::endl;
|
|
|
|
|
|
}
|
|
#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) {
|
|
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;
|
|
}
|
|
// Restore. Failed load keeps the OLD instance — unmute it
|
|
// back (silenced at job start); a successful assign leaves
|
|
// the fresh instance unmuted by construction.
|
|
if (!ok) g_engine->setReloading(ch, false);
|
|
for (uint32_t y : spOld) unmute_if_not_closing(y, "load");
|
|
for (uint32_t y : spNew) unmute_if_not_closing(y, "load");
|
|
});
|
|
} else if (c.type == 1) { // PANIC
|
|
instruments.allNotesOff();
|
|
std::cout << "[NativeBridge] PANIC — all notes off" << std::endl;
|
|
} else if (c.type == 3) { // TRANSPORT (A13)
|
|
if (c.arg0 == 0) { // STOP → flush every note immediately
|
|
transportStopped = true;
|
|
// Release sustain pedal TRUOC (CC64=0) — nhieu VSTi giu note
|
|
// khi pedal con down -> note-off cua allNotesOff bi bo qua
|
|
// -> am treo loop (V8 bug 3).
|
|
for (uint32_t ch = 0; ch < 16; ++ch)
|
|
instruments.controlChange(ch, 64, 0); // manager drops unassigned/reloading
|
|
instruments.allNotesOff();
|
|
std::cout << "[NativeBridge] transport STOP — all notes off" << std::endl;
|
|
} else if (c.arg0 == 1) { // PLAY
|
|
transportStopped = false;
|
|
playheadSamples = c.arg1;
|
|
std::cout << "[NativeBridge] transport PLAY playhead=" << playheadSamples << std::endl;
|
|
} 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
|
|
uint32_t guiCh = c.channel;
|
|
if (guiCh >= 16) guiCh = 0;
|
|
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];
|
|
for (uint32_t i = 0; i < nEvents; ++i) evts[i] = shmIPC->midiQueue[i];
|
|
shmIPC->midiQueueCount = 0;
|
|
|
|
// A11 sample-accurate: sort by sampleOffset, dispatch at each boundary,
|
|
// render the sub-block before the boundary. Events with offset 0 (live
|
|
// keyboard, current JS) are dispatched first and shape the whole block.
|
|
std::stable_sort(evts, evts + nEvents,
|
|
[](const SharedAudioBufferIPC::MidiEventIPC& a,
|
|
const SharedAudioBufferIPC::MidiEventIPC& b) {
|
|
return a.sampleOffset < b.sampleOffset;
|
|
});
|
|
uint32_t cursor = 0;
|
|
uint32_t ei = 0;
|
|
while (ei < nEvents && evts[ei].sampleOffset <= cursor) { dispatch(evts[ei]); ++ei; }
|
|
for (; ei < nEvents; ++ei) {
|
|
uint32_t off = evts[ei].sampleOffset < block ? evts[ei].sampleOffset : block;
|
|
if (off > cursor) { renderSegment(cursor, off); cursor = off; }
|
|
dispatch(evts[ei]);
|
|
}
|
|
if (cursor < block) renderSegment(cursor, block);
|
|
shmIPC->bridgeWriteIndex++;
|
|
|
|
// D. Parent died / window closed -> exit (no orphan process)
|
|
if (parentPid != 0 && !parent_alive(parentPid)) {
|
|
std::cout << "[NativeBridge] parent gone — exiting." << std::endl;
|
|
break;
|
|
}
|
|
|
|
// E. Synchronize with real-time audio playback
|
|
blockCount++;
|
|
auto targetTime = startTime + std::chrono::microseconds(static_cast<int64_t>(blockCount * blockDurationMs * 1000.0));
|
|
auto now = std::chrono::steady_clock::now();
|
|
if (now < targetTime) {
|
|
auto diff = std::chrono::duration_cast<std::chrono::microseconds>(targetTime - now).count();
|
|
if (diff > 1000) {
|
|
sleep_ms(diff / 1000);
|
|
}
|
|
while (std::chrono::steady_clock::now() < targetTime) {
|
|
std::this_thread::yield();
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
UnmapViewOfFile(shmIPC);
|
|
CloseHandle(hMapFile);
|
|
timeEndPeriod(1);
|
|
#else
|
|
std::free(shmIPC);
|
|
#endif
|
|
return 0;
|
|
}
|