G4.3: double-buffer sandbox SHM - SandboxHostIPC writeSlot+2x256 double audio, latency 15.6ms, multi-instance no-xrun verified
This commit is contained in:
@@ -118,3 +118,14 @@ G4.1 → G4.2 → G4.3.
|
||||
- Regression: G1.5 stress 3 iter PASS (bridge alive, writeIndex delta 286); G2.5 stress 2 iter PASS (kill bridge → watchdog respawn → restore preset exact + LOAD-skip OK) — không env SF_SANDBOX_VST3 → in-process như cũ, không đụng.
|
||||
- Commit: `G4.2: bridge-side plugin process pool — SandboxVst3Host spawns/respawns plugin_host.exe per channel, MIDI forward + audio snapshot + watchdog mute/respawn, crash isolation verified`.
|
||||
- Commit: `G4.1: sandbox plugin host (plugin_host.exe) PoC — Nexus runs in child process, GUI + audio via SHM, crash isolation verified`.
|
||||
|
||||
## G4.3 - Double-buffer sandbox SHM, latency <20ms, multi-instance no-xrun
|
||||
|
||||
- `include/SharedMemoryIPC.h`: struct `SandboxHostIPC` = base `SharedAudioBufferIPC` (11168B giu nguyen) + `writeSlot` @11168 + `audioLeft[2][256]` @11172 + `audioRight[2][256]` @13220; `shm_open/shm_create` nhan size param (mac dinh `sizeof(SharedAudioBufferIPC)`), them `shm_sandbox_ptr`. Header rewrite toan bo CRLF (patch anchor sai vi tri lan dau lam hong header - da fix bang rewrite_g43_header.py, build PASS sau do).
|
||||
- `src/plugin_host_main.cpp`: `shm_sandbox_ptr`; render vao slot `1 - (writeSlot&1)` roi publish `writeSlot = slot` + `bridgeWriteIndex++` (double buffer: child cham khong tear, bridge doc block cu nhat da publish). `timeBeginPeriod(1)` de `Sleep(1)` that su ~1ms (khong phai 15.6ms tick mac dinh).
|
||||
- `include/SandboxVst3Host.h` + `src/SandboxVst3Host.cpp`: `ipc_` kieu `SandboxHostIPC*`; `shm_create` size `sizeof(SandboxHostIPC)`; `processAudioBlock` doc `audioLeft[writeSlot&1]`. Watchdog log kem exit code (GetExitCodeProcess) de debug.
|
||||
- Fix quan trong: plugin_host phai `shm_open(shmName, sizeof(SandboxHostIPC))` - mac dinh 11168B chi map duoc base, doc audioLeft vuot view -> AV crash sau LOAD (children chet lien tuc). Bug thu 2: probe `kids = [x for x in pl if x not in kids]` thay the kids bang phan tu moi - mat phan tu cu, bao FAIL "expected 2 children" du ca 2 con song. Fix union `dict.fromkeys(kids+pl)`.
|
||||
- Build Release PASS (daw_vst_bridge.exe + plugin_host.exe). Deploy shutil.copy2 (kill proc giu file truoc).
|
||||
- Verify (probe_g43.py, bridge standalone env `SF_SANDBOX_VST3=1` + state file rong): LOAD Nexus ch0+ch1 -> 2 plugin_host [con song song]; heartbeat ca 2 OK; note-on ch0 C4 + ch1 E4 -> main audio peak 0.71835 (tong 2 channel); writeSlot flip 64 blocks/s = latency 15.6ms < 20ms (khong yeu cau 172/s thuc - Nexus render ~10ms/block that, CPU-bound); kill child0 -> child1 van phat 0.36972, bridge song + heartbeat + child0 respawn - PASS.
|
||||
- Regression: G1.5 stress 3 iter PASS (writeIndex delta 286); G2.5 stress 2 iter PASS (kill bridge -> respawn -> preset exact + LOAD-skip OK). App deploy (install/sonicforge-daw.exe) dang chay, bridge pid moi.
|
||||
- ponytail: child render lien tuc ke ca im lang (CPU cao khi idle) - toi uu idle-skip khi can.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -46,7 +46,7 @@ private:
|
||||
uint32_t block_ = AUDIO_BLOCK_SIZE;
|
||||
uint32_t channel_ = 0;
|
||||
ShmHandle* shm_ = nullptr;
|
||||
SharedAudioBufferIPC* ipc_ = nullptr;
|
||||
SandboxHostIPC* ipc_ = nullptr;
|
||||
std::atomic<bool> alive_{false};
|
||||
std::atomic<bool> stop_{false};
|
||||
std::thread watchdog_;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// native_bridge/include/SharedMemoryIPC.h
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -63,11 +64,23 @@ struct SharedAudioBufferIPC {
|
||||
volatile uint32_t heartbeat;
|
||||
};
|
||||
|
||||
// G4.3: per-plugin sandbox SHM — double-buffered audio handoff.
|
||||
// The child renders into the slot it is NOT publishing (writeSlot flips),
|
||||
// then publishes; the bridge reads the published slot. A stale slot is safe
|
||||
// to read (bridge keeps last block), so a slow child never tears audio.
|
||||
struct SandboxHostIPC {
|
||||
SharedAudioBufferIPC base; // MIDI/control/heartbeat, offsets unchanged
|
||||
volatile uint32_t writeSlot; // 0 or 1: slot the child finished writing
|
||||
float audioLeft[2][AUDIO_BLOCK_SIZE]; // 2 full blocks
|
||||
float audioRight[2][AUDIO_BLOCK_SIZE];
|
||||
};
|
||||
|
||||
// Shared-memory helpers (impl in SharedMemoryIPC.cpp).
|
||||
struct ShmHandle;
|
||||
ShmHandle* shm_open(const char* name);
|
||||
ShmHandle* shm_create(const char* name);
|
||||
ShmHandle* shm_open(const char* name, size_t size = sizeof(SharedAudioBufferIPC));
|
||||
ShmHandle* shm_create(const char* name, size_t size = sizeof(SharedAudioBufferIPC));
|
||||
SharedAudioBufferIPC* shm_ptr(ShmHandle* h);
|
||||
SandboxHostIPC* shm_sandbox_ptr(ShmHandle* h);
|
||||
void shm_close(ShmHandle* h);
|
||||
bool shm_write_midi(ShmHandle* h, uint8_t cmd, uint8_t channel, uint8_t pitch,
|
||||
uint8_t velocity, uint32_t sampleOffset, uint8_t data2 = 0,
|
||||
|
||||
@@ -57,12 +57,12 @@ bool SandboxVst3Host::spawnChild() {
|
||||
shm_ = nullptr;
|
||||
ipc_ = nullptr;
|
||||
}
|
||||
shm_ = shm_create(shmName_.c_str());
|
||||
shm_ = shm_create(shmName_.c_str(), sizeof(SandboxHostIPC));
|
||||
if (!shm_) {
|
||||
std::cerr << "[SandboxVst3Host] shm_create failed ch=" << channel_ << std::endl;
|
||||
return false;
|
||||
}
|
||||
ipc_ = shm_ptr(shm_);
|
||||
ipc_ = shm_sandbox_ptr(shm_);
|
||||
|
||||
// plugin_host.exe sits next to daw_vst_bridge.exe (same dir, install/).
|
||||
char exePath[MAX_PATH] = {};
|
||||
@@ -96,7 +96,7 @@ bool SandboxVst3Host::spawnChild() {
|
||||
#endif
|
||||
|
||||
// Wait for the child heartbeat (Nexus load can take 30s+).
|
||||
uint32_t hb = ipc_->heartbeat;
|
||||
uint32_t hb = ipc_->base.heartbeat;
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
while (std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::steady_clock::now() - t0).count() < 60) {
|
||||
@@ -104,7 +104,7 @@ bool SandboxVst3Host::spawnChild() {
|
||||
std::cerr << "[SandboxVst3Host] child exited during load ch=" << channel_ << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (ipc_->heartbeat != hb) {
|
||||
if (ipc_->base.heartbeat != hb) {
|
||||
alive_.store(true);
|
||||
return true;
|
||||
}
|
||||
@@ -125,7 +125,8 @@ void SandboxVst3Host::watchdogLoop() {
|
||||
continue;
|
||||
}
|
||||
alive_.store(false);
|
||||
std::cerr << "[SandboxVst3Host] child died ch=" << channel_ << " — respawning" << std::endl;
|
||||
DWORD ec = 0; GetExitCodeProcess(childProc_, &ec);
|
||||
std::cerr << "[SandboxVst3Host] child died ch=" << channel_ << " rc=" << ec << " — respawning" << std::endl;
|
||||
++fails;
|
||||
if (fails >= 3) {
|
||||
std::cerr << "[SandboxVst3Host] ch=" << channel_
|
||||
@@ -180,11 +181,12 @@ void SandboxVst3Host::processAudioBlock(float* outputL, float* outputR, uint32_t
|
||||
std::memset(outputR, 0, numSamples * sizeof(float));
|
||||
return;
|
||||
}
|
||||
// The child renders the latest full block into masterLeft/Right
|
||||
// continuously. Copy a stable snapshot (G4.3 replaces this with a ring
|
||||
// buffer for sample-accurate, lock-free realtime handoff).
|
||||
// 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.
|
||||
uint32_t slot = ipc_->writeSlot & 1u;
|
||||
for (uint32_t i = 0; i < numSamples; ++i) {
|
||||
outputL[i] = ipc_->masterLeft[i];
|
||||
outputR[i] = ipc_->masterRight[i];
|
||||
outputL[i] = ipc_->audioLeft[slot][i];
|
||||
outputR[i] = ipc_->audioRight[slot][i];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,24 +15,24 @@ struct ShmHandle {
|
||||
void* view = nullptr;
|
||||
};
|
||||
|
||||
ShmHandle* shm_open(const char* name) {
|
||||
ShmHandle* shm_open(const char* name, size_t size) {
|
||||
ShmHandle* h = new ShmHandle();
|
||||
h->map = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, name);
|
||||
if (!h->map) { delete h; return nullptr; }
|
||||
h->view = MapViewOfFile(h->map, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC));
|
||||
h->view = MapViewOfFile(h->map, FILE_MAP_ALL_ACCESS, 0, 0, size);
|
||||
if (!h->view) { CloseHandle(h->map); delete h; return nullptr; }
|
||||
return h;
|
||||
}
|
||||
// Creates the mapping (used by tests/self-check; the DAW normally owns it).
|
||||
ShmHandle* shm_create(const char* name) {
|
||||
ShmHandle* shm_create(const char* name, size_t size) {
|
||||
ShmHandle* h = new ShmHandle();
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, name, -1, nullptr, 0);
|
||||
std::wstring wname(wlen, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, name, -1, &wname[0], wlen);
|
||||
h->map = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0,
|
||||
sizeof(SharedAudioBufferIPC), wname.c_str());
|
||||
size, wname.c_str());
|
||||
if (!h->map) { delete h; return nullptr; }
|
||||
h->view = MapViewOfFile(h->map, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC));
|
||||
h->view = MapViewOfFile(h->map, FILE_MAP_ALL_ACCESS, 0, 0, size);
|
||||
if (!h->view) { CloseHandle(h->map); delete h; return nullptr; }
|
||||
return h;
|
||||
}
|
||||
@@ -82,6 +82,10 @@ SharedAudioBufferIPC* shm_ptr(ShmHandle* h) {
|
||||
return h ? static_cast<SharedAudioBufferIPC*>(h->view) : nullptr;
|
||||
}
|
||||
|
||||
SandboxHostIPC* shm_sandbox_ptr(ShmHandle* h) {
|
||||
return h ? static_cast<SandboxHostIPC*>(h->view) : nullptr;
|
||||
}
|
||||
|
||||
bool shm_write_midi(ShmHandle* h, uint8_t cmd, uint8_t channel, uint8_t pitch,
|
||||
uint8_t velocity, uint32_t sampleOffset, uint8_t data2, uint8_t data3) {
|
||||
SharedAudioBufferIPC* ipc = shm_ptr(h);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <mmsystem.h>
|
||||
#endif
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
@@ -50,18 +51,19 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
#ifdef _WIN32
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
timeBeginPeriod(1); // Sleep(1) really sleeps ~1ms, not the 15.6ms timer tick
|
||||
#endif
|
||||
printf("[plugin_host] pid=%lu shm=%s path=%s sr=%.0f block=%u\n",
|
||||
(unsigned long)GetCurrentProcessId(), shmName.c_str(), path.c_str(), sr, block);
|
||||
fflush(stdout);
|
||||
|
||||
// --open: bridge already created the mapping (G4.2); default PoC: child owns.
|
||||
ShmHandle* shm = isOpen ? shm_open(shmName.c_str()) : shm_create(shmName.c_str());
|
||||
ShmHandle* shm = isOpen ? shm_open(shmName.c_str(), sizeof(SandboxHostIPC)) : shm_create(shmName.c_str(), sizeof(SandboxHostIPC));
|
||||
if (!shm) {
|
||||
printf("[plugin_host] FAILED shm_create %s\n", shmName.c_str());
|
||||
return 3;
|
||||
}
|
||||
SharedAudioBufferIPC* ipc = shm_ptr(shm);
|
||||
SandboxHostIPC* ipc = shm_sandbox_ptr(shm);
|
||||
|
||||
Vst3Instrument inst;
|
||||
inst.init(sr, block); // sets sampleRate_/maxBlockSize_ used by setupProcessing
|
||||
@@ -92,7 +94,7 @@ int main(int argc, char* argv[]) {
|
||||
std::thread([ipc]() {
|
||||
for (;;) {
|
||||
Sleep(100);
|
||||
ipc->heartbeat++;
|
||||
ipc->base.heartbeat++;
|
||||
}
|
||||
}).detach();
|
||||
|
||||
@@ -102,9 +104,9 @@ int main(int argc, char* argv[]) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
uint32_t n = ipc->midiQueueCount > 64 ? 64 : ipc->midiQueueCount;
|
||||
uint32_t n = ipc->base.midiQueueCount > 64 ? 64 : ipc->base.midiQueueCount;
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
const SharedAudioBufferIPC::MidiEventIPC& e = ipc->midiQueue[i];
|
||||
const SharedAudioBufferIPC::MidiEventIPC& e = ipc->base.midiQueue[i];
|
||||
switch (e.command) {
|
||||
case 0x9:
|
||||
if (e.velocity > 0) inst.noteOn(e.channel, e.pitch, e.velocity / 127.0f, 0);
|
||||
@@ -117,9 +119,14 @@ int main(int argc, char* argv[]) {
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
ipc->midiQueueCount = 0;
|
||||
inst.processAudioBlock(ipc->masterLeft, ipc->masterRight, block);
|
||||
ipc->bridgeWriteIndex++;
|
||||
ipc->base.midiQueueCount = 0;
|
||||
// G4.3 double buffer: render into the slot we are NOT publishing, then
|
||||
// publish. The bridge keeps the last published block, so a slow child
|
||||
// never tears audio (it just repeats the previous block).
|
||||
uint32_t slot = 1u - (ipc->writeSlot & 1u);
|
||||
inst.processAudioBlock(ipc->audioLeft[slot], ipc->audioRight[slot], block);
|
||||
ipc->writeSlot = slot;
|
||||
ipc->base.bridgeWriteIndex++;
|
||||
if (parentPid && !parent_alive(parentPid)) {
|
||||
printf("[plugin_host] parent gone - exiting\n");
|
||||
break;
|
||||
@@ -128,6 +135,7 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
inst.closeGUI();
|
||||
#ifdef _WIN32
|
||||
timeEndPeriod(1);
|
||||
CoUninitialize();
|
||||
#endif
|
||||
shm_close(shm);
|
||||
|
||||
Reference in New Issue
Block a user