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
+13
View File
@@ -104,4 +104,17 @@ G4.1 → G4.2 → G4.3.
- Verify (probe_g41.py): Nexus (`C:\Program Files\Common Files\VST3\Nexus.vst3`) load trong con OK; heartbeat tăng hb=1 sau ~1s; GUI window 4 hwnd thuộc pid con; note-on C4 (vel 100) → peak audio 0.38274 (nonzero); terminate con → `sonicforge-daw.exe` 15928 + `daw_vst_bridge.exe` 20996 vẫn sống — isolation đúng yêu cầu G4.1.
- Lưu ý test-env: probe python phải set `OpenFileMappingA.restype = MapViewOfFile.restype = c_void_p` (Win64 con trỏ 64-bit, mặc định c_int cắt → AV khi đọc view).
- G4.2 (bridge → pool) chưa bắt đầu — bước tiếp theo sau PoC PASS.
## G4.2 — Bridge → client process pool (sandbox per channel) ✅
- `include/SandboxVst3Host.h` + `src/SandboxVst3Host.cpp` (mới): `INativeInstrument` wrapper quanh plugin_host.exe. `loadPlugin(path, sr, channel)` → `shm_create("SonicForge_PluginHost_<bridgePid>_<channel>")` + `CreateProcessA` plugin_host.exe (cạnh daw_vst_bridge.exe, cùng install/) với `--open --shm ... --path ... --sr ... --block ... --parent <bridgePid>`; chờ child heartbeat ≤60s. MIDI noteOn/noteOff/controlChange/programChange/pitchBend → `shm_write_midi` (velocity float→0..127). `processAudioBlock` copy snapshot masterLeft/Right (G4.3 thay bằng ring buffer). `watchdogLoop` (thread riêng): Sleep 500ms, child chết → `alive_=false` + log + respawn sau 1s; 3 fail liên tiếp → mute vĩnh viễn đến khi reload. Destructor: stop_ + join + TerminateProcess + shm_close.
- `src/plugin_host_main.cpp`: thêm flag `--open` → `shm_open` thay vì `shm_create` (bridge tạo mapping, con mở).
- `include/SharedMemoryIPC.h`: thêm prototype `shm_write_midi` (default args data2/data3 chỉ ở header); bỏ default args trùng trong `SharedMemoryIPC.cpp` (C2572).
- `src/NativeInstrumentEngine.cpp`: `create_instrument` VST3 case — `SF_SANDBOX_VST3` env set → `SandboxVst3Host`, ngược lại `Vst3Instrument` (regression an toàn); `assign` dùng dynamic_cast: Vst3 → loadPlugin(path,sr)+setChannel, Sandbox → loadPlugin(path,sr,channel). assign() chạy trên uiWorker (ChannelWorker) — spawn blocking OK.
- `CMakeLists.txt`: thêm `src/SandboxVst3Host.cpp` vào target `daw_vst_bridge`.
- Build Release PASS (`daw_vst_bridge.exe` + `plugin_host.exe`).
- Deploy: kill app+bridge → copy exe mới vào install/ (dùng shutil, `copy` shell fail im lặng — exe cũ 3900416B không có sandbox, phải verify chuỗi `SF_SANDBOX_VST3` trong binary) → start lại app.
- Verify (probe_g42.py, bridge standalone env `SF_SANDBOX_VST3=1` + state file rỗng): LOAD Nexus ch0 → plugin_host pid spawn; child heartbeat OK; GUI 4 hwnd thuộc pid con; note-on C4 → peak 0.38276; **kill child** → bridge sống, heartbeat tiếp tục, respawn pid mới sau ~1.5s, GUI mới 4 hwnd, audio lại 0.38275 — PASS.
- Lưu ý probe: bridge chỉ OPEN mapping `SonicForge_DAW_IPC` (Rust shell tạo) → probe phải `CreateFileMappingA` trước; state file cũ (7 instruments) làm guard G2.4 skip LOAD → dùng `SF_STATE_FILE` trỏ file rỗng.
- 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`.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -59,6 +59,7 @@ add_executable(daw_vst_bridge
src/SharedMemoryIPC.cpp
src/Vst3Instrument.cpp
src/StateStore.cpp
src/SandboxVst3Host.cpp
)
# VST3 SDK hosting: sdk_hosting lacks the platform module loader — module_win32.cpp
+58
View File
@@ -0,0 +1,58 @@
// native_bridge/include/SandboxVst3Host.h
// G4.2: VST3 instrument hosted in a CHILD PROCESS (plugin_host.exe). The
// plugin's crash kills only the child; this host mutes the channel, respawns
// the child, and the bridge + other channels keep running. MIDI flows bridge
// -> child SHM (midiQueue), audio flows child -> bridge (masterLeft/Right).
#ifndef SANDBOX_VST3_HOST_H
#define SANDBOX_VST3_HOST_H
#include "INativeInstrument.h"
#include "SharedMemoryIPC.h"
#include <atomic>
#include <string>
#include <thread>
#ifdef _WIN32
#include <windows.h>
#endif
class SandboxVst3Host : public INativeInstrument {
public:
SandboxVst3Host();
~SandboxVst3Host() override;
bool loadPlugin(const std::string& path, double sampleRate, uint32_t channel);
bool init(double sampleRate, uint32_t maxBlockSize) override;
void selectProgram(uint32_t channel, uint32_t bank, uint32_t program) override {}
void noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) override;
void noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) override;
void controlChange(uint32_t channel, uint32_t cc, uint32_t value) override;
void programChange(uint32_t channel, uint32_t program) override;
void pitchBend(uint32_t channel, uint32_t bend14) override;
// The child owns its GUI window (plugin_host opens it on its main thread).
bool openGUI(void* parentWindowHandle) override { (void)parentWindowHandle; return true; }
void closeGUI() override {} // child window dies with the child process
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
private:
bool spawnChild();
void watchdogLoop();
std::string path_;
std::string shmName_;
double sampleRate_ = 44100.0;
uint32_t block_ = AUDIO_BLOCK_SIZE;
uint32_t channel_ = 0;
ShmHandle* shm_ = nullptr;
SharedAudioBufferIPC* ipc_ = nullptr;
std::atomic<bool> alive_{false};
std::atomic<bool> stop_{false};
std::thread watchdog_;
#ifdef _WIN32
HANDLE childProc_ = nullptr;
#endif
};
#endif // SANDBOX_VST3_HOST_H
+3
View File
@@ -69,3 +69,6 @@ ShmHandle* shm_open(const char* name);
ShmHandle* shm_create(const char* name);
SharedAudioBufferIPC* shm_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,
uint8_t data3 = 0);
+17 -5
View File
@@ -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;
+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];
}
}
+1 -1
View File
@@ -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.
+4 -2
View File
@@ -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;