Files

9.0 KiB
Raw Permalink Blame History

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_<APP_PID>; 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]

  • Patch gui_probe.cpp: usage string, branch variant == "shared_worker" || variant == "ui_thread", isDeferred list.
  • Fix build: CMakeLists gui_probe target thêm memorystream.cpp + sdk_common (LNK2019).
  • 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]

  • Thay per-channel workers (16) bằng 1 UiThread: COM STA + message pump + job queue.
  • Audio loop bỏ PeekMessageW.
  • 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).
  • 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]

  • channelQuiet chi con reloading (bo editorOpenFn check); xoa setEditorOpenPredicate/g_editorOpen/g_editorPathCount/clear_editor_open.
  • 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]

  • stress driver native_bridge/debug/test_g15_stress.py: 5 + 10 iterations LOAD/OPEN_GUI/PLAY/STOP/WM_CLOSE Nexus+EZkeys.
  • 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).