V45.42: xoa toolbar label 'VST GUI - nhung (frame-capture)' trong VstGuiEmbed (GUI hien thi thang o row, khong con header bar) + fix flash trang khi load GUI embed o tang C++: RenderFxJob.cpp them global g_fxGuiEditorReady (set sau khi editor attach thanh cong tren pump thread), FxGuiServer.cpp /frame tu choi 503 khi chua ready, tinh mean-luma trong captureHwndToJpeg de loai frame trang/den (avg <0.02 hoac >0.98), cache g_lastGood de serve frame tot cuoi thay vi frame blank khi window dang repaint. Rebuild daw_vst_bridge+fx_vst_bridge+engine+installer NSIS/MSI v=202609021810.

This commit is contained in:
2026-09-02 18:31:41 +07:00
parent 0da2a41330
commit 9f3b072be0
11 changed files with 58 additions and 23 deletions
+2 -8
View File
@@ -13380,7 +13380,7 @@ const ExportProgressModal = ({ open, exportProgress, isExporting, onClose }) =>
const boxRef = React.useRef(null);
const deadRef = React.useRef(false);
const screenRectRef = React.useRef(null);
const [dead, setDead] = React.useState(false);
const [, setDead] = React.useState(false);
const [frameLoaded, setFrameLoaded] = React.useState(false);
const post = (payload) => { try { fetch(url + '/input', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).catch(() => {}); } catch (e) {} };
React.useEffect(() => {
@@ -13475,13 +13475,7 @@ const ExportProgressModal = ({ open, exportProgress, isExporting, onClose }) =>
};
return (
<div className="relative w-full h-full flex flex-col">
<div className="flex items-center justify-between px-3 py-1.5 bg-slate-900/80 border border-slate-800 rounded-t-lg shrink-0">
<span className="text-[10px] text-violet-300 font-mono">VST GUI — nhúng (frame-capture · ~10fps)</span>
<div className="flex items-center gap-2">
{dead && <span className="text-[9px] text-red-400 font-mono">bridge mất kết nối</span>}
</div>
</div>
<div ref={boxRef} tabIndex={0} className="flex-1 relative bg-slate-950 border border-slate-800 rounded-b-lg overflow-hidden outline-none"
<div ref={boxRef} tabIndex={0} className="flex-1 relative bg-slate-950 border border-slate-800 rounded-lg overflow-hidden outline-none"
onMouseDown={handle} onMouseUp={handle} onMouseMove={handle}
onMouseLeave={() => post({ type: 'mouseup', x: -1, y: -1, button: 0 })}
onWheel={e => { const p = pt(e); if (p) post({ type: 'wheel', x: p.x, y: p.y, deltaY: e.deltaY, rect: screenRectRef.current || undefined }); }}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -51,7 +51,7 @@
<script src="/static/js/services/promptTemplateManager.js?v=202608290945"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202608290945"></script>
<script src="/static/js/services/chordTheory.js?v=202608290945"></script>
<script src="/static/js/app.precompiled.js?v=202609021718" defer></script>
<script src="/static/js/app.precompiled.js?v=202609021810" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202608290945">
<style>
:root {
+40 -10
View File
@@ -49,9 +49,17 @@ extern void fxGuiRequestCapture(void* hwnd, std::string& outB64, int timeoutMs);
// Tell RenderFxJob's SHM audio feeder which realtime session is live (the
// frontend pushes the session name; "" = session stopped/restarted).
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;
namespace {
// Last good captured JPEG: /frame falls back to it while the editor window is
// blank/repainting (avoids flashing the panel white/black). Only touched by
// the single HTTP worker thread — no lock needed.
std::vector<unsigned char> g_lastGood;
// --- 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)
@@ -163,7 +171,7 @@ void sendOptions(SOCKET c) {
// 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) {
bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality, float* outAvg = nullptr) {
RECT rc;
if (!GetClientRect(hwnd, &rc)) return false;
const int w = rc.right - rc.left;
@@ -204,13 +212,20 @@ bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality)
ReleaseDC(hwnd, win);
if (!px) return false;
// BGRA (GDI) -> RGB, then JPEG.
// BGRA (GDI) -> RGB, then JPEG. Accumulate mean luma (0..1) so the
// caller can reject blank frames (host window not painted yet).
std::vector<unsigned char> rgb((size_t)w * h * 3);
double lumaSum = 0;
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];
const unsigned char b = px[(size_t)i * 4 + 0];
const unsigned char g = px[(size_t)i * 4 + 1];
const unsigned char r = px[(size_t)i * 4 + 2];
rgb[(size_t)i * 3 + 0] = r;
rgb[(size_t)i * 3 + 1] = g;
rgb[(size_t)i * 3 + 2] = b;
lumaSum += (double)r + g + b;
}
if (outAvg) *outAvg = (float)(lumaSum / ((double)w * h * 3 * 255.0));
jpeg_compress_struct cinfo = {};
jpeg_error_mgr jerr;
@@ -485,11 +500,26 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
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);
if (!g_fxGuiEditorReady.load()) {
// VST still loading / editor attaching (pump thread): refuse
// rather than stream the empty host window — the webview used
// to show that blank frame as a white flash on open.
sendResponse(c, 503, "application/json", "{\"error\":\"loading\"}", 19);
} else {
std::vector<unsigned char> jpg;
float avg = -1.0f;
if (captureHwndToJpeg(hwnd, jpg, 70, &avg) &&
avg >= 0.02f && avg <= 0.98f) {
g_lastGood = jpg; // single HTTP worker thread — no lock
sendResponse(c, 200, "image/jpeg", jpg.data(), jpg.size());
} else if (!g_lastGood.empty()) {
// Blank/transient frame (window still repainting) — keep
// the last good frame so the panel never flashes blank.
sendResponse(c, 200, "image/jpeg", g_lastGood.data(), g_lastGood.size());
} else {
sendResponse(c, 503, "application/json", "{\"error\":\"loading\"}", 19);
}
}
} else if (req.path == "/input") {
json_parse_result_s pres = {};
json_value_s* root = json_parse_ex(req.body.data(), req.body.size(),
+11
View File
@@ -62,6 +62,12 @@
#include <thread>
#include <vector>
// Editor-ready flag (fx-gui server mode). Set true once the VST editor
// attached to the host window (fxGuiLoadAttach, pump thread). FxGuiServer.cpp
// 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};
namespace {
// --- sheredom/json.h helpers (same as RenderJob.cpp) ------------------------
@@ -1819,11 +1825,16 @@ static int fxGuiLoadAttach(Vst3Fx* fx, const std::string& path, const std::strin
std::cerr << "[FxGui] preset apply FAILED (default kept): " << path << std::endl;
else if (!presetB64.empty())
std::cerr << "[FxGui] preset applied: " << path << std::endl;
g_fxGuiEditorReady.store(false);
if (!fxGuiAttachSafe(fx, hwnd)) {
std::cerr << "[FxGui] editor attach failed for " << path << std::endl;
DestroyWindow(hwnd);
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);
std::cout << "[FxGui] editor open: " << (name.empty() ? path : name) << std::endl;
return 0;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.