diff --git a/20260815-tasks-g4.md b/20260815-tasks-g4.md index a417956..bc36b31 100644 --- a/20260815-tasks-g4.md +++ b/20260815-tasks-g4.md @@ -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__")` + `CreateProcessA` plugin_host.exe (cạnh daw_vst_bridge.exe, cùng install/) với `--open --shm ... --path ... --sr ... --block ... --parent `; 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`. diff --git a/install/daw_vst_bridge.exe b/install/daw_vst_bridge.exe index b769b6a..3dea827 100644 Binary files a/install/daw_vst_bridge.exe and b/install/daw_vst_bridge.exe differ diff --git a/install/plugin_host.exe b/install/plugin_host.exe new file mode 100644 index 0000000..477bbde Binary files /dev/null and b/install/plugin_host.exe differ diff --git a/native_bridge/CMakeLists.txt b/native_bridge/CMakeLists.txt index 69d28b6..f3554d5 100644 --- a/native_bridge/CMakeLists.txt +++ b/native_bridge/CMakeLists.txt @@ -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 diff --git a/native_bridge/include/SandboxVst3Host.h b/native_bridge/include/SandboxVst3Host.h new file mode 100644 index 0000000..e4e7c3b --- /dev/null +++ b/native_bridge/include/SandboxVst3Host.h @@ -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 +#include +#include + +#ifdef _WIN32 +#include +#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 alive_{false}; + std::atomic stop_{false}; + std::thread watchdog_; +#ifdef _WIN32 + HANDLE childProc_ = nullptr; +#endif +}; + +#endif // SANDBOX_VST3_HOST_H diff --git a/native_bridge/include/SharedMemoryIPC.h b/native_bridge/include/SharedMemoryIPC.h index 30ec7eb..e7666a4 100644 --- a/native_bridge/include/SharedMemoryIPC.h +++ b/native_bridge/include/SharedMemoryIPC.h @@ -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); diff --git a/native_bridge/src/NativeInstrumentEngine.cpp b/native_bridge/src/NativeInstrumentEngine.cpp index 81c2203..920cfd4 100644 --- a/native_bridge/src/NativeInstrumentEngine.cpp +++ b/native_bridge/src/NativeInstrumentEngine.cpp @@ -4,6 +4,7 @@ #include #include #include "Vst3Instrument.h" +#include "SandboxVst3Host.h" // void* members keep fluid types out of the public header; cast here. #define FS_SYNTH (static_cast(synth)) @@ -166,7 +167,12 @@ std::unique_ptr InstrumentEngineManager::create_instrument(In switch (type) { case InstrumentType::SOUNDFONT_SF2_SF3: return std::make_unique(); case InstrumentType::SFZ: return std::make_unique(); - case InstrumentType::VST3: return std::make_unique(); + 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(); + return std::make_unique(); // 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(inst.get())->loadSoundFontFile(path, sampleRate); else if (type == InstrumentType::SFZ) loaded = static_cast(inst.get())->loadSfzFile(path, sampleRate); - else if (type == InstrumentType::VST3) - loaded = static_cast(inst.get())->loadPlugin(path, sampleRate); - if (type == InstrumentType::VST3) - static_cast(inst.get())->setChannel(channel); + else if (type == InstrumentType::VST3) { + if (auto* v = dynamic_cast(inst.get())) + loaded = v->loadPlugin(path, sampleRate); + else if (auto* s = dynamic_cast(inst.get())) + loaded = s->loadPlugin(path, sampleRate, channel); + } + if (type == InstrumentType::VST3) { + if (auto* v = dynamic_cast(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; diff --git a/native_bridge/src/SandboxVst3Host.cpp b/native_bridge/src/SandboxVst3Host.cpp new file mode 100644 index 0000000..be9d15c --- /dev/null +++ b/native_bridge/src/SandboxVst3Host.cpp @@ -0,0 +1,190 @@ +// native_bridge/src/SandboxVst3Host.cpp +#include "SandboxVst3Host.h" + +#include +#include +#include +#include +#include + +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 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::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]; + } +} diff --git a/native_bridge/src/SharedMemoryIPC.cpp b/native_bridge/src/SharedMemoryIPC.cpp index 12bfa58..d49c826 100644 --- a/native_bridge/src/SharedMemoryIPC.cpp +++ b/native_bridge/src/SharedMemoryIPC.cpp @@ -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. diff --git a/native_bridge/src/plugin_host_main.cpp b/native_bridge/src/plugin_host_main.cpp index 8b326c2..efa2f6c 100644 --- a/native_bridge/src/plugin_host_main.cpp +++ b/native_bridge/src/plugin_host_main.cpp @@ -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 --path [--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;