V45.46: GUI embed VST full-panel trong DAW - phuc hoi embed (frame-capture ~10fps, relay input), bo toolbar header 'X Dong GUI' trong VstGuiEmbed + bo bar FX RT telemetry trong Mastering (phan tach GUI vs chain FX slot = border chain strip); dong GUI = chain card / module builtin / chon VST khac / onDead; projects API /disk list+open + api.js wrappers; plugins.py them SF_FXGUI_FLOAT=1 khi spawn bridge GUI; FxGuiServer/RenderFxJob fix GUI embed frame capture + show/hide re-anchor theo panel rect. Rebuild daw_vst_bridge+fx_vst_bridge+plugin_host v=202609032200.
This commit is contained in:
@@ -1,16 +1,24 @@
|
||||
// native_bridge/src/FxGuiServer.cpp
|
||||
// Embedded VST GUI server (Phase 0 of PLAN_MASTERBUS_VST_GUI_EMBED.md):
|
||||
// `daw_vst_bridge --fx-gui <job.json>` loads ONE VST3 effect plugin, attaches
|
||||
// its native editor to an off-screen host window, and serves the window as
|
||||
// JPEG frames over a tiny HTTP server so the web frontend can embed it in a
|
||||
// panel (frame-capture + input relay, VNC-style). Windows only.
|
||||
// VST GUI server (Phase 0 of PLAN_MASTERBUS_VST_GUI_EMBED.md; P2 float mode of
|
||||
// the gop-process plan): `daw_vst_bridge --fx-gui <job.json>` loads ONE VST3
|
||||
// effect plugin and attaches its native editor to a host window.
|
||||
// - Embedded mode (default): window off-screen; JPEG frames + input relay
|
||||
// over HTTP so the webview can show it in a panel (VNC-style).
|
||||
// - Float mode (SF_FXGUI_FLOAT=1): the window is a REAL on-screen window the
|
||||
// user moves/sizes freely; /frame and /input are not used. WM_CLOSE only
|
||||
// hides it (plugin instance stays loaded — reopen via /show is instant);
|
||||
// /close and /exit tear the process down for real.
|
||||
// Windows only.
|
||||
//
|
||||
// Routes:
|
||||
// GET /ping -> {"ok":true}
|
||||
// GET /frame -> image/jpeg (BitBlt, fallback PrintWindow)
|
||||
// GET /frame -> image/jpeg (BitBlt, fallback PrintWindow) — embed
|
||||
// POST /input -> {type:"mousedown|mousemove|mouseup|wheel|keydown|keyup",
|
||||
// x,y,button,deltaY,keyCode} -> PostMessage to HWND
|
||||
// x,y,button,deltaY,keyCode} — embed
|
||||
// POST /show -> show (+focus, float) / park at panel rect (embed)
|
||||
// POST /hide -> hide the window (bridge keeps running)
|
||||
// POST /close -> detach + exit 0
|
||||
// POST /exit -> alias of /close
|
||||
// GET /preset -> {"preset_b64":"..."} current plugin state
|
||||
//
|
||||
// The HTTP server runs on a worker thread; the calling (STA) thread runs the
|
||||
@@ -53,6 +61,13 @@ extern void fxGuiSetShm(const std::string& name);
|
||||
// Set by RenderFxJob once the VST editor attached to the host window; /frame
|
||||
// refuses (503) until then so the webview never shows the empty host window.
|
||||
extern std::atomic<bool> g_fxGuiEditorReady;
|
||||
// Set by float /show when the editor is still loading/attaching; consumed by
|
||||
// fxGuiLoadAttach after attach so a pre-ready /show is not lost (Bug 2).
|
||||
extern std::atomic<bool> g_fxGuiShowPending;
|
||||
// Float mode flag (RenderFxJob.cpp global, set from SF_FXGUI_FLOAT).
|
||||
extern bool g_fxGuiFloatMode;
|
||||
// Ask the pump thread to destroy the window (full exit in float mode).
|
||||
extern void fxGuiRequestExit(HWND hwnd);
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -403,6 +418,9 @@ void realClick(int sx, int sy, bool down, int button) {
|
||||
|
||||
// --- input relay -------------------------------------------------------------
|
||||
void relayInput(HWND hwnd, const json_object_s* o) {
|
||||
// Float mode: the user interacts with the real window directly — the
|
||||
// relay/dance machinery is embed-only.
|
||||
if (g_fxGuiFloatMode) return;
|
||||
// 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).
|
||||
@@ -502,6 +520,42 @@ void relayInput(HWND hwnd, const json_object_s* o) {
|
||||
}
|
||||
}
|
||||
|
||||
// One-time first-paint kick (shared by /show and /frame). 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).
|
||||
static void kickFirstPaint(HWND hwnd, const char* via) {
|
||||
if (g_paintKicked.load()) return;
|
||||
g_paintKicked = true;
|
||||
const int cw = g_embedRect.right - g_embedRect.left;
|
||||
const int ch = g_embedRect.bottom - g_embedRect.top;
|
||||
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=" << via << " @" << cx
|
||||
<< "," << cy << std::endl;
|
||||
}
|
||||
|
||||
// --- HTTP server loop (worker thread) ---------------------------------------
|
||||
void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
||||
WSADATA wsa;
|
||||
@@ -557,8 +611,8 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
||||
// 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.
|
||||
if (g_embedOnscreen && !g_anchored && !g_dance &&
|
||||
g_embedRect.right > g_embedRect.left &&
|
||||
if (!g_fxGuiFloatMode && g_embedOnscreen && !g_anchored &&
|
||||
!g_dance && g_embedRect.right > g_embedRect.left &&
|
||||
g_embedRect.bottom > g_embedRect.top) {
|
||||
const int cw = g_embedRect.right - g_embedRect.left;
|
||||
const int ch = g_embedRect.bottom - g_embedRect.top;
|
||||
@@ -569,6 +623,13 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
||||
SetWindowPos(hwnd, HWND_BOTTOM, g_embedRect.left,
|
||||
g_embedRect.top, cw, ch,
|
||||
SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
// First-paint kick via /frame poll: the one-shot /show
|
||||
// may have landed while the editor was still loading
|
||||
// (ready=false) and is deduped, so no later /show fires;
|
||||
// iZotope-class editors paint only when visible+activated
|
||||
// -> black panel until a user click. Kick here once the
|
||||
// editor is ready and the window is parked at the rect.
|
||||
kickFirstPaint(hwnd, "frame");
|
||||
}
|
||||
std::vector<unsigned char> jpg;
|
||||
float avg = -1.0f;
|
||||
@@ -633,6 +694,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_fxGuiShowPending.store(false); // panel closed while attaching: don't auto-show later
|
||||
// Reopen re-anchors from scratch (/show + rect) — clear the
|
||||
// real-input state or the reopened panel would never re-place
|
||||
// the window at the panel rect (stuck anchored/offscreen).
|
||||
@@ -642,6 +704,52 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
||||
g_paintKicked = false; // next /show re-kicks the first paint
|
||||
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
||||
} else if (req.path == "/show") {
|
||||
if (g_fxGuiFloatMode) {
|
||||
// P2 float mode: reopen the hidden window (WM_CLOSE only hides
|
||||
// in float mode — the plugin instance stays loaded, so a show
|
||||
// is instant). Optional {rect:{x,y,w,h}} positions/sizes the
|
||||
// window explicitly; without one, show at the previous spot and
|
||||
// try to focus (SetForegroundWindow is best-effort — the
|
||||
// Windows foreground lock may swallow it; the user's first
|
||||
// click reactivates the window anyway). Guarded by
|
||||
// g_fxGuiEditorReady: while the pump thread is still loading +
|
||||
// attaching, a SetWindowPos from this worker would block.
|
||||
if (g_fxGuiEditorReady.load()) {
|
||||
json_parse_result_s spres = {};
|
||||
json_value_s* sroot = json_parse_ex(req.body.data(), req.body.size(),
|
||||
json_parse_flags_default, nullptr, nullptr, &spres);
|
||||
bool rectOk = false;
|
||||
if (sroot && sroot->type == json_type_object) {
|
||||
const json_object_s* so = static_cast<const json_object_s*>(sroot->payload);
|
||||
const json_value_s* srv = memberValue(so, "rect");
|
||||
if (srv && srv->type == json_type_object) {
|
||||
const json_object_s* sro = static_cast<const json_object_s*>(srv->payload);
|
||||
const LONG srw = (LONG)memberInt(sro, "w", 0);
|
||||
const LONG srh = (LONG)memberInt(sro, "h", 0);
|
||||
if (srw > 0 && srh > 0) {
|
||||
SetWindowPos(hwnd, nullptr,
|
||||
(int)memberInt(sro, "x", 0),
|
||||
(int)memberInt(sro, "y", 0),
|
||||
(int)srw, (int)srh,
|
||||
SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
rectOk = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sroot) std::free(sroot);
|
||||
if (!rectOk) {
|
||||
ShowWindow(hwnd, SW_SHOW);
|
||||
bringToTop(hwnd);
|
||||
}
|
||||
} else {
|
||||
// Editor still loading/attaching (slow VST): cannot show
|
||||
// yet. Remember the request — fxGuiLoadAttach shows the
|
||||
// window right after attach, else the GUI stays hidden
|
||||
// forever on a reopen during attach (Bug 2).
|
||||
g_fxGuiShowPending.store(true);
|
||||
}
|
||||
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
||||
} else {
|
||||
// Optional body {rect:{x,y,w,h}} (screen px) — anchored placement
|
||||
// over the embed panel; without a rect just show + top (never
|
||||
// topmost — the GUI must not stay above other apps).
|
||||
@@ -697,40 +805,9 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
||||
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;
|
||||
}
|
||||
// First-paint kick — one-time, runs from the shared helper;
|
||||
// /frame also kicks (the deduped /show may land pre-ready).
|
||||
kickFirstPaint(hwnd, "click");
|
||||
} else if (!IsWindowVisible(hwnd)) {
|
||||
// No usable rect yet (pre-layout / still attaching): keep the
|
||||
// window somewhere harmless until the frontend supplies it.
|
||||
@@ -740,6 +817,7 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
||||
// 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);
|
||||
} // end embed-mode /show body (else of g_fxGuiFloatMode)
|
||||
} else if (req.path == "/preset") {
|
||||
std::string b64;
|
||||
fxGuiRequestCapture(hwnd, b64, 500); // 1500 blocked the HTTP worker, stalling /input (clicks)
|
||||
@@ -758,10 +836,13 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
||||
if (sroot) std::free(sroot);
|
||||
sendResponse(c, 400, "application/json", "{\"error\":\"bad json\"}", 20);
|
||||
}
|
||||
} else if (req.path == "/close") {
|
||||
} else if (req.path == "/close" || req.path == "/exit") {
|
||||
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
||||
stop.store(true);
|
||||
PostMessageA(hwnd, WM_CLOSE, 0, 0); // pump exits -> process returns
|
||||
// Float mode: WM_CLOSE only hides — WM_FXGUI_EXIT destroys the
|
||||
// window so the pump exits and the process returns. Embedded mode
|
||||
// is unchanged (there WM_CLOSE already destroyed).
|
||||
fxGuiRequestExit(hwnd); // pump exits -> process returns
|
||||
} else {
|
||||
sendResponse(c, 404, "application/json", "{\"error\":\"not found\"}", 22);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,13 @@
|
||||
// refuses to serve /frame while false: before attach the host window is empty
|
||||
// and capturing it would stream a blank frame -> white flash in the webview.
|
||||
std::atomic<bool> g_fxGuiEditorReady{false};
|
||||
// /show that arrived while the editor was still loading/attaching (slow VST,
|
||||
// e.g. iZotope 20-70s cold attach) must not be silently dropped: FxGuiServer's
|
||||
// float /show sets this when it cannot show yet; fxGuiLoadAttach consumes it
|
||||
// right after a successful attach and shows the window. Without it, closing
|
||||
// the Mastering panel while the GUI is attaching and reopening it later leaves
|
||||
// the GUI permanently hidden (Bug 2).
|
||||
std::atomic<bool> g_fxGuiShowPending{false};
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -614,6 +621,31 @@ public:
|
||||
}
|
||||
state_ = s;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
// Unload the current instance so a retry can load a fresh one. On the
|
||||
// retry path the editor never attached (view is null) and closeEditor()
|
||||
// only frees the plugFrame leaked by the crashed attach. state_ is
|
||||
// cleared BEFORE the plugin teardown calls: a crash in one of them
|
||||
// (SEH-caught by fxGuiUnloadSafe) leaves this object usable again — the
|
||||
// old Vst3FxState leaks (accepted: the retry loop gives up anyway).
|
||||
void unload() {
|
||||
#ifndef HAVE_VST3SDK
|
||||
return;
|
||||
#else
|
||||
auto* s = static_cast<Vst3FxState*>(state_);
|
||||
if (!s) return;
|
||||
closeEditor();
|
||||
state_ = nullptr;
|
||||
if (s->component) {
|
||||
FUnknownPtr<IAudioProcessor> processor(s->component);
|
||||
if (processor) processor->setProcessing(false);
|
||||
s->component->setActive(false);
|
||||
s->component->terminate();
|
||||
}
|
||||
if (s->controller && !s->controllerIsComponent) s->controller->terminate();
|
||||
s->processData.unprepare();
|
||||
delete s;
|
||||
#endif
|
||||
}
|
||||
// Audio in → audio out, one block. Must be SEH-wrapped by the caller.
|
||||
@@ -1500,6 +1532,13 @@ void RealtimeFxChain::Impl::workerLoop() {
|
||||
static void fxGuiResizeSafe(void* fx, HWND hwnd, int w, int h);
|
||||
static void fxGuiCaptureSafe(void* fx, std::string& out);
|
||||
|
||||
// Float mode (P2 gop-process plan): SF_FXGUI_FLOAT=1 makes --fx-gui a REAL
|
||||
// on-screen window instead of the embedded frame-capture server. WM_CLOSE only
|
||||
// hides it (plugin instance stays loaded — reopen is instant); /exit, /close
|
||||
// or a dead parent tear the process down for real. Set in run_fx_gui_server
|
||||
// from the env, before the window is created.
|
||||
bool g_fxGuiFloatMode = false;
|
||||
|
||||
namespace {
|
||||
|
||||
const char* kFxGuiWndClass = "SonicForge_FX_GUI_Class";
|
||||
@@ -1521,13 +1560,42 @@ static bool fxGuiParentAlive(uint32_t pid) {
|
||||
CloseHandle(h);
|
||||
return true;
|
||||
}
|
||||
// Attach attempts per process (VẤN ĐỀ #2 auto-retry): Ozone 11's editor
|
||||
// crashes (0xC0000005) on a COLD first attach (createInstance ~74s) but a
|
||||
// warm reload (~20-30s) usually attaches fine. Retrying in-process keeps the
|
||||
// SAME hwnd, HTTP port and process so the frontend embed never sees the
|
||||
// bridge die. Env SF_FXGUI_ATTACH_RETRIES overrides the count (1 = no retry,
|
||||
// the pre-V45.46 behavior; cap 6).
|
||||
static int fxGuiAttachRetries() {
|
||||
const char* e = std::getenv("SF_FXGUI_ATTACH_RETRIES");
|
||||
int n = e ? std::atoi(e) : 3;
|
||||
if (n < 1) n = 1;
|
||||
if (n > 6) n = 6;
|
||||
return n;
|
||||
}
|
||||
// Leftover plugin child windows (GL surfaces etc.) from a crashed attach must
|
||||
// go before the next attempt — the fresh instance creates its own children.
|
||||
// SEH-wrapped: a bad child wndproc after a crash must not kill the retry.
|
||||
static BOOL CALLBACK fxGuiDestroyChildProc(HWND child, LPARAM) {
|
||||
__try { if (IsWindow(child)) DestroyWindow(child); }
|
||||
__except (EXCEPTION_EXECUTE_HANDLER) {}
|
||||
return TRUE; // keep enumerating
|
||||
}
|
||||
static void fxGuiDestroyChildWindows(HWND hwnd) {
|
||||
if (IsWindow(hwnd)) EnumChildWindows(hwnd, fxGuiDestroyChildProc, 0);
|
||||
}
|
||||
// Float-mode exit message: destroy the window -> pump loop ends -> process
|
||||
// exits for real (WM_CLOSE in float mode only hides).
|
||||
#define WM_FXGUI_EXIT (WM_APP + 0x53)
|
||||
|
||||
static void fxGuiStartWatchdog(HWND hwnd, uint32_t parentPid) {
|
||||
if (parentPid == 0) return;
|
||||
std::thread([hwnd, parentPid]() {
|
||||
while (IsWindow(hwnd)) {
|
||||
Sleep(2000);
|
||||
if (!fxGuiParentAlive(parentPid)) {
|
||||
PostMessageA(hwnd, WM_CLOSE, 0, 0); // pump exits -> process returns
|
||||
PostMessageA(hwnd, g_fxGuiFloatMode ? WM_FXGUI_EXIT : WM_CLOSE,
|
||||
0, 0); // pump exits -> process returns
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1552,11 +1620,39 @@ LRESULT CALLBACK FxGuiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
FxGuiCtx* ctx = (FxGuiCtx*)GetWindowLongPtrA(hwnd, GWLP_USERDATA);
|
||||
switch (msg) {
|
||||
case WM_CLOSE:
|
||||
// Float mode: hide, keep the window + plugin instance alive so a
|
||||
// reopen (/show) is instant. Only embedded mode (or WM_FXGUI_EXIT)
|
||||
// destroys the window for real.
|
||||
if (g_fxGuiFloatMode) {
|
||||
ShowWindow(hwnd, SW_HIDE);
|
||||
return 0;
|
||||
}
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
case WM_FXGUI_EXIT:
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
if (ctx) ctx->closing = true;
|
||||
return 0;
|
||||
case WM_EXITSIZEMOVE:
|
||||
// Float mode: user finished dragging/moving the frame. The plugin
|
||||
// child window does NOT follow host resizes by itself, so the
|
||||
// newly exposed/stale client area keeps old pixels (ảnh ma, Bug 1).
|
||||
// Resize the editor view to the new client rect (plugin clamps via
|
||||
// checkSizeConstraint; resizeEditor re-sizes the host to match) and
|
||||
// force a full repaint of window + children. Runs on the pump
|
||||
// thread (owner of the editor), so the direct call is safe.
|
||||
if (g_fxGuiFloatMode && !(ctx && ctx->closing)) {
|
||||
RECT rc;
|
||||
if (GetClientRect(hwnd, &rc) && rc.right > 0 && rc.bottom > 0) {
|
||||
fxGuiResizeSafe((void*)GetPropA(hwnd, "FXGUI_FX"), hwnd,
|
||||
(int)rc.right, (int)rc.bottom);
|
||||
}
|
||||
RedrawWindow(hwnd, nullptr, nullptr,
|
||||
RDW_INVALIDATE | RDW_ALLCHILDREN);
|
||||
}
|
||||
return 0;
|
||||
case WM_FXGUI_RESIZE:
|
||||
fxGuiResizeSafe((void*)GetPropA(hwnd, "FXGUI_FX"), hwnd, (int)wp, (int)lp);
|
||||
return 0;
|
||||
@@ -1596,6 +1692,18 @@ static void fxGuiDetachSafe(void* fx) {
|
||||
(void)fx;
|
||||
#endif
|
||||
}
|
||||
// Unload a GUI instance under SEH — the plugin's terminate may crash (same
|
||||
// rule as fxGuiAttachSafe: raw-pointer frame, C2712-safe). False = exception
|
||||
// mid-teardown → instance state unknown → stop retrying.
|
||||
static bool fxGuiUnloadSafe(Vst3Fx* fx) {
|
||||
#ifdef HAVE_VST3SDK
|
||||
__try { if (fx) fx->unload(); return true; }
|
||||
__except (EXCEPTION_EXECUTE_HANDLER) { return false; }
|
||||
#else
|
||||
(void)fx;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
static void fxGuiResizeSafe(void* fx, HWND hwnd, int w, int h) {
|
||||
#ifdef HAVE_VST3SDK
|
||||
__try { static_cast<Vst3Fx*>(fx)->resizeEditor(hwnd, w, h); }
|
||||
@@ -1634,6 +1742,13 @@ void fxGuiRequestCapture(void* hwndV, std::string& outB64, int timeoutMs) {
|
||||
}
|
||||
}
|
||||
|
||||
// Float mode close: post WM_FXGUI_EXIT so the pump thread destroys the window
|
||||
// (full process exit — WM_CLOSE in float mode only hides). Used by the
|
||||
// FxGuiServer /close and /exit routes; embedded mode is unaffected.
|
||||
void fxGuiRequestExit(HWND hwnd) {
|
||||
PostMessageA(hwnd, WM_FXGUI_EXIT, 0, 0);
|
||||
}
|
||||
|
||||
|
||||
// --- SHM audio feeder (GUI meters) -----------------------------------------
|
||||
// The GUI process PEEKS the SAME input ring the realtime session uses (never
|
||||
@@ -1828,13 +1943,19 @@ static int fxGuiLoadAttach(Vst3Fx* fx, const std::string& path, const std::strin
|
||||
g_fxGuiEditorReady.store(false);
|
||||
if (!fxGuiAttachSafe(fx, hwnd)) {
|
||||
std::cerr << "[FxGui] editor attach failed for " << path << std::endl;
|
||||
DestroyWindow(hwnd);
|
||||
// Do NOT destroy hwnd here — the retry loop reuses the same window;
|
||||
// after the last failed attempt the caller posts WM_CLOSE (teardown).
|
||||
return 2;
|
||||
}
|
||||
// Attach OK: allow /frame. First captures may still be blank (plugin paints
|
||||
// on its first WM_PAINT) — FxGuiServer's luma check rejects those and
|
||||
// serves the last good frame instead.
|
||||
g_fxGuiEditorReady.store(true);
|
||||
if (g_fxGuiShowPending.exchange(false)) {
|
||||
// A /show was waiting while this editor attached (Bug 2): show now
|
||||
// that the plugin view exists. Runs on the pump thread (owner).
|
||||
ShowWindow(hwnd, SW_SHOW);
|
||||
}
|
||||
std::cout << "[FxGui] editor open: " << (name.empty() ? path : name) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
@@ -1890,6 +2011,15 @@ int run_fx_gui_server(const std::string& jobPath) {
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
struct ComGuard { ~ComGuard() { CoUninitialize(); } } comGuard;
|
||||
|
||||
// P2 float mode: SF_FXGUI_FLOAT=1 (engine sets it for --fx-gui) turns the
|
||||
// embed server into a REAL on-screen window. Must be read before
|
||||
// fxGuiCreateWindow + watchdog (window style + WM_CLOSE handling differ).
|
||||
const char* floatEnv = std::getenv("SF_FXGUI_FLOAT");
|
||||
g_fxGuiFloatMode = floatEnv != nullptr && floatEnv[0] != '0';
|
||||
if (g_fxGuiFloatMode)
|
||||
std::cout << "[FxGui] float mode (real on-screen window; WM_CLOSE=hide)"
|
||||
<< std::endl;
|
||||
|
||||
Vst3Fx fx;
|
||||
HWND hwnd = nullptr;
|
||||
// Fast path: parse job + create the host window, then start the HTTP
|
||||
@@ -1899,7 +2029,7 @@ int run_fx_gui_server(const std::string& jobPath) {
|
||||
std::string path, name, shm, presetB64;
|
||||
int rc = fxGuiParseJob(jobPath, path, name, shm, presetB64);
|
||||
if (rc) return rc;
|
||||
rc = fxGuiCreateWindow(name, /*offscreen=*/true, 0, &hwnd);
|
||||
rc = fxGuiCreateWindow(name, /*offscreen=*/!g_fxGuiFloatMode, 0, &hwnd);
|
||||
if (rc) return rc;
|
||||
fxGuiStartWatchdog(hwnd, fxGuiParentPid());
|
||||
SetPropA(hwnd, "FXGUI_FX", &fx); // for WM_FXGUI_RESIZE on the pump thread
|
||||
@@ -1920,25 +2050,52 @@ int run_fx_gui_server(const std::string& jobPath) {
|
||||
<< " SF_FXGUI_DEBUG=" << (std::getenv("SF_FXGUI_DEBUG") ? 1 : 0)
|
||||
<< std::endl;
|
||||
const int srv = fxGuiServerLoop(hwnd, [&]() {
|
||||
// Some plugin GL editors (e.g. iZotope Ozone 11 on Intel UHD 630) crash
|
||||
// when their editor window is parked offscreen during attach. Env toggle
|
||||
// SF_FXGUI_ATTACH_ONSCREEN=1: show the window on-screen first, then park
|
||||
// it back once the editor attached. Visible for one frame max (feeder is
|
||||
// started only after attach succeeds).
|
||||
if (attachOnscreen)
|
||||
SetWindowPos(hwnd, nullptr, 100, 100, 0, 0,
|
||||
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
loadRc = fxGuiLoadAttach(&fx, path, name, hwnd, presetB64);
|
||||
if (loadRc == 0) {
|
||||
fxGuiStartFeederThread(&fx, hwnd); // after load+attach only
|
||||
// EMBED_ONSCREEN: leave the window where attach put it (on a
|
||||
// monitor); the /show + /frame reconcile park it at the panel
|
||||
// rect (HWND_BOTTOM) once the frontend supplies the rect.
|
||||
if (attachOnscreen && !embedOnscreen)
|
||||
SetWindowPos(hwnd, nullptr, -20000, -20000, 0, 0,
|
||||
// Auto-retry attach (V45.46, VẤN ĐỀ #2): iZotope Ozone 11's editor
|
||||
// crashes (0xC0000005 in iZOzone11Core.dll) on a COLD first attach.
|
||||
// Each retry unloads the instance and loads a fresh one (warm load
|
||||
// ~20-30s vs ~74s cold) while keeping the SAME hwnd, HTTP port and
|
||||
// process — the frontend embed poll never sees the bridge die and
|
||||
// its spinner just keeps waiting. SF_FXGUI_ATTACH_RETRIES overrides
|
||||
// the count (1 = old no-retry behavior; cap 6). "attach attempt N/M"
|
||||
// in the engine log lets the user verify retries happened.
|
||||
const int maxAttempts = fxGuiAttachRetries();
|
||||
int attempt = 0;
|
||||
for (; attempt < maxAttempts; ++attempt) {
|
||||
// Some plugin GL editors (e.g. iZotope Ozone 11 on Intel UHD 630)
|
||||
// crash when parked offscreen during attach. Show on-screen first,
|
||||
// park back once the editor attached. Visible for one frame max
|
||||
// (feeder is started only after attach succeeds).
|
||||
if (attachOnscreen)
|
||||
SetWindowPos(hwnd, nullptr, 100, 100, 0, 0,
|
||||
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
std::cout << "[FxGui] attach attempt " << (attempt + 1) << "/"
|
||||
<< maxAttempts << std::endl;
|
||||
if (attempt > 0) {
|
||||
// Let audio/session settle after the crash, drop the crashed
|
||||
// instance's leftover child windows, then unload it.
|
||||
Sleep(1500);
|
||||
fxGuiDestroyChildWindows(hwnd);
|
||||
if (!fxGuiUnloadSafe(&fx)) break; // instance state unknown — give up
|
||||
}
|
||||
loadRc = fxGuiLoadAttach(&fx, path, name, hwnd, presetB64);
|
||||
if (loadRc == 0) {
|
||||
fxGuiStartFeederThread(&fx, hwnd); // after load+attach only
|
||||
// EMBED_ONSCREEN: leave the window where attach put it (on a
|
||||
// monitor); the /show + /frame reconcile park it at the panel
|
||||
// rect (HWND_BOTTOM) once the frontend supplies the rect.
|
||||
// Float mode: leave the window where attach put it (visible,
|
||||
// user-positionable) — nothing to park off-screen.
|
||||
if (!g_fxGuiFloatMode && attachOnscreen && !embedOnscreen)
|
||||
SetWindowPos(hwnd, nullptr, -20000, -20000, 0, 0,
|
||||
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
break;
|
||||
}
|
||||
std::cerr << "[FxGui] attach attempt " << (attempt + 1) << "/"
|
||||
<< maxAttempts << " FAILED (rc=" << loadRc << ")" << std::endl;
|
||||
}
|
||||
if (loadRc != 0) PostMessageA(hwnd, WM_CLOSE, 0, 0); // exit; port already printed
|
||||
if (loadRc != 0)
|
||||
PostMessageA(hwnd, g_fxGuiFloatMode ? WM_FXGUI_EXIT : WM_CLOSE,
|
||||
0, 0); // exit; port already printed
|
||||
});
|
||||
RemovePropA(hwnd, "FXGUI_FX");
|
||||
fxGuiDetachSafe(&fx);
|
||||
|
||||
Reference in New Issue
Block a user