fix: VST2 qua jbridge bị tạch tạch — pace child theo consumer-sync + bridge copy full block

Root cause: child plugin_host render ~50x real-time (9323 blocks/s), bridge
đọc slot đang bị child ghi đè (torn read) và mỗi segment của 1 DAW block
đọc từ nhiều child block khác nhau -> discontinuity -> tạch thay vì âm thật.

- SandboxHostIPC: thêm bridgeConsumed[2] cuối struct (append-only, offsets
  cũ giữ nguyên) — child đếm publish/slot, bridge đếm consume/slot.
- plugin_host_main.cpp: sau khi publish 1 block, chờ bridge consume xong
  block đó mới render block kế (wait-sau-publish, không phải wait-trước-
  render — wait-trước vẫn cho phép publish 2 block liên tiếp giữa 2 lần
  consume -> bridge đọc latest sẽ bỏ block). Child giờ publish đúng 1
  block/DAW block = real-time; bridge không bao giờ stall (main loop DAW
  render + pace mọi iteration) nên không deadlock; nếu bridge chết,
  parent_alive break wait.
- SandboxVst2Host: copy full 256 mẫu vào local buffer khi slot đổi, serve
  mọi segment của 1 DAW block từ 1 child block nhất quán.

Verify: child pace 371 blocks/2s (real-time 375, trước 18647); domFreq của
waveform = 257.8Hz đúng nốt C4 (note 60) — âm thật, không phải noise;
correlation giữa các block liên tiếp âm đều ~-0.4 (waveform tuần hoàn);
GUI open path không deadlock (pace vẫn real-time, jBridge dialog + Qin_VST_
Window hiện đủ); GUI đóng -> child idle delta 0; silence_all master peak
0.000016.
This commit is contained in:
2026-08-21 09:43:11 +07:00
parent 0f6235c9b9
commit da7ae02509
4 changed files with 51 additions and 5 deletions
+17
View File
@@ -227,6 +227,10 @@ int main(int argc, char* argv[]) {
}
}).detach();
// G4.7: publish counter per slot (how many times this child published
// each slot). Paired with the bridge's bridgeConsumed[2] - see the
// wait after publish below.
uint32_t childPub[2] = {0, 0};
int activeNotes = 0;
bool idle = false;
bool quit = false;
@@ -345,6 +349,7 @@ int main(int argc, char* argv[]) {
uint32_t slot = 1u - (ipc->writeSlot & 1u);
inst.processAudioBlock(ipc->audioLeft[slot], ipc->audioRight[slot], block);
ipc->writeSlot = slot;
childPub[slot]++;
ipc->base.bridgeWriteIndex++;
if (activeNotes == 0) {
float peak = 0.0f;
@@ -358,6 +363,18 @@ int main(int argc, char* argv[]) {
}
if (peak < 0.0005f) idle = true;
}
// G4.7: consumer-sync pacing. The bridge (DAW) consumes exactly one
// block per real-time block; the old unpaced child rendered ~50x
// real-time, so the bridge skipped most child blocks -> audio jumps
// ("tạch tạch" instead of the instrument). Wait AFTER publishing
// until the bridge copied THIS block before rendering the next one.
// The bridge never stalls (DAW main loop renders + paces every
// iteration), so this wait is bounded (~5ms). If the bridge dies,
// parentPid points at it - the parent_alive check breaks the wait.
while (ipc->bridgeConsumed[slot] < childPub[slot]) {
if (parentPid && !parent_alive(parentPid)) break;
Sleep(1);
}
Sleep(0);
}
inst.closeGUI();