# Sonic Forge Studio — Big Tasks (ROADMAP G0–G4) Trạng thái: 2026-08-15. Bug gốc: PLAY + mở GUI VST → câm, GUI không load. User duyệt roadmap G0–G4 ("Hãy bắt đầu"). Đang ở G1. ## Ngữ cảnh kỹ thuật - Logs: `C:/Users/locpham/AppData/Roaming/SonicForgeDAW/logs/` — `bridge.log` (stdout bridge), `spawn.log` (watchdog restart), `engine.log`. - VST test: `C:\Program Files\Common Files\VST3\Nexus.vst3`. - SHM: `SonicForge_DAW_IPC_`; offsets: controlQueue @2836 (8×1040), controlQueueCount @11156, attach_in_flight @11160, writeIndex @4, size 11164. Control types: 2=LOAD, 3=TRANSPORT (0=STOP/1=PLAY), 4=OPEN_GUI. - Branch: `standalone-shm-bridge`. App: sonicforge-daw, daw_engine, daw_vst_bridge (3 process). - Probe: `native_bridge/build/Release/gui_probe.exe` — standalone VST3 GUI attach probe. Variants: main_own | worker_own_nopump | worker_own_pump | worker_foreign_nopump | worker_foreign_pump | bridge_like | same_thread | two_instances | two_instances_close | close_reopen | bridge_two | shared_worker | ui_thread | two_workers_close. - File CRLF toàn bộ native_bridge + CMakeLists.txt → patch multi-line qua python `io.open(newline='')`. ## G0 — Hoàn thành (đã commit, branch standalone-shm-bridge) - G0.1 (1839480): defer OPEN_GUI khi PLAY. - G0.2 (2f78618): openVstGuiRetry ≤2 lần + `__bridgePlaying` guard (app.precompiled.js). - G0.3 (5313d85): watchdog giữ 15s khi attach-in-flight (SHM flag offset 11160) + RAII guard. - G0.4 (eac7f2f): save/restore preset quanh reload — captureState/restoreState trong Vst3Instrument (getState/setState qua MemoryStream), gọi trong post_close_gui; INativeInstrument.h virtual no-op; CMakeLists link memorystream.cpp vào daw_vst_bridge + sdk_common. VERIFIED PASS (probe_g04_preset.py). ## G1 — 1 UiThread chung (ĐANG LÀM) Mục tiêu: 1 thread chung host mọi VST3 instance thay per-channel workers → hết "2 thread 1 DLL" hang/crash (bridge_two HANG/EXIT3221225477 vs shared_worker OK). ### G1.1 — Probe variant ui_thread (alias shared_worker) [HOÀN THÀNH] - [x] Patch gui_probe.cpp: usage string, branch `variant == "shared_worker" || variant == "ui_thread"`, isDeferred list. - [x] Fix build: CMakeLists gui_probe target thêm `memorystream.cpp` + `sdk_common` (LNK2019). - [x] Fix crash 0xC0000409: ProbeWorker thiếu destructor join → std::terminate. Thêm destructor + join + closeGUI trước teardown + gỡ IME. VERIFIED `shared_worker 15`: RESULT done=1 aRes=1 bRes=1, EXIT=0, 6.73s. Commit 3587a34. ### G1.2 — Chuyển tạo window + attach job từ ChannelWorker → UiThread mới (1 thread chung) [HOÀN THÀNH — 9e6e72a] - [x] Thay per-channel workers (16) bằng 1 UiThread: COM STA + message pump + job queue. - [x] Audio loop bỏ PeekMessageW. - [x] Option B (đóng editor channel khác trước attach) chuyển PostMessage(WM_CLOSE)+5s → close_editor_now đồng bộ (1 thread: job close sau attach job cùng queue → không bao giờ chạy). - [x] VERIFIED: build EXIT=0; live test PASS — LOAD 7, PLAY writeIndex advancing, defer OK, attach OK, Option B close đồng bộ OK, WM_CLOSE→reload OK, 0 crash/restart. ### G1.3 — Close/resize/IME sang UiThread + bỏ pump gate cũ [HOÀN THÀNH — b92c85e] ### G1.4 — Bỏ mute-when-editor-open [HOÀN THÀNH — 7629783] - [x] channelQuiet chi con reloading (bo editorOpenFn check); xoa setEditorOpenPredicate/g_editorOpen/g_editorPathCount/clear_editor_open. - [x] VERIFIED: build PASS; live test PASS — PLAY+editor open writeIndex advancing (het cam), LOAD-while-editor OK, close/reopen x4 OK, regression G1.2 PASS, 0 crash/restart. ### G1.5 — Regression driver stress [HOÀN THÀNH] - [x] stress driver native_bridge/debug/test_g15_stress.py: 5 + 10 iterations LOAD/OPEN_GUI/PLAY/STOP/WM_CLOSE Nexus+EZkeys. - [x] VERIFIED: PASS ca hai run — PLAY+editor open writeIndex advancing moi iter (delta ~290-330), WM_CLOSE state restored OK, bridge alive 30512, 0 EXCEPTION/FAILED/crash. ## G2 — Persistence restart (2-3 tuần) Mục tiêu: restart không mất instrument — "stop/play nghe lại" ngay sau respawn. - G2.1 save/restore đầy đủ: toàn bộ trạng thái instrument (path, preset, params, transport position) ra đĩa. - G2.2 dump state trước kill: trước khi DAW/bridge bị kill, dump trạng thái hiện tại. - G2.3 auto-reload sau respawn: sau khi bridge respawn, tự load lại từ dump. - G2.4 bỏ !isPlayingRef chặn restore: gỡ guard chặn restore khi đang PLAY. ## G3 — Health / Watchdog thông minh (1-2 tuần) Mục tiêu: Watchdog thông minh, cô lập lỗi. - G3.1 heartbeat riêng: heartbeat bridge/engine tách khỏi logic audio. - G3.2 phân loại stall: phân biệt hang/crash/stall để xử lý đúng mức. - G3.3 SEH quanh process từng channel: 1 plugin crash → chỉ mute channel đó, không sập cả bridge. ## G4 — Sandbox plugin (tuỳ chọn, 2-3 tháng) Mục tiêu: plugin process con — VST3 không có chuẩn OOP như AUv3 → wrapper tự làm. Lưu ý: G1+G3 đã lo 90% kịch bản. Khuyến nghị làm sau. ## Kiến trúc hiện tại (main.cpp 1254 dòng, sau G1.4) — tham chiếu G1.2–G1.5 - 1 UiThread chung (class ChannelWorker, main.cpp ~236): COM STA + job queue + message pump (cap 128 msg/iteration). MỌI window (native VST + editor children) thuộc thread này; CBT hook IME per-thread + priority ABOVE_NORMAL. loadPlugin/createView/close_editor_now/LOAD jobs đều chạy ở đây — thread sống cả đời bridge, COM STA apartment không chết → Nexus attached() không treo. (G1.1/G1.2) - Audio/main loop (main.cpp ~972-1235): drain control queue → pending-GUI sweep → MIDI snapshot → dispatch sample-accurate (A11: sort theo sampleOffset, renderSegment từng sub-block) → renderAll → bridgeWriteIndex++ → sync real-time. KHÔNG còn PeekMessageW (G1.3); try/catch net bọc cả loop (plugin exception không escape). - Control (SHM): controlQueue @2836 (8×1040), count @11156, attachInFlight @11160, writeIndex @4. Types: 1=PANIC, 2=LOAD, 3=TRANSPORT (0=STOP flush notes + CC64, 1=PLAY, 2=SET_POSITION), 4=OPEN_GUI. - OPEN_GUI (handleOpenGui): defer khi chưa load hoặc đang PLAY → g_pendingGui, sweep khi transportStopped && loaded (G0.1); attach job trên UiThread (g_loadMutex serialize createView); attach-in-flight báo watchdog qua SHM attachInFlight RAII (G0.3); Option B đóng editor channel khác ĐỒNG BỘ qua close_editor_now (G1.2 — bỏ PostMessage(WM_CLOSE)+5s timeout). - LOAD (assign trên UiThread): setReloading(ch + mọi same-path channel) TRƯỚC khi vào DLL; hide + detach editor cũ (closeGUI + destroy_window_on_owner); restore unmute sau job (g_closeInFlight + g_attachPaths ngăn unmute nhầm). - Guards còn lại: g_loadMutex (serialize mọi plugin-DLL entry: createInstance + createView), reloadingCh_[16] (engine — channelQuiet CHỈ mute khi reloading; G1.4 bỏ mute-when-editor-open), g_attachPaths + g_attachMutex (attach-in-flight registry), g_closeInFlight[16], g_guiMutex + g_guiWindows + g_hwndToCh (window registry: giữ sống + reuse + WM_DESTROY cleanup), g_pendingGui + g_pendingGuiMutex, g_cbtHook (strip IMC trên JUCE windows — chống IME recursion 0xC00000FD), destroy_window_on_owner (destroy editor child trên UiThread, không cross-thread — hết 0xc000041d). - VstWindowProc: WM_CLOSE → post_close_gui + hide (KHÔNG DefWindowProc destroy khi view attached — tránh cross-thread destroy); WM_DESTROY → registry cleanup + post_close_gui; WM_SIZE → MoveWindow child + post_resize_view; WM_ERASEBKGND/WM_PAINT no-op (chống trắng loá). - G0.4: save/restore preset quanh close/reload — captureState/restoreState qua MemoryStream (post_close_gui/LOAD); bridge + gui_probe link memorystream.cpp. - Watchdog thread riêng (main.cpp ~600): parent chết → TerminateProcess ngay + check birth-time chống PID reuse (V8 bug 2). ## Learnings - read_file lỗi với main.cpp/lib.rs/gui_probe.cpp → dùng `sed -n` qua exec_command. - MemoryStream::write cần 3 args (buffer, size, int32* numBytesWritten); seek(0, kIBSeekSet, nullptr). memorystream.cpp không thuộc target SDK → add source trực tiếp vào mỗi target dùng Vst3Instrument.cpp (bridge ĐÃ thêm, gui_probe ĐÃ thêm). - tasklist CSV parse: dùng `subprocess.run(['tasklist'])` full output + `line.split()[1]`. - Probe wait_log: log cùng job xuất hiện ~đồng thời → dùng chung `since` window, không reset. - Probe PASS có thể dương tính giả → luôn kiểm tra bridge.log/spawn.log thực. - Deploy: taskkill 3 process → chờ 2s → copy exe → start → chờ 5s → tasklist lấy PID mới → update APP_PID trong probe. - `sleep` không tồn tại trong sh → `python -c "import time; time.sleep(N)"`. - `timeout` Windows khác GNU → dùng python subprocess wrapper. - CRLF: `s.count(old)` với `\n` thật trong heredoc `<<'EOF'` KHÔNG khớp CRLF; dùng script .py file thay vì `python -c` chuỗi dài (escaping lỗi). - 0xC0000409 trong ucrtbase = fail-fast/terminate (không phải C++ exception thường bắt được bằng catch(...) nếu là SEH/fail-fast).