feat: embed native VST GUI in mastering + fx rack panels (bridge --fx-gui frame capture + input relay)

Linux parts (0L1-4L1 per TASKS_LINUX_WINDOWS.md):
- native_bridge: FxGuiServer.cpp HTTP server (Winsock) — /frame BitBlt +
  PrintWindow fallback -> JPEG (libjpeg-turbo q70), /input PostMessage relay
  (mouse/wheel/key, coords scaled), /close, /ping; RenderFxJob fxGuiSetup +
  run_fx_gui_server (offscreen); main.cpp --fx-gui dispatch; vcpkg +
  CMakeLists WIN32-only + JPEG.
- api: open_fx_gui embed flag spawn --fx-gui, parse SF_FXGUI_PORT -> embed_url,
  fx-gui/close endpoint; 4 unit tests.
- ui: VstGuiEmbed component (frame refresh 100ms, ping 2s, input relay,
  scale coords) shared by MasteringModal + FXRackModal; swap graph<->GUI,
  cleanup on unmount, dead-bridge toast fallback.
- docs: TASKS_LINUX_WINDOWS.md, WALKTHROUGH_WINDOWS.md, PLAN status.
Windows tasks 0W1-4W1 remain (build + E2E real VST).
This commit is contained in:
2026-08-17 22:37:55 +07:00
parent 38f4cbd8b5
commit c382afe2a6
13 changed files with 3098 additions and 2051 deletions
+53 -13
View File
@@ -931,12 +931,11 @@ static void fxGuiDetachSafe(void* fx) {
#endif
}
int run_open_fx_gui(const std::string& jobPath) {
// The editor window and its message pump live on this (STA) thread for the
// whole GUI session — VST3 editors need the COM apartment + pump together.
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
struct ComGuard { ~ComGuard() { CoUninitialize(); } } comGuard;
// Shared setup for both GUI modes: parse job, load plugin, create host window
// (offscreen for embedded capture), attach editor (SEH-guarded). Returns 0 ok,
// 1 job/argument error, 2 plugin load or editor attach failure.
static int fxGuiSetup(const std::string& jobPath, Vst3Fx* fx, HWND* hwndOut,
bool offscreen, LONG_PTR userData) {
// 1. Parse job { path, name }.
std::ifstream jf(jobPath, std::ios::binary);
if (!jf) { std::cerr << "[FxGui] cannot read job file: " << jobPath << std::endl; return 1; }
@@ -954,31 +953,47 @@ int run_open_fx_gui(const std::string& jobPath) {
if (path.empty()) { std::cerr << "[FxGui] job missing path" << std::endl; return 1; }
// 2. Load the plugin (fixed 44.1k/512 — GUI only, no audio processed here).
Vst3Fx fx;
if (!fx.load(path, 44100.0, 512)) { std::cerr << "[FxGui] plugin load failed: " << path << std::endl; return 2; }
if (!fx->load(path, 44100.0, 512)) { std::cerr << "[FxGui] plugin load failed: " << path << std::endl; return 2; }
// 3. Host window.
// 3. Host window. Embedded mode parks it far off-screen (never steals
// focus; still visible to GDI so BitBlt captures real pixels).
WNDCLASSA wc = {};
wc.lpfnWndProc = FxGuiWndProc;
wc.hInstance = GetModuleHandleA(nullptr);
wc.lpszClassName = kFxGuiWndClass;
RegisterClassA(&wc);
FxGuiCtx ctx;
const int x = offscreen ? -20000 : CW_USEDEFAULT;
const int y = offscreen ? -20000 : CW_USEDEFAULT;
HWND hwnd = CreateWindowExA(0, kFxGuiWndClass,
(name.empty() ? "VST FX" : name).c_str(),
WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
x, y, 800, 600,
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
if (!hwnd) { std::cerr << "[FxGui] create window failed" << std::endl; return 1; }
SetWindowLongPtrA(hwnd, GWLP_USERDATA, (LONG_PTR)&ctx);
SetWindowLongPtrA(hwnd, GWLP_USERDATA, userData);
// 4. Attach the plugin editor (SEH-guarded — plugin GUI code may crash).
if (!fxGuiAttachSafe(&fx, hwnd)) {
if (!fxGuiAttachSafe(fx, hwnd)) {
std::cerr << "[FxGui] editor attach failed for " << path << std::endl;
DestroyWindow(hwnd);
return 2;
}
std::cout << "[FxGui] editor open: " << (name.empty() ? path : name) << std::endl;
*hwndOut = hwnd;
return 0;
}
int run_open_fx_gui(const std::string& jobPath) {
// The editor window and its message pump live on this (STA) thread for the
// whole GUI session — VST3 editors need the COM apartment + pump together.
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
struct ComGuard { ~ComGuard() { CoUninitialize(); } } comGuard;
Vst3Fx fx;
HWND hwnd = nullptr;
FxGuiCtx ctx;
const int rc = fxGuiSetup(jobPath, &fx, &hwnd, /*offscreen=*/false, (LONG_PTR)&ctx);
if (rc) return rc;
// 5. Pump messages until the window is destroyed.
MSG msg;
@@ -995,6 +1010,25 @@ int run_open_fx_gui(const std::string& jobPath) {
return 0;
}
// Embedded GUI server mode: same setup but off-screen, then hand the message
// pump + HTTP capture/input to FxGuiServer.cpp (see PLAN_MASTERBUS_VST_GUI_EMBED.md).
extern int fxGuiServerLoop(HWND hwnd);
int run_fx_gui_server(const std::string& jobPath) {
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
struct ComGuard { ~ComGuard() { CoUninitialize(); } } comGuard;
Vst3Fx fx;
HWND hwnd = nullptr;
const int rc = fxGuiSetup(jobPath, &fx, &hwnd, /*offscreen=*/true, 0);
if (rc) return rc;
const int srv = fxGuiServerLoop(hwnd);
fxGuiDetachSafe(&fx);
std::cout << "[FxGuiServer] closed" << std::endl;
return srv;
}
#else // !_WIN32
int run_open_fx_gui(const std::string& jobPath) {
@@ -1003,6 +1037,12 @@ int run_open_fx_gui(const std::string& jobPath) {
return 1;
}
int run_fx_gui_server(const std::string& jobPath) {
(void)jobPath;
std::cerr << "[FxGui] GUI mode chỉ hỗ trợ Windows" << std::endl;
return 1;
}
#endif // _WIN32
// --- --scan: classify VST3 modules as instrument/effect ----------------------