fix GUI VSTi trắng/không load: DPI per-monitor v2 + WS_CLIPCHILDREN + view->onSize() sau attached; restore API manager header/cpp (2596372 xoá nhầm) và deploy exe mới

This commit is contained in:
2026-08-15 16:12:29 +07:00
parent 90241f626f
commit 0cec3d3cce
5 changed files with 127 additions and 1 deletions
Binary file not shown.
@@ -8,6 +8,7 @@
#include <map>
#include <mutex>
#include <functional>
#include <memory>
#include <string>
#include <vector>
@@ -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<bool(const std::string& lowerPath)> fn);
// Flush every sounding note on every assigned channel.
void allNotesOff();
@@ -103,6 +127,13 @@ private:
std::map<uint32_t, std::string> paths_; // last assigned path per channel
// Per-instrument scratch so engines that overwrite (not mix) stay additive.
std::vector<float> 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<bool(const std::string&)> editorOpenFn_;
};
#endif // NATIVE_INSTRUMENT_ENGINE_H
@@ -9,6 +9,8 @@
#define FS_SYNTH (static_cast<fluid_synth_t*>(synth))
#define FS_SETTINGS (static_cast<fluid_settings_t*>(settings))
#include <algorithm>
#include <cctype>
#include <cstring>
#include <iostream>
@@ -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<std::mutex> 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<std::mutex> lock(mu_);
return channel < 16 && reloadingCh_[channel];
}
void InstrumentEngineManager::setEditorOpenPredicate(std::function<bool(const std::string&)> fn) {
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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);
+8
View File
@@ -555,10 +555,12 @@ bool Vst3Instrument::attachView(void* parentWindowHandle) {
auto* s = static_cast<Vst3HostState*>(state_);
if (!s || !s->controller || !parentWindowHandle) return false;
IPlugView* rawView = nullptr;
s->controller->queryInterface(IPlugView::iid, (void**)&rawView);
FUnknownPtr<IPlugView> 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<IPlugView>(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;
+7 -1
View File
@@ -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