V45.45: fix GUI embed VST den tren box (Nectar/iZotope) - first-paint kick real click + BitBlt black-frame fallback + on-screen parking MAC DINH (SF_FXGUI_EMBED_ONSCREEN=0 de tat) + plugins.py log TAT CA stdout bridge (khong nuot dong truoc SF_FXGUI_PORT=) + auto-open GUI khi add VST FX vao Mastering slot. Rebuild daw_vst_bridge+fx_vst_bridge+installer NSIS.

This commit is contained in:
2026-09-03 07:13:21 +07:00
parent f958348a20
commit db9123438d
10 changed files with 137 additions and 63 deletions
+100 -38
View File
@@ -172,6 +172,14 @@ void sendOptions(SOCKET c) {
// frame, while the GDI BitBlt view is a stale first paint for GPU-rendered
// editors (JUCE/FAST Compressor) and never updates. BitBlt stays as fallback.
// Returns JPEG.
// Mean of the raw BGRA frame (0..255) — black-frame guard below.
static double frameMean(const unsigned char* px, int w, int h) {
const size_t n = (size_t)w * h;
unsigned long long s = 0;
for (size_t i = 0; i < n; ++i)
s += px[i * 4 + 2] + px[i * 4 + 1] + px[i * 4 + 0];
return (double)s / (3.0 * (double)n);
}
bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality,
float* outAvg = nullptr, bool* outOkB = nullptr,
bool* outOkP = nullptr) {
@@ -207,7 +215,18 @@ bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality,
// for GPU-rendered editors (JUCE/FAST Compressor) and never updates, which
// made the panel look frozen. BitBlt stays only as a fallback.
unsigned char* px = nullptr;
if (okP) px = pxP.data();
if (okP && okB) {
// Black-frame guard: PrintWindow can return TRUE with an ALL-BLACK
// frame while the editor has not painted yet (iZotope-class, e.g.
// Nectar — those editors paint only after their window was activated;
// the /show first-paint kick fixes that, but until it fires BitBlt is
// the only source of real pixels). If PrintWindow is pure black and
// the GDI view has content, send the GDI view instead of a black panel.
const double mP = frameMean(pxP.data(), w, h);
const double mB = frameMean(pxB.data(), w, h);
if (mP < 6.0 && mB > 6.0) px = pxB.data();
else px = pxP.data();
} else if (okP) px = pxP.data();
else if (okB) px = pxB.data();
if (outOkB) *outOkB = okB;
if (outOkP) *outOkP = okP;
@@ -277,15 +296,26 @@ POINT g_savedCursor = {0, 0}; // cursor to restore after the dance
// rect = legacy cursor-relative mode.
RECT g_embedRect = {0, 0, 0, 0};
bool g_anchored = false; // window anchored over panel (stays until /close)
// iZotope-class editors (Nectar/Ozone) paint their surface ONLY after the
// window was actually visible AND activated at least once; parked HWND_BOTTOM
// under the DAW they never paint -> black forever. /show performs a one-time
// kick (raise above the DAW + invalidate + real click), then demotes the
// window back under the DAW — the painted surface survives the demote.
// Reset on /hide so each reopen re-kicks.
std::atomic<bool> g_paintKicked{false};
// SF_FXGUI_EMBED_ONSCREEN=1: park the host window ON a monitor at the embed
// panel rect (HWND_BOTTOM, under the DAW -> user never sees it) instead of
// off-screen at -20000. DWM does not redirect windows parked fully off the
// desktop on some drivers (Intel UHD 630 + iZotope GL editor) -> PrintWindow
// captures only black. On a monitor (even occluded by the DAW above) DWM
// keeps a redirection surface -> /frame gets real pixels. Real-input dance
// still lifts the window to HWND_TOP for actual clicks (g_anchored guard).
static const bool g_embedOnscreen = std::getenv("SF_FXGUI_EMBED_ONSCREEN") != nullptr;
// On-screen embed parking (DEFAULT since V45.45): park the host window ON a
// monitor at the embed panel rect (HWND_BOTTOM, under the DAW -> user never
// sees it) instead of off-screen at -20000. DWM does not redirect windows
// parked fully off the desktop on some drivers (Intel UHD 630 + iZotope GL
// editor) -> capture sees only black. On a monitor (even occluded by the DAW
// above) DWM keeps a redirection surface -> /frame gets real pixels. Real-input
// dance still lifts the window to HWND_TOP for actual clicks (g_anchored
// guard). SF_FXGUI_EMBED_ONSCREEN=0 disables (env kept for debugging).
static const bool g_embedOnscreen = [] {
const char* v = std::getenv("SF_FXGUI_EMBED_ONSCREEN");
return v == nullptr || v[0] != '0';
}();
// SF_FXGUI_DEBUG=1: rate-limited /frame capture ground truth ([FxGuiDbg] lines).
static const bool g_fxGuiDbg = std::getenv("SF_FXGUI_DEBUG") != nullptr;
@@ -522,8 +552,8 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
// to show that blank frame as a white flash on open.
sendResponse(c, 503, "application/json", "{\"error\":\"loading\"}", 19);
} else {
// EMBED_ONSCREEN: keep the host window ON a monitor at the
// embed panel rect (HWND_BOTTOM, under the DAW) so DWM
// On-screen embed parking: keep the host window ON a monitor
// at the embed panel rect (HWND_BOTTOM, under the DAW) so DWM
// redirects the editor's GPU surface and capture sees real
// pixels — the one-shot rect /show may have run pre-attach
// (deduped), so reconcile here on every poll until it fits.
@@ -609,6 +639,7 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
g_anchored = false;
g_dance = false;
g_parkUntil = 0;
g_paintKicked = false; // next /show re-kicks the first paint
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
} else if (req.path == "/show") {
// Optional body {rect:{x,y,w,h}} (screen px) — anchored placement
@@ -635,43 +666,74 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
}
}
if (sroot) std::free(sroot);
// Embedded mode: the native window NEVER comes on-screen — it stays
// parked off-screen at -20000,-20000 (GDI capture + synthetic input
// work regardless of position). The panel rect sizes the host
// window + plugin view so the capture frame matches the embed panel
// (layout bug); /hide hid the window, so re-show it off-screen so
// /frame keeps capturing.
if (rectOk) {
// Embedded mode: park the host window ON a monitor at the embed
// panel rect (HWND_BOTTOM, under the DAW -> user never sees it)
// instead of off-screen at -20000. iZotope-class editors never
// paint while fully off-screen, and DWM does not redirect windows
// parked off the desktop on some drivers (Intel UHD 630) — on a
// monitor (even occluded by the DAW above) capture sees real
// pixels. The panel rect also sizes the host window + plugin view
// so the capture frame matches the embed panel (layout bug);
// /hide hid the window, so re-show it here.
// Skip while the editor is still attaching (pump thread busy;
// SetWindowPos from this worker would block until load/attach
// finishes) and while anchored/dancing (real-input owns the
// window position during clicks).
if (g_embedOnscreen && rectOk && g_fxGuiEditorReady.load() &&
!g_anchored && !g_dance) {
const int cw = g_embedRect.right - g_embedRect.left;
const int ch = g_embedRect.bottom - g_embedRect.top;
RECT cur = {};
GetWindowRect(hwnd, &cur);
// Skip when already the right size: frontend re-POSTs /show
const bool samePlace =
cur.left == g_embedRect.left && cur.top == g_embedRect.top &&
cur.right - cur.left == cw && cur.bottom - cur.top == ch;
// Skip when already parked right: frontend re-POSTs /show
// every ~500ms; re-laying-out the plugin view every tick is
// wasteful (WM_FXGUI_RESIZE -> checkSizeConstraint/onSize).
if (cur.right - cur.left != cw || cur.bottom - cur.top != ch)
fxGuiRequestResize(hwnd, cw, ch);
}
// EMBED_ONSCREEN: park the window ON the monitor at the embed
// panel rect, HWND_BOTTOM (below the DAW -> user never sees it)
// so DWM redirects the editor's GPU surface and /frame can
// capture it. Skip while the editor is still attaching (the
// pump thread is busy; SetWindowPos from here would block this
// worker until load/attach finishes) and while anchored/dancing
// (real-input keeps the window on top over the panel for clicks).
if (g_embedOnscreen && rectOk && !g_anchored && !g_dance &&
g_fxGuiEditorReady.load()) {
const int cw = g_embedRect.right - g_embedRect.left;
const int ch = g_embedRect.bottom - g_embedRect.top;
RECT cur = {};
GetWindowRect(hwnd, &cur);
if (!IsWindowVisible(hwnd) || cur.left != g_embedRect.left ||
cur.top != g_embedRect.top || cur.right - cur.left != cw ||
cur.bottom - cur.top != ch)
if (!samePlace || !IsWindowVisible(hwnd))
SetWindowPos(hwnd, HWND_BOTTOM, g_embedRect.left,
g_embedRect.top, cw, ch,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
if (cur.right - cur.left != cw || cur.bottom - cur.top != ch)
fxGuiRequestResize(hwnd, cw, ch);
// First-paint kick (one-time per /show cycle, only after the
// editor attached). iZotope-class editors (Nectar/Ozone) paint
// ONLY when their window is actually visible AND activated:
// parked HWND_BOTTOM under the DAW they never paint -> black
// forever, and SetForegroundWindow from this background process
// is blocked by the Windows foreground lock. So: raise the
// window ABOVE the DAW at the panel rect, paint it there, then
// demote back under the DAW — the painted surface survives the
// demote (capture stays non-black at HWND_BOTTOM).
if (!g_paintKicked) {
g_paintKicked = true;
SetWindowPos(hwnd, HWND_TOP, g_embedRect.left, g_embedRect.top,
cw, ch, SWP_SHOWWINDOW | SWP_NOACTIVATE);
ShowWindow(hwnd, SW_SHOW);
InvalidateRect(hwnd, nullptr, TRUE);
RedrawWindow(hwnd, nullptr, nullptr,
RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN);
// Foreground-lock bypass: a real synthesized click (system
// input, not subject to the SetForegroundWindow lock)
// activates the window the same way a user click would.
Sleep(200);
RECT wr = {};
GetWindowRect(hwnd, &wr);
const int cx = wr.left + (wr.right - wr.left) / 2;
const int cy = wr.top + 8; // title-bar strip, avoid knobs
SetCursorPos(cx, cy);
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
Sleep(500); // give the editor a beat to paint while on top
SetWindowPos(hwnd, HWND_BOTTOM, g_embedRect.left, g_embedRect.top,
cw, ch, SWP_NOACTIVATE);
std::cerr << "[FxGui] first-paint kick fg=click @" << cx
<< "," << cy << std::endl;
}
} else if (!IsWindowVisible(hwnd)) {
// No usable rect yet (pre-layout / still attaching): keep the
// window somewhere harmless until the frontend supplies it.
SetWindowPos(hwnd, HWND_NOTOPMOST, -20000, -20000, 0, 0,
SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
}
+4 -1
View File
@@ -1910,7 +1910,10 @@ int run_fx_gui_server(const std::string& jobPath) {
// crash fix (attach on a real monitor) and the black-capture fix (stay
// on a monitor afterwards, parked under the DAW at the panel rect) go
// together — setting only EMBED_ONSCREEN must not regress the crash.
const bool embedOnscreen = std::getenv("SF_FXGUI_EMBED_ONSCREEN") != nullptr;
// V45.45: on-screen embed parking la MAC DINH (DWM Intel UHD 630 khong
// redirect window parked -20000). Env chi con de TAT: SF_FXGUI_EMBED_ONSCREEN=0.
const char* eosEnv = std::getenv("SF_FXGUI_EMBED_ONSCREEN");
const bool embedOnscreen = eosEnv == nullptr || eosEnv[0] != '0';
const bool attachOnscreen = embedOnscreen || std::getenv("SF_FXGUI_ATTACH_ONSCREEN") != nullptr;
std::cout << "[FxGui] env SF_FXGUI_ATTACH_ONSCREEN=" << (attachOnscreen ? 1 : 0)
<< " SF_FXGUI_EMBED_ONSCREEN=" << (embedOnscreen ? 1 : 0)