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

This commit is contained in:
2026-08-16 10:52:04 +07:00
parent 7de00955d8
commit 462a5104d2
10 changed files with 287 additions and 8 deletions
+190
View File
@@ -0,0 +1,190 @@
// native_bridge/src/SandboxVst3Host.cpp
#include "SandboxVst3Host.h"
#include <chrono>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
static bool proc_alive(HANDLE h) {
#ifdef _WIN32
if (!h) return false;
DWORD code = 0;
if (!GetExitCodeProcess(h, &code)) return false;
return code == STILL_ACTIVE;
#else
return true;
#endif
}
SandboxVst3Host::SandboxVst3Host() {}
SandboxVst3Host::~SandboxVst3Host() {
stop_.store(true);
if (watchdog_.joinable()) watchdog_.join();
#ifdef _WIN32
if (childProc_) {
TerminateProcess(childProc_, 0);
CloseHandle(childProc_);
childProc_ = nullptr;
}
#endif
if (shm_) {
shm_close(shm_);
shm_ = nullptr;
ipc_ = nullptr;
}
}
bool SandboxVst3Host::loadPlugin(const std::string& path, double sampleRate, uint32_t channel) {
path_ = path;
sampleRate_ = sampleRate;
channel_ = channel;
char name[128];
snprintf(name, sizeof(name), "SonicForge_PluginHost_%lu_%u",
(unsigned long)GetCurrentProcessId(), channel);
shmName_ = name;
if (!spawnChild()) return false;
watchdog_ = std::thread([this]() { watchdogLoop(); });
return true;
}
bool SandboxVst3Host::spawnChild() {
// Mapping created by the bridge (parent); the child opens it with --open.
if (shm_) {
shm_close(shm_);
shm_ = nullptr;
ipc_ = nullptr;
}
shm_ = shm_create(shmName_.c_str());
if (!shm_) {
std::cerr << "[SandboxVst3Host] shm_create failed ch=" << channel_ << std::endl;
return false;
}
ipc_ = shm_ptr(shm_);
// plugin_host.exe sits next to daw_vst_bridge.exe (same dir, install/).
char exePath[MAX_PATH] = {};
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
std::string dir(exePath);
size_t slash = dir.find_last_of("\\/");
std::string hostExe = (slash == std::string::npos)
? "plugin_host.exe"
: dir.substr(0, slash + 1) + "plugin_host.exe";
std::string cmd = "\"" + hostExe + "\" --open --shm " + shmName_ +
" --path \"" + path_ + "\" --sr " + std::to_string((int)sampleRate_) +
" --block " + std::to_string(block_) +
" --parent " + std::to_string((unsigned long)GetCurrentProcessId());
std::cerr << "[SandboxVst3Host] spawn ch=" << channel_ << " " << cmd << std::endl;
STARTUPINFOA si = {};
si.cb = sizeof(si);
PROCESS_INFORMATION pi = {};
std::vector<char> buf(cmd.begin(), cmd.end());
buf.push_back('\0');
if (!CreateProcessA(nullptr, buf.data(), nullptr, nullptr, FALSE,
CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) {
std::cerr << "[SandboxVst3Host] CreateProcessA failed err=" << (int)GetLastError()
<< " ch=" << channel_ << std::endl;
return false;
}
CloseHandle(pi.hThread);
#ifdef _WIN32
if (childProc_) CloseHandle(childProc_);
childProc_ = pi.hProcess;
#endif
// Wait for the child heartbeat (Nexus load can take 30s+).
uint32_t hb = ipc_->heartbeat;
auto t0 = std::chrono::steady_clock::now();
while (std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::steady_clock::now() - t0).count() < 60) {
if (!proc_alive(childProc_)) {
std::cerr << "[SandboxVst3Host] child exited during load ch=" << channel_ << std::endl;
return false;
}
if (ipc_->heartbeat != hb) {
alive_.store(true);
return true;
}
Sleep(200);
}
std::cerr << "[SandboxVst3Host] child heartbeat timeout ch=" << channel_ << std::endl;
return false;
}
void SandboxVst3Host::watchdogLoop() {
int fails = 0;
while (!stop_.load()) {
Sleep(500);
if (stop_.load()) break;
if (!alive_.load()) continue;
if (proc_alive(childProc_)) {
fails = 0;
continue;
}
alive_.store(false);
std::cerr << "[SandboxVst3Host] child died ch=" << channel_ << " — respawning" << std::endl;
++fails;
if (fails >= 3) {
std::cerr << "[SandboxVst3Host] ch=" << channel_
<< " respawn limit hit — muted until reload" << std::endl;
continue; // stays dead; processAudioBlock returns silence
}
Sleep(1000);
if (stop_.load()) break;
if (spawnChild()) {
fails = 0;
std::cerr << "[SandboxVst3Host] ch=" << channel_ << " respawned" << std::endl;
}
}
}
bool SandboxVst3Host::init(double sampleRate, uint32_t maxBlockSize) {
sampleRate_ = sampleRate;
block_ = maxBlockSize;
return true;
}
void SandboxVst3Host::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
if (!alive_.load() || !ipc_) return;
shm_write_midi(shm_, 0x9, (uint8_t)channel, (uint8_t)pitch,
(uint8_t)(velocity * 127.0f), sampleOffset);
}
void SandboxVst3Host::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
if (!alive_.load() || !ipc_) return;
shm_write_midi(shm_, 0x8, (uint8_t)channel, (uint8_t)pitch, 0, sampleOffset);
}
void SandboxVst3Host::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
if (!alive_.load() || !ipc_) return;
shm_write_midi(shm_, 0xB, (uint8_t)channel, (uint8_t)cc, 0, 0, (uint8_t)value, 0);
}
void SandboxVst3Host::programChange(uint32_t channel, uint32_t program) {
if (!alive_.load() || !ipc_) return;
shm_write_midi(shm_, 0xC, (uint8_t)channel, 0, 0, 0, (uint8_t)program, 0);
}
void SandboxVst3Host::pitchBend(uint32_t channel, uint32_t bend14) {
if (!alive_.load() || !ipc_) return;
shm_write_midi(shm_, 0xE, (uint8_t)channel, 0, 0, 0,
(uint8_t)(bend14 & 0x7F), (uint8_t)((bend14 >> 7) & 0x7F));
}
void SandboxVst3Host::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
if (!alive_.load() || !ipc_) {
std::memset(outputL, 0, numSamples * sizeof(float));
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).
for (uint32_t i = 0; i < numSamples; ++i) {
outputL[i] = ipc_->masterLeft[i];
outputR[i] = ipc_->masterRight[i];
}
}