diff --git a/install/daw_vst_bridge.exe b/install/daw_vst_bridge.exe index 0a02019..40576e7 100644 Binary files a/install/daw_vst_bridge.exe and b/install/daw_vst_bridge.exe differ diff --git a/native_bridge/include/NativeInstrumentEngine.h b/native_bridge/include/NativeInstrumentEngine.h index f5b716e..8dfa266 100644 --- a/native_bridge/include/NativeInstrumentEngine.h +++ b/native_bridge/include/NativeInstrumentEngine.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -72,6 +73,19 @@ public: INativeInstrument* get(uint32_t channel); + // Real-time MIDI dispatch (audio loop thread). Each call holds mu_ for the + // WHOLE call — lookup + reloading check + instrument call under one lock — + // so a worker thread's assign()/unload() can never swap the map and destroy + // the old instance while dispatch is inside a method on it (use-after-free + // when loading a new VSTi while others play). Events for unassigned or + // reloading channels are dropped (a mid-reload instance must not be touched + // while its worker rebuilds state_ in place). + void noteOn(uint32_t channel, uint32_t pitch, float velocity); + void noteOff(uint32_t channel, uint32_t pitch); + void controlChange(uint32_t channel, uint32_t cc, uint32_t value); + void programChange(uint32_t channel, uint32_t program); + void pitchBend(uint32_t channel, uint32_t bend14); + // Remove and destroy the instrument on `channel` (its destructor may call // VST terminate — MUST run on the channel worker thread, caller's duty). // Used by Option B: two live instances of the same plugin DLL (Nexus) @@ -87,6 +101,16 @@ public: // teardown never races the audio thread. void setReloading(uint32_t channel, bool on); + // True while a channel is being rebuilt (instance teardown/reload on its + // worker thread): real-time code drops MIDI for it. Channel-level flag — + // survives the instance swap (the per-instance flag dies with the object). + bool isReloading(uint32_t channel) const; + // Editor-open predicate: while any VST editor is attached, channels whose + // plugin DLL path has an open editor count as quiet (same-plugin channels + // too -- the worker pumps the editor's window proc inside the DLL while the + // audio loop process()es other instances of the same DLL). + void setEditorOpenPredicate(std::function fn); + // Flush every sounding note on every assigned channel. void allNotesOff(); @@ -103,6 +127,13 @@ private: std::map paths_; // last assigned path per channel // Per-instrument scratch so engines that overwrite (not mix) stay additive. std::vector scratchL_, scratchR_; + // Channel-level reload flag (see isReloading) — indexed by MIDI channel. + bool reloadingCh_[16] = {}; + // True when the channel's plugin DLL has an attached editor (or its reload + // is in progress): real-time code drops MIDI and skips rendering. Lowercased + // compare inside -- Windows plugin paths are case-insensitive. + bool channelQuiet(uint32_t ch) const; + std::function editorOpenFn_; }; #endif // NATIVE_INSTRUMENT_ENGINE_H diff --git a/native_bridge/src/NativeInstrumentEngine.cpp b/native_bridge/src/NativeInstrumentEngine.cpp index b2b6a11..d876c72 100644 --- a/native_bridge/src/NativeInstrumentEngine.cpp +++ b/native_bridge/src/NativeInstrumentEngine.cpp @@ -9,6 +9,8 @@ #define FS_SYNTH (static_cast(synth)) #define FS_SETTINGS (static_cast(settings)) +#include +#include #include #include @@ -187,6 +189,10 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type, if (it != channels_.end()) oldInst = std::move(it->second); channels_[channel] = std::move(inst); paths_[channel] = path; + // Fresh instance is by construction not reloading — clear the channel + // flag so real-time MIDI dispatch (which drops reloading channels) + // flows to it again. + reloadingCh_[channel] = false; } oldInst.reset(); return true; @@ -203,6 +209,7 @@ void InstrumentEngineManager::unload(uint32_t channel) { if (it != channels_.end()) oldInst = std::move(it->second); channels_.erase(channel); paths_.erase(channel); + reloadingCh_[channel] = false; } oldInst.reset(); } @@ -221,13 +228,86 @@ INativeInstrument* InstrumentEngineManager::get(uint32_t channel) { void InstrumentEngineManager::setReloading(uint32_t channel, bool on) { std::lock_guard lock(mu_); + if (channel < 16) reloadingCh_[channel] = on; auto it = channels_.find(channel); if (it != channels_.end()) it->second->setReloading(on); } +bool InstrumentEngineManager::isReloading(uint32_t channel) const { + std::lock_guard lock(mu_); + return channel < 16 && reloadingCh_[channel]; +} + +void InstrumentEngineManager::setEditorOpenPredicate(std::function fn) { + std::lock_guard lock(mu_); + editorOpenFn_ = std::move(fn); +} + +// Quiet = reloading OR an editor attached for the channel's plugin DLL path. +// Called with mu_ held by the real-time dispatch (the predicate locks +// g_editorMutex -- lock order mu_ -> g_editorMutex; g_editorMutex is never +// held while taking mu_). +bool InstrumentEngineManager::channelQuiet(uint32_t ch) const { + if (ch >= 16 || reloadingCh_[ch]) return true; + if (!editorOpenFn_) return false; + auto it = paths_.find(ch); + if (it == paths_.end() || it->second.empty()) return false; + std::string lp = it->second; + std::transform(lp.begin(), lp.end(), lp.begin(), + [](unsigned char c) { return (char)::tolower(c); }); + return editorOpenFn_(lp); +} + +// Real-time MIDI dispatch: hold mu_ for the WHOLE call so assign()/unload() +// (map swap + old-instance destruction outside the lock) and reload()/reloadForGUI() +// (state_ deleted in place on the worker) can never destroy/free the instance +// while dispatch is inside a method on it — use-after-free when loading a new +// VSTi while other channels keep playing. Reloading channels are skipped: their +// instance is mid-teardown and must not be touched. +void InstrumentEngineManager::noteOn(uint32_t channel, uint32_t pitch, float velocity) { + std::lock_guard lock(mu_); + if (channelQuiet(channel)) return; + auto it = channels_.find(channel); + if (it == channels_.end()) return; + it->second->noteOn(channel, pitch, velocity, 0); +} + +void InstrumentEngineManager::noteOff(uint32_t channel, uint32_t pitch) { + std::lock_guard lock(mu_); + if (channelQuiet(channel)) return; + auto it = channels_.find(channel); + if (it == channels_.end()) return; + it->second->noteOff(channel, pitch, 0); +} + +void InstrumentEngineManager::controlChange(uint32_t channel, uint32_t cc, uint32_t value) { + std::lock_guard lock(mu_); + if (channelQuiet(channel)) return; + auto it = channels_.find(channel); + if (it == channels_.end()) return; + it->second->controlChange(channel, cc, value); +} + +void InstrumentEngineManager::programChange(uint32_t channel, uint32_t program) { + std::lock_guard lock(mu_); + if (channelQuiet(channel)) return; + auto it = channels_.find(channel); + if (it == channels_.end()) return; + it->second->programChange(channel, program); +} + +void InstrumentEngineManager::pitchBend(uint32_t channel, uint32_t bend14) { + std::lock_guard lock(mu_); + if (channelQuiet(channel)) return; + auto it = channels_.find(channel); + if (it == channels_.end()) return; + it->second->pitchBend(channel, bend14); +} + void InstrumentEngineManager::allNotesOff() { std::lock_guard lock(mu_); for (auto& [ch, inst] : channels_) { + if (channelQuiet(ch)) continue; // mid-rebuild / editor open: do not touch the instance for (uint32_t n = 0; n < 128; ++n) inst->noteOff(ch, n, 0); } } @@ -242,6 +322,7 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t scratchR_.resize(numSamples); } for (auto& [ch, inst] : channels_) { + if (channelQuiet(ch)) continue; // editor open on this plugin DLL: do not process std::memset(scratchL_.data(), 0, numSamples * sizeof(float)); std::memset(scratchR_.data(), 0, numSamples * sizeof(float)); inst->processAudioBlock(scratchL_.data(), scratchR_.data(), numSamples); diff --git a/native_bridge/src/Vst3Instrument.cpp b/native_bridge/src/Vst3Instrument.cpp index 92cfb78..e603533 100644 --- a/native_bridge/src/Vst3Instrument.cpp +++ b/native_bridge/src/Vst3Instrument.cpp @@ -555,10 +555,12 @@ bool Vst3Instrument::attachView(void* parentWindowHandle) { auto* s = static_cast(state_); if (!s || !s->controller || !parentWindowHandle) return false; IPlugView* rawView = nullptr; + s->controller->queryInterface(IPlugView::iid, (void**)&rawView); FUnknownPtr view(rawView); if (!view) { // Mot so plugin khong expose IPlugView tren edit controller; thu component. IPlugView* rawViewC = nullptr; + s->component->queryInterface(IPlugView::iid, (void**)&rawViewC); view = FUnknownPtr(rawViewC); } if (!view) { @@ -566,7 +568,9 @@ bool Vst3Instrument::attachView(void* parentWindowHandle) { // JUCE-based plugins (Scaler2) expose the editor only this way. view = owned(s->controller->createView(Steinberg::Vst::ViewType::kEditor)); } + if (!view) return false; view->setFrame(&s->plugFrame); + view->isPlatformTypeSupported(kPlatformTypeHWND); tresult ta = view->attached(parentWindowHandle, kPlatformTypeHWND); if (ta != kResultOk) return false; #ifdef _WIN32 @@ -578,6 +582,10 @@ bool Vst3Instrument::attachView(void* parentWindowHandle) { RECT r = { 0, 0, w, h }; AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE); SetWindowPos(hwnd, nullptr, 0, 0, r.right - r.left, r.bottom - r.top, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); + // Tell the plugin the client size right after attach — some editors + // (Skia/OpenGL-based) skip their first paint until onSize arrives, + // leaving a blank/white surface. + view->onSize(&rect); } #endif s->view = view; diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index ac8122a..7470ddb 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -222,7 +222,7 @@ static void* create_native_vst_window(const char* title) { registered = true; } HWND hwnd = CreateWindowExA(0, kWndClass, title ? title : "VST", - WS_OVERLAPPEDWINDOW | WS_VISIBLE, + WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, nullptr, nullptr, GetModuleHandleA(nullptr), nullptr); return (void*)hwnd; @@ -672,6 +672,12 @@ static void post_close_gui(uint32_t ch, HWND hwnd) { int main(int argc, char* argv[]) { std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl; #ifdef _WIN32 + // DPI: declare per-monitor v2 FIRST (before any window is created). + // Without it, on scaled displays (125/150%) the plugin's own renderer + // paints at 100% into an OS-scaled window -> blank/white panel with dark + // strips (reported "GUI trắng, không load"). Runtime call works even + // without an exe manifest. Ignore failure (already aware). + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); // Default IMC = none on the main thread (IME recursion fix). ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT); // Catch JUCE_* window creation and strip its IMC at birth. Global hooks