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
+19 -5
View File
@@ -321,12 +321,26 @@ void SandboxVst2Host::processAudioBlock(float* outputL, float* outputR, uint32_t
std::memset(outputR, 0, numSamples * sizeof(float));
return;
}
// G4.3: the child publishes the slot it finished rendering (writeSlot).
// Copy that block. A stale slot means the child is still working on the
// other one - we repeat the previous block instead of tearing.
// G4.7: copy the newest child block into a LOCAL buffer once per slot
// change, then serve every segment of this DAW block from it. The old
// direct read tore when the unpaced child (~50x real-time) overwrote
// the slot mid-read, and mixed samples from several child blocks across
// the segments of one DAW block. With consumer-sync pacing the child
// publishes exactly one block per DAW block, so the slot only changes
// at a block boundary. ponytail: a child publish landing between two
// segments of one DAW block (sub-ms race) would still copy mid-block;
// impossible in practice - the child is asleep in Sleep(1) when the
// DAW dispatches segments (<100us). Upgrade path: pass the DAW block
// index into processAudioBlock and copy once per block.
uint32_t slot = ipc_->writeSlot & 1u;
if (slot != lastWriteSlot_) {
std::memcpy(lastLeft_, ipc_->audioLeft[slot], sizeof(lastLeft_));
std::memcpy(lastRight_, ipc_->audioRight[slot], sizeof(lastRight_));
ipc_->bridgeConsumed[slot]++;
lastWriteSlot_ = slot;
}
for (uint32_t i = 0; i < numSamples; ++i) {
outputL[i] = ipc_->audioLeft[slot][i];
outputR[i] = ipc_->audioRight[slot][i];
outputL[i] = lastLeft_[i];
outputR[i] = lastRight_[i];
}
}