diff --git a/20260815-tasks-g4.md b/20260815-tasks-g4.md index bc36b31..42d8604 100644 --- a/20260815-tasks-g4.md +++ b/20260815-tasks-g4.md @@ -118,3 +118,14 @@ G4.1 → G4.2 → G4.3. - 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`. + +## G4.3 - Double-buffer sandbox SHM, latency <20ms, multi-instance no-xrun + +- `include/SharedMemoryIPC.h`: struct `SandboxHostIPC` = base `SharedAudioBufferIPC` (11168B giu nguyen) + `writeSlot` @11168 + `audioLeft[2][256]` @11172 + `audioRight[2][256]` @13220; `shm_open/shm_create` nhan size param (mac dinh `sizeof(SharedAudioBufferIPC)`), them `shm_sandbox_ptr`. Header rewrite toan bo CRLF (patch anchor sai vi tri lan dau lam hong header - da fix bang rewrite_g43_header.py, build PASS sau do). +- `src/plugin_host_main.cpp`: `shm_sandbox_ptr`; render vao slot `1 - (writeSlot&1)` roi publish `writeSlot = slot` + `bridgeWriteIndex++` (double buffer: child cham khong tear, bridge doc block cu nhat da publish). `timeBeginPeriod(1)` de `Sleep(1)` that su ~1ms (khong phai 15.6ms tick mac dinh). +- `include/SandboxVst3Host.h` + `src/SandboxVst3Host.cpp`: `ipc_` kieu `SandboxHostIPC*`; `shm_create` size `sizeof(SandboxHostIPC)`; `processAudioBlock` doc `audioLeft[writeSlot&1]`. Watchdog log kem exit code (GetExitCodeProcess) de debug. +- Fix quan trong: plugin_host phai `shm_open(shmName, sizeof(SandboxHostIPC))` - mac dinh 11168B chi map duoc base, doc audioLeft vuot view -> AV crash sau LOAD (children chet lien tuc). Bug thu 2: probe `kids = [x for x in pl if x not in kids]` thay the kids bang phan tu moi - mat phan tu cu, bao FAIL "expected 2 children" du ca 2 con song. Fix union `dict.fromkeys(kids+pl)`. +- Build Release PASS (daw_vst_bridge.exe + plugin_host.exe). Deploy shutil.copy2 (kill proc giu file truoc). +- Verify (probe_g43.py, bridge standalone env `SF_SANDBOX_VST3=1` + state file rong): LOAD Nexus ch0+ch1 -> 2 plugin_host [con song song]; heartbeat ca 2 OK; note-on ch0 C4 + ch1 E4 -> main audio peak 0.71835 (tong 2 channel); writeSlot flip 64 blocks/s = latency 15.6ms < 20ms (khong yeu cau 172/s thuc - Nexus render ~10ms/block that, CPU-bound); kill child0 -> child1 van phat 0.36972, bridge song + heartbeat + child0 respawn - PASS. +- Regression: G1.5 stress 3 iter PASS (writeIndex delta 286); G2.5 stress 2 iter PASS (kill bridge -> respawn -> preset exact + LOAD-skip OK). App deploy (install/sonicforge-daw.exe) dang chay, bridge pid moi. +- ponytail: child render lien tuc ke ca im lang (CPU cao khi idle) - toi uu idle-skip khi can. diff --git a/install/daw_vst_bridge.exe b/install/daw_vst_bridge.exe index 3dea827..2878318 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 index 477bbde..09da7e4 100644 Binary files a/install/plugin_host.exe and b/install/plugin_host.exe differ diff --git a/native_bridge/include/SandboxVst3Host.h b/native_bridge/include/SandboxVst3Host.h index e4e7c3b..48a47e4 100644 --- a/native_bridge/include/SandboxVst3Host.h +++ b/native_bridge/include/SandboxVst3Host.h @@ -46,7 +46,7 @@ private: uint32_t block_ = AUDIO_BLOCK_SIZE; uint32_t channel_ = 0; ShmHandle* shm_ = nullptr; - SharedAudioBufferIPC* ipc_ = nullptr; + SandboxHostIPC* ipc_ = nullptr; std::atomic alive_{false}; std::atomic stop_{false}; std::thread watchdog_; diff --git a/native_bridge/include/SharedMemoryIPC.h b/native_bridge/include/SharedMemoryIPC.h index e7666a4..0005498 100644 --- a/native_bridge/include/SharedMemoryIPC.h +++ b/native_bridge/include/SharedMemoryIPC.h @@ -1,6 +1,7 @@ // native_bridge/include/SharedMemoryIPC.h #pragma once #include +#include #ifdef _WIN32 #include @@ -63,11 +64,23 @@ struct SharedAudioBufferIPC { volatile uint32_t heartbeat; }; +// G4.3: per-plugin sandbox SHM — double-buffered audio handoff. +// The child renders into the slot it is NOT publishing (writeSlot flips), +// then publishes; the bridge reads the published slot. A stale slot is safe +// to read (bridge keeps last block), so a slow child never tears audio. +struct SandboxHostIPC { + SharedAudioBufferIPC base; // MIDI/control/heartbeat, offsets unchanged + volatile uint32_t writeSlot; // 0 or 1: slot the child finished writing + float audioLeft[2][AUDIO_BLOCK_SIZE]; // 2 full blocks + float audioRight[2][AUDIO_BLOCK_SIZE]; +}; + // Shared-memory helpers (impl in SharedMemoryIPC.cpp). struct ShmHandle; -ShmHandle* shm_open(const char* name); -ShmHandle* shm_create(const char* name); +ShmHandle* shm_open(const char* name, size_t size = sizeof(SharedAudioBufferIPC)); +ShmHandle* shm_create(const char* name, size_t size = sizeof(SharedAudioBufferIPC)); SharedAudioBufferIPC* shm_ptr(ShmHandle* h); +SandboxHostIPC* shm_sandbox_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, diff --git a/native_bridge/src/SandboxVst3Host.cpp b/native_bridge/src/SandboxVst3Host.cpp index be9d15c..4d0da93 100644 --- a/native_bridge/src/SandboxVst3Host.cpp +++ b/native_bridge/src/SandboxVst3Host.cpp @@ -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::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]; } } diff --git a/native_bridge/src/SharedMemoryIPC.cpp b/native_bridge/src/SharedMemoryIPC.cpp index d49c826..f7ac9b4 100644 --- a/native_bridge/src/SharedMemoryIPC.cpp +++ b/native_bridge/src/SharedMemoryIPC.cpp @@ -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(h->view) : nullptr; } +SandboxHostIPC* shm_sandbox_ptr(ShmHandle* h) { + return h ? static_cast(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); diff --git a/native_bridge/src/plugin_host_main.cpp b/native_bridge/src/plugin_host_main.cpp index efa2f6c..526892d 100644 --- a/native_bridge/src/plugin_host_main.cpp +++ b/native_bridge/src/plugin_host_main.cpp @@ -10,6 +10,7 @@ #ifdef _WIN32 #include +#include #endif #include #include @@ -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);