fix(bridge): first-paint kick real click — iZotope GUI embed hien thi that
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user