G4.1: sandbox plugin host (plugin_host.exe) PoC — Nexus runs in child process, GUI + audio via SHM, crash isolation verified
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# 20260815 — KẾ HOẠCH THỰC HIỆN G4 (SANDBOX PLUGIN / PROCESS CON)
|
||||
|
||||
Nguồn: `big_tasks.md` — header **G4 — Sandbox plugin (3-5x G1+G2, 2-3 tháng)**.
|
||||
Trạng thái roadmap: G0 ✅, G1 ✅, G2 ✅, G3 ✅ (G3.1–G3.3). Branch: `standalone-shm-bridge`.
|
||||
User chủ động yêu cầu: làm dirty trước, sau đó làm luôn G4.
|
||||
|
||||
---
|
||||
|
||||
## 1. PHÂN TÍCH G4
|
||||
|
||||
### 1.1 Mục tiêu
|
||||
VST3 chạy trong process con (`plugin_host.exe`) — crash plugin chỉ chết process con,
|
||||
bridge + GUI + các plugin khác sống. VST3 không có chuẩn out-of-process như AUv3 →
|
||||
wrapper tự làm.
|
||||
|
||||
### 1.2 Hiện trạng (đã verify trong code)
|
||||
| Thành phần | File | Ghi chú |
|
||||
|---|---|---|
|
||||
| VST3 host | `native_bridge/src/Vst3Instrument.cpp` + `include/Vst3Instrument.h` | pimpl (`Vst3HostState`), độc lập, reuse được trong process con: `loadPlugin(path, sr)` (tự activate + setupProcessing với maxBlockSize_), `openGUI(HWND)`, `processAudioBlock(L,R,n)` (zero output buffer trước process, copy ra ngoài), MIDI dispatch `noteOn/noteOff/controlChange/programChange/pitchBend`, `closeGUI()`, `reload()`. |
|
||||
| SHM layout | `include/SharedMemoryIPC.h` | `SharedAudioBufferIPC` 11168B: `bridgeWriteIndex`@4, `masterLeft`@8 (float[256]), `masterRight`@1032, `midiQueue[64]`@2064 (12B/evt), `midiQueueCount`@2832, `controlQueue[8]`@2836, `heartbeat`@11164 (G3.1). Helpers `shm_open/shm_create/shm_ptr/shm_close/shm_write_midi/shm_write_control` trong `SharedMemoryIPC.cpp` (trước G4.1 không có prototype trong header — đã bổ sung). |
|
||||
| CMake target pattern | `native_bridge/CMakeLists.txt` | `gui_probe`/`seh_channel_test`: `HAVE_VST3SDK=1` + `${VST3_SDK_TARGET} sdk_hosting sdk_common` + `module_win32.cpp` + `memorystream.cpp` + `${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm`. |
|
||||
|
||||
### 1.3 Khoảng trống (gap)
|
||||
1. VST3 chạy cùng process bridge → 1 plugin crash có thể hạ bridge (đã giảm thiểu phần nào bằng SEH G3.3 nhưng vẫn chung process).
|
||||
2. Chưa có cơ chế spawn con + SHM con (MIDI in / audio out / heartbeat) + giám sát respawn.
|
||||
|
||||
### 1.4 Nguyên tắc thiết kế (đề xuất)
|
||||
- Mỗi plugin 1 process con `plugin_host.exe`, argv: `--shm <name> --path <vst3> [--sr] [--block] [--parent <pid>]`.
|
||||
- Con tự tạo mapping SHM riêng (tái dùng layout `SharedAudioBufferIPC` — không phát sinh struct mới, bridge đọc được nguyên layout).
|
||||
- GUI attach trên main thread con (editorhost pattern: attach rồi pump); heartbeat ~10Hz thread riêng (pattern G3.1).
|
||||
- Con exit khi parent chết (kiểm tra `parent_alive` mỗi block) — chống orphan.
|
||||
- G4.2/G4.3: bridge mở mapping con thay vì con tự tạo; pool + watchdog respawn; ring buffer realtime.
|
||||
|
||||
---
|
||||
|
||||
## 2. PHÂN RÃ TASKS + ESTIMATE
|
||||
|
||||
| ID | Task | File đích | Nội dung | Estimate |
|
||||
|---|---|---|---|---|
|
||||
| G4.1 | PoC plugin_host độc lập | `src/plugin_host_main.cpp`, `CMakeLists.txt`, `include/SharedMemoryIPC.h` | argv(shm,path,sr,block,parent) → COM STA → load VST3 → GUI attach → audio loop qua SHM (drain MIDI → dispatch → processAudioBlock → ghi masterLeft/Right → bridgeWriteIndex++) → heartbeat thread → exit khi parent chết. Done khi: Nexus chạy trong con, GUI + audio OK, kill con → chỉ con chết. | 1-2 ngày |
|
||||
| G4.2 | Bridge → client pool | `main.cpp` (bridge) | bridge spawn 1 con/plugin, forward MIDI, đọc audio mix, watchdog (crash → respawn, mute/restore như G3.3). | 2-3 tuần |
|
||||
| G4.3 | IPC audio realtime | SHM ring buffer | double-buffer SHM con, latency <20ms, 6 Nexus song song không xrun. | 2-3 tuần |
|
||||
|
||||
### Phụ thuộc
|
||||
`G4.2 → G4.1` (cần plugin_host chạy được độc lập) · `G4.3 → G4.2`.
|
||||
|
||||
### Thứ tự triển khai khuyến nghị
|
||||
G4.1 → G4.2 → G4.3.
|
||||
|
||||
---
|
||||
|
||||
## 3. KẾ HOẠCH THỰC HIỆN (G4.1 PoC — hoàn tất hôm nay)
|
||||
|
||||
### G4.1a — Đọc code, chốt API host
|
||||
- `Vst3Instrument`: `init(sr, block)` set `sampleRate_/maxBlockSize_` (gọi trước `loadPlugin` — loadPlugin dùng `maxBlockSize_` trong `setupProcessing`; default 256 = AUDIO_BLOCK_SIZE). `loadPlugin` tự activate + setProcessing + prepare. `processAudioBlock` zero output rồi process, copy stereo ra ngoài, clear eventList.
|
||||
- Drain MIDI theo convention bridge (main.cpp): snapshot `midiQueue[0..count-1]` (bounded 64), reset `midiQueueCount=0`, dispatch theo command 0x9/0x8/0xB/0xC/0xE.
|
||||
- Heartbeat: thread riêng `Sleep(100)` → `heartbeat++` (pattern G3.1).
|
||||
|
||||
### G4.1b — Viết `plugin_host_main.cpp`
|
||||
- Parse argv `--shm/--path/--sr/--block/--parent`.
|
||||
- `CoInitializeEx(STA)` trên main thread (GUI thuộc main thread con).
|
||||
- `shm_create(name)` — PoC: con sở hữu mapping (G4.2 đổi thành mở).
|
||||
- `init(sr,block)` → `loadPlugin(path,sr)` → tạo own top-level window → `openGUI(hwnd)` (editorhost pattern) → heartbeat thread → main loop: pump messages + drain MIDI + `processAudioBlock` + `bridgeWriteIndex++` + `parent_alive` check → `closeGUI()` + `CoUninitialize()` + `shm_close`.
|
||||
- ASCII-only, LF → normalize CRLF (repo convention).
|
||||
|
||||
### G4.1c — CMake target `plugin_host` + build
|
||||
- Copy pattern `gui_probe`: `src/plugin_host_main.cpp + src/SharedMemoryIPC.cpp + src/Vst3Instrument.cpp` + SDK hosting libs (không cần NativeInstrumentEngine.cpp).
|
||||
- Bổ sung prototype `shm_open/shm_create/shm_ptr/shm_close` vào `SharedMemoryIPC.h` (trước đây chỉ có trong .cpp, plugin_host cần khai báo).
|
||||
- Build: `cmake --build build --config Release --target plugin_host -- /m` PASS → `build/Release/plugin_host.exe`.
|
||||
|
||||
### G4.1d — Probe với Nexus (probe_g41.py)
|
||||
- Spawn con với `C:\Program Files\Common Files\VST3\Nexus.vst3`, SHM `SonicForge_PluginHost_Probe`.
|
||||
- Verify: heartbeat tăng (hb=1 trong ~1s); GUI window xuất hiện (4 hwnd thuộc pid con, gồm PluginHostClass + editor Nexus); ghi note-on C4 → audio peak 0.38274 (nonzero); terminate con → app 15928 + bridge 20996 vẫn sống (isolation).
|
||||
|
||||
### G4.1e — Commit + journal + push
|
||||
|
||||
---
|
||||
|
||||
## 4. RỦI RO
|
||||
| Rủi ro | Ảnh hưởng | Giảm thiểu |
|
||||
|---|---|---|
|
||||
| Con chết khi đang render → dữ liệu audio mất 1 block | Click/pop | G4.3 ring buffer + G4.2 mute/restore; PoC chấp nhận. |
|
||||
| Parent chết → con orphan | Process treo | `--parent` + check mỗi block (đã làm). |
|
||||
| Plugin GUI yêu cầu thread/COM riêng | Hang/blank view | Attach trên main thread con (editorhost pattern đã verify qua gui_probe). |
|
||||
| SHM con đổi layout sau này | Mất tương thích | Tái dùng `SharedAudioBufferIPC` — layout ổn định append-only. |
|
||||
|
||||
---
|
||||
|
||||
## 5. BÁO CÁO KẾT QUẢ (đã thực hiện)
|
||||
| ID | Cài đặt | Kiểm tra | Bugs bắt được | Fix | Trạng thái |
|
||||
|---|---|---|---|---|---|
|
||||
| G4.1a | Chốt API host từ Vst3Instrument + convention drain MIDI/heartbeat (main.cpp). | — | — | — | ✅ |
|
||||
| G4.1b | `plugin_host_main.cpp`: argv parse, COM STA, shm_create, loadPlugin, openGUI own window, audio loop + heartbeat + parent check. | Build PASS (sau fix prototype SHM). | `ShmHandle/shm_create/shm_ptr/shm_close` không có prototype trong header → C2065. | Thêm prototype vào `SharedMemoryIPC.h`. | ✅ |
|
||||
| G4.1c | CMake target `plugin_host` (pattern gui_probe, không engine). | Build PASS → `build/Release/plugin_host.exe`. | Patch block bị chèn vào giữa `if(WIN32)` của gui_probe + `endif()` thừa. | Script sửa vị trí + xóa endif. | ✅ |
|
||||
| G4.1d | Probe: spawn + Nexus + heartbeat + GUI + audio + isolation. | PASS: hb=1 (1s); 4 hwnd; peak 0.38274; kill con → app 15928 + bridge 20996 sống. | Probe python đọc SHM AV — `MapViewOfFile` restype mặc định c_int cắt con trỏ 64-bit. | Set `restype=c_void_p`. | ✅ |
|
||||
| G4.1e | Commit + journal + push. | — | — | — | ✅ |
|
||||
|
||||
### Nhật ký thực hiện
|
||||
|
||||
### G4.1 PoC — Hoàn tất (2026-08-15)
|
||||
- `native_bridge/src/plugin_host_main.cpp` (mới): process con sandbox — argv `--shm/--path/--sr/--block/--parent`; COM STA; `shm_create` (PoC con sở hữu mapping, G4.2 đổi thành `shm_open`); `init` → `loadPlugin` → own window + `openGUI` (editorhost pattern) → heartbeat thread `Sleep(100)` (G3.1 pattern) → main loop pump + drain MIDI (snapshot 64, reset count, dispatch 0x9/0x8/0xB/0xC/0xE) + `processAudioBlock(masterLeft, masterRight, block)` + `bridgeWriteIndex++` + `parent_alive` check mỗi block (chống orphan) → `closeGUI` + `CoUninitialize` + `shm_close`.
|
||||
- `include/SharedMemoryIPC.h`: bổ sung prototype helpers `shm_open/shm_create/shm_ptr/shm_close` (chỉ khai báo, không đổi layout — struct giữ nguyên 11168B).
|
||||
- `CMakeLists.txt`: target `plugin_host` (src/plugin_host_main.cpp + SharedMemoryIPC.cpp + Vst3Instrument.cpp + SDK hosting libs). Build Release PASS.
|
||||
- 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.
|
||||
- Commit: `G4.1: sandbox plugin host (plugin_host.exe) PoC — Nexus runs in child process, GUI + audio via SHM, crash isolation verified`.
|
||||
@@ -101,6 +101,24 @@ endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(gui_probe PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm)
|
||||
endif()
|
||||
add_executable(plugin_host
|
||||
src/plugin_host_main.cpp
|
||||
src/SharedMemoryIPC.cpp
|
||||
src/Vst3Instrument.cpp
|
||||
)
|
||||
if(VST3_SDK_TARGET)
|
||||
target_compile_definitions(plugin_host PRIVATE HAVE_VST3SDK=1)
|
||||
target_link_libraries(plugin_host PRIVATE ${VST3_SDK_TARGET} sdk_hosting sdk_common)
|
||||
if(WIN32)
|
||||
target_sources(plugin_host PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/common/memorystream.cpp
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(plugin_host PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm)
|
||||
endif()
|
||||
|
||||
# G3.3: seh_channel_test — SEH per-channel crash isolation (debug tool, not shipped)
|
||||
add_executable(seh_channel_test
|
||||
|
||||
@@ -62,3 +62,10 @@ struct SharedAudioBufferIPC {
|
||||
// Append-only field: existing offsets stay stable.
|
||||
volatile uint32_t heartbeat;
|
||||
};
|
||||
|
||||
// Shared-memory helpers (impl in SharedMemoryIPC.cpp).
|
||||
struct ShmHandle;
|
||||
ShmHandle* shm_open(const char* name);
|
||||
ShmHandle* shm_create(const char* name);
|
||||
SharedAudioBufferIPC* shm_ptr(ShmHandle* h);
|
||||
void shm_close(ShmHandle* h);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// native_bridge/src/plugin_host_main.cpp - G4.1 sandbox VST3 host (child process).
|
||||
// Proof-of-concept (NOT shipped): runs ONE VST3 out-of-process. The plugin's
|
||||
// own crash can only kill this process, never the DAW/bridge. Audio + MIDI +
|
||||
// heartbeat travel over a child-owned SHM mapping reusing the SharedAudioBufferIPC
|
||||
// layout, so the bridge can later adopt the same struct without layout churn.
|
||||
//
|
||||
// argv: --shm <name> --path <vst3> [--sr <rate>] [--block <n>] [--parent <pid>]
|
||||
#include "Vst3Instrument.h"
|
||||
#include "SharedMemoryIPC.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
static bool parent_alive(uint32_t pid) {
|
||||
#ifdef _WIN32
|
||||
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
||||
if (!h) return false;
|
||||
CloseHandle(h);
|
||||
return true;
|
||||
#else
|
||||
return pid == 0 || (kill(pid, 0) == 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
std::string shmName, path;
|
||||
double sr = 44100.0;
|
||||
uint32_t block = AUDIO_BLOCK_SIZE;
|
||||
uint32_t parentPid = 0;
|
||||
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);
|
||||
}
|
||||
if (shmName.empty() || path.empty()) {
|
||||
printf("usage: plugin_host --shm <name> --path <vst3> [--sr rate] [--block n] [--parent pid]\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
#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);
|
||||
|
||||
// PoC: the child owns the mapping. Bridge adoption (G4.2) opens instead.
|
||||
ShmHandle* shm = shm_create(shmName.c_str());
|
||||
if (!shm) {
|
||||
printf("[plugin_host] FAILED shm_create %s\n", shmName.c_str());
|
||||
return 3;
|
||||
}
|
||||
SharedAudioBufferIPC* ipc = shm_ptr(shm);
|
||||
|
||||
Vst3Instrument inst;
|
||||
inst.init(sr, block); // sets sampleRate_/maxBlockSize_ used by setupProcessing
|
||||
if (!inst.loadPlugin(path, sr)) {
|
||||
printf("[plugin_host] loadPlugin failed\n");
|
||||
return 4;
|
||||
}
|
||||
|
||||
// GUI on the main thread (editorhost pattern: attach then pump below).
|
||||
HWND hwnd = nullptr;
|
||||
{
|
||||
WNDCLASSEX wc = {};
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.lpfnWndProc = DefWindowProc;
|
||||
wc.hInstance = GetModuleHandle(nullptr);
|
||||
wc.lpszClassName = "PluginHostClass";
|
||||
RegisterClassEx(&wc);
|
||||
hwnd = CreateWindowEx(0, "PluginHostClass", "PluginHost", WS_OVERLAPPEDWINDOW,
|
||||
0, 0, 800, 600, nullptr, nullptr, wc.hInstance, nullptr);
|
||||
}
|
||||
printf("[plugin_host] ownHwnd=%p openGUI...\n", (void*)hwnd);
|
||||
fflush(stdout);
|
||||
bool gui = inst.openGUI(hwnd);
|
||||
printf("[plugin_host] openGUI=%d\n", gui ? 1 : 0);
|
||||
fflush(stdout);
|
||||
|
||||
// Heartbeat ~10Hz, independent of the audio loop (G3.1 pattern).
|
||||
std::thread([ipc]() {
|
||||
for (;;) {
|
||||
Sleep(100);
|
||||
ipc->heartbeat++;
|
||||
}
|
||||
}).detach();
|
||||
|
||||
for (;;) {
|
||||
MSG msg;
|
||||
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
uint32_t n = ipc->midiQueueCount > 64 ? 64 : ipc->midiQueueCount;
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
const SharedAudioBufferIPC::MidiEventIPC& e = ipc->midiQueue[i];
|
||||
switch (e.command) {
|
||||
case 0x9:
|
||||
if (e.velocity > 0) inst.noteOn(e.channel, e.pitch, e.velocity / 127.0f, 0);
|
||||
else inst.noteOff(e.channel, e.pitch, 0);
|
||||
break;
|
||||
case 0x8: inst.noteOff(e.channel, e.pitch, 0); break;
|
||||
case 0xB: inst.controlChange(e.channel, e.pitch, e.data2); break;
|
||||
case 0xC: inst.programChange(e.channel, e.data2); break;
|
||||
case 0xE: inst.pitchBend(e.channel, e.data2 | (uint32_t(e.data3) << 7)); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
ipc->midiQueueCount = 0;
|
||||
inst.processAudioBlock(ipc->masterLeft, ipc->masterRight, block);
|
||||
ipc->bridgeWriteIndex++;
|
||||
if (parentPid && !parent_alive(parentPid)) {
|
||||
printf("[plugin_host] parent gone - exiting\n");
|
||||
break;
|
||||
}
|
||||
Sleep(1);
|
||||
}
|
||||
inst.closeGUI();
|
||||
#ifdef _WIN32
|
||||
CoUninitialize();
|
||||
#endif
|
||||
shm_close(shm);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user