fix(bridge): VST2 host dung ABI SDK — crash outs>2, NaN input, editOpen(NULL), opcodes; docs A14 MIDI-out + phan tich Ozone -Inf

- aeffect.h: bo sung opcodes 4-22, 53, 55 + audioMaster 15/41-44 + ERect (dung chuan VST2.4 SDK, DUNE load OK khong crash)
- Vst2Instrument: cap du pointer arrays theo numInputs/numOutputs (fix AV 0xC0000005 Qin outs=32), input fill 0.0f (het NaN poison effect), bo probe effEditOpen(NULL) (crash WaveObserver), GetDirectory + SizeWindow hook, canDo sendVstEvents/sendVstTimeInfo
- TASKS.md: A14 MIDI-out tu plugin (Scaler2/Instacomposer/MelodicFlow xuat MIDI khong phai audio)
- PLAN_MASTERBUS_FX_RACK_VST.md: phan tich Ozone -Inf — chain day du, nguyen nhan = audio DAW khong chay qua bridge master ring
This commit is contained in:
2026-08-19 20:57:16 +07:00
parent 927597a822
commit 6efef85e47
9 changed files with 714 additions and 73 deletions
+239 -44
View File
@@ -4,6 +4,7 @@
#include "INativeInstrument.h"
#include "SharedMemoryIPC.h"
#include "NativeInstrumentEngine.h"
#include "Vst2Instrument.h"
#include "StateStore.h"
#include "RenderJob.h"
#include "RenderFxJob.h"
@@ -14,6 +15,8 @@
#include <mmsystem.h>
#include <process.h>
#include <cstdlib>
#include <cstdio>
#include <dbghelp.h>
#include <thread>
#else
#include <unistd.h>
@@ -117,6 +120,20 @@ 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;
// 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;
@@ -318,6 +335,16 @@ public:
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
@@ -577,7 +604,115 @@ static void post_close_gui(uint32_t ch, HWND 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
@@ -752,6 +887,22 @@ int main(int argc, char* argv[]) {
// 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);
});
});
// 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).
@@ -1061,7 +1212,9 @@ int main(int argc, char* argv[]) {
// 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 ->
@@ -1071,6 +1224,7 @@ int main(int argc, char* argv[]) {
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;
@@ -1123,6 +1277,7 @@ int main(int argc, char* argv[]) {
// 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; }
}
@@ -1163,28 +1318,34 @@ int main(int argc, char* argv[]) {
std::string preset = e.presetBase64;
uint32_t bank = e.bank, program = e.program;
uiWorker.post([&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 (t == InstrumentType::VST3 && !preset.empty())
inst->loadSerializedState(preset);
else
inst->selectProgram(ch, bank, program);
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;
}
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;
}
if (ok) {
if (auto* inst = instruments.get(ch)) {
if (t == InstrumentType::VST3 && !preset.empty())
inst->loadSerializedState(preset);
else
inst->selectProgram(ch, bank, program);
}
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
@@ -1325,36 +1486,48 @@ int main(int argc, char* argv[]) {
ShowWindow(hToHide, SW_HIDE);
}
#endif
std::lock_guard<std::mutex> lg(g_loadMutex);
// 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.
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;
// 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);
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 {
std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch
<< " type=" << (int)t << " " << path << std::endl;
doAssign();
}
// 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
});
} else if (c.type == 1) { // PANIC
instruments.allNotesOff();
@@ -1397,6 +1570,25 @@ int main(int argc, char* argv[]) {
}
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).
{
@@ -1418,6 +1610,8 @@ int main(int argc, char* argv[]) {
// 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();
@@ -1437,6 +1631,7 @@ int main(int argc, char* argv[]) {
uiWorker.post([&]() { saveStateJob(); });
}
}
} // SF_NO_AUTOSAVE
// V8 bug 5 (stale-reload watchdog): a close/attach job can hang