G4.3: double-buffer sandbox SHM - SandboxHostIPC writeSlot+2x256 double audio, latency 15.6ms, multi-instance no-xrun verified

This commit is contained in:
2026-08-16 11:35:31 +07:00
parent 462a5104d2
commit 9436e66835
8 changed files with 64 additions and 26 deletions
+12 -10
View File
@@ -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];
}
}
+9 -5
View File
@@ -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);
+16 -8
View File
@@ -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);