// native_bridge/src/FxGuiServer.cpp // Embedded VST GUI server (Phase 0 of PLAN_MASTERBUS_VST_GUI_EMBED.md): // `daw_vst_bridge --fx-gui ` 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=`. #ifdef _WIN32 #include #include #include #pragma comment(lib, "ws2_32.lib") #include #include #include #include #include #include #include #include #include #include // 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(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(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= 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& 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 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 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 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 g_dance{false}; // window temporarily on-screen/topmost std::atomic 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 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(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& 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 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(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(sroot->payload); const json_value_s* srv = memberValue(so, "rect"); if (srv && srv->type == json_type_object) { const json_object_s* sro = static_cast(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& onStarted) { std::atomic 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