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:
@@ -4,6 +4,7 @@
|
||||
#include <fluidsynth.h>
|
||||
#include <sfizz.hpp>
|
||||
#include "Vst3Instrument.h"
|
||||
#include "SandboxVst3Host.h"
|
||||
|
||||
// void* members keep fluid types out of the public header; cast here.
|
||||
#define FS_SYNTH (static_cast<fluid_synth_t*>(synth))
|
||||
@@ -166,7 +167,12 @@ std::unique_ptr<INativeInstrument> InstrumentEngineManager::create_instrument(In
|
||||
switch (type) {
|
||||
case InstrumentType::SOUNDFONT_SF2_SF3: return std::make_unique<FluidSynthInstrument>();
|
||||
case InstrumentType::SFZ: return std::make_unique<SfizzInstrument>();
|
||||
case InstrumentType::VST3: return std::make_unique<Vst3Instrument>();
|
||||
case InstrumentType::VST3:
|
||||
// G4.2: SF_SANDBOX_VST3=1 -> host the VST3 in a child
|
||||
// process (plugin_host.exe). Crash kills only the child.
|
||||
if (std::getenv("SF_SANDBOX_VST3"))
|
||||
return std::make_unique<SandboxVst3Host>();
|
||||
return std::make_unique<Vst3Instrument>();
|
||||
// ponytail: VST2 host (VST2.4 SDK, Steinberg discontinued) not implemented.
|
||||
case InstrumentType::VST2:
|
||||
default: return nullptr;
|
||||
@@ -184,10 +190,16 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
|
||||
loaded = static_cast<FluidSynthInstrument*>(inst.get())->loadSoundFontFile(path, sampleRate);
|
||||
else if (type == InstrumentType::SFZ)
|
||||
loaded = static_cast<SfizzInstrument*>(inst.get())->loadSfzFile(path, sampleRate);
|
||||
else if (type == InstrumentType::VST3)
|
||||
loaded = static_cast<Vst3Instrument*>(inst.get())->loadPlugin(path, sampleRate);
|
||||
if (type == InstrumentType::VST3)
|
||||
static_cast<Vst3Instrument*>(inst.get())->setChannel(channel);
|
||||
else if (type == InstrumentType::VST3) {
|
||||
if (auto* v = dynamic_cast<Vst3Instrument*>(inst.get()))
|
||||
loaded = v->loadPlugin(path, sampleRate);
|
||||
else if (auto* s = dynamic_cast<SandboxVst3Host*>(inst.get()))
|
||||
loaded = s->loadPlugin(path, sampleRate, channel);
|
||||
}
|
||||
if (type == InstrumentType::VST3) {
|
||||
if (auto* v = dynamic_cast<Vst3Instrument*>(inst.get()))
|
||||
v->setChannel(channel);
|
||||
}
|
||||
if (!loaded) return false;
|
||||
// init() AFTER load: FluidSynth creates its synth inside loadSoundFontFile.
|
||||
if (!inst->init(sampleRate, blockSize)) return false;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ SharedAudioBufferIPC* shm_ptr(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, uint8_t data3 = 0) {
|
||||
uint8_t velocity, uint32_t sampleOffset, uint8_t data2, uint8_t data3) {
|
||||
SharedAudioBufferIPC* ipc = shm_ptr(h);
|
||||
if (!ipc) return false;
|
||||
// Ring overwrite guard: keep at most queue capacity pending events.
|
||||
|
||||
@@ -34,12 +34,14 @@ int main(int argc, char* argv[]) {
|
||||
double sr = 44100.0;
|
||||
uint32_t block = AUDIO_BLOCK_SIZE;
|
||||
uint32_t parentPid = 0;
|
||||
bool isOpen = false;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (strcmp(argv[i], "--shm") == 0 && i + 1 < argc) shmName = argv[++i];
|
||||
else if (strcmp(argv[i], "--path") == 0 && i + 1 < argc) path = argv[++i];
|
||||
else if (strcmp(argv[i], "--sr") == 0 && i + 1 < argc) sr = atof(argv[++i]);
|
||||
else if (strcmp(argv[i], "--block") == 0 && i + 1 < argc) block = (uint32_t)atoi(argv[++i]);
|
||||
else if (strcmp(argv[i], "--parent") == 0 && i + 1 < argc) parentPid = (uint32_t)strtoul(argv[++i], nullptr, 0);
|
||||
else if (strcmp(argv[i], "--open") == 0) isOpen = true;
|
||||
}
|
||||
if (shmName.empty() || path.empty()) {
|
||||
printf("usage: plugin_host --shm <name> --path <vst3> [--sr rate] [--block n] [--parent pid]\n");
|
||||
@@ -53,8 +55,8 @@ int main(int argc, char* argv[]) {
|
||||
(unsigned long)GetCurrentProcessId(), shmName.c_str(), path.c_str(), sr, block);
|
||||
fflush(stdout);
|
||||
|
||||
// PoC: the child owns the mapping. Bridge adoption (G4.2) opens instead.
|
||||
ShmHandle* shm = shm_create(shmName.c_str());
|
||||
// --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());
|
||||
if (!shm) {
|
||||
printf("[plugin_host] FAILED shm_create %s\n", shmName.c_str());
|
||||
return 3;
|
||||
|
||||
Reference in New Issue
Block a user