From 6efef85e479e130d889ef05562b5911749240a16 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Wed, 19 Aug 2026 20:57:16 +0700 Subject: [PATCH] =?UTF-8?q?fix(bridge):=20VST2=20host=20dung=20ABI=20SDK?= =?UTF-8?q?=20=E2=80=94=20crash=20outs>2,=20NaN=20input,=20editOpen(NULL),?= =?UTF-8?q?=20opcodes;=20docs=20A14=20MIDI-out=20+=20phan=20tich=20Ozone?= =?UTF-8?q?=20-Inf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- PLAN_MASTERBUS_FX_RACK_VST.md | 25 +- TASKS.md | 2 + native_bridge/include/INativeInstrument.h | 3 + .../include/NativeInstrumentEngine.h | 4 + native_bridge/src/NativeInstrumentEngine.cpp | 62 +++- native_bridge/src/Vst2Instrument.cpp | 346 +++++++++++++++++- native_bridge/src/Vst2Instrument.h | 23 +- native_bridge/src/main.cpp | 283 +++++++++++--- native_bridge/vestige/aeffect.h | 39 +- 9 files changed, 714 insertions(+), 73 deletions(-) diff --git a/PLAN_MASTERBUS_FX_RACK_VST.md b/PLAN_MASTERBUS_FX_RACK_VST.md index bbd11e1..2558be0 100644 --- a/PLAN_MASTERBUS_FX_RACK_VST.md +++ b/PLAN_MASTERBUS_FX_RACK_VST.md @@ -178,7 +178,30 @@ bridge). khác rõ so với không chain, không clip. - Chạy full test suite hiện có — không phá 114 tests. -## 6. License +## 6. Kết quả phân tích Ozone -Inf input (2026-08-18) + +Verify code toàn chuỗi realtime master VST3 FX chain — **đã đủ, không thiếu setActive/setProcessing**: + +| Chặng | File | Trạng thái | +|---|---|---| +| Frontend gửi chain | `app/static/js/app.jsx:12732-12734` + `app.precompiled.js:803` — `useEffect` trên `ozState.vstFxChain` → `NativeBridgeService.setFxChain(JSON)` | ✅ Có | +| Rust | `src-tauri/src/lib.rs:862-872` `set_fx_chain` → SHM control `type=6` | ✅ Có | +| C++ nhận | `main.cpp:1567-1568` `type==6` → `g_fxChain->setChain(json, sr, block)` | ✅ Có | +| Load plugin | `RenderFxJob.cpp` `vst3FxLoadInner` (397-470): activateBus toàn bộ, `setupProcessing(kRealtime,kSample32)`, `setActive(true)` (454), `setProcessing(true)` (455), `processData.prepare` | ✅ Đủ | +| Process | `RealtimeFxChain::process` (audio thread, in-place trên ring master, SEH quanh từng plugin) + `Vst3Fx::processAudio` (copy input → zero output → process → copy ra) | ✅ Đúng | +| processContext | tempo 120, sampleRate, kPlaying, projectTimeSamples tăng mỗi block | ✅ Có | + +→ 4 giả thuyết user: #3 (thiếu setActive/setProcessing) **SAI** — đã có đủ. +**Nguyên nhân -Inf thực tế: audio DAW không chảy qua bridge.** Chain chạy trên +`shmIPC->ringLeft/ringRight[slot]` — master mix CHỈ chứa audio từ instrument +trong bridge. DAW master bus (WebAudio graph: audio track, SonicSF fallback, +non-bridge source) KHÔNG vào SHM → Ozone nhận silence → input -Inf. +→ Hướng sửa: (a) pipe DAW master bus audio vào bridge (SHM audio-in mới) để +chain xử lý đúng master thật, hoặc (b) chạy chain trên WebAudio graph; hoặc +(c) chỉ dùng bridge chain cho phần bridge-instrument. Chưa quyết — ghi nhận +để bước tiếp. + +## 7. License - Ozone (iZotope/Native Instruments) + Scaler (PluginBoutique/Scale Software): plugin **thương mại** — KHÔNG nhúng, chỉ host bằng trình load của diff --git a/TASKS.md b/TASKS.md index 888cad9..fb6f46e 100644 --- a/TASKS.md +++ b/TASKS.md @@ -152,6 +152,7 @@ Các task nhóm H lấp những khâu còn hở giữa A-G để pipeline chạy | ✅ A11 | `native_bridge/src/main.cpp` | **[BỔ SUNG]** Sample-accurate: không xử lý MIDI ngay; buffer event theo `sampleOffset`, rẽ block 256 thành các đoạn theo offset, gọi `noteOn/Off` đúng vị trí trước khi render đoạn đó | note timeline không lệch nhịp (>±1 sample test) | | ✅ A12 | `native_bridge/include/SharedMemoryIPC.h` + engine | **[BỔ SUNG]** Mở rộng `MidiEventIPC`: field `data2` (+`data3` PB MSB); hỗ trợ `0xB CC` (FluidSynth `fluid_synth_cc`), `0xC program change`, `0xE pitch bend` (`fluid_synth_pitch_bend`); VST3 → tham số tương ứng | sustain/CC/pitchbend qua bridge hoạt động | | ✅ A13 | `native_bridge/src/main.cpp` | **[BỔ SUNG]** Control `type=3 TRANSPORT`: `arg0=0 STOP` → flush note-off toàn channel + `fluid_synth_all_notes_off`; `arg0=1 PLAY` kèm `arg1=playheadSamples` → đồng bộ timeline | Stop → hết âm ngân tức thì | +| A14 | `native_bridge/src/main.cpp` + `Vst2Instrument.cpp`/`Vst3Instrument.cpp` + `SharedMemoryIPC.h` + `src-tauri/src/lib.rs` + `nativeBridgeService.js` | **[BỔ SUNG]** MIDI-out từ plugin: bridge hiện CHỈ gửi MIDI vào (kAudioMasterWantMidi=1, processEvents) — không đọc MIDI plugin xuất ra. MIDI-generator (Scaler2/Instacomposer/MelodicFlow) xuất chord/MIDI không phải audio → câm trong test audio. Cần: plugin gọi audioMaster → host thu VstEvents (VST2 `audioMaster` callback `audioMasterGetVstVersion`+event queue; VST3 `IEventList` output bus) → push vào SHM midiQueue mới (out) → Rust emit → JS route về DAW track | plugin MIDI-gen xuất event → DAW nhận note (test Scaler2 gõ chord → MIDI ghi nhận) | ### H-B. Rust — bổ sung @@ -243,5 +244,6 @@ VU analyser tap (C5) → track VU nhảy - Resampler khi SR AudioContext ≠ bridge (B8/C5). - Ring buffer chống underrun + VU tap (C5) — kế thừa hành vi `_gainNode.__vuAnalyser` cũ. - Fallback SonicSF (C1/C6/C7/G6) — spec không đề cập; giữ để app không chết khi bridge lỗi. +- **MIDI-out từ plugin (A14)** — bridge chỉ host MIDI-in; Scaler2/Instacomposer/MelodicFlow xuất MIDI nên không nghe được qua bridge. - Dev-mode mock (C7) — chạy được trên Linux/browser không có `__TAURI__`. diff --git a/native_bridge/include/INativeInstrument.h b/native_bridge/include/INativeInstrument.h index 23cdf1d..20a002f 100644 --- a/native_bridge/include/INativeInstrument.h +++ b/native_bridge/include/INativeInstrument.h @@ -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). diff --git a/native_bridge/include/NativeInstrumentEngine.h b/native_bridge/include/NativeInstrumentEngine.h index 9758d70..498511d 100644 --- a/native_bridge/include/NativeInstrumentEngine.h +++ b/native_bridge/include/NativeInstrumentEngine.h @@ -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 lock(mu_); return channels_.size(); } void setBypassed(bool on) { isBypassed_.store(on); } diff --git a/native_bridge/src/NativeInstrumentEngine.cpp b/native_bridge/src/NativeInstrumentEngine.cpp index 6adff53..b39adac 100644 --- a/native_bridge/src/NativeInstrumentEngine.cpp +++ b/native_bridge/src/NativeInstrumentEngine.cpp @@ -23,6 +23,9 @@ #include #include #include +#ifdef _WIN32 +#include // _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& 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(v.data()); return reinterpret_cast((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 lock(mu_); + for (auto& kv : channels_) { + kv.second->guiIdle(); + } +} diff --git a/native_bridge/src/Vst2Instrument.cpp b/native_bridge/src/Vst2Instrument.cpp index a15c601..748a89b 100644 --- a/native_bridge/src/Vst2Instrument.cpp +++ b/native_bridge/src/Vst2Instrument.cpp @@ -25,6 +25,11 @@ std::unordered_map 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 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 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(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(""); + 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(&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(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(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(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(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(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& 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(v.data()); return reinterpret_cast((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 inBufs(nIn > 0 ? nIn : 1, nullptr); + std::vector 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 { diff --git a/native_bridge/src/Vst2Instrument.h b/native_bridge/src/Vst2Instrument.h index 325c47f..008d812 100644 --- a/native_bridge/src/Vst2Instrument.h +++ b/native_bridge/src/Vst2Instrument.h @@ -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 inL_; std::vector inR_; + std::vector extraOut_; }; #endif // VST2_INSTRUMENT_H diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index 85f0235..7bf9458 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -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 #include #include +#include +#include #include #else #include @@ -117,6 +120,20 @@ static std::mutex g_guiMutex; static std::map g_guiWindows; // channel -> HWND (keep window alive) static std::map 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> g_mainDeferred; +static void runOnMain(std::function job) { + std::lock_guard 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 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 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( + [&instruments, t, ch, path, preset, bank, program, sampleRate, block]() { + std::lock_guard 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 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( + [&instruments, t, ch, path, sampleRate, block, spOld, spNew]() { + std::lock_guard 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> deferred; + { + std::lock_guard 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 diff --git a/native_bridge/vestige/aeffect.h b/native_bridge/vestige/aeffect.h index 713d9c5..99d6109 100644 --- a/native_bridge/vestige/aeffect.h +++ b/native_bridge/vestige/aeffect.h @@ -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).