Fix FX GUI panel frozen: prefer PrintWindow live frame + real-input fallback for GPU-rendered editors
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
@@ -149,9 +150,10 @@ void sendOptions(SOCKET c) {
|
||||
// --- frame capture -----------------------------------------------------------
|
||||
// Capture the window client rect twice — BitBlt (GDI view) and PrintWindow
|
||||
// with PW_RENDERFULLCONTENT (DWM composition, includes DirectX/GPU surfaces
|
||||
// that GDI cannot see) — and keep the frame with more visual detail. Ozone 11
|
||||
// and other GPU-rendered plugins paint WHITE into the GDI DC, so the old
|
||||
// all-black fallback never fired and a white frame was served. Returns JPEG.
|
||||
// that GDI cannot see). PrintWindow is preferred: it returns the live composed
|
||||
// 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.
|
||||
bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality) {
|
||||
RECT rc;
|
||||
if (!GetClientRect(hwnd, &rc)) return false;
|
||||
@@ -175,29 +177,17 @@ bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality)
|
||||
bi.bmiHeader.biCompression = BI_RGB;
|
||||
return GetDIBits(mem, bmp, 0, h, p, &bi, DIB_RGB_COLORS) == h;
|
||||
};
|
||||
auto detailScore = [&](const unsigned char* p) -> long long {
|
||||
// Count sampled pixels that are neither near-black nor near-white.
|
||||
// GPU-rendered plugins (Ozone 11, ...) are invisible to GDI: BitBlt
|
||||
// yields a white/black-strip frame with ~2 distinct values, while the
|
||||
// real UI has many mid-grey levels. Pick the frame with more detail.
|
||||
long long d = 0;
|
||||
const int step = 4;
|
||||
for (int y = 0; y < h; y += step)
|
||||
for (int x = 0; x < w; x += step) {
|
||||
const size_t i = ((size_t)y * w + x) * 4;
|
||||
const unsigned lum = ((unsigned)p[i] * 77u + (unsigned)p[i + 1] * 150u + (unsigned)p[i + 2] * 29u) >> 8;
|
||||
if (lum >= 28 && lum <= 232) ++d; // exclude near-black/white
|
||||
}
|
||||
return d;
|
||||
};
|
||||
|
||||
bool okB = BitBlt(mem, 0, 0, w, h, win, 0, 0, SRCCOPY | CAPTUREBLT) != 0 && grabInto(pxB.data());
|
||||
// PW_RENDERFULLCONTENT asks DWM to compose the window (includes child
|
||||
// windows + DirectX surfaces) — the only way to see GPU-rendered editors.
|
||||
bool okP = PrintWindow(hwnd, mem, PW_RENDERFULLCONTENT) != 0 && grabInto(pxP.data());
|
||||
// Prefer the PrintWindow frame: PW_RENDERFULLCONTENT composes the live
|
||||
// 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.
|
||||
unsigned char* px = nullptr;
|
||||
if (okB && okP) px = detailScore(pxP.data()) > detailScore(pxB.data()) ? pxP.data() : pxB.data();
|
||||
else if (okP) px = pxP.data();
|
||||
if (okP) px = pxP.data();
|
||||
else if (okB) px = pxB.data();
|
||||
SelectObject(mem, oldBmp);
|
||||
DeleteObject(bmp);
|
||||
@@ -240,32 +230,162 @@ bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality)
|
||||
return !out.empty();
|
||||
}
|
||||
|
||||
// --- real-input fallback ------------------------------------------------------
|
||||
// Some editors (JUCE, iZotope) reject synthetic PostMessage input entirely and
|
||||
// only react to real hardware input while their window is visible. Probe: if 2
|
||||
// consecutive clicks do not change the frame, switch this plugin to real-input
|
||||
// mode — briefly move the window topmost to the work-area corner, inject real
|
||||
// mouse events at the mapped screen position, then restore it off-screen.
|
||||
std::atomic<bool> g_realInput{false}; // plugin needs real hardware input
|
||||
// probe: hit (frame changed) resets g_miss; 2 consecutive misses switch
|
||||
// to real input. A one-off "hit" (e.g. initial settle repaint) must NOT
|
||||
// lock us onto PostMessage, so there is no permanent "detected" state.
|
||||
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
|
||||
int g_miss = 0; // consecutive no-response PostMessage clicks
|
||||
uint64_t g_preClickHash = 0;
|
||||
|
||||
uint64_t frameHash(HWND hwnd) {
|
||||
std::vector<unsigned char> jpg;
|
||||
if (!captureHwndToJpeg(hwnd, jpg, 70)) return 0;
|
||||
uint64_t h = 1469598103934665603ull; // FNV-1a 64
|
||||
for (unsigned char b : jpg) { h ^= b; h *= 1099511628211ull; }
|
||||
return h;
|
||||
}
|
||||
|
||||
void danceStart(HWND hwnd) {
|
||||
if (g_dance) return;
|
||||
RECT wa = {0}, wr = {0};
|
||||
SystemParametersInfoA(SPI_GETWORKAREA, 0, &wa, 0);
|
||||
GetWindowRect(hwnd, &wr);
|
||||
const int w = wr.right - wr.left, h = wr.bottom - wr.top;
|
||||
GetCursorPos(&g_savedCursor);
|
||||
SetWindowPos(hwnd, HWND_TOPMOST, wa.right - w, wa.bottom - h, 0, 0,
|
||||
SWP_NOSIZE | SWP_SHOWWINDOW);
|
||||
Sleep(60); // let the compositor repaint so the editor sees itself active
|
||||
g_dance = true;
|
||||
g_parkUntil = GetTickCount() + 3000;
|
||||
}
|
||||
|
||||
void danceEnd(HWND hwnd) {
|
||||
if (!g_dance) return;
|
||||
SetWindowPos(hwnd, HWND_NOTOPMOST, -20000, -20000, 0, 0,
|
||||
SWP_NOSIZE | SWP_NOACTIVATE);
|
||||
SetCursorPos(g_savedCursor.x, g_savedCursor.y);
|
||||
g_dance = false;
|
||||
g_parkUntil = 0;
|
||||
}
|
||||
|
||||
void realClick(int sx, int sy, bool down, int button) {
|
||||
SetCursorPos(sx, sy);
|
||||
Sleep(10);
|
||||
DWORD flags;
|
||||
if (button == 2) flags = down ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP;
|
||||
else if (button == 1) flags = down ? MOUSEEVENTF_MIDDLEDOWN : MOUSEEVENTF_MIDDLEUP;
|
||||
else flags = down ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP;
|
||||
mouse_event(flags, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
// --- input relay -------------------------------------------------------------
|
||||
void relayInput(HWND hwnd, const json_object_s* o) {
|
||||
// The plugin editor is a CHILD window created by IPlugView::attached().
|
||||
// DefWindowProc on the top-level host does NOT forward mouse/keyboard to
|
||||
// it, so post directly to the view (fallback to the host window).
|
||||
HWND target = FindWindowExA(hwnd, nullptr, nullptr, nullptr);
|
||||
if (!target) target = hwnd;
|
||||
const std::string type = memberString(o, "type", "");
|
||||
const int x = (int)memberInt(o, "x", 0);
|
||||
const int y = (int)memberInt(o, "y", 0);
|
||||
const int button = (int)memberInt(o, "button", 0);
|
||||
const int buttons = (int)memberInt(o, "buttons", 0); // DOM e.buttons bitmask
|
||||
const int deltaY = (int)memberInt(o, "deltaY", 0);
|
||||
const WPARAM keyCode = (WPARAM)memberInt(o, "keyCode", 0);
|
||||
const LPARAM lp = MAKELPARAM(x, y);
|
||||
// DOM buttons: 1=left 2=right 4=middle -> MK_LBUTTON/MK_RBUTTON/MK_MBUTTON.
|
||||
WPARAM down = 0;
|
||||
if (buttons & 1) down |= MK_LBUTTON;
|
||||
if (buttons & 2) down |= MK_RBUTTON;
|
||||
if (buttons & 4) down |= MK_MBUTTON;
|
||||
const bool isDown = type == "mousedown";
|
||||
const bool isUp = type == "mouseup";
|
||||
// Mouseleave reports -1,-1: finish the drag without clicking a bogus point.
|
||||
const bool outside = x < 0 || y < 0;
|
||||
|
||||
// Probe while still on PostMessage: a changed frame means PostMessage
|
||||
// responds (reset miss); two consecutive no-response clicks mean the
|
||||
// editor rejects synthetic input — switch to real hardware input.
|
||||
if (!g_realInput) {
|
||||
if (isDown) {
|
||||
g_preClickHash = frameHash(hwnd);
|
||||
} else if (isUp && g_preClickHash) {
|
||||
Sleep(250);
|
||||
uint64_t nowHash = frameHash(hwnd);
|
||||
bool resp = nowHash != g_preClickHash;
|
||||
if (resp) {
|
||||
g_miss = 0; // PostMessage responded — keep it
|
||||
} else if (++g_miss >= 2) {
|
||||
g_realInput = true; // synthetic input rejected — go real
|
||||
g_miss = 0;
|
||||
}
|
||||
g_preClickHash = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_realInput && (isDown || isUp || type == "mousemove" || type == "wheel")) {
|
||||
if (isDown) {
|
||||
danceStart(hwnd);
|
||||
POINT pt = { x, y };
|
||||
if (!outside) ClientToScreen(target, &pt);
|
||||
realClick(pt.x, pt.y, true, button);
|
||||
} else if (type == "mousemove") {
|
||||
if (g_dance) {
|
||||
POINT pt = { x, y };
|
||||
ClientToScreen(target, &pt);
|
||||
SetCursorPos(pt.x, pt.y);
|
||||
g_parkUntil = GetTickCount() + 3000; // keep up during drag
|
||||
}
|
||||
} else if (isUp) {
|
||||
if (g_dance) {
|
||||
if (!outside) {
|
||||
POINT pt = { x, y };
|
||||
ClientToScreen(target, &pt);
|
||||
realClick(pt.x, pt.y, false, button);
|
||||
}
|
||||
danceEnd(hwnd);
|
||||
}
|
||||
} else if (type == "wheel") {
|
||||
danceStart(hwnd);
|
||||
POINT pt = { x, y };
|
||||
if (!outside) ClientToScreen(target, &pt);
|
||||
SetCursorPos(pt.x, pt.y);
|
||||
mouse_event(MOUSEEVENTF_WHEEL, 0, 0, (DWORD)(-(int64_t)deltaY * 120), 0);
|
||||
g_parkUntil = GetTickCount() + 600; // stay one beat, then restore
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == "mousemove") {
|
||||
PostMessageA(hwnd, WM_MOUSEMOVE, 0, lp);
|
||||
PostMessageA(target, WM_MOUSEMOVE, down, lp); // button state => drag works
|
||||
} else if (type == "mousedown") {
|
||||
UINT m = button == 2 ? WM_RBUTTONDOWN : button == 1 ? WM_MBUTTONDOWN : WM_LBUTTONDOWN;
|
||||
PostMessageA(hwnd, m, MAKEWPARAM(0, button == 2 ? MK_RBUTTON : button == 1 ? MK_MBUTTON : MK_LBUTTON), lp);
|
||||
PostMessageA(hwnd, WM_MOUSEMOVE, 0, lp); // plugins tracking absolute pos
|
||||
WPARAM w = button == 2 ? MK_RBUTTON : button == 1 ? MK_MBUTTON : MK_LBUTTON;
|
||||
PostMessageA(target, m, w, lp);
|
||||
PostMessageA(target, WM_MOUSEMOVE, w, lp); // plugins tracking absolute pos
|
||||
SetFocus(target);
|
||||
} else if (type == "mouseup") {
|
||||
UINT m = button == 2 ? WM_RBUTTONUP : button == 1 ? WM_MBUTTONUP : WM_LBUTTONUP;
|
||||
PostMessageA(hwnd, m, 0, lp);
|
||||
PostMessageA(target, m, 0, lp);
|
||||
} else if (type == "wheel") {
|
||||
PostMessageA(hwnd, WM_MOUSEWHEEL, MAKEWPARAM(0, (short)(-(int64_t)deltaY * 120)), lp);
|
||||
// WM_MOUSEWHEEL lParam carries SCREEN coords of the cursor.
|
||||
POINT pt = { x, y };
|
||||
ClientToScreen(target, &pt);
|
||||
PostMessageA(target, WM_MOUSEWHEEL, MAKEWPARAM(0, (short)(-(int64_t)deltaY * 120)), MAKELPARAM(pt.x, pt.y));
|
||||
} else if (type == "keydown") {
|
||||
SetFocus(hwnd);
|
||||
PostMessageA(hwnd, WM_KEYDOWN, keyCode, 0);
|
||||
SetFocus(target);
|
||||
PostMessageA(target, WM_KEYDOWN, keyCode, 0);
|
||||
} else if (type == "keyup") {
|
||||
PostMessageA(hwnd, WM_KEYUP, keyCode, 0);
|
||||
PostMessageA(target, WM_KEYUP, keyCode, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +428,11 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
||||
} else if (req.path == "/ping") {
|
||||
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
||||
} else if (req.path == "/frame") {
|
||||
// Real-input wheel parks the window on-screen; restore when the
|
||||
// 600ms beat expires (checked here since /frame polls ~100ms).
|
||||
if (g_dance && g_parkUntil &&
|
||||
(int32_t)(GetTickCount() - g_parkUntil) >= 0)
|
||||
danceEnd(hwnd);
|
||||
std::vector<unsigned char> jpg;
|
||||
if (captureHwndToJpeg(hwnd, jpg, 70))
|
||||
sendResponse(c, 200, "image/jpeg", jpg.data(), jpg.size());
|
||||
|
||||
Reference in New Issue
Block a user