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
|
// Tell RenderFxJob's SHM audio feeder which realtime session is live (the
|
||||||
// frontend pushes the session name; "" = session stopped/restarted).
|
// frontend pushes the session name; "" = session stopped/restarted).
|
||||||
extern void fxGuiSetShm(const std::string& name);
|
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 {
|
namespace {
|
||||||
|
|
||||||
@@ -163,6 +166,14 @@ void sendOptions(SOCKET c) {
|
|||||||
// frame, while the GDI BitBlt view is a stale first paint for GPU-rendered
|
// 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.
|
// editors (JUCE/FAST Compressor) and never updates. BitBlt stays as fallback.
|
||||||
// Returns JPEG.
|
// 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) {
|
bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality) {
|
||||||
RECT rc;
|
RECT rc;
|
||||||
if (!GetClientRect(hwnd, &rc)) return false;
|
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
|
// window (child + GPU surfaces). The GDI BitBlt view is a stale first paint
|
||||||
// for GPU-rendered editors (JUCE/FAST Compressor) and never updates, which
|
// for GPU-rendered editors (JUCE/FAST Compressor) and never updates, which
|
||||||
// made the panel look frozen. BitBlt stays only as a fallback.
|
// 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;
|
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();
|
else if (okB) px = pxB.data();
|
||||||
SelectObject(mem, oldBmp);
|
SelectObject(mem, oldBmp);
|
||||||
DeleteObject(bmp);
|
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<bool> g_dance{false}; // window temporarily on-screen/topmost
|
||||||
std::atomic<DWORD> g_parkUntil{0}; // safety restore deadline (GetTickCount)
|
std::atomic<DWORD> g_parkUntil{0}; // safety restore deadline (GetTickCount)
|
||||||
POINT g_savedCursor = {0, 0}; // cursor to restore after the dance
|
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
|
// 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
|
// 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
|
// 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,
|
SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0,
|
||||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||||
ShowWindow(hwnd, SW_HIDE);
|
ShowWindow(hwnd, SW_HIDE);
|
||||||
|
g_paintKicked = false; // next /show re-kicks the first paint
|
||||||
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
||||||
} else if (req.path == "/show") {
|
} else if (req.path == "/show") {
|
||||||
// Optional body {rect:{x,y,w,h}} (screen px) — anchored placement
|
// 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);
|
if (sroot) std::free(sroot);
|
||||||
// Embedded mode: the native window NEVER comes on-screen — it stays
|
// Embedded mode: the native window stays parked ON-SCREEN under
|
||||||
// parked off-screen at -20000,-20000 (GDI capture + synthetic input
|
// the DAW (HWND_BOTTOM at the embed panel rect — invisible, the
|
||||||
// work regardless of position). The panel rect sizes the host
|
// DAW window covers it). iZotope-class editors (Nectar) NEVER
|
||||||
// window + plugin view so the capture frame matches the embed panel
|
// paint while off-screen: both PrintWindow and BitBlt stay pure
|
||||||
// (layout bug); /hide hid the window, so re-show it off-screen so
|
// black, so the panel showed "can't load GUI". On-screen the
|
||||||
// /frame keeps capturing.
|
// editor paints once (first-paint kick below) and the capture
|
||||||
if (rectOk) {
|
// 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 cw = g_embedRect.right - g_embedRect.left;
|
||||||
const int ch = g_embedRect.bottom - g_embedRect.top;
|
const int ch = g_embedRect.bottom - g_embedRect.top;
|
||||||
RECT cur = {};
|
RECT cur = {};
|
||||||
GetWindowRect(hwnd, &cur);
|
GetWindowRect(hwnd, &cur);
|
||||||
// Skip when already the right size: frontend re-POSTs /show
|
const bool samePlace =
|
||||||
// every ~500ms; re-laying-out the plugin view every tick is
|
cur.left == g_embedRect.left && cur.top == g_embedRect.top &&
|
||||||
// wasteful (WM_FXGUI_RESIZE -> checkSizeConstraint/onSize).
|
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)
|
if (cur.right - cur.left != cw || cur.bottom - cur.top != ch)
|
||||||
fxGuiRequestResize(hwnd, cw, ch);
|
fxGuiRequestResize(hwnd, cw, ch);
|
||||||
}
|
if (!g_paintKicked && fxGuiEditorAttached()) {
|
||||||
if (!IsWindowVisible(hwnd))
|
// 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,
|
SetWindowPos(hwnd, HWND_NOTOPMOST, -20000, -20000, 0, 0,
|
||||||
SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||||
|
}
|
||||||
// Dance/anchor state is owned by the input relay — /show must not
|
// Dance/anchor state is owned by the input relay — /show must not
|
||||||
// yank the window off-screen (real input is always on).
|
// yank the window off-screen (real input is always on).
|
||||||
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
||||||
|
|||||||
@@ -1519,6 +1519,11 @@ namespace {
|
|||||||
|
|
||||||
const char* kFxGuiWndClass = "SonicForge_FX_GUI_Class";
|
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 {
|
struct FxGuiCtx {
|
||||||
bool closing = false;
|
bool closing = false;
|
||||||
};
|
};
|
||||||
@@ -1658,6 +1663,9 @@ void fxGuiRequestResize(HWND hwnd, int w, int h) {
|
|||||||
PostMessageA(hwnd, WM_FXGUI_RESIZE, (WPARAM)w, (LPARAM)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.
|
// Capture the GUI plugin state on the pump thread; blocks up to timeoutMs.
|
||||||
void fxGuiRequestCapture(void* hwndV, std::string& outB64, int timeoutMs) {
|
void fxGuiRequestCapture(void* hwndV, std::string& outB64, int timeoutMs) {
|
||||||
HWND hwnd = (HWND)hwndV;
|
HWND hwnd = (HWND)hwndV;
|
||||||
@@ -1889,6 +1897,7 @@ static int fxGuiLoadAttach(Vst3Fx* fx, const std::string& path, const std::strin
|
|||||||
if (fxGuiAttachSafe(fresh, hwnd)) {
|
if (fxGuiAttachSafe(fresh, hwnd)) {
|
||||||
fx->stealState(fresh);
|
fx->stealState(fresh);
|
||||||
delete 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;
|
std::cout << "[FxGui] editor open: " << (name.empty() ? path : name) << std::endl;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user