e7e974b3e0
- plugin_host_main.cpp: chặn WM_CLOSE (đóng GUI nhưng giữ window sống, hide thay vì destroy), WM_DESTROY reset hwnd; dùng g_hostHwnd toàn cục thay hwnd local để control OPEN_GUI/CLOSE_GUI tái sử dụng đúng window. - main.cpp: bỏ qua dedupe hasAttachedView cho instrument child-own (SandboxVst2Host) — bridge guiOpen_ stale sau khi child tự đóng nên OPEN_GUI lặp lại phải forward control 4; child tự chặn double-open. - Verify: WM_CLOSE -> window ẩn còn sống; OPEN_GUI lần 2 -> window hiện lại, âm ch1 vẫn kêu (peak 0.15), không câm.
1854 lines
92 KiB
C++
1854 lines
92 KiB
C++
// 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"
|
|
#include "Vst2Instrument.h"
|
|
#include "StateStore.h"
|
|
#include "RenderJob.h"
|
|
#include "RenderFxJob.h"
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#include <imm.h>
|
|
#include <mmsystem.h>
|
|
#include <process.h>
|
|
#include <cstdlib>
|
|
#include <cstdio>
|
|
#include <dbghelp.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 <set>
|
|
#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
|
|
// IME recursion fix (Nexus 0xC00000FD -> USER32 0xC000041D): the plugin's
|
|
// wndproc calls ImmIsUIMessageW for every message and forwards WM_IME_* to the
|
|
// IME window; with a live IMC on the editor window that bounces back to the
|
|
// same hwnd -> infinite SendMessageW recursion -> stack overflow. Removing the
|
|
// IMC (ImmAssociateContext NULL) stops the forwarding.
|
|
static void disable_ime_contexts(HWND w) {
|
|
if (w && IsWindow(w)) {
|
|
char cls[64] = {0};
|
|
GetClassNameA(w, cls, 63);
|
|
ImmAssociateContext(w, nullptr);
|
|
for (HWND c = GetWindow(w, GW_CHILD); c; c = GetWindow(c, GW_HWNDNEXT)) {
|
|
char cls2[64] = {0};
|
|
GetClassNameA(c, cls2, 63);
|
|
ImmAssociateContext(c, nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
// G1.3: bo pump gate cu (g_juceOwnerTids / g_ownerTid /
|
|
// is_teardown_window): teardown (close/LOAD job) va message pump cung chay tren
|
|
// 1 thread (UiThread) � job chay thi pump dung, job xong thi editor window da
|
|
// destroy nen USER32 tu huy message toi window chet. Khong con "2 thread trong
|
|
// 1 DLL" khi teardown (multi-worker cu).
|
|
|
|
// CBT hook: strip the IMC the moment any JUCE_* window is born (JUCE message
|
|
// window AND editor child) — the post-attach disable_ime_contexts runs too
|
|
// late; the recursion can start inside attachView (0xC00000FD observed, run
|
|
// 9). Runs on the window's creating thread, so the association is legal.
|
|
static HHOOK g_cbtHook = nullptr;
|
|
static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam) {
|
|
if (nCode == HCBT_CREATEWND) {
|
|
// Class name via GetClassNameA, NOT lpszClass: JUCE creates windows
|
|
// with MAKEINTATOM class names (HIWORD(lpszClass)==0), and ANSI-created
|
|
// windows deliver a CREATESTRUCTA — lpszClass is unusable in both cases.
|
|
HWND w = (HWND)wParam;
|
|
char cls[64] = {0};
|
|
if (GetClassNameA(w, cls, 63) > 0 && std::strncmp(cls, "JUCE_", 5) == 0) {
|
|
ImmAssociateContext(w, nullptr);
|
|
|
|
}
|
|
}
|
|
return CallNextHookEx(g_cbtHook, nCode, wParam, lParam);
|
|
}
|
|
#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 void post_resize_view(uint32_t ch, int w, int h); // defined after ChannelWorker
|
|
|
|
static InstrumentEngineManager* g_engine = nullptr; // declared unconditionally (Linux --render build needs the symbol)
|
|
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 ChannelWorker* g_uiWorker = nullptr;
|
|
static SharedAudioBufferIPC* g_shmIPC = nullptr; // A14: midi-out hook writes SHM
|
|
// VST2 (DUNE 3) thread contract: entry + init dispatcher + processReplacing
|
|
// must share ONE thread. processReplacing runs on the audio (main) thread,
|
|
// so VST2 loads (LoadLibrary + VSTPluginMain + init dispatcher) are deferred
|
|
// to main too — loading on the uiWorker made DUNE write ~2M floats past n
|
|
// into in/out buffers and corrupt the heap (0xC0000374). Other instrument
|
|
// types (VST3 COM STA apartment) keep their worker. Drained each loop before
|
|
// render; a VST2 load stalls the audio thread for its duration (DUNE loads
|
|
// well under the 3s watchdog kill).
|
|
static std::mutex g_mainDeferredMutex;
|
|
static std::vector<std::function<void()>> g_mainDeferred;
|
|
static void runOnMain(std::function<void()> job) {
|
|
std::lock_guard<std::mutex> lock(g_mainDeferredMutex);
|
|
g_mainDeferred.push_back(std::move(job));
|
|
}
|
|
// T2.4: realtime master FX chain (live audio through VST3 FX). Created in
|
|
// main() once sampleRate is known; fed via control queue type 6.
|
|
static std::unique_ptr<RealtimeFxChain> g_fxChain;
|
|
|
|
// 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 };
|
|
|
|
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 == WM_ERASEBKGND) {
|
|
return 1; // Do not paint background to prevent white flashes
|
|
} else if (uMsg == WM_PAINT) {
|
|
PAINTSTRUCT ps;
|
|
HDC hdc = BeginPaint(hwnd, &ps);
|
|
EndPaint(hwnd, &ps);
|
|
return 0;
|
|
} else if (uMsg == WM_SIZE) {
|
|
int w = LOWORD(lParam);
|
|
int h = HIWORD(lParam);
|
|
HWND child = FindWindowExA(hwnd, nullptr, nullptr, nullptr);
|
|
if (child) {
|
|
MoveWindow(child, 0, 0, w, h, TRUE);
|
|
}
|
|
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) {
|
|
post_resize_view(ch, w, h);
|
|
}
|
|
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.
|
|
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;
|
|
// G2.1: state persistence. g_stateDirty is set by load/unload/transport/periodic
|
|
// marks; the main loop debounces and posts one save job to the UiThread, so disk
|
|
// IO never runs on the audio loop. Playing is tracked separately from
|
|
// transportStopped because the audio loop must not write files.
|
|
static std::atomic<bool> g_stateDirty{false};
|
|
static std::atomic<bool> g_transportPlaying{false};
|
|
static std::mutex g_lastSnapMutex;
|
|
static BridgeStateSnapshot g_lastSnap;
|
|
// Same instrument set and transport -> nothing to write. Playhead is excluded:
|
|
// comparing it would rewrite the file every ~2s while playing.
|
|
static bool snapshotsEqual(const BridgeStateSnapshot& a, const BridgeStateSnapshot& b) {
|
|
if (a.playing != b.playing || a.instruments.size() != b.instruments.size()) return false;
|
|
for (size_t i = 0; i < a.instruments.size(); ++i) {
|
|
const auto& x = a.instruments[i];
|
|
const auto& y = b.instruments[i];
|
|
if (x.channel != y.channel || x.type != y.type || x.path != y.path ||
|
|
x.presetBase64 != y.presetBase64 || x.bank != y.bank || x.program != y.program)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// 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 std::once_flag registerWindowOnce;
|
|
static void* create_native_vst_window(const char* title) {
|
|
#ifdef _WIN32
|
|
static const char* kWndClass = "SonicForge_Native_VST3_Class";
|
|
std::call_once(registerWindowOnce, []() {
|
|
WNDCLASSA wc = {};
|
|
wc.lpfnWndProc = VstWindowProc;
|
|
wc.hInstance = GetModuleHandleA(nullptr);
|
|
wc.lpszClassName = kWndClass;
|
|
RegisterClassA(&wc);
|
|
});
|
|
HWND hwnd = CreateWindowExA(0, kWndClass, title ? title : "VST",
|
|
WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
|
|
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
|
|
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
|
|
return (void*)hwnd;
|
|
#else
|
|
(void)title;
|
|
return nullptr;
|
|
#endif
|
|
}
|
|
|
|
// G1.2: single persistent UiThread — ONE thread owns the COM STA apartment for
|
|
// ALL instruments for the whole bridge 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() {
|
|
#ifdef _WIN32
|
|
jobEvent_ = CreateEvent(nullptr, FALSE, FALSE, nullptr);
|
|
#endif
|
|
th_ = std::thread([this] {
|
|
#ifdef _WIN32
|
|
OleInitialize(nullptr);
|
|
OleInitialize(nullptr);
|
|
// Default IMC = none on this thread: new editor windows get
|
|
// no IME context (see disable_ime_contexts).
|
|
ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT);
|
|
// CBT hook on this worker: strip IMC on JUCE_* windows at birth.
|
|
// Global hooks fail from an exe module — install per thread, and
|
|
// this thread creates the plugin editor windows.
|
|
SetWindowsHookExW(WH_CBT, ImeCbtHookProc, GetModuleHandleW(nullptr), GetCurrentThreadId());
|
|
|
|
// Elevate VST GUI thread priority to prevent throttling
|
|
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);
|
|
#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;
|
|
}
|
|
lk.unlock();
|
|
MSG msg;
|
|
bool hadMessages = false;
|
|
// Cap the idle pump per iteration to avoid starving the job queue,
|
|
// but pump up to 128 messages for high GUI responsiveness.
|
|
for (int pumped = 0; pumped < 128; ++pumped) {
|
|
if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break;
|
|
hadMessages = true;
|
|
// G1.3: bo pump gate cu � teardown (close/LOAD job)
|
|
// va pump cung 1 thread (UiThread): job chay thi pump dung;
|
|
// het job thi editor window da destroy -> message chet bi
|
|
// USER32 huy tu dong. Khong can drop theo close_in_flight.
|
|
|
|
TranslateMessage(&msg);
|
|
// CRASH FIX (0xc000041d STATUS_FATAL_USER_CALLBACK_EXCEPTION):
|
|
// a plugin window proc (Nexus throws nlohmann::json::out_of_range
|
|
// internally) that raises a C++ exception would otherwise escape
|
|
// DispatchMessageW and std::terminate this worker thread. Catch
|
|
// it here — the editor may glitch, but the bridge survives.
|
|
try {
|
|
DispatchMessageW(&msg);
|
|
} catch (...) {
|
|
std::cerr << "[NativeBridge] worker pump EXCEPTION — plugin window proc threw" << std::endl;
|
|
}
|
|
}
|
|
// VST2 editor idle tick: effEditIdle (repaints / size change
|
|
// notifications). Editors were opened on this thread; the
|
|
// engine's default guiIdle() is a no-op for other types.
|
|
if (g_engine) {
|
|
try {
|
|
g_engine->guiIdleAll();
|
|
} catch (...) {
|
|
std::cerr << "[NativeBridge] worker guiIdle EXCEPTION — plugin threw" << std::endl;
|
|
}
|
|
}
|
|
lk.lock();
|
|
if (!hadMessages && jobs_.empty()) {
|
|
#ifdef _WIN32
|
|
if (jobEvent_) {
|
|
lk.unlock();
|
|
MsgWaitForMultipleObjects(1, &jobEvent_, FALSE, 10, QS_ALLINPUT);
|
|
lk.lock();
|
|
} else {
|
|
cv_.wait_for(lk, std::chrono::milliseconds(2));
|
|
}
|
|
#else
|
|
cv_.wait_for(lk, std::chrono::milliseconds(2));
|
|
#endif
|
|
}
|
|
}
|
|
#ifdef _WIN32
|
|
OleUninitialize();
|
|
OleUninitialize();
|
|
#endif
|
|
});
|
|
}
|
|
~ChannelWorker() {
|
|
{
|
|
std::lock_guard<std::mutex> lk(mu_);
|
|
stop_ = true;
|
|
}
|
|
#ifdef _WIN32
|
|
if (jobEvent_) {
|
|
SetEvent(jobEvent_);
|
|
}
|
|
#endif
|
|
cv_.notify_all();
|
|
if (th_.joinable()) th_.join();
|
|
#ifdef _WIN32
|
|
if (jobEvent_) {
|
|
CloseHandle(jobEvent_);
|
|
jobEvent_ = nullptr;
|
|
}
|
|
#endif
|
|
}
|
|
void post(std::function<void()> job) {
|
|
{
|
|
std::lock_guard<std::mutex> lk(mu_);
|
|
jobs_.push_back(std::move(job));
|
|
}
|
|
#ifdef _WIN32
|
|
if (jobEvent_) {
|
|
SetEvent(jobEvent_);
|
|
}
|
|
#endif
|
|
cv_.notify_all();
|
|
}
|
|
|
|
|
|
private:
|
|
std::thread th_;
|
|
std::mutex mu_;
|
|
std::condition_variable cv_;
|
|
std::deque<std::function<void()>> jobs_;
|
|
bool stop_ = false;
|
|
#ifdef _WIN32
|
|
HANDLE jobEvent_ = nullptr;
|
|
#endif
|
|
};
|
|
|
|
// resizeView() must run on the channel worker (the view was attached in its
|
|
// COM STA apartment), never on the main pump thread.
|
|
static void post_resize_view(uint32_t ch, int w, int h) {
|
|
if (g_uiWorker) {
|
|
g_uiWorker->post([ch, w, h]() {
|
|
if (!g_engine) return;
|
|
auto* i = g_engine->get(ch);
|
|
if (i) i->resizeView(w, h);
|
|
});
|
|
}
|
|
}
|
|
|
|
// --- 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];
|
|
}
|
|
|
|
|
|
|
|
// G1.3: 1 UiThread � editor windows tao trong attach job (UiThread), destroy
|
|
// chi goi tu UiThread job (close_editor_now / LOAD) -> ownerTid luon == current
|
|
// tid -> destroy truc tiep. Neu owner khac thread (khong con xay ra) -> bo
|
|
// (leak > crash): cross-thread DestroyWindow khong an toan.
|
|
static void destroy_window_on_owner(HWND hwnd) {
|
|
if (!hwnd || !IsWindow(hwnd)) return;
|
|
DWORD ownerTid = GetWindowThreadProcessId(hwnd, nullptr);
|
|
DWORD curTid = GetCurrentThreadId();
|
|
if (ownerTid == 0 || ownerTid == curTid) {
|
|
if (IsWindow(hwnd)) DestroyWindow(hwnd);
|
|
return;
|
|
}
|
|
std::cerr << "[NativeBridge] destroy_window_on_owner: owner tid=" << ownerTid
|
|
<< " != current " << curTid << " - leaving window " << hwnd << std::endl;
|
|
}
|
|
|
|
|
|
// 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) {
|
|
(void)why;
|
|
if (close_in_flight(y)) {
|
|
return;
|
|
}
|
|
g_engine->setReloading(y, false);
|
|
}
|
|
|
|
// closeGUI() MUST run on the UiThread (its COM STA apartment) — the view was
|
|
// attached there. Calling view->removed() from another thread is a
|
|
// cross-apartment COM call that corrupts the plugin; Nexus then hangs on the
|
|
// NEXT view->attached(). G1.2: 1 UiThread chung — close chay synchronous tren
|
|
// job; goi truc tiep (Option B trong attach job) hoac qua post_close_gui
|
|
// (WM_CLOSE/WM_DESTROY).
|
|
static void close_editor_now(uint32_t ch, HWND hwnd) {
|
|
// Guard window-replaced race: neu OPEN_GUI moi da dung window khac
|
|
// (hoac da xoa), khong detach view cua window moi.
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_guiMutex);
|
|
auto it = g_guiWindows.find(ch);
|
|
if (it == g_guiWindows.end() || it->second != hwnd) {
|
|
return;
|
|
}
|
|
}
|
|
// Silence first: the audio loop must not process() the instance
|
|
// while we tear its editor down on this thread.
|
|
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;
|
|
}
|
|
if (auto* i = g_engine->get(ch)) {
|
|
// closeGUI() nulls the view WITHOUT view->removed() (Nexus
|
|
// removed() deadlocks this worker - modal wait for a message
|
|
// only its own pump can dispatch, but it is inside removed()).
|
|
try {
|
|
i->closeGUI();
|
|
} catch (...) {
|
|
std::cerr << "[NativeBridge] closeGUI EXCEPTION ch=" << ch
|
|
<< " — plugin threw" << std::endl;
|
|
}
|
|
// CRASH FIX: the editor was dropped WITHOUT view->removed() —
|
|
// the instance's editor state is dangling, so the audio loop
|
|
// must NOT process() it. Rebuild a fresh instance NOW on this
|
|
// worker (its COM STA apartment is alive); reload() ==
|
|
// terminate + loadPlugin (createInstance). Same-plugin
|
|
// channels are silenced so no second thread enters this DLL
|
|
// during createInstance (2 threads in one DLL -> Nexus exits
|
|
// silently, observed in probes). Serialized with other loads
|
|
// by g_loadMutex.
|
|
std::vector<uint32_t> sp = same_plugin_channels(ch);
|
|
for (uint32_t y : sp) {
|
|
g_engine->setReloading(y, true);
|
|
}
|
|
#ifdef _WIN32
|
|
// CRASH FIX (0xc000041d): destroy the editor's child windows
|
|
// on the thread that OWNS them (the plugin's JUCE
|
|
// MessageManager thread - a single worker), never
|
|
// cross-thread. Wait for each destroy to finish (owner worker
|
|
// serializes it with its pump) BEFORE reload() - otherwise
|
|
// reload's createInstance enters the DLL while the owner
|
|
// thread is still inside the plugin wndproc (2 threads in one
|
|
// DLL -> Nexus crash). Kept OUTSIDE g_loadMutex: the wait
|
|
// must not block other loads (the owner worker may itself be
|
|
// waiting on that lock).
|
|
disable_ime_contexts(hwnd);
|
|
if (hwnd && IsWindow(hwnd)) {
|
|
while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) {
|
|
destroy_window_on_owner(c);
|
|
}
|
|
}
|
|
#endif
|
|
{
|
|
// reload() = terminate + loadPlugin (createInstance) —
|
|
// serialized with all other plugin-DLL entry points.
|
|
// Nexus may throw a C++ exception here too; catch it,
|
|
// treat as failed reload, and let the restore below
|
|
// unmute the channel (instance may be broken; next
|
|
// load/assign rebuilds it).
|
|
std::lock_guard<std::mutex> lg(g_loadMutex);
|
|
try {
|
|
// G0.4: reload() terminates + recreates the instance;
|
|
// capture the preset first, restore it after, so the
|
|
// sound survives a GUI close (reopen keeps it too).
|
|
i->captureState();
|
|
i->reload();
|
|
i->restoreState();
|
|
} catch (...) {
|
|
std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch
|
|
<< " — plugin threw (createInstance)" << std::endl;
|
|
}
|
|
}
|
|
// 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's
|
|
// 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) {
|
|
for (uint32_t y : sp) unmute_if_not_closing(y, "closeGUI");
|
|
g_engine->setReloading(ch, false);
|
|
}
|
|
} else {
|
|
// Instrument already gone (unloaded) — nothing to rebuild.
|
|
g_engine->setReloading(ch, false);
|
|
std::lock_guard<std::mutex> lk(g_attachMutex);
|
|
g_closeInFlight[ch] = false;
|
|
}
|
|
}
|
|
|
|
static void post_close_gui(uint32_t ch, HWND hwnd) {
|
|
if (g_uiWorker) {
|
|
g_uiWorker->post([ch, hwnd]() { close_editor_now(ch, hwnd); });
|
|
}
|
|
}
|
|
|
|
|
|
// --- crash minidump handler (temp diagnostics) -------------------------------
|
|
// WER LocalDumps produces NO dump for 0xC0000374 (heap corruption) — write our
|
|
// own minidump via dbghelp.dll when an unhandled exception reaches this filter.
|
|
#ifdef _WIN32
|
|
static LONG WINAPI CrashDumpHandler(EXCEPTION_POINTERS* ep) {
|
|
static LONG once = 0;
|
|
if (InterlockedCompareExchange(&once, 1, 0)) return EXCEPTION_CONTINUE_SEARCH;
|
|
PEXCEPTION_RECORD er = ep ? ep->ExceptionRecord : nullptr;
|
|
std::cerr << "[CRASH] exception code=0x" << std::hex
|
|
<< (er ? er->ExceptionCode : 0) << " addr=0x"
|
|
<< (er ? (void*)er->ExceptionAddress : nullptr) << std::dec << std::endl;
|
|
if (er) {
|
|
HMODULE hMod = nullptr;
|
|
char modPath[MAX_PATH] = {0};
|
|
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
|
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
|
(LPCSTR)er->ExceptionAddress, &hMod) && hMod) {
|
|
GetModuleFileNameA(hMod, modPath, MAX_PATH);
|
|
std::cerr << "[CRASH] faulting module: " << modPath << std::endl;
|
|
}
|
|
}
|
|
const char* dumpDir = "C:/Users/locpham/AppData/Local/Temp/sf_dumps";
|
|
CreateDirectoryA(dumpDir, nullptr);
|
|
char dumpPath[MAX_PATH];
|
|
snprintf(dumpPath, sizeof(dumpPath), "%s/bridge_crash_%lu.dmp", dumpDir, GetCurrentProcessId());
|
|
HMODULE hDbg = LoadLibraryA("dbghelp.dll");
|
|
if (hDbg) {
|
|
typedef BOOL(WINAPI* MiniDumpWriteDumpFn)(HANDLE, DWORD, HANDLE, MINIDUMP_TYPE,
|
|
PMINIDUMP_EXCEPTION_INFORMATION, PMINIDUMP_USER_STREAM_INFORMATION,
|
|
PMINIDUMP_CALLBACK_INFORMATION);
|
|
auto pMiniDumpWriteDump = (MiniDumpWriteDumpFn)GetProcAddress(hDbg, "MiniDumpWriteDump");
|
|
if (pMiniDumpWriteDump) {
|
|
HANDLE hFile = CreateFileA(dumpPath, GENERIC_WRITE, 0, nullptr,
|
|
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (hFile != INVALID_HANDLE_VALUE) {
|
|
MINIDUMP_EXCEPTION_INFORMATION mei = {};
|
|
mei.ThreadId = GetCurrentThreadId();
|
|
mei.ExceptionPointers = ep;
|
|
mei.ClientPointers = TRUE;
|
|
BOOL ok = pMiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
|
|
hFile, MiniDumpNormal, &mei, nullptr, nullptr);
|
|
CloseHandle(hFile);
|
|
std::cerr << "[CRASH] minidump " << (ok ? "written: " : "FAILED: ") << dumpPath << std::endl;
|
|
} else {
|
|
std::cerr << "[CRASH] cannot open dump file" << std::endl;
|
|
}
|
|
} else {
|
|
std::cerr << "[CRASH] MiniDumpWriteDump not found in dbghelp.dll" << std::endl;
|
|
}
|
|
} else {
|
|
std::cerr << "[CRASH] cannot load dbghelp.dll" << std::endl;
|
|
}
|
|
return EXCEPTION_CONTINUE_SEARCH; // let WER proceed, process dies normally
|
|
}
|
|
#endif
|
|
|
|
|
|
// Vectored handler: catches first-chance exceptions INCLUDING heap-corruption
|
|
// raises that never reach SetUnhandledExceptionFilter (0xC0000374 observed to
|
|
// bypass it on this machine). Writes the same minidump then continues.
|
|
#ifdef _WIN32
|
|
|
|
// Unwind the LIVE stack from the exception context (MiniDumpWriteDump captures
|
|
// the context at dump time, not at raise time — by then the handler's own
|
|
// frames replace the real ones). Print return-address candidates that land in
|
|
// loaded modules, lowest stack address first.
|
|
#ifdef _WIN32
|
|
static void DumpStackTraceFromContext(const CONTEXT* ctx) {
|
|
if (!ctx) return;
|
|
std::cerr << "[VCRASH] stack trace (raw scan):" << std::endl;
|
|
const unsigned char* sp = (const unsigned char*)ctx->Rsp;
|
|
for (int i = 0; i < 512; i++) {
|
|
uintptr_t val = 0;
|
|
memcpy(&val, sp + i * 8, sizeof(val)); // stack is readable in a handler
|
|
HMODULE h = nullptr;
|
|
char m[MAX_PATH] = {0};
|
|
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
|
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
|
(LPCSTR)val, &h) && h && GetModuleFileNameA(h, m, MAX_PATH)) {
|
|
const char* base = strrchr(m, '\\');
|
|
base = base ? base + 1 : m;
|
|
std::cerr << " +" << i * 8 << " " << (void*)val << " " << base
|
|
<< "+0x" << std::hex << (val - (uintptr_t)h) << std::dec << std::endl;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static LONG WINAPI VectoredCrashHandler(PEXCEPTION_POINTERS ep) {
|
|
PEXCEPTION_RECORD er = ep ? ep->ExceptionRecord : nullptr;
|
|
DWORD code = er ? er->ExceptionCode : 0;
|
|
if (code != 0xC0000374 && code != 0xC0000005 && code != 0xC0000409) {
|
|
return EXCEPTION_CONTINUE_SEARCH; // ignore benign first-chance noise
|
|
}
|
|
std::cerr << "[VCRASH] first-chance code=0x" << std::hex << code
|
|
<< " addr=0x" << (er ? (void*)er->ExceptionAddress : nullptr)
|
|
<< std::dec << " tid=" << GetCurrentThreadId() << std::endl;
|
|
DumpStackTraceFromContext(ep ? ep->ContextRecord : nullptr);
|
|
// route into the same dump writer
|
|
return CrashDumpHandler(ep) == EXCEPTION_CONTINUE_SEARCH
|
|
? EXCEPTION_CONTINUE_SEARCH
|
|
: EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
#endif
|
|
|
|
int main(int argc, char* argv[]) {
|
|
SetUnhandledExceptionFilter(CrashDumpHandler);
|
|
AddVectoredExceptionHandler(1, VectoredCrashHandler);
|
|
#ifdef _WIN32
|
|
// Background daemon: detach the console window unless SF_KEEP_CONSOLE is
|
|
// set. Console apps spawned from the GUI (tauri-shell, Popen) flash a
|
|
// console window otherwise. Redirected stdio (pipe to bridge.log / the
|
|
// fx-gui port reader) survives FreeConsole.
|
|
if (!std::getenv("SF_KEEP_CONSOLE")) FreeConsole();
|
|
#endif
|
|
// stderr, not stdout: stdout is reserved for machine-readable output in
|
|
// one-shot modes (--scan JSON, --fx-gui "SF_FXGUI_PORT=...").
|
|
std::cerr << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl;
|
|
|
|
// JBridge per-plugin settings bootstrap: Kong Audio Qin bridges all
|
|
// instances through a SHARED auxhost.exe unless the per-plugin settings
|
|
// file disables "run in existing auxhost"; the shared auxhost then caps
|
|
// at 2 live instances (3rd floods ML_RELEASE_ERROR forever). Writing
|
|
// RUN_IN_EXISTING_AUXHOST 0 gives every instance its own auxhost ->
|
|
// unlimited Qin tracks. File is created if missing, never overwritten
|
|
// (user tweaks are respected).
|
|
try {
|
|
const char* appdata = std::getenv("APPDATA");
|
|
if (appdata) {
|
|
const char* dirs[] = {"SettingsFilesFor64BitPlugins", "SettingsFilesFor32BitPlugins"};
|
|
for (const char* d : dirs) {
|
|
std::string dir = std::string(appdata) + "\\jBridge\\" + d;
|
|
std::string file = dir + "\\Qin_RV.jBridge";
|
|
CreateDirectoryA(dir.c_str(), nullptr);
|
|
HANDLE h = CreateFileA(file.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
|
|
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (h == INVALID_HANDLE_VALUE) {
|
|
const char* content =
|
|
"//Qin_RV.jBridge - jBridge per-plugin settings\r\n"
|
|
"RUN_IN_EXISTING_AUXHOST 0\r\n"
|
|
"RUN_IN_SAME_AUXHOST_GROUP 0\r\n"
|
|
"AUXHOST_PRIORITY 0\r\n"
|
|
"USE_SEPARATED_GUI 0\r\n"
|
|
"USE_DEPRECATED_OPCODES 0\r\n";
|
|
HANDLE w = CreateFileA(file.c_str(), GENERIC_WRITE, 0, nullptr,
|
|
CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (w != INVALID_HANDLE_VALUE) {
|
|
DWORD wr = 0;
|
|
WriteFile(w, content, (DWORD)strlen(content), &wr, nullptr);
|
|
CloseHandle(w);
|
|
std::cerr << "[NativeBridge] jBridge settings created: " << file << std::endl;
|
|
}
|
|
} else {
|
|
CloseHandle(h);
|
|
}
|
|
}
|
|
}
|
|
} catch (...) {
|
|
std::cerr << "[NativeBridge] jBridge settings bootstrap failed (non-fatal)" << std::endl;
|
|
}
|
|
#ifdef _WIN32
|
|
// Disable Power Throttling (Efficiency Mode) for this background process
|
|
#ifndef PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION
|
|
#define PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION 0x00000004
|
|
#endif
|
|
#ifndef PROCESS_POWER_THROTTLING_EXECUTION_SPEED
|
|
#define PROCESS_POWER_THROTTLING_EXECUTION_SPEED 0x00000001
|
|
#endif
|
|
PROCESS_POWER_THROTTLING_STATE powerState = {};
|
|
powerState.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
|
|
powerState.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION | PROCESS_POWER_THROTTLING_EXECUTION_SPEED;
|
|
powerState.StateMask = 0; // Turn off throttling
|
|
SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &powerState, sizeof(powerState));
|
|
|
|
// DPI: declare per-monitor v2 FIRST (before any window is created).
|
|
// Without it, on scaled displays (125/150%) the plugin's own renderer
|
|
// paints at 100% into an OS-scaled window -> blank/white panel with dark
|
|
// strips (reported "GUI trắng, không load"). Runtime call works even
|
|
// without an exe manifest. Ignore failure (already aware).
|
|
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
|
// Default IMC = none on the main thread (IME recursion fix).
|
|
// Default IMC = none on the main thread (IME recursion fix).
|
|
ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT);
|
|
|
|
#endif
|
|
|
|
// --render <job.json> --out <clip.wav>: one-shot offline render, no SHM/loop.
|
|
std::string renderJob, renderOut;
|
|
for (int i = 1; i < argc; ++i) {
|
|
if (std::strcmp(argv[i], "--render") == 0 && i + 1 < argc) renderJob = argv[i + 1];
|
|
else if (std::strcmp(argv[i], "--out") == 0 && i + 1 < argc) renderOut = argv[i + 1];
|
|
}
|
|
if (!renderJob.empty()) {
|
|
if (renderOut.empty()) {
|
|
std::cerr << "[RenderJob] --render requires --out <clip.wav>" << std::endl;
|
|
return 1;
|
|
}
|
|
return run_render_job(renderJob, renderOut);
|
|
}
|
|
|
|
// --render-fx <job.json> --in <input.wav> --out <output.wav>: one-shot
|
|
// offline FX render (VST3 effects + builtins), no SHM/loop.
|
|
std::string renderFxJob, renderFxIn, renderFxOut;
|
|
for (int i = 1; i < argc; ++i) {
|
|
if (std::strcmp(argv[i], "--render-fx") == 0 && i + 1 < argc) renderFxJob = argv[i + 1];
|
|
else if (std::strcmp(argv[i], "--in") == 0 && i + 1 < argc) renderFxIn = argv[i + 1];
|
|
else if (std::strcmp(argv[i], "--out") == 0 && i + 1 < argc) renderFxOut = argv[i + 1];
|
|
}
|
|
if (!renderFxJob.empty()) {
|
|
if (renderFxIn.empty() || renderFxOut.empty()) {
|
|
std::cerr << "[RenderFxJob] --render-fx requires --in <input.wav> --out <output.wav>" << std::endl;
|
|
return 1;
|
|
}
|
|
return run_render_fx_job(renderFxJob, renderFxIn, renderFxOut);
|
|
}
|
|
|
|
// --scan <dir>: one-shot plugin scan, no SHM/loop.
|
|
std::string scanDir;
|
|
for (int i = 1; i + 1 < argc; ++i) {
|
|
if (std::strcmp(argv[i], "--scan") == 0) scanDir = argv[i + 1];
|
|
}
|
|
if (!scanDir.empty()) return run_scan_dir(scanDir);
|
|
|
|
// --open-fx-gui <job.json>: one-shot native editor window for a VST3 FX
|
|
// (Mastering/FX Rack → "mở VST GUI"); runs until the user closes it.
|
|
std::string fxGuiJob;
|
|
for (int i = 1; i + 1 < argc; ++i) {
|
|
if (std::strcmp(argv[i], "--open-fx-gui") == 0) fxGuiJob = argv[i + 1];
|
|
}
|
|
if (!fxGuiJob.empty()) return run_open_fx_gui(fxGuiJob);
|
|
|
|
// --fx-gui <job.json>: embedded GUI server (frame capture + input relay).
|
|
// See PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 0.
|
|
std::string fxGuiServerJob;
|
|
for (int i = 1; i + 1 < argc; ++i) {
|
|
if (std::strcmp(argv[i], "--fx-gui") == 0) fxGuiServerJob = argv[i + 1];
|
|
}
|
|
if (!fxGuiServerJob.empty()) return run_fx_gui_server(fxGuiServerJob);
|
|
|
|
// 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; }
|
|
g_shmIPC = shmIPC; // A14: midi-out hook (audio thread) writes SHM via this
|
|
// VSTi crackle fix: 8-slot audio ring (see SharedMemoryIPC.h). Must be set
|
|
// on Windows too — ringMask stays 0 otherwise and the ring collapses to
|
|
// 1 slot (slot = writeIndex & 0), dropping whole blocks on any jank.
|
|
shmIPC->ringMask = 7;
|
|
#else
|
|
(void)shmName; // POSIX shm mapping (shm_open) added when porting off Windows
|
|
auto* shmIPC = (SharedAudioBufferIPC*)std::calloc(1, sizeof(SharedAudioBufferIPC));
|
|
// VSTi crackle fix: 8-slot audio ring (see SharedMemoryIPC.h).
|
|
shmIPC->ringMask = 7;
|
|
if (!shmIPC) return 1;
|
|
#endif
|
|
|
|
// G3.1: dedicated heartbeat thread, independent of the audio loop.
|
|
// Increments every 100ms (~10Hz). Tauri watchdog keys liveness on this.
|
|
// Started right after the SHM map so a slow instrument restore at
|
|
// startup (or after watchdog respawn) never looks like a dead bridge.
|
|
std::thread([shmIPC]() {
|
|
for (;;) {
|
|
Sleep(100);
|
|
shmIPC->heartbeat++;
|
|
}
|
|
}).detach();
|
|
|
|
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()).
|
|
// G1.2: MOT UiThread chung (COM STA + message pump + job queue) cho moi
|
|
// channel — thay 16 ChannelWorker. Verify gui_probe `shared_worker` (G1.1):
|
|
// load + openGUI 2 instance tren 1 worker OK, khong crash/hang.
|
|
ChannelWorker uiWorker;
|
|
g_uiWorker = &uiWorker;
|
|
// VST2 editor resize hook: plugin audioMaster(SizeWindow) -> resize the
|
|
// host window on the UI worker (the window belongs to its pump).
|
|
Vst2Instrument::setResizeHook([](void* hwnd, int w, int h) {
|
|
if (!g_uiWorker) return;
|
|
g_uiWorker->post([hwnd, w, h]() {
|
|
HWND parent = (HWND)hwnd;
|
|
if (!parent || !IsWindow(parent)) return;
|
|
RECT rc;
|
|
if (!GetClientRect(parent, &rc)) return;
|
|
int cw = rc.right - rc.left;
|
|
int chh = rc.bottom - rc.top;
|
|
if (cw != w || chh != h)
|
|
SetWindowPos(parent, nullptr, 0, 0, w, h,
|
|
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
});
|
|
});
|
|
// A14: plugin MIDI output sink. audioMaster(kAudioMasterProcessEvents)
|
|
// (VstEvents from Scaler2/Instacomposer/...) -> SHM midiOutQueue,
|
|
// drained by the Tauri pump to `bridge-midi-out`, JS routes notes to
|
|
// DAW tracks. Append-only, no mutex — same pattern as midiQueue (audio
|
|
// thread writes, pump reads+resets). Cap 64; drop when full.
|
|
Vst2Instrument::setMidiOutHook([](uint8_t cmd, uint8_t ch, uint8_t d1,
|
|
uint8_t d2, uint32_t deltaFrames) {
|
|
if (!g_shmIPC) return;
|
|
if (g_shmIPC->midiOutCount >= 64) return;
|
|
auto& ev = g_shmIPC->midiOutQueue[g_shmIPC->midiOutCount++];
|
|
ev.command = cmd;
|
|
ev.channel = ch;
|
|
ev.pitch = d1;
|
|
ev.velocity = d2;
|
|
ev.data2 = 0;
|
|
ev.data3 = 0;
|
|
ev.reserved[0] = 0;
|
|
ev.reserved[1] = 0;
|
|
ev.sampleOffset = deltaFrames;
|
|
});
|
|
// 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;
|
|
}
|
|
// G2.1: state file. The Rust app sets SF_STATE_FILE; fall back to the default.
|
|
std::string stateFile;
|
|
if (const char* e = std::getenv("SF_STATE_FILE")) stateFile = e;
|
|
if (stateFile.empty()) stateFile = StateStore::defaultPath();
|
|
const uint32_t block = AUDIO_BLOCK_SIZE;
|
|
// T2.4: realtime master FX chain — created once, fed via control type 6.
|
|
g_fxChain = std::make_unique<RealtimeFxChain>();
|
|
std::atomic<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) {
|
|
// Manager-level dispatch: ONE engine-mutex hold per event (get +
|
|
// channelQuiet + instrument call) so assign()/unload() can never
|
|
// destroy the instance mid-call — use-after-free when loading a new
|
|
// VSTi while other channels keep playing. channelQuiet drops events
|
|
// for channels mid-reload only (G1.4: bo mute-when-editor-open).
|
|
// CRASH FIX: noteOn/noteOff/CC enter the plugin (DLL entry); a C++
|
|
// exception from the plugin must not escape the audio loop.
|
|
try {
|
|
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 && !evt.reserved[0]) return; // V8 bug 3: drop note-on sau STOP, tru live input (reserved[0])
|
|
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 && !evt.reserved[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;
|
|
}
|
|
} catch (...) {
|
|
std::cerr << "[NativeBridge] dispatch EXCEPTION ch=" << (int)evt.channel
|
|
<< " cmd=" << std::hex << (int)evt.command << std::dec
|
|
<< " — plugin threw (note/CC)" << std::endl;
|
|
}
|
|
};
|
|
|
|
// Render only [from, to) of the block — used by sample-accurate splitting.
|
|
// VSTi crackle fix: target the multi-slot ring slot for the block being
|
|
// built, so old blocks stay readable until the reader drains them.
|
|
auto renderSegment = [&](uint32_t from, uint32_t to) {
|
|
if (to <= from) return;
|
|
uint32_t slot = shmIPC->bridgeWriteIndex & shmIPC->ringMask;
|
|
instruments.renderAll(shmIPC->ringLeft[slot] + from, shmIPC->ringRight[slot] + 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).
|
|
// G0.3: while an attach job is in flight, tell the Tauri watchdog via
|
|
// SHM so a long Nexus reload/attach (can exceed 3s) is not mistaken
|
|
// for a dead bridge and killed. RAII -> cleared on every exit path.
|
|
struct AttachInFlightGuard {
|
|
SharedAudioBufferIPC* ipc;
|
|
explicit AttachInFlightGuard(SharedAudioBufferIPC* p) : ipc(p) { if (ipc) ipc->attachInFlight = 1; }
|
|
~AttachInFlightGuard() { if (ipc) ipc->attachInFlight = 0; }
|
|
};
|
|
auto handleOpenGui = [&](uint32_t guiCh, uintptr_t arg1, const std::string& pluginId) {
|
|
if (!instruments.has(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;
|
|
}
|
|
// VST2 headless (no editor) hoac instrument load fail -> khong tao
|
|
// window, khong reuse window cua channel khac (window trong voi title
|
|
// sai plugin). Frontend hien "VST2 headless - no GUI" thay vi window.
|
|
if (instruments.withInstrument(guiCh, [](INativeInstrument* i0) {
|
|
return !i0 || !i0->canOpenGUI();
|
|
})) {
|
|
std::cerr << "[NativeBridge] GUI skip ch=" << guiCh
|
|
<< " plugin=" << pluginId << " (headless - no editor)" << std::endl;
|
|
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 (instruments.withInstrument(guiCh, [](INativeInstrument* i0) {
|
|
return i0 && i0->hasAttachedView() && !i0->ownsGuiWindow();
|
|
})) {
|
|
return;
|
|
}
|
|
// G0.1 (historical): mo GUI khi dang PLAY bi defer toi khi STOP vi 2
|
|
// thread 1 plugin (audio loop process + attach worker) -> Nexus throw.
|
|
// LEVEL-1 (2026-08-15): bo defer. G0.1b (setReloading tren MAIN thread
|
|
// truoc job, o duoi) da silence same-path channels va G1.2 (1 UiThread,
|
|
// audio loop khong pump window messages) chan race 2 thread 1 DLL.
|
|
// GUI mo ngay khi play; mute ngan (~0.1-0.3s lan dau: hasAttachedOnce_
|
|
// false nen reloadForGUI khong reload, Vst3Instrument.cpp 688). Note
|
|
// dang ngan khong mat. Reopen sau khi dong VAN reload (1-3s mute).
|
|
// G1.2: window + attach chay tren UiThread (1 thread chung, COM STA +
|
|
// message pump + job queue). Window messages do UiThread pump dispatch —
|
|
// audio loop khong con PeekMessageW (xoa block pump o main loop).
|
|
// G1.1 verify: gui_probe shared_worker = 1 worker load+openGUI 2 inst OK.
|
|
uiWorker.post([&instruments, shmIPC, guiCh, arg1, arg2 = pluginId]() {
|
|
AttachInFlightGuard _aifGuard{shmIPC};
|
|
if (!instruments.get(guiCh)) {
|
|
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=0"
|
|
<< " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl;
|
|
return;
|
|
}
|
|
if (auto* inst0 = instruments.get(guiCh)) {
|
|
if (inst0->hasAttachedView() && !inst0->ownsGuiWindow()) {
|
|
// Dedupe race: main da silence (setReloading) truoc khi post —
|
|
// restore de channel khong cam vinh vien.
|
|
std::vector<uint32_t> sp = same_plugin_channels(guiCh);
|
|
for (uint32_t y : sp) unmute_if_not_closing(y, "openGUI-dedupe");
|
|
unmute_if_not_closing(guiCh, "openGUI-dedupe");
|
|
return;
|
|
}
|
|
}
|
|
// G4.6: child-own GUI (sandbox VST2 like Qin). The child process
|
|
// opens its OWN top-level window on control 4 — no native parent
|
|
// window to create, no g_guiWindows registry, no same-path
|
|
// silencing, no closing of other editors (separate process;
|
|
// nothing to serialize). Just forward the OPEN_GUI control.
|
|
if (auto* inst0 = instruments.get(guiCh)) {
|
|
if (inst0->ownsGuiWindow()) {
|
|
std::cerr << "[NativeBridge] GUI via child-own ch=" << guiCh
|
|
<< " plugin=" << arg2 << std::endl;
|
|
try {
|
|
inst0->attachView(nullptr);
|
|
} catch (...) {
|
|
std::cerr << "[NativeBridge] GUI child-own attach EXCEPTION ch=" << guiCh
|
|
<< std::endl;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
void* hwnd = (void*)arg1;
|
|
#ifdef _WIN32
|
|
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, 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.
|
|
SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
|
|
} else {
|
|
nativeHwnd = (HWND)create_native_vst_window(arg2.c_str());
|
|
if (!nativeHwnd) {
|
|
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl;
|
|
std::vector<uint32_t> sp = same_plugin_channels(guiCh);
|
|
for (uint32_t y : sp) unmute_if_not_closing(y, "openGUI-createfail");
|
|
unmute_if_not_closing(guiCh, "openGUI-createfail");
|
|
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
|
|
// G0.1b: silence same-path instances on the MAIN thread BEFORE the
|
|
// attach job runs - renderAll of this loop iteration (and any before
|
|
// the worker sets reloading_) would otherwise process() another Nexus
|
|
// while the job's reload() (createInstance/terminate) enters the DLL
|
|
// -> 2 threads in 1 DLL -> Nexus hang -> writeIndex stall -> watchdog
|
|
// kills the bridge (observed: defer OK, attach after STOP still
|
|
// restarted). setReloading is a flag under the engine mutex; renderAll
|
|
// skips reloading channels, so no process() can race the job. The job
|
|
// keeps its own setReloading + restore logic unchanged. Dat o day (sau
|
|
// window create) de window fail khong keo mute vinh vien.
|
|
{
|
|
std::string gp0 = lower_plugin_path(guiCh);
|
|
if (!gp0.empty()) {
|
|
for (uint32_t y = 0; y < 16; ++y) {
|
|
if (y == guiCh) continue;
|
|
if (!instruments.get(y)) continue;
|
|
if (lower_plugin_path(y) == gp0)
|
|
instruments.setReloading(y, true);
|
|
}
|
|
instruments.setReloading(guiCh, true);
|
|
}
|
|
}
|
|
#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 thread khac -> treo.
|
|
// G1.2: 1 UiThread -> dong editor cua channel khac DONG BO (goi
|
|
// close_editor_now truc tiep). Khong the PostMessage(WM_CLOSE) cho
|
|
// job khac nhu cu — job close nam sau job hien tai trong queue cua
|
|
// CUNG thread -> khong bao gio chay -> 5s timeout -> attach abort.
|
|
{
|
|
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) {
|
|
// 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()) {
|
|
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;
|
|
close_editor_now(y, yHwnd);
|
|
}
|
|
}
|
|
#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).
|
|
// G1.4: bo mute-when-editor-open - channel process() binh
|
|
// thuong khi editor mo trong PLAY (verified: writeIndex
|
|
// advancing voi GUI open, Nexus khong crash). reloading_ giu
|
|
// im channel chi trong luc createView/createInstance chay
|
|
// (2 thread 1 DLL); clear ngay sau attach xong.
|
|
// 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()) {
|
|
for (uint32_t y = 0; y < 16; ++y) {
|
|
if (y == guiCh) continue;
|
|
if (!instruments.get(y)) continue;
|
|
if (lower_plugin_path(y) == gp) {
|
|
instruments.setReloading(y, true);
|
|
samePathSilenced.push_back(y);
|
|
}
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
bool ok = false;
|
|
{
|
|
// Serialize ALL plugin-DLL entry (createInstance AND
|
|
// createView) with other loads: two threads inside one
|
|
// DLL (Nexus) crash. createView outside the lock raced a
|
|
// same-path close job's reload (createInstance) -> Nexus
|
|
// exited silently (observed in probes).
|
|
std::lock_guard<std::mutex> lg(g_loadMutex);
|
|
// CRASH FIX (0xc000041d): attachView -> view->attached()
|
|
// and reloadForGUI both enter the plugin DLL — a C++
|
|
// exception (Nexus) must not escape this job (would
|
|
// std::terminate the worker). Treat as attach failure;
|
|
// the else branch below closes the empty window.
|
|
try {
|
|
std::cerr << "[NativeBridge] GUI job step1 reloadForGUI ch=" << guiCh << std::endl;
|
|
ok = inst->reloadForGUI();
|
|
std::cerr << "[NativeBridge] GUI job step2 reloadForGUI ok=" << ok << " ch=" << guiCh << std::endl;
|
|
// IME recursion fix: strip the editor's IMC before
|
|
// attachView creates the editor child — a live IMC on
|
|
// a JUCE_ window + plugin wndproc (ImmIsUIMessageW ->
|
|
// SendMessageW WM_IME_SETCONTEXT same hwnd) = infinite
|
|
// recursion (0xC00000FD, observed inside attachView).
|
|
#ifdef _WIN32
|
|
if (ok) disable_ime_contexts((HWND)hwnd);
|
|
#endif
|
|
if (ok) ok = inst->attachView(hwnd);
|
|
std::cerr << "[NativeBridge] GUI job step3 attachView ok=" << ok << " ch=" << guiCh << std::endl;
|
|
} catch (...) {
|
|
std::cerr << "[NativeBridge] GUI attach EXCEPTION ch=" << guiCh
|
|
<< " — plugin threw (createView/attached)" << std::endl;
|
|
ok = false;
|
|
}
|
|
}
|
|
// Attach done (ok or failed) — close jobs may unmute again.
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
// Restore same-path silence. Channels whose own close job is
|
|
// still inside createInstance stay muted — that job owns
|
|
// their unmute and performs it once reload finished.
|
|
for (uint32_t y : samePathSilenced)
|
|
unmute_if_not_closing(y, "openGUI");
|
|
if (ok) {
|
|
|
|
#ifdef _WIN32
|
|
disable_ime_contexts((HWND)hwnd);
|
|
#endif
|
|
|
|
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
|
|
<< " plugin=" << arg2 << " ch=" << guiCh
|
|
<< std::endl;
|
|
instruments.setReloading(guiCh, false);
|
|
}
|
|
else {
|
|
// Attach failed -> no editor running -> safe to process again.
|
|
instruments.setReloading(guiCh, false);
|
|
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
|
|
<< " plugin=" << arg2 << std::endl;
|
|
#ifdef _WIN32
|
|
// Attach that — khong co view (VD: channel la SF2/SFZ hoac plugin
|
|
// loi). Dong ngay cua so vo nghia de khong con window treo trong
|
|
// registry; WM_CLOSE -> UiThread pump destroy (owner = UiThread).
|
|
// PostMessage an toan cross-thread (khong nhu DestroyWindow).
|
|
|
|
PostMessageA((HWND)hwnd, WM_CLOSE, 0, 0);
|
|
#endif
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
// G2.1: persist instrument state to disk. Runs on the UiThread (posted via
|
|
// uiWorker.post when g_stateDirty flips) so file IO never blocks the audio
|
|
// loop. Deferred while any channel is mid-reload (half-dead instance).
|
|
auto saveStateJob = [&]() {
|
|
std::cerr << "[NativeBridge] saveStateJob begin" << std::endl;
|
|
for (uint32_t ch = 0; ch < 16; ++ch) {
|
|
if (instruments.isReloading(ch)) { g_stateDirty = true; return; }
|
|
}
|
|
BridgeStateSnapshot snap;
|
|
if (!instruments.snapshot(snap)) return;
|
|
snap.playing = g_transportPlaying.load();
|
|
snap.playhead = (double)playheadSamples.load();
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_lastSnapMutex);
|
|
if (snapshotsEqual(snap, g_lastSnap)) return;
|
|
g_lastSnap = snap;
|
|
}
|
|
if (StateStore::save(snap, stateFile))
|
|
std::cout << "[NativeBridge] state saved: " << snap.instruments.size()
|
|
<< " instruments, playing=" << (snap.playing ? "true" : "false")
|
|
<< std::endl;
|
|
else
|
|
std::cerr << "[NativeBridge] state save FAILED: " << stateFile << std::endl;
|
|
};
|
|
|
|
// G2.3: auto-reload instruments from the state file after a respawn.
|
|
// Runs before the audio loop: read the file once, post one LOAD job per
|
|
// saved channel to the UiThread (assign + preset restore), then resume
|
|
// transport if the session was PLAYing. Only restores channels that are
|
|
// empty (the frontend may already have loaded some by the time the state
|
|
// file becomes visible). All jobs serialize on g_loadMutex; file IO here
|
|
// is startup-only, never in the audio loop.
|
|
{
|
|
BridgeStateSnapshot snap;
|
|
if (StateStore::load(snap, stateFile)) {
|
|
std::cout << "[NativeBridge] restoring " << snap.instruments.size()
|
|
<< " instruments from " << stateFile << std::endl;
|
|
for (const auto& e : snap.instruments) {
|
|
uint32_t ch = e.channel & 0xF;
|
|
if (e.path.empty() || instruments.has(ch)) continue; // guard: empty channel only
|
|
InstrumentType t = (InstrumentType)e.type;
|
|
std::string path = e.path;
|
|
std::string preset = e.presetBase64;
|
|
uint32_t bank = e.bank, program = e.program;
|
|
uiWorker.post([&instruments, t, ch, path, preset, bank, program, sampleRate, block]() {
|
|
auto doRestore = std::function<void()>(
|
|
[&instruments, t, ch, path, preset, bank, program, sampleRate, block]() {
|
|
std::lock_guard<std::mutex> lg(g_loadMutex);
|
|
bool ok = false;
|
|
try {
|
|
ok = instruments.assign(ch, t, path, sampleRate, block);
|
|
} catch (...) {
|
|
std::cerr << "[NativeBridge] restore assign EXCEPTION ch=" << ch
|
|
<< " type=" << (int)t << " path=" << path << std::endl;
|
|
ok = false;
|
|
}
|
|
if (ok) {
|
|
if (auto* inst = instruments.get(ch)) {
|
|
}
|
|
if (ok) {
|
|
auto* inst2 = instruments.get(ch);
|
|
if (!inst2) {
|
|
std::cerr << "[NativeBridge] restore lost inst ch=" << ch << std::endl;
|
|
} else {
|
|
if ((t == InstrumentType::VST3 || t == InstrumentType::VST2) && preset.size() >= 100)
|
|
inst2->loadSerializedState(preset);
|
|
else
|
|
inst2->selectProgram(ch, bank, program);
|
|
// Re-calibrate AFTER the preset exists: assign()
|
|
// calibrated the empty instance (VST2 like Qin
|
|
// probes silent -> makeup=1.0, quieter than the
|
|
// other VSTi). Probe-render runs off mu_ so the
|
|
// audio loop never stalls.
|
|
if (t == InstrumentType::VST3 || t == InstrumentType::VST2)
|
|
instruments.recalibrate(ch, sampleRate, block);
|
|
}
|
|
}
|
|
std::cout << "[NativeBridge] restored ch=" << ch
|
|
<< " type=" << (int)t << " " << path << std::endl;
|
|
} else {
|
|
std::cerr << "[NativeBridge] restore FAILED ch=" << ch
|
|
<< " type=" << (int)t << " " << path << std::endl;
|
|
}
|
|
});
|
|
// DUNE 3: VST2 entry+init must run on the audio thread.
|
|
if (t == InstrumentType::VST2) runOnMain(doRestore);
|
|
else doRestore();
|
|
});
|
|
}
|
|
// FIX auto-play: KHONG tu bat transport khi restore state -- bridge
|
|
// khong duoc tu phat am. Frontend la chu transport: page dang play
|
|
// that su se tu gui PLAY lai sau reconnect; page fresh thi im lang.
|
|
if (snap.playing) {
|
|
std::cout << "[NativeBridge] restore transport SKIPPED (snap.playing=true)"
|
|
<< std::endl;
|
|
}
|
|
} else {
|
|
std::cout << "[NativeBridge] no state file to restore (" << stateFile << ")" << std::endl;
|
|
}
|
|
}
|
|
|
|
// 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
|
|
while (true) {
|
|
// CRASH FIX (0xc000041d): last-resort net — any C++ exception that
|
|
// escapes the targeted guards (e.g. inside renderAll -> process())
|
|
// must not kill the bridge. Log and continue the loop.
|
|
try {
|
|
// G1.2: audio loop KHONG pump messages nua — moi window (native VST +
|
|
// editor children) thuoc UiThread; UiThread pump dispatch chung.
|
|
// 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;
|
|
uiWorker.post([&instruments, t, ch, path, sampleRate, block]() {
|
|
// G2.4: double-load guard - sau respawn/bootstrap, JS
|
|
// reconnect reload gui LOAD cung path (C++ da tu restore
|
|
// tu state file); thay instance se mat preset. Skip khi
|
|
// channel da co DUNG instrument nay.
|
|
if (!path.empty() && !lower_plugin_path(ch).empty() &&
|
|
lower_plugin_path(ch) == lower_path_str(path)) {
|
|
std::cout << "[NativeBridge] LOAD skip ch=" << ch
|
|
<< " (already loaded: " << path << ")" << std::endl;
|
|
return;
|
|
}
|
|
// 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);
|
|
}
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
#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.
|
|
// G1.2/G1.3: window tao tren UiThread (handleOpenGui post attach
|
|
// job; create_native_vst_window chay trong job). LOAD job cung
|
|
// chay tren UiThread -> khong cross-thread destroy. An window,
|
|
// giu registry de reuse; editor detach + reload o duoi.
|
|
|
|
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);
|
|
// CRASH FIX: the plugin's editor window procs run on
|
|
// this worker; if one throws (Nexus) the exception must
|
|
// not escape the LOAD job (would std::terminate the
|
|
// worker). Detach best-effort and continue.
|
|
try {
|
|
disable_ime_contexts(hToHide);
|
|
if (auto* i = instruments.get(ch)) i->closeGUI();
|
|
// CRASH FIX (0xc000041d): destroy the old
|
|
// editor's child windows on the thread that OWNS
|
|
// them (JUCE MessageManager worker), never
|
|
// cross-thread - DestroyWindow from this LOAD
|
|
// worker raced the owner's pump dispatching
|
|
// WM_PAINT into the plugin wndproc -> AV. Wait
|
|
// for each destroy before assign() below. The
|
|
// parent window is hidden and kept for reuse.
|
|
// See post_close_gui for the same pattern.
|
|
while (HWND c = FindWindowExA(hToHide, nullptr, nullptr, nullptr)) {
|
|
destroy_window_on_owner(c);
|
|
}
|
|
} catch (...) {
|
|
std::cerr << "[NativeBridge] LOAD close-editor EXCEPTION ch=" << ch
|
|
<< " — plugin window proc threw" << std::endl;
|
|
}
|
|
ShowWindow(hToHide, SW_HIDE);
|
|
}
|
|
#endif
|
|
// CRASH FIX (0xc000041d): Nexus throws nlohmann::json::
|
|
// out_of_range inside createInstance (loadPlugin) — the
|
|
// C++ exception must not escape the LOAD job (would
|
|
// std::terminate the worker). Catch it, treat as failed
|
|
// load, keep the old instance, and let the restore below
|
|
// unmute the channel. g_loadMutex held INSIDE the lambda
|
|
// so the deferred (main-thread) VST2 load still serializes
|
|
// with worker loads.
|
|
auto doAssign = std::function<void()>(
|
|
[&instruments, t, ch, path, sampleRate, block, spOld, spNew]() {
|
|
std::lock_guard<std::mutex> lg(g_loadMutex);
|
|
// G4.6: deferred double-load re-check. Restore's
|
|
// doRestore (also deferred via runOnMain) can assign
|
|
// the SAME path FIRST — a second assign here replaces
|
|
// the restored instance with a seed-only one (doAssign
|
|
// applies NO preset) and clobbers the real patch in
|
|
// the state file (ch3 63ba3667 -> 7f07473c observed).
|
|
// Skip like the job-level guard; undo the silence.
|
|
if (!path.empty() && !lower_plugin_path(ch).empty() &&
|
|
lower_plugin_path(ch) == lower_path_str(path)) {
|
|
std::cout << "[NativeBridge] LOAD skip (deferred re-check) ch=" << ch
|
|
<< " path=" << path << std::endl;
|
|
g_engine->setReloading(ch, false);
|
|
for (uint32_t y : spOld) unmute_if_not_closing(y, "load-skip");
|
|
for (uint32_t y : spNew) unmute_if_not_closing(y, "load-skip");
|
|
g_stateDirty = true;
|
|
return;
|
|
}
|
|
bool ok = false;
|
|
try {
|
|
ok = instruments.assign(ch, t, path, sampleRate, block);
|
|
} catch (...) {
|
|
std::cerr << "[NativeBridge] assign EXCEPTION ch=" << ch
|
|
<< " type=" << (int)t << " path=" << path
|
|
<< " — plugin threw (createInstance)" << std::endl;
|
|
ok = false;
|
|
}
|
|
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");
|
|
g_stateDirty = true; // G2.1: instrument changed
|
|
});
|
|
if (t == InstrumentType::VST2) {
|
|
// DUNE 3: entry+init must share the processReplacing
|
|
// thread → defer the whole load to the audio thread.
|
|
runOnMain(doAssign);
|
|
} else {
|
|
doAssign();
|
|
}
|
|
});
|
|
} 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.withInstrument(ch, [&](INativeInstrument* inst) {
|
|
if (inst) inst->controlChange(ch, 64, 0);
|
|
});
|
|
}
|
|
instruments.allNotesOff();
|
|
g_transportPlaying = false;
|
|
g_stateDirty = true; // G2.1: transport changed
|
|
std::cout << "[NativeBridge] transport STOP — all notes off" << std::endl;
|
|
} else if (c.arg0 == 1) { // PLAY
|
|
transportStopped = false;
|
|
g_transportPlaying = true;
|
|
g_stateDirty = true; // G2.1: transport changed
|
|
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;
|
|
g_stateDirty = true; // G2.1: transport changed
|
|
}
|
|
} 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));
|
|
} else if (c.type == 5) { // DUMP_STATE (G2.2): Rust pre-kill flush
|
|
std::cout << "[NativeBridge] DUMP_STATE - flushing state now" << std::endl;
|
|
uiWorker.post([&]() { saveStateJob(); });
|
|
} else if (c.type == 6) { // SET_FX_CHAIN (T2.4): arg2 = JSON [{"path","bypass"},...]
|
|
if (g_fxChain) g_fxChain->setChain(std::string(c.arg2), sampleRate, (int32_t)block);
|
|
}
|
|
}
|
|
shmIPC->controlQueueCount = 0;
|
|
|
|
// VST2 deferred loads (entry + init must run on THIS thread — DUNE 3
|
|
// thread contract). Drained after the control queue so a LOAD ctrl and
|
|
// its deferred assign land in the same iteration, before any render.
|
|
{
|
|
std::vector<std::function<void()>> deferred;
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_mainDeferredMutex);
|
|
deferred.swap(g_mainDeferred);
|
|
}
|
|
for (auto& job : deferred) {
|
|
try {
|
|
job();
|
|
} catch (...) {
|
|
std::cerr << "[NativeBridge] main-deferred job EXCEPTION"
|
|
<< std::endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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(); ) {
|
|
// G0.1: fulfill as soon as instrument is ready, even while playing
|
|
if (instruments.has(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);
|
|
}
|
|
|
|
// G2.1: state persistence. The VST GUI can change the preset with no
|
|
// control/MIDI traffic, so mark dirty every ~2s while instruments are
|
|
// loaded; a 300ms debounce posts one save job to the UiThread.
|
|
// TEMP diag: SF_NO_AUTOSAVE=1 disables the 2s mark (race test).
|
|
if (!std::getenv("SF_NO_AUTOSAVE")) {
|
|
{
|
|
static auto lastMark = std::chrono::steady_clock::now();
|
|
auto nowMark = std::chrono::steady_clock::now();
|
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(nowMark - lastMark).count() >= 2000) {
|
|
lastMark = nowMark;
|
|
for (uint32_t ch = 0; ch < 16; ++ch) {
|
|
if (instruments.has(ch)) { g_stateDirty = true; break; }
|
|
}
|
|
}
|
|
}
|
|
if (g_stateDirty.load()) {
|
|
static auto lastStatePost = std::chrono::steady_clock::now();
|
|
auto nowPost = std::chrono::steady_clock::now();
|
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(nowPost - lastStatePost).count() >= 300) {
|
|
lastStatePost = nowPost;
|
|
g_stateDirty = false;
|
|
uiWorker.post([&]() { saveStateJob(); });
|
|
}
|
|
}
|
|
} // SF_NO_AUTOSAVE
|
|
|
|
|
|
// V8 bug 5 (stale-reload watchdog): a close/attach job can hang
|
|
// inside createInstance (Nexus, observed) — reloading_ stays true
|
|
// forever, the channel and its same-plugin siblings stay muted,
|
|
// heartbeat keeps ticking so the Rust watchdog never respawns us:
|
|
// permanent silence with no log. Scan once per second; a reload
|
|
// older than 20s with no close/attach job in flight, or older than
|
|
// 60s regardless, is stale — force-clear so the audio loop processes
|
|
// the instance again. Risk (2 threads in one DLL) is bounded: SEH
|
|
// SafeProcessChannel re-mutes on crash; a hard DLL deadlock stalls
|
|
// writeIndex and Rust respawns the bridge (which recovers sound —
|
|
// observed: restart fixed muted channels).
|
|
{
|
|
static bool reloadWasSet[16] = { false };
|
|
static std::chrono::steady_clock::time_point reloadSeen[16];
|
|
static auto lastScan = std::chrono::steady_clock::now();
|
|
auto nowScan = std::chrono::steady_clock::now();
|
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(nowScan - lastScan).count() >= 1000) {
|
|
lastScan = nowScan;
|
|
for (uint32_t ch = 0; ch < 16; ++ch) {
|
|
bool rl = instruments.isReloading(ch);
|
|
if (!rl) { reloadWasSet[ch] = false; continue; }
|
|
if (!reloadWasSet[ch]) { reloadWasSet[ch] = true; reloadSeen[ch] = nowScan; continue; }
|
|
double sec = std::chrono::duration<double>(nowScan - reloadSeen[ch]).count();
|
|
bool jobInFlight = shmIPC->attachInFlight != 0 || close_in_flight(ch);
|
|
if (sec > 60.0 || (sec > 20.0 && !jobInFlight)) {
|
|
std::cerr << "[NativeBridge] STALE-RELOAD watchdog ch=" << ch
|
|
<< " reloading " << (int)sec << "s (attachInFlight="
|
|
<< shmIPC->attachInFlight << " closeInFlight="
|
|
<< close_in_flight(ch) << ") — force unmute" << std::endl;
|
|
instruments.setReloading(ch, false);
|
|
reloadWasSet[ch] = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
// T2.4: realtime master FX chain — apply to the just-rendered slot
|
|
// (in-place, owned buffers) before publishing.
|
|
if (g_fxChain) {
|
|
uint32_t slot = shmIPC->bridgeWriteIndex & shmIPC->ringMask;
|
|
g_fxChain->process(shmIPC->ringLeft[slot], shmIPC->ringRight[slot], 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();
|
|
}
|
|
}
|
|
} catch (...) {
|
|
std::cerr << "[NativeBridge] main loop EXCEPTION — plugin threw during render/control" << std::endl;
|
|
}
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
UnmapViewOfFile(shmIPC);
|
|
CloseHandle(hMapFile);
|
|
timeEndPeriod(1);
|
|
if (g_fxChain) g_fxChain->shutdown(); // destroy VST3 FX before COM teardown
|
|
CoUninitialize();
|
|
#else
|
|
std::free(shmIPC);
|
|
#endif
|
|
return 0;
|
|
}
|