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:
@@ -55,6 +55,9 @@ public:
|
||||
virtual bool reloadForGUI() { return true; }
|
||||
virtual bool attachView(void* /*parentWindowHandle*/) { return false; }
|
||||
virtual void resizeView(int /*w*/, int /*h*/) {}
|
||||
// Periodic idle tick for attached editors (VST2 effEditIdle repaints;
|
||||
// others: no-op). Called on the UI worker's pump loop.
|
||||
virtual void guiIdle() {}
|
||||
// True while the editor view is attached (GUI window open). The bridge uses
|
||||
// it to dedupe repeated OPEN_GUI (frontend retry spam) — re-attaching a
|
||||
// view that is already attached corrupts plugins (Nexus createView→null).
|
||||
|
||||
@@ -149,6 +149,10 @@ public:
|
||||
// Zero L/R then sum each assigned instrument into it.
|
||||
void renderAll(float* outputL, float* outputR, uint32_t numSamples);
|
||||
|
||||
// Periodic idle tick for every attached editor (VST2 effEditIdle). Runs
|
||||
// on the UI worker thread's pump loop; no-op for engines without editors.
|
||||
void guiIdleAll();
|
||||
|
||||
size_t count() const { std::lock_guard<std::mutex> lock(mu_); return channels_.size(); }
|
||||
|
||||
void setBypassed(bool on) { isBypassed_.store(on); }
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#ifdef _WIN32
|
||||
#include <malloc.h> // _heapchk (TEMP diag)
|
||||
#endif
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 1. SOUNDFONT ENGINE (.SF2 / .SF3) VIA FLUIDSYNTH C API
|
||||
@@ -108,7 +111,7 @@ void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
|
||||
|
||||
void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
|
||||
if (!synth) return;
|
||||
// fluid_synth_pitch_bend takes the raw 14-bit value (center 8192).
|
||||
// fluid_synth_pitch_bend takes the raw 14-bit value (center 2097152).
|
||||
fluid_synth_pitch_bend(FS_SYNTH, channel, bend14);
|
||||
}
|
||||
|
||||
@@ -435,7 +438,10 @@ void InstrumentEngineManager::allNotesOff() {
|
||||
// audio buffers; plain std::vector is only 16B-aligned. Over-allocate and
|
||||
// align the pointer handed to plugins to 64B.
|
||||
static float* alignF(std::vector<float>& v, size_t n) {
|
||||
if (v.size() < n + 16) v.assign(n + 16, 0.0f);
|
||||
// 64B-aligned pointer can sit up to 63B into the allocation; TEMP diag:
|
||||
// huge tail so we can measure how far DUNE writes past n.
|
||||
const size_t kTail = 2097152 + 64;
|
||||
if (v.size() < n + kTail) v.assign(n + kTail, 0.0f);
|
||||
uintptr_t a = reinterpret_cast<uintptr_t>(v.data());
|
||||
return reinterpret_cast<float*>((a + 63) & ~(uintptr_t)63);
|
||||
}
|
||||
@@ -457,6 +463,16 @@ static bool SafeProcessChannel(INativeInstrument* inst, float* outL, float* outR
|
||||
return true;
|
||||
} __except (g_sehCode = GetExceptionCode(),
|
||||
g_sehAddr = GetExceptionInformation()->ExceptionRecord->ExceptionAddress,
|
||||
std::cerr << "[NativeBridge][G3.3][diag] SEH code=0x" << std::hex
|
||||
<< g_sehCode << " rip=0x" << (uintptr_t)g_sehAddr
|
||||
<< " fault=0x" << std::hex
|
||||
<< (GetExceptionInformation()->ExceptionRecord->NumberParameters > 1
|
||||
? GetExceptionInformation()->ExceptionRecord->ExceptionInformation[1]
|
||||
: 0)
|
||||
<< " isWrite=" << (GetExceptionInformation()->ExceptionRecord->NumberParameters > 0
|
||||
? GetExceptionInformation()->ExceptionRecord->ExceptionInformation[0]
|
||||
: 0)
|
||||
<< std::dec << std::endl,
|
||||
EXCEPTION_EXECUTE_HANDLER) {
|
||||
*outCode = g_sehCode;
|
||||
return false;
|
||||
@@ -546,8 +562,17 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
|
||||
if (channelQuiet(ch)) continue; // editor open / crashed: do not process
|
||||
float* spL = alignF(scratchL_, numSamples);
|
||||
float* spR = alignF(scratchR_, numSamples);
|
||||
std::memset(spL, 0, numSamples * sizeof(float));
|
||||
std::memset(spR, 0, numSamples * sizeof(float));
|
||||
std::cerr << "[NativeBridge][diag] renderAll ch=" << ch << " n=" << numSamples
|
||||
<< " spL=0x" << std::hex << (uintptr_t)spL
|
||||
<< " spR=0x" << (uintptr_t)spR
|
||||
<< " capL=" << std::dec << scratchL_.size()
|
||||
<< " capR=" << scratchR_.size() << std::endl;
|
||||
// TEMP diag: fill canary with a NaN pattern so writes of 0.0 (silence)
|
||||
// are still detected.
|
||||
for (uint32_t i = 0; i < numSamples + 2097152; ++i) {
|
||||
spL[i] = 0x7FC00000u; // quiet NaN
|
||||
spR[i] = 0x7FC00000u;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
DWORD sehCode = 0;
|
||||
#else
|
||||
@@ -592,6 +617,28 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// TEMP diag: plugin output-overrun canary + heap health.
|
||||
{
|
||||
static uint64_t diagBlock = 0; ++diagBlock;
|
||||
int farL = -1, farR = -1;
|
||||
for (uint32_t i = 0; i < 2097152; ++i) {
|
||||
uint32_t bL, bR;
|
||||
std::memcpy(&bL, &spL[numSamples + i], 4);
|
||||
std::memcpy(&bR, &spR[numSamples + i], 4);
|
||||
if (bL != 0x7FC00000u) farL = (int)i;
|
||||
if (bR != 0x7FC00000u) farR = (int)i;
|
||||
}
|
||||
if (farL >= 0 || farR >= 0)
|
||||
std::cerr << "[NativeBridge][diag] block " << diagBlock
|
||||
<< " OUTPUT OVERRUN past n: L=" << farL
|
||||
<< " R=" << farR << " (ch=" << ch << ")" << std::endl;
|
||||
#ifdef _WIN32
|
||||
int h = _heapchk();
|
||||
if (h != _HEAPOK)
|
||||
std::cerr << "[NativeBridge][diag] block " << diagBlock
|
||||
<< " heap NOT OK code=" << h << std::endl;
|
||||
#endif
|
||||
}
|
||||
// Auto-normalize: makeup calibrated (constant per-channel gain, no steps).
|
||||
auto mit = makeup_.find(ch);
|
||||
const float g = (mit != makeup_.end()) ? mit->second : 1.0f;
|
||||
@@ -620,3 +667,10 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
|
||||
outputR[i] *= limiterGain_;
|
||||
}
|
||||
}
|
||||
|
||||
void InstrumentEngineManager::guiIdleAll() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
for (auto& kv : channels_) {
|
||||
kv.second->guiIdle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ std::unordered_map<const void*, Vst2Instrument*> g_v2Registry;
|
||||
// the callback may be invoked with a null effect (some plugins query the
|
||||
// sample rate during construction).
|
||||
thread_local Vst2Instrument* t_loading = nullptr;
|
||||
// TEMP probe: log every audioMaster call while effEditOpen is running.
|
||||
thread_local bool t_editingProbe = false;
|
||||
// Resize hook registered by the bridge (main.cpp): plugin audioMaster
|
||||
// (SizeWindow) -> move the host window on the UI worker thread.
|
||||
static void (*g_vst2ResizeHook)(void*, int, int) = nullptr;
|
||||
|
||||
// base64 encode/decode (RFC 4648) — local copies; Vst3Instrument.cpp keeps
|
||||
// its own so neither file depends on the other's statics.
|
||||
@@ -81,6 +86,7 @@ struct Vst2HostState {
|
||||
HMODULE hModule = nullptr;
|
||||
AEffect* effect = nullptr;
|
||||
std::vector<uint8_t> savedChunk; // preset chunk from effGetChunk
|
||||
std::string pluginDir; // directory of the plugin DLL (GetDirectory)
|
||||
};
|
||||
|
||||
// audioMaster callback — the plugin's channel to the host. VST2.4: return
|
||||
@@ -88,6 +94,9 @@ struct Vst2HostState {
|
||||
intptr_t audioMaster(AEffect* effect, int32_t opcode, int32_t index,
|
||||
intptr_t value, void* ptr, float opt) {
|
||||
(void)index; (void)value; (void)ptr; (void)opt;
|
||||
if (t_editingProbe || true)
|
||||
std::cerr << "[Vst2Instrument][editprobe] audioMaster op=" << opcode
|
||||
<< " idx=" << index << " val=" << value << " ptr=" << ptr << std::endl;
|
||||
Vst2Instrument* self = nullptr;
|
||||
if (effect) {
|
||||
std::lock_guard<std::mutex> lock(g_v2Mutex);
|
||||
@@ -111,10 +120,16 @@ intptr_t audioMaster(AEffect* effect, int32_t opcode, int32_t index,
|
||||
case kAudioMasterWantMidi: return 1; // we send MIDI events
|
||||
case kAudioMasterGetTime: {
|
||||
static VstTimeInfo ti;
|
||||
static double pos = 0.0;
|
||||
std::memset(&ti, 0, sizeof(ti));
|
||||
ti.samplePos = pos;
|
||||
ti.sampleRate = self ? self->sampleRate() : 44100.0;
|
||||
ti.nanoSeconds = pos / 44100.0 * 1e9;
|
||||
ti.tempo = 120.0;
|
||||
ti.flags = kVstTempoValid | kVstTransportPlaying;
|
||||
ti.ppqPos = pos * 120.0 / 60.0 / 44100.0;
|
||||
ti.flags = kVstTempoValid | kVstTransportPlaying; // TEMP diag: DUNE silent - try forcing transport playing
|
||||
pos += (self ? self->maxBlockSize() : 512);
|
||||
std::cerr << "[Vst2Instrument][diag] getTime flags=" << ti.flags << " pos=" << pos << std::endl;
|
||||
return (intptr_t)&ti;
|
||||
}
|
||||
case kAudioMasterGetCurrentProcessLevel: return 1; // audio thread
|
||||
@@ -131,14 +146,118 @@ intptr_t audioMaster(AEffect* effect, int32_t opcode, int32_t index,
|
||||
if (!ptr) return 0;
|
||||
const char* s = static_cast<const char*>(ptr);
|
||||
if (std::strcmp(s, "receiveVstEvents") == 0 ||
|
||||
std::strcmp(s, "receiveVstMidiEvent") == 0)
|
||||
std::strcmp(s, "receiveVstMidiEvent") == 0 ||
|
||||
std::strcmp(s, "sendVstEvents") == 0 ||
|
||||
std::strcmp(s, "sendVstMidiEvent") == 0 ||
|
||||
std::strcmp(s, "sendVstTimeInfo") == 0 ||
|
||||
std::strcmp(s, "sizeWindow") == 0 ||
|
||||
std::strcmp(s, "supplyIdle") == 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
case kAudioMasterAutomate: return 1; // param automation accepted (no-op)
|
||||
case kAudioMasterUpdateDisplay: return 1;
|
||||
case kAudioMasterBeginEdit: return 1;
|
||||
case kAudioMasterEndEdit: return 1;
|
||||
case kAudioMasterGetDirectory: {
|
||||
// Return the plugin's own directory. Some plugins (JUCE, skin-
|
||||
// loading synths) cache this during VSTPluginMain and later use
|
||||
// it to locate resources when the editor opens; a NULL answer
|
||||
// can crash effEditOpen.
|
||||
if (self && !self->pluginDir().empty())
|
||||
return (intptr_t)self->pluginDir().c_str();
|
||||
return 0;
|
||||
}
|
||||
case kAudioMasterSizeWindow: {
|
||||
// index = width, value = height (VST2.4 ABI).
|
||||
if (g_vst2ResizeHook && self)
|
||||
g_vst2ResizeHook(self->editorParent(), (int)index, (int)value);
|
||||
return 1;
|
||||
}
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static std::string v2_modName(uintptr_t a) {
|
||||
HMODULE m = nullptr;
|
||||
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
(LPCWSTR)a, &m))
|
||||
return "";
|
||||
wchar_t buf[260];
|
||||
DWORD n = GetModuleFileNameW(m, buf, 260);
|
||||
if (!n) return "";
|
||||
std::wstring w(buf, n);
|
||||
std::string out;
|
||||
for (wchar_t c : w) out += (char)c;
|
||||
size_t slash = out.find_last_of("\/");
|
||||
return slash == std::string::npos ? out : out.substr(slash + 1);
|
||||
}
|
||||
|
||||
static void v2_dumpSEH(const CONTEXT& ctx) {
|
||||
std::cerr << "[Vst2Instrument] rip=0x" << std::hex << (uintptr_t)ctx.Rip
|
||||
<< " rsp=0x" << (uintptr_t)ctx.Rsp << std::dec << std::endl;
|
||||
std::cerr << "[Vst2Instrument] rax=0x" << std::hex << (uintptr_t)ctx.Rax
|
||||
<< " rbx=0x" << (uintptr_t)ctx.Rbx
|
||||
<< " rcx=0x" << (uintptr_t)ctx.Rcx
|
||||
<< " rdx=0x" << (uintptr_t)ctx.Rdx
|
||||
<< " rsi=0x" << (uintptr_t)ctx.Rsi
|
||||
<< " rdi=0x" << (uintptr_t)ctx.Rdi
|
||||
<< " r8=0x" << (uintptr_t)ctx.R8
|
||||
<< " r9=0x" << (uintptr_t)ctx.R9 << std::dec << std::endl;
|
||||
auto tryStr = [](uintptr_t a) {
|
||||
if (!a || IsBadReadPtr((void*)a, 64)) return std::string("<bad>");
|
||||
std::string out;
|
||||
const char* p = (const char*)a;
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
char c = p[i];
|
||||
if (!c) break;
|
||||
out += (c >= 32 && c < 127) ? c : '?';
|
||||
}
|
||||
return out;
|
||||
};
|
||||
std::cerr << "[Vst2Instrument] rcx=str: " << tryStr((uintptr_t)ctx.Rcx)
|
||||
<< " | rdx=str: " << tryStr((uintptr_t)ctx.Rdx) << std::endl;
|
||||
std::cerr << "[Vst2Instrument] crash module: " << v2_modName((uintptr_t)ctx.Rip)
|
||||
<< std::endl;
|
||||
const uintptr_t* sp = (const uintptr_t*)ctx.Rsp;
|
||||
for (int i = 0; i < 0x4000 / 8 && i < 128; ++i) {
|
||||
uintptr_t v;
|
||||
if (IsBadReadPtr(sp + i, sizeof(v))) break;
|
||||
v = sp[i];
|
||||
std::string mn = v2_modName(v);
|
||||
if (!mn.empty())
|
||||
std::cerr << " [" << i << "] 0x" << std::hex << v
|
||||
<< " -> " << mn << std::dec << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatcher wrapper that catches SEH (access violations) inside the plugin
|
||||
// and reports code + fault address. C++ exceptions from the plugin are also
|
||||
// swallowed (return -1). /EHa makes catch(...) see SEH, but the code/address
|
||||
// are only available via __except.
|
||||
static intptr_t v2_dispatch(AEffect* fx, int32_t op, int32_t idx, intptr_t val,
|
||||
void* ptr, float opt, const char* where, uint32_t ch) {
|
||||
static DWORD g_sehCode = 0;
|
||||
static void* g_sehAddr = nullptr;
|
||||
static CONTEXT g_ctxCopy;
|
||||
__try {
|
||||
if (op == kEffectEditOpen) t_editingProbe = true;
|
||||
intptr_t r = fx->dispatcher(fx, op, idx, val, ptr, opt);
|
||||
if (op == kEffectEditOpen) t_editingProbe = false;
|
||||
return r;
|
||||
} __except (g_sehCode = GetExceptionCode(),
|
||||
g_sehAddr = GetExceptionInformation()->ExceptionRecord->ExceptionAddress,
|
||||
g_ctxCopy = *GetExceptionInformation()->ContextRecord,
|
||||
std::cerr << "[Vst2Instrument] " << where << " SEH code=0x" << std::hex
|
||||
<< g_sehCode << std::dec << " addr=0x" << g_sehAddr
|
||||
<< " ch=" << ch << std::endl,
|
||||
EXCEPTION_EXECUTE_HANDLER) {
|
||||
v2_dumpSEH(g_ctxCopy);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
Vst2Instrument::Vst2Instrument() : state_(nullptr), sampleRate_(44100.0),
|
||||
maxBlockSize_(512), loaded_(false) {}
|
||||
|
||||
@@ -147,6 +266,8 @@ Vst2Instrument::~Vst2Instrument() {
|
||||
if (!s) return;
|
||||
try {
|
||||
if (s->effect) {
|
||||
if (guiAttached_)
|
||||
s->effect->dispatcher(s->effect, kEffectEditClose, 0, 0, nullptr, 0);
|
||||
s->effect->dispatcher(s->effect, kEffectMainsChanged, 0, 0, nullptr, 0);
|
||||
s->effect->dispatcher(s->effect, kEffectStopProcess, 0, 0, nullptr, 0);
|
||||
s->effect->dispatcher(s->effect, kEffectClose, 0, 0, nullptr, 0);
|
||||
@@ -178,6 +299,10 @@ bool Vst2Instrument::loadPlugin(const std::string& path, double sampleRate) {
|
||||
}
|
||||
auto* s = new Vst2HostState();
|
||||
s->hModule = h;
|
||||
{
|
||||
size_t slash = path.find_last_of("\/");
|
||||
pluginDir_ = (slash == std::string::npos) ? std::string() : path.substr(0, slash);
|
||||
}
|
||||
t_loading = this;
|
||||
std::cerr << "[Vst2Instrument] calling VSTPluginMain..." << std::endl;
|
||||
AEffect* fx = entry(&audioMaster);
|
||||
@@ -237,12 +362,73 @@ bool Vst2Instrument::init(double sampleRate, uint32_t maxBlockSize) {
|
||||
maxBlockSize_ = maxBlockSize;
|
||||
AEffect* fx = s->effect;
|
||||
try {
|
||||
fx->dispatcher(fx, kEffectOpen, 0, 0, nullptr, 0);
|
||||
// TEMP diag variant B: sampleRate/blockSize BEFORE effOpen, block size
|
||||
// matches the actual render size (256), no StartProcess.
|
||||
fx->dispatcher(fx, kEffectSetSampleRate, 0, 0, nullptr, (float)sampleRate);
|
||||
fx->dispatcher(fx, kEffectSetBlockSize, 0, (intptr_t)maxBlockSize, nullptr, 0);
|
||||
fx->dispatcher(fx, kEffectOpen, 0, 0, nullptr, 0);
|
||||
fx->dispatcher(fx, kEffectSetProgram, 0, 0, nullptr, 0);
|
||||
fx->dispatcher(fx, kEffectMainsChanged, 0, 1, nullptr, 0);
|
||||
fx->dispatcher(fx, kEffectStartProcess, 0, 0, nullptr, 0);
|
||||
// TEMP diag: dump first params to see if the default patch is audible
|
||||
{
|
||||
const int pis[] = {0, 1, 10, 100, 500, 1000, 1160, 1168};
|
||||
for (int pi : pis) {
|
||||
if (pi >= fx->numParams) continue;
|
||||
char nm[128] = {0}, disp[128] = {0}, lbl[128] = {0};
|
||||
fx->dispatcher(fx, kEffectGetParamName, 0, pi, nm, 0);
|
||||
fx->dispatcher(fx, kEffectGetParamDisplay, 0, pi, disp, 0);
|
||||
fx->dispatcher(fx, kEffectGetParamLabel, 0, pi, lbl, 0);
|
||||
std::cerr << "[Vst2Instrument][diag] param " << pi << " name=" << nm << " disp=" << disp << " label=" << lbl << std::endl;
|
||||
}
|
||||
}
|
||||
// TEMP diag: does the plugin itself claim to receive MIDI?
|
||||
{
|
||||
const char* probes[] = {"receiveVstEvents", "receiveVstMidiEvent", "midiProgramNames", "bypass"};
|
||||
for (auto pr : probes) {
|
||||
intptr_t r = fx->dispatcher(fx, kEffectCanDo, 0, 0, (void*)pr, 0);
|
||||
std::cerr << "[Vst2Instrument][diag] plugin canDo " << pr << " -> " << r << std::endl;
|
||||
}
|
||||
}
|
||||
// TEMP diag: send raw MIDI directly (bypass SHM path). CC7 vol max + noteOn
|
||||
// with kVstMidiEventIsRealtime + retriggers.
|
||||
{
|
||||
auto sendMidi = [&](int b0, int b1, int b2) {
|
||||
VstMidiEvent e;
|
||||
std::memset(&e, 0, sizeof(e));
|
||||
e.type = kVstMidiType;
|
||||
e.byteSize = (int32_t)sizeof(e);
|
||||
e.flags = kVstMidiEventIsRealtime;
|
||||
e.midiData[0] = (char)b0; e.midiData[1] = (char)b1; e.midiData[2] = (char)b2;
|
||||
VstEvent* evs[1] = { reinterpret_cast<VstEvent*>(&e) };
|
||||
VstEvents events;
|
||||
std::memset(&events, 0, sizeof(events));
|
||||
events.numEvents = 1;
|
||||
events.events[0] = evs[0];
|
||||
fx->dispatcher(fx, kEffectProcessEvents, 0, 0, &events, 0);
|
||||
};
|
||||
std::cerr << "[Vst2Instrument][diag] raw CC7 127 + noteOn 60/100 x8" << std::endl;
|
||||
sendMidi(0xB0, 7, 127);
|
||||
for (int r = 0; r < 8; ++r) sendMidi(0x90, 60, 100);
|
||||
}
|
||||
// TEMP diag: does DUNE hold a preset chunk? dump its head as hex+ascii
|
||||
{
|
||||
void* chunkPtr = nullptr;
|
||||
intptr_t sz = fx->dispatcher(fx, kEffectGetChunk, 0, 1, &chunkPtr, 0);
|
||||
std::cerr << "[Vst2Instrument][diag] getChunk size=" << sz << " ptr=" << chunkPtr << std::endl;
|
||||
if (sz > 0 && chunkPtr) {
|
||||
const unsigned char* b = (const unsigned char*)chunkPtr;
|
||||
std::string ascii;
|
||||
for (intptr_t i = 0; i < sz && i < 512; ++i) {
|
||||
ascii += (b[i] >= 32 && b[i] < 127) ? (char)b[i] : '.';
|
||||
}
|
||||
std::cerr << "[Vst2Instrument][diag] chunkhead: " << ascii << std::endl;
|
||||
std::cerr << "[Vst2Instrument][diag] chunkbytes: ";
|
||||
for (intptr_t i = 0; i < sz && i < 64; ++i) {
|
||||
char hx[8]; snprintf(hx, sizeof(hx), "%02x ", b[i]); std::cerr << hx;
|
||||
}
|
||||
std::cerr << std::endl;
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
std::cerr << "[Vst2Instrument] init dispatcher EXCEPTION ch=" << channel_ << std::endl;
|
||||
return false;
|
||||
@@ -336,7 +522,7 @@ void Vst2Instrument::programChange(uint32_t channel, uint32_t program) {
|
||||
void Vst2Instrument::pitchBend(uint32_t channel, uint32_t bend14) {
|
||||
auto* s = static_cast<Vst2HostState*>(state_);
|
||||
if (!s || !s->effect) return;
|
||||
int32_t bend = (int32_t)bend14 - 8192; // 14-bit signed, center 8192
|
||||
int32_t bend = (int32_t)bend14 - 2097152; // 14-bit signed, center 2097152
|
||||
VstMidiEvent e;
|
||||
std::memset(&e, 0, sizeof(e));
|
||||
e.type = kVstMidiType;
|
||||
@@ -353,14 +539,81 @@ void Vst2Instrument::pitchBend(uint32_t channel, uint32_t bend14) {
|
||||
s->effect->dispatcher(s->effect, kEffectProcessEvents, 0, 0, &events, 0);
|
||||
}
|
||||
|
||||
bool Vst2Instrument::openGUI(void* parentWindowHandle) {
|
||||
(void)parentWindowHandle;
|
||||
return false; // headless — no editor (deferred per DESIGN_VST2_BACKWARD_COMPAT.md)
|
||||
bool Vst2Instrument::canOpenGUI() const {
|
||||
auto* e = effect();
|
||||
return loaded_ && e && (((const AEffect*)e)->flags & kEffectFlagsHasEditor);
|
||||
}
|
||||
|
||||
void Vst2Instrument::closeGUI() {}
|
||||
bool Vst2Instrument::openGUI(void* parentWindowHandle) {
|
||||
return attachView(parentWindowHandle);
|
||||
}
|
||||
|
||||
bool Vst2Instrument::hasAttachedView() const { return false; }
|
||||
bool Vst2Instrument::attachView(void* parentWindowHandle) {
|
||||
auto* s = static_cast<Vst2HostState*>(state_);
|
||||
if (!s || !s->effect || guiAttached_) return guiAttached_;
|
||||
if (!(s->effect->flags & kEffectFlagsHasEditor)) return false;
|
||||
HWND parent = (HWND)parentWindowHandle;
|
||||
if (!parent || !IsWindow(parent)) {
|
||||
std::cerr << "[Vst2Instrument] attachView: bad parent hwnd ch=" << channel_ << std::endl;
|
||||
return false;
|
||||
}
|
||||
std::cerr << "[Vst2Instrument] attachView CALLING effEditOpen ch=" << channel_
|
||||
<< " parent=0x" << std::hex << (uintptr_t)parent << std::dec << std::endl;
|
||||
intptr_t rc = v2_dispatch(s->effect, kEffectEditOpen, 0, 0, parent, 0,
|
||||
"effEditOpen", channel_);
|
||||
std::cerr << "[Vst2Instrument] attachView effEditOpen returned rc=" << rc
|
||||
<< " ch=" << channel_ << std::endl;
|
||||
if (rc == -1) return false; // SEH / plugin exception inside the DLL
|
||||
std::cout << "[Vst2Instrument] editOpen OK ch=" << channel_ << std::endl;
|
||||
ERect* r = nullptr;
|
||||
rc = v2_dispatch(s->effect, kEffectEditGetRect, 0, 0, &r, 0,
|
||||
"effEditGetRect", channel_);
|
||||
if (rc != -1 && rc && r) {
|
||||
int w = r->right - r->left;
|
||||
int h = r->bottom - r->top;
|
||||
std::cout << "[Vst2Instrument] editor size " << w << "x" << h << " ch=" << channel_ << std::endl;
|
||||
if (w > 0 && h > 0)
|
||||
SetWindowPos(parent, nullptr, 0, 0, w, h,
|
||||
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
editorParent_ = parent;
|
||||
guiAttached_ = true;
|
||||
std::cout << "[Vst2Instrument] GUI attached ch=" << channel_ << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Vst2Instrument::closeGUI() {
|
||||
auto* s = static_cast<Vst2HostState*>(state_);
|
||||
if (!s || !s->effect || !guiAttached_) {
|
||||
guiAttached_ = false;
|
||||
editorParent_ = nullptr;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
s->effect->dispatcher(s->effect, kEffectEditClose, 0, 0, nullptr, 0);
|
||||
} catch (...) {
|
||||
std::cerr << "[Vst2Instrument] closeGUI EXCEPTION ch=" << channel_ << std::endl;
|
||||
}
|
||||
guiAttached_ = false;
|
||||
editorParent_ = nullptr;
|
||||
std::cout << "[Vst2Instrument] GUI closed ch=" << channel_ << std::endl;
|
||||
}
|
||||
|
||||
bool Vst2Instrument::hasAttachedView() const { return guiAttached_; }
|
||||
|
||||
void Vst2Instrument::guiIdle() {
|
||||
auto* s = static_cast<Vst2HostState*>(state_);
|
||||
if (!s || !s->effect || !guiAttached_) return;
|
||||
try {
|
||||
s->effect->dispatcher(s->effect, kEffectEditIdle, 0, 0, nullptr, 0);
|
||||
} catch (...) {
|
||||
std::cerr << "[Vst2Instrument] guiIdle EXCEPTION ch=" << channel_ << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void Vst2Instrument::setResizeHook(void (*fn)(void*, int, int)) {
|
||||
g_vst2ResizeHook = fn;
|
||||
}
|
||||
|
||||
void Vst2Instrument::captureState() {
|
||||
auto* s = static_cast<Vst2HostState*>(state_);
|
||||
@@ -412,19 +665,78 @@ void Vst2Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
|
||||
AEffect* fx = s->effect;
|
||||
// 64B-align inputs: DUNE 3 etc. use aligned SIMD on audio buffers.
|
||||
auto alignF = [](std::vector<float>& v, size_t n) -> float* {
|
||||
if (v.size() < n + 16) v.assign(n + 16, 0.0f);
|
||||
// TEMP diag (heap-corrupt hunt): huge tail + input canary to measure
|
||||
// how far DUNE writes past n.
|
||||
const size_t kTail = 2097152 + 64;
|
||||
if (v.size() < n + kTail) v.assign(n + kTail, 0.0f);
|
||||
uintptr_t a = reinterpret_cast<uintptr_t>(v.data());
|
||||
return reinterpret_cast<float*>((a + 63) & ~(uintptr_t)63);
|
||||
};
|
||||
float* inL = alignF(inL_, numSamples);
|
||||
float* inR = alignF(inR_, numSamples);
|
||||
std::memset(inL, 0, numSamples * sizeof(float));
|
||||
std::memset(inR, 0, numSamples * sizeof(float));
|
||||
float* inBufs[2] = { inL, inR };
|
||||
float* outBufs[2] = { outputL, outputR };
|
||||
std::cerr << "[Vst2Instrument] processReplacing IN ch=" << channel_ << " n=" << numSamples << std::endl;
|
||||
fx->processReplacing(fx, inBufs, outBufs, (int32_t)numSamples);
|
||||
for (uint32_t i = 0; i < numSamples + 2097152; ++i) {
|
||||
inL[i] = 0.0f; // TEMP: clean input (NaN poisons effect plugins)
|
||||
inR[i] = 0.0f;
|
||||
}
|
||||
// Build pointer arrays covering the plugin's full ins/outs: plugins with
|
||||
// numOutputs > 2 (e.g. Qin_RV outs=32) read all pointers — short arrays
|
||||
// hand them garbage and crash.
|
||||
const int nIn = fx->numInputs;
|
||||
const int nOut = fx->numOutputs;
|
||||
std::vector<float*> inBufs(nIn > 0 ? nIn : 1, nullptr);
|
||||
std::vector<float*> outBufs(nOut > 0 ? nOut : 1, nullptr);
|
||||
if (nIn > 0) {
|
||||
inBufs[0] = inL;
|
||||
if (nIn > 1) inBufs[1] = inR;
|
||||
}
|
||||
if (nOut > 0) {
|
||||
outBufs[0] = outputL;
|
||||
if (nOut > 1) outBufs[1] = outputR;
|
||||
}
|
||||
if (nOut > 2) {
|
||||
// scratch for channels 2..nOut-1 (zeroed; host ring only keeps L/R)
|
||||
extraOut_.assign((size_t)nOut * numSamples, 0.0f);
|
||||
for (int c = 2; c < nOut; ++c)
|
||||
outBufs[c] = extraOut_.data() + (size_t)c * numSamples;
|
||||
}
|
||||
if (nIn == 0)
|
||||
inBufs[0] = nullptr; // ins=0: pass NULL input array entry
|
||||
std::cerr << "[Vst2Instrument] processReplacing IN ch=" << channel_ << " n=" << numSamples
|
||||
<< " ins=" << nIn << " outs=" << nOut
|
||||
<< " inL=0x" << std::hex << (uintptr_t)inL << " inR=0x" << (uintptr_t)inR
|
||||
<< " outL=0x" << (uintptr_t)outputL << " outR=0x" << (uintptr_t)outputR
|
||||
<< std::dec << std::endl;
|
||||
fx->processReplacing(fx, inBufs.data(), outBufs.data(), (int32_t)numSamples);
|
||||
std::cerr << "[Vst2Instrument] processReplacing OUT ch=" << channel_ << std::endl;
|
||||
// TEMP diag: does the plugin write any audio?
|
||||
{
|
||||
static int diagBlk = 0;
|
||||
float peakL = 0.f, peakR = 0.f;
|
||||
for (uint32_t i = 0; i < numSamples; ++i) {
|
||||
float a = outputL[i], b = outputR[i];
|
||||
if (a > peakL) peakL = a; else if (-a > peakL) peakL = -a;
|
||||
if (b > peakR) peakR = b; else if (-b > peakR) peakR = -b;
|
||||
}
|
||||
if (peakL != 0.f || peakR != 0.f || diagBlk < 10) {
|
||||
std::cerr << "[Vst2Instrument][diag] PEAK blk=" << diagBlk
|
||||
<< " n=" << numSamples << " L=" << peakL << " R=" << peakR << std::endl;
|
||||
diagBlk++;
|
||||
}
|
||||
}
|
||||
// TEMP diag: how far past n did DUNE write into the INPUT buffers?
|
||||
{
|
||||
int farL = -1, farR = -1;
|
||||
for (uint32_t i = 0; i < 2097152; ++i) {
|
||||
uint32_t bL, bR;
|
||||
std::memcpy(&bL, &inL[numSamples + i], 4);
|
||||
std::memcpy(&bR, &inR[numSamples + i], 4);
|
||||
if (bL != 0x7FC00000u) farL = (int)i;
|
||||
if (bR != 0x7FC00000u) farR = (int)i;
|
||||
}
|
||||
if (farL >= 0 || farR >= 0)
|
||||
std::cerr << "[Vst2Instrument][diag] INPUT OVERRUN past n: L=" << farL
|
||||
<< " R=" << farR << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
const void* Vst2Instrument::effect() const {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// native_bridge/src/Vst2Instrument.h
|
||||
// VST2 (VST 2.4, 64-bit) instrument host. Clean-room ABI header
|
||||
// (native_bridge/vestige/aeffect.h) — no Steinberg SDK, no LMMS code.
|
||||
// Headless: no GUI editor (openGUI returns false); preset state is kept via
|
||||
// effGetChunk/effSetChunk (fxp-style chunks).
|
||||
// GUI editor: supported for plugins with kEffectFlagsHasEditor (effEditOpen/
|
||||
// effEditClose/effEditIdle); preset state is kept via effGetChunk/effSetChunk
|
||||
// (fxp-style chunks).
|
||||
#ifndef VST2_INSTRUMENT_H
|
||||
#define VST2_INSTRUMENT_H
|
||||
|
||||
@@ -27,9 +28,11 @@ public:
|
||||
void programChange(uint32_t channel, uint32_t program) override;
|
||||
void pitchBend(uint32_t channel, uint32_t bend14) override;
|
||||
bool openGUI(void* parentWindowHandle) override;
|
||||
bool canOpenGUI() const override { return false; }
|
||||
bool canOpenGUI() const override;
|
||||
bool attachView(void* parentWindowHandle) override;
|
||||
void closeGUI() override;
|
||||
bool hasAttachedView() const override;
|
||||
void guiIdle() override;
|
||||
void captureState() override;
|
||||
void restoreState() override;
|
||||
std::string serializeState() const override;
|
||||
@@ -39,19 +42,31 @@ public:
|
||||
// Accessors used by the audioMaster callback (same translation unit's
|
||||
// friend-ish helpers) without exposing AEffect* in the header.
|
||||
double sampleRate() const { return sampleRate_; }
|
||||
const std::string& pluginDir() const { return pluginDir_; }
|
||||
uint32_t maxBlockSize() const { return maxBlockSize_; }
|
||||
const void* effect() const; // AEffect*; null until loaded
|
||||
// Host window the editor is attached to (attachView sets, closeGUI
|
||||
// clears). The audioMaster SizeWindow handler moves it via the hook.
|
||||
void* editorParent() const { return editorParent_; }
|
||||
// Register the SizeWindow hook (main.cpp posts the resize to the UI
|
||||
// worker). Probe builds never set it -> no-op.
|
||||
static void setResizeHook(void (*fn)(void* hwnd, int w, int h));
|
||||
|
||||
private:
|
||||
void* state_; // Vst2HostState* (pimpl — aeffect.h stays out of the header)
|
||||
std::string path_;
|
||||
std::string pluginDir_;
|
||||
double sampleRate_;
|
||||
uint32_t maxBlockSize_;
|
||||
bool loaded_;
|
||||
bool guiAttached_ = false; // effEditOpen done, not yet effEditClose'd
|
||||
void* editorParent_ = nullptr; // host window the editor is a child of
|
||||
uint32_t channel_ = 0;
|
||||
// scratch buffers for silent plugin inputs
|
||||
// scratch buffers for silent plugin inputs and extra output channels
|
||||
// (plugins with numOutputs > 2 need full pointer arrays)
|
||||
std::vector<float> inL_;
|
||||
std::vector<float> inR_;
|
||||
std::vector<float> extraOut_;
|
||||
};
|
||||
|
||||
#endif // VST2_INSTRUMENT_H
|
||||
|
||||
+239
-44
@@ -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
|
||||
|
||||
@@ -76,9 +76,26 @@ enum {
|
||||
kEffectClose = 1, // effClose
|
||||
kEffectSetProgram = 2, // effSetProgram
|
||||
kEffectGetProgram = 3, // effGetProgram
|
||||
kEffectSetSampleRate = 10, // effSetSampleRate
|
||||
kEffectSetBlockSize = 11, // effSetBlockSize
|
||||
kEffectSetProgramName = 4, // effSetProgramName (ptr = char* name)
|
||||
kEffectGetProgramName = 5, // effGetProgramName (ptr = char* buffer)
|
||||
kEffectGetParamLabel = 6, // effGetParamLabel (ptr = char* buffer)
|
||||
kEffectGetParamDisplay = 7, // effGetParamDisplay (ptr = char* buffer)
|
||||
kEffectGetParamName = 8, // effGetParamName (ptr = char* buffer)
|
||||
kEffectGetVu = 9, // effGetVu (deprecated; occupies the slot so
|
||||
// the core opcodes below match the SDK)
|
||||
kEffectSetSampleRate = 10, // effSetSampleRate (opt = sample rate)
|
||||
kEffectSetBlockSize = 11, // effSetBlockSize (value = block size)
|
||||
kEffectMainsChanged = 12, // effMainsChanged (value 1 = on, 0 = off)
|
||||
kEffectEditGetRect = 13, // effEditGetRect (ptr = ERect**)
|
||||
kEffectEditOpen = 14, // effEditOpen (ptr = host window HWND)
|
||||
kEffectEditClose = 15, // effEditClose
|
||||
kEffectEditDraw = 16, // effEditDraw (deprecated)
|
||||
kEffectEditMouse = 17, // effEditMouse (deprecated)
|
||||
kEffectEditKey = 18, // effEditKey (deprecated)
|
||||
kEffectEditIdle = 19, // effEditIdle
|
||||
kEffectEditTop = 20, // effEditTop (deprecated)
|
||||
kEffectEditSleep = 21, // effEditSleep (deprecated)
|
||||
kEffectIdentify = 22, // effIdentify (deprecated)
|
||||
kEffectGetChunk = 23, // effGetChunk
|
||||
kEffectSetChunk = 24, // effSetChunk
|
||||
kEffectProcessEvents = 25, // effProcessEvents
|
||||
@@ -88,6 +105,8 @@ enum {
|
||||
kEffectGetProductString = 48,
|
||||
kEffectGetVendorVersion = 49,
|
||||
kEffectCanDo = 51,
|
||||
kEffectIdle = 53, // effIdle
|
||||
kEffectSetViewPosition = 55, // effSetViewPosition
|
||||
kEffectGetVstVersion = 58,
|
||||
kEffectStartProcess = 71,
|
||||
kEffectStopProcess = 72
|
||||
@@ -110,7 +129,20 @@ enum {
|
||||
kAudioMasterGetVendorVersion = 34,
|
||||
kAudioMasterVendorSpecific = 35,
|
||||
kAudioMasterCanDo = 37,
|
||||
kAudioMasterGetLanguage = 38
|
||||
kAudioMasterGetLanguage = 38,
|
||||
kAudioMasterSizeWindow = 15, // index = width, value = height
|
||||
kAudioMasterGetDirectory = 41, // ptr = char* (VST dir); 0 = none
|
||||
kAudioMasterUpdateDisplay = 42, // request host display refresh
|
||||
kAudioMasterBeginEdit = 43, // parameter automation begin
|
||||
kAudioMasterEndEdit = 44 // parameter automation end
|
||||
};
|
||||
|
||||
// Editor rectangle returned by effEditGetRect (short coords per VST2 ABI).
|
||||
struct ERect {
|
||||
short top;
|
||||
short left;
|
||||
short bottom;
|
||||
short right;
|
||||
};
|
||||
|
||||
// MIDI event passed via effProcessEvents. The plugin receives a VstEvents
|
||||
@@ -161,6 +193,7 @@ struct VstTimeInfo {
|
||||
};
|
||||
|
||||
#define kVstMidiType 1
|
||||
#define kVstMidiEventIsRealtime 1 // VstMidiEvent::flags: live input (not sequencer playback)
|
||||
#define kEffectMagic 0x56737450 // 'VstP'
|
||||
|
||||
// VstTimeInfo::flags bits (kAudioMasterGetTime).
|
||||
|
||||
Reference in New Issue
Block a user