Files
SonicForgeStudio/native_bridge/src/FxGuiServer.cpp
T
admin da0f8fa40e feat(native): realtime master FX chain + fx-gui fast start, hide/show, non-topmost
- FxGuiServer: HWND_TOP (no longer always-on-top), /hide + /show endpoints,
  onStarted callback prints SF_FXGUI_PORT before plugin load (fast connect)
- RenderFxJob: RealtimeFxChain (mutex+worker, SEH-guarded, bypass slots,
  deferred Vst3Fx dtor); --fx-gui split into parse/create/load-attach stages
- main: FreeConsole unless SF_KEEP_CONSOLE; control type 6 SET_FX_CHAIN feeds
  g_fxChain on the audio loop; shutdown before CoUninitialize
- plugins.py: CREATE_NO_WINDOW on both Popen paths
- lib.rs: set_fx_chain command -> push_control(6)
- app.jsx/nativeBridgeService: setFxChain sync from MasteringModal; panel close
  uses /hide + refs to fix closure bug; VstGuiEmbed rect refresh -> /show
2026-08-19 09:59:13 +07:00

601 lines
26 KiB
C++

// 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.
//
// Routes:
// GET /ping -> {"ok":true}
// GET /frame -> image/jpeg (BitBlt, fallback PrintWindow)
// POST /input -> {type:"mousedown|mousemove|mouseup|wheel|keydown|keyup",
// x,y,button,deltaY,keyCode} -> PostMessage to HWND
// POST /close -> detach + exit 0
//
// The HTTP server runs on a worker thread; the calling (STA) thread runs the
// message pump — VST3 editors need COM apartment + pump on the same thread.
// The listen port is random and printed to stdout as `SF_FXGUI_PORT=<port>`.
#ifdef _WIN32
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#include <cstdio>
#include <jpeglib.h>
#include <atomic>
#include <cstring>
#include <cstdint>
#include <functional>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
// sheredom/json.h (public domain) — vendored copy; SDK copy as fallback.
#if __has_include("sheredom_json.h")
#include "sheredom_json.h"
#elif __has_include("vst3sdk/public.sdk/source/vst/moduleinfo/json.h")
#include "vst3sdk/public.sdk/source/vst/moduleinfo/json.h"
#endif
// Defined in RenderFxJob.cpp (global scope); declared here outside the
// anonymous namespace so both TUs see the same symbol.
extern void fxGuiRequestResize(HWND hwnd, int w, int h);
namespace {
// --- tiny JSON helpers (same API as RenderFxJob.cpp) ------------------------
const json_object_element_s* member(const json_object_s* o, const char* key) {
for (const json_object_element_s* e = o ? o->start : nullptr; e; e = e->next)
if (e->name && e->name->string && std::strcmp(e->name->string, key) == 0)
return e;
return nullptr;
}
const json_value_s* memberValue(const json_object_s* o, const char* key) {
const json_object_element_s* m = member(o, key);
return m ? m->value : nullptr;
}
std::string memberString(const json_object_s* o, const char* key, const std::string& def) {
const json_value_s* v = memberValue(o, key);
if (v && v->type == json_type_string) {
const auto* s = static_cast<const json_string_s*>(v->payload);
return std::string(s->string, s->string_size);
}
return def;
}
int64_t memberInt(const json_object_s* o, const char* key, int64_t def) {
const json_value_s* v = memberValue(o, key);
if (v && v->type == json_type_number)
return (int64_t)std::atof(static_cast<const json_number_s*>(v->payload)->number);
return def;
}
// --- HTTP plumbing -----------------------------------------------------------
struct HttpReq {
std::string method;
std::string path;
std::string body;
};
// Read one request (headers up to \r\n\r\n + Content-Length body). Returns
// false on socket error or malformed request line.
bool readRequest(SOCKET c, HttpReq& out) {
std::string raw;
char buf[4096];
while (raw.find("\r\n\r\n") == std::string::npos) {
if (raw.size() > 16384) return false;
int n = recv(c, buf, (int)sizeof(buf), 0);
if (n <= 0) return false;
raw.append(buf, (size_t)n);
}
const size_t headEnd = raw.find("\r\n\r\n");
const std::string head = raw.substr(0, headEnd);
const size_t sp1 = head.find(' ');
const size_t sp2 = head.find(' ', sp1 + 1);
if (sp1 == std::string::npos || sp2 == std::string::npos) return false;
out.method = head.substr(0, sp1);
out.path = head.substr(sp1 + 1, sp2 - sp1 - 1);
// strip query string: frontend cache-busts /frame?t=<ms>
const size_t q = out.path.find('?');
if (q != std::string::npos) out.path.resize(q);
long contentLength = 0;
size_t pos = 0;
while ((pos = head.find("\r\n", pos)) != std::string::npos) {
const size_t lineEnd = head.find("\r\n", pos + 2);
const std::string line = head.substr(pos + 2, lineEnd == std::string::npos ? std::string::npos : lineEnd - pos - 2);
pos = lineEnd == std::string::npos ? std::string::npos : lineEnd;
if (line.rfind("Content-Length:", 0) == 0 || line.rfind("content-length:", 0) == 0) {
contentLength = std::atol(line.c_str() + 15);
break;
}
}
if (contentLength > 0) {
while (raw.size() < headEnd + 4 + (size_t)contentLength) {
int n = recv(c, buf, (int)sizeof(buf), 0);
if (n <= 0) return false;
raw.append(buf, (size_t)n);
}
out.body = raw.substr(headEnd + 4, (size_t)contentLength);
}
return true;
}
void sendResponse(SOCKET c, int code, const char* contentType,
const void* data, size_t len) {
char hdr[512];
std::snprintf(hdr, sizeof(hdr),
"HTTP/1.1 %d %s\r\nContent-Type: %s\r\n"
"Content-Length: %zu\r\nAccess-Control-Allow-Origin: *\r\n"
"Connection: close\r\n\r\n",
code, code == 200 ? "OK" : "Error", contentType, len);
send(c, hdr, (int)std::strlen(hdr), 0);
if (len > 0) send(c, (const char*)data, (int)len, 0);
}
// CORS preflight reply: the embed page (origin 127.0.0.1:8000) POSTs JSON to
// this server (a different port), so the browser sends an OPTIONS request
// first. Without this handler the preflight fell into the /input JSON parser
// and failed with 400, so the embedded GUI looked frozen: frames streamed but
// clicks never reached the plugin.
void sendOptions(SOCKET c) {
const char* r =
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"
"Access-Control-Allow-Headers: Content-Type\r\n"
"Content-Length: 0\r\nConnection: close\r\n\r\n";
send(c, r, (int)std::strlen(r), 0);
}
// --- 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). 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;
const int w = rc.right - rc.left;
const int h = rc.bottom - rc.top;
if (w <= 0 || h <= 0) return false;
HDC win = GetDC(hwnd);
HDC mem = CreateCompatibleDC(win);
HBITMAP bmp = CreateCompatibleBitmap(win, w, h);
HGDIOBJ oldBmp = SelectObject(mem, bmp);
std::vector<unsigned char> pxB((size_t)w * h * 4), pxP((size_t)w * h * 4);
auto grabInto = [&](unsigned char* p) -> bool {
BITMAPINFO bi = {};
bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bi.bmiHeader.biWidth = w;
bi.bmiHeader.biHeight = -h; // top-down
bi.bmiHeader.biPlanes = 1;
bi.bmiHeader.biBitCount = 32;
bi.bmiHeader.biCompression = BI_RGB;
return GetDIBits(mem, bmp, 0, h, p, &bi, DIB_RGB_COLORS) == h;
};
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 (okP) px = pxP.data();
else if (okB) px = pxB.data();
SelectObject(mem, oldBmp);
DeleteObject(bmp);
DeleteDC(mem);
ReleaseDC(hwnd, win);
if (!px) return false;
// BGRA (GDI) -> RGB, then JPEG.
std::vector<unsigned char> rgb((size_t)w * h * 3);
for (int i = 0; i < w * h; ++i) {
rgb[(size_t)i * 3 + 0] = px[(size_t)i * 4 + 2];
rgb[(size_t)i * 3 + 1] = px[(size_t)i * 4 + 1];
rgb[(size_t)i * 3 + 2] = px[(size_t)i * 4 + 0];
}
jpeg_compress_struct cinfo = {};
jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
unsigned char* memBuf = nullptr;
unsigned long memSize = 0;
jpeg_mem_dest(&cinfo, &memBuf, &memSize);
cinfo.image_width = (JDIMENSION)w;
cinfo.image_height = (JDIMENSION)h;
cinfo.input_components = 3;
cinfo.in_color_space = JCS_RGB;
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE);
jpeg_start_compress(&cinfo, TRUE);
JSAMPROW rows[1];
rows[0] = rgb.data();
while (cinfo.next_scanline < cinfo.image_height) {
jpeg_write_scanlines(&cinfo, rows, 1);
rows[0] += (size_t)w * 3;
}
jpeg_finish_compress(&cinfo);
if (memBuf && memSize > 0) out.assign(memBuf, memBuf + memSize);
std::free(memBuf);
jpeg_destroy_compress(&cinfo);
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;
// 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
// rect = legacy cursor-relative mode.
RECT g_embedRect = {0, 0, 0, 0};
bool g_anchored = false; // window anchored over panel (stays until /close)
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, HWND target, int x, int y) {
// Anchored mode: place the host window's client area exactly over the embed
// panel rect (borderless + topmost) so real mouse events hit the plugin
// without moving the cursor, then ask the pump thread to size the plugin
// view to the panel. No park deadline -- the window stays until /close.
// Re-runs on every click so a scrolled/resized panel re-anchors.
if (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;
LONG_PTR style = GetWindowLongPtrA(hwnd, GWL_STYLE);
if (style & (WS_CAPTION | WS_THICKFRAME | WS_SYSMENU |
WS_MINIMIZEBOX | WS_MAXIMIZEBOX)) {
style &= ~(WS_CAPTION | WS_THICKFRAME | WS_SYSMENU |
WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
SetWindowLongPtrA(hwnd, GWL_STYLE, style);
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
SWP_NOACTIVATE | SWP_FRAMECHANGED);
}
SetWindowPos(hwnd, HWND_TOP, g_embedRect.left, g_embedRect.top,
cw, ch, SWP_NOACTIVATE | SWP_SHOWWINDOW);
fxGuiRequestResize(hwnd, cw, ch);
Sleep(60); // let the compositor repaint so the editor sees itself active
g_dance = true;
g_anchored = true;
g_parkUntil = 0; // anchored: stay until /close
return;
}
if (g_dance) return;
// Dat window ngay tai vi tri cursor (dang nam tren panel embed trong DAW)
// chu khong nhay ra goc man hinh: shift window sao cho client point (x,y)
// cua editor trung voi screen point dang click -> GUI xuat hien dung cho
// panel, khong con floating ngoai cua so DAW.
POINT pt = { x, y };
ClientToScreen(target, &pt); // screen cua diem click (window con offscreen)
POINT cur;
GetCursorPos(&cur);
RECT wr = {0};
GetWindowRect(hwnd, &wr);
const int nx = wr.left + (cur.x - pt.x);
const int ny = wr.top + (cur.y - pt.y);
GetCursorPos(&g_savedCursor);
SetWindowPos(hwnd, HWND_TOP, nx, ny, 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;
if (g_anchored) return; // anchored: stays until /close
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);
// Anchored rect from the embed panel (screen coords, physical px).
const json_value_s* rv = memberValue(o, "rect");
if (rv && rv->type == json_type_object) {
const json_object_s* ro = static_cast<const json_object_s*>(rv->payload);
const LONG rw = (LONG)memberInt(ro, "w", 0);
const LONG rh = (LONG)memberInt(ro, "h", 0);
if (rw > 0 && rh > 0) {
g_embedRect.left = (LONG)memberInt(ro, "x", 0);
g_embedRect.top = (LONG)memberInt(ro, "y", 0);
g_embedRect.right = g_embedRect.left + rw;
g_embedRect.bottom = g_embedRect.top + rh;
} else {
g_embedRect = {0, 0, 0, 0}; // empty rect -> back to cursor mode
}
}
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 (g_embedRect.right > g_embedRect.left) {
// Anchored mode exists for plugins that need real input; the window
// appears exactly over the panel, so probing PostMessage first only
// wastes clicks.
g_realInput = true;
} else 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, target, x, y);
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, target, x, y);
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(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;
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(target, m, 0, lp);
} else if (type == "wheel") {
// 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(target);
PostMessageA(target, WM_KEYDOWN, keyCode, 0);
} else if (type == "keyup") {
PostMessageA(target, WM_KEYUP, keyCode, 0);
}
}
// --- HTTP server loop (worker thread) ---------------------------------------
void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { std::cerr << "[FxGuiServer] WSAStartup failed" << std::endl; return; }
SOCKET lsock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (lsock == INVALID_SOCKET) { WSACleanup(); return; }
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = 0; // random free port
if (bind(lsock, (sockaddr*)&addr, sizeof(addr)) != 0 || listen(lsock, 4) != 0) {
closesocket(lsock);
WSACleanup();
std::cerr << "[FxGuiServer] bind/listen failed" << std::endl;
return;
}
sockaddr_in bound = {};
int blen = sizeof(bound);
getsockname(lsock, (sockaddr*)&bound, &blen);
std::cout << "SF_FXGUI_PORT=" << ntohs(bound.sin_port) << std::endl;
std::cout.flush();
while (!stop.load()) {
fd_set rf;
FD_ZERO(&rf);
FD_SET(lsock, &rf);
timeval tv = { 0, 100000 }; // 100ms poll so /close can break out
int sel = select(0, &rf, nullptr, nullptr, &tv);
if (sel <= 0) continue;
SOCKET c = accept(lsock, nullptr, nullptr);
if (c == INVALID_SOCKET) continue;
HttpReq req;
if (!readRequest(c, req)) { closesocket(c); continue; }
if (req.method == "OPTIONS") {
sendOptions(c);
} 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());
else
sendResponse(c, 500, "application/json", "{\"error\":\"capture failed\"}", 25);
} else if (req.path == "/input") {
json_parse_result_s pres = {};
json_value_s* root = json_parse_ex(req.body.data(), req.body.size(),
json_parse_flags_default, nullptr, nullptr, &pres);
if (root && root->type == json_type_object) {
relayInput(hwnd, static_cast<const json_object_s*>(root->payload));
std::free(root);
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
} else {
if (root) std::free(root);
sendResponse(c, 400, "application/json", "{\"error\":\"bad json\"}", 20);
}
} else if (req.path == "/hide") {
ShowWindow(hwnd, SW_HIDE);
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
} else if (req.path == "/show") {
// 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).
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) {
g_embedRect.left = (LONG)memberInt(sro, "x", 0);
g_embedRect.top = (LONG)memberInt(sro, "y", 0);
g_embedRect.right = g_embedRect.left + srw;
g_embedRect.bottom = g_embedRect.top + srh;
rectOk = true;
}
}
}
if (sroot) std::free(sroot);
if (rectOk) {
SetWindowPos(hwnd, HWND_TOP, g_embedRect.left, g_embedRect.top,
g_embedRect.right - g_embedRect.left,
g_embedRect.bottom - g_embedRect.top,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
fxGuiRequestResize(hwnd, g_embedRect.right - g_embedRect.left,
g_embedRect.bottom - g_embedRect.top);
g_dance = true;
g_anchored = true;
g_parkUntil = 0;
} else {
ShowWindow(hwnd, SW_SHOW);
SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
}
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
} else if (req.path == "/close") {
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
stop.store(true);
PostMessageA(hwnd, WM_CLOSE, 0, 0); // pump exits -> process returns
} else {
sendResponse(c, 404, "application/json", "{\"error\":\"not found\"}", 22);
}
closesocket(c);
}
closesocket(lsock);
WSACleanup();
}
} // namespace
// Runs the embedded GUI server: starts the HTTP worker thread, then pumps
// messages on this (STA) thread until the host window is destroyed (user
// closed it or POST /close). Returns 0.
int fxGuiServerLoop(HWND hwnd, const std::function<void()>& onStarted) {
std::atomic<bool> stop{false};
std::thread http(runHttpServer, hwnd, std::ref(stop));
// onStarted runs on the pump (STA) thread AFTER the HTTP server is up and
// the port printed — used to defer the slow plugin load/attach in server
// mode so the caller (plugins.py) doesn't block on VST load.
if (onStarted) onStarted();
MSG msg;
while (IsWindow(hwnd)) {
BOOL r = GetMessageA(&msg, nullptr, 0, 0);
if (r <= 0) break;
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
stop.store(true);
if (http.joinable()) http.join();
return 0;
}
#endif // _WIN32