fix(bridge): first-paint kick real click — iZotope GUI embed hien thi that

This commit is contained in:
2026-09-02 11:44:16 +07:00
parent d3094a3ba5
commit f793ebce9b
2 changed files with 93 additions and 13 deletions
+84 -13
View File
@@ -49,6 +49,9 @@ extern void fxGuiRequestCapture(void* hwnd, std::string& outB64, int timeoutMs);
// Tell RenderFxJob's SHM audio feeder which realtime session is live (the
// frontend pushes the session name; "" = session stopped/restarted).
extern void fxGuiSetShm(const std::string& name);
// True once the VST editor view is attached (RenderFxJob.cpp) — /show must
// not run its first-paint kick before the editor exists.
extern bool fxGuiEditorAttached();
namespace {
@@ -163,6 +166,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 luma (0..255) of the raw BGRA frame — 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) {
RECT rc;
if (!GetClientRect(hwnd, &rc)) return false;
@@ -195,8 +206,18 @@ bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality)
// window (child + GPU surfaces). The GDI BitBlt view is a stale first paint
// for GPU-rendered editors (JUCE/FAST Compressor) and never updates, which
// made the panel look frozen. BitBlt stays only as a fallback.
// Black-frame guard: PrintWindow returns TRUE with an ALL-BLACK frame for
// editors that have not painted yet (iZotope-class, e.g. Nectar — the
// /show first-paint kick fixes the window, but until then BitBlt is the
// only source of real pixels). When PrintWindow is pure black and the GDI
// view has content, send the GDI view instead of a black panel.
unsigned char* px = nullptr;
if (okP) px = pxP.data();
if (okP && okB) {
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();
SelectObject(mem, oldBmp);
DeleteObject(bmp);
@@ -251,6 +272,11 @@ std::atomic<bool> g_realInput{true}; // always real hardware input
std::atomic<bool> g_dance{false}; // window temporarily on-screen/topmost
std::atomic<DWORD> g_parkUntil{0}; // safety restore deadline (GetTickCount)
POINT g_savedCursor = {0, 0}; // cursor to restore after the dance
// iZotope-class editors only paint their surface once the window has been
// activated (SetForegroundWindow) at least once; parked off-screen they stay
// black forever. /show performs a one-time kick (on-screen + foreground +
// invalidate), then demotes the window under the DAW. Reset on /hide.
std::atomic<bool> g_paintKicked{false};
// Anchored mode: the frontend sends the embed panel's screen rect (physical
// px) with every /input, so the real-input dance places the plugin window
// exactly over the panel -- cursor never moves, no park/restore churn. Zero
@@ -508,6 +534,7 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
ShowWindow(hwnd, SW_HIDE);
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
@@ -534,26 +561,70 @@ 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: the native window stays parked ON-SCREEN under
// the DAW (HWND_BOTTOM at the embed panel rect — invisible, the
// DAW window covers it). iZotope-class editors (Nectar) NEVER
// paint while off-screen: both PrintWindow and BitBlt stay pure
// black, so the panel showed "can't load GUI". On-screen the
// editor paints once (first-paint kick below) and the capture
// surface keeps content. 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.
if (rectOk && !g_anchored) {
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
// every ~500ms; re-laying-out the plugin view every tick is
// wasteful (WM_FXGUI_RESIZE -> checkSizeConstraint/onSize).
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 (!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);
}
if (!IsWindowVisible(hwnd))
if (!g_paintKicked && fxGuiEditorAttached()) {
// First-paint kick (one-time per /show cycle). iZotope-class
// editors (Nectar) paint ONLY when their window is actually
// visible (not covered) AND activated: parked HWND_BOTTOM
// under the DAW the editor never paints -> black forever,
// and SetForegroundWindow from this background process is
// blocked by the Windows foreground lock (returns FALSE).
// So: raise the window ABOVE the DAW (invisible blink — the
// DAW covers the panel rect anyway), paint it there, then
// demote back under the DAW; the painted surface survives
// the demote (verified: capture stays non-black at HWND_BOTTOM).
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.
// iZotope-class editors paint only after such an activation.
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 (!rectOk && !IsWindowVisible(hwnd)) {
SetWindowPos(hwnd, HWND_NOTOPMOST, -20000, -20000, 0, 0,
SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
}
// Dance/anchor state is owned by the input relay — /show must not
// yank the window off-screen (real input is always on).
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
+9
View File
@@ -1519,6 +1519,11 @@ namespace {
const char* kFxGuiWndClass = "SonicForge_FX_GUI_Class";
// Set once the editor view is attached (fxGuiLoadAttach success) — the
// FxGuiServer /show handler skips its one-time first-paint kick until the
// editor exists, so the kick cannot fire on a bare host window.
std::atomic<bool> g_fxGuiEditorAttached{false};
struct FxGuiCtx {
bool closing = false;
};
@@ -1658,6 +1663,9 @@ void fxGuiRequestResize(HWND hwnd, int w, int h) {
PostMessageA(hwnd, WM_FXGUI_RESIZE, (WPARAM)w, (LPARAM)h);
}
// Editor-attach state for the /show first-paint kick (FxGuiServer.cpp).
bool fxGuiEditorAttached() { return g_fxGuiEditorAttached.load(); }
// Capture the GUI plugin state on the pump thread; blocks up to timeoutMs.
void fxGuiRequestCapture(void* hwndV, std::string& outB64, int timeoutMs) {
HWND hwnd = (HWND)hwndV;
@@ -1889,6 +1897,7 @@ static int fxGuiLoadAttach(Vst3Fx* fx, const std::string& path, const std::strin
if (fxGuiAttachSafe(fresh, hwnd)) {
fx->stealState(fresh);
delete fresh;
g_fxGuiEditorAttached = true; // /show first-paint kick only after editor exists
std::cout << "[FxGui] editor open: " << (name.empty() ? path : name) << std::endl;
return 0;
}