diff --git a/native_bridge/CMakeLists.txt b/native_bridge/CMakeLists.txt index e49d2ca..ab45882 100644 --- a/native_bridge/CMakeLists.txt +++ b/native_bridge/CMakeLists.txt @@ -101,3 +101,22 @@ endif() if(WIN32) target_link_libraries(gui_probe 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 + tests/seh_channel_test.cpp + src/Vst3Instrument.cpp +) +if(VST3_SDK_TARGET) + target_compile_definitions(seh_channel_test PRIVATE HAVE_VST3SDK=1) + target_link_libraries(seh_channel_test PRIVATE ${VST3_SDK_TARGET} sdk_hosting sdk_common) + if(WIN32) + target_sources(seh_channel_test 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(seh_channel_test PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm) +endif() diff --git a/native_bridge/include/NativeInstrumentEngine.h b/native_bridge/include/NativeInstrumentEngine.h index 2007652..2f8747f 100644 --- a/native_bridge/include/NativeInstrumentEngine.h +++ b/native_bridge/include/NativeInstrumentEngine.h @@ -119,6 +119,13 @@ public: // worker thread): real-time code drops MIDI for it. Channel-level flag — // survives the instance swap (the per-instance flag dies with the object). bool isReloading(uint32_t channel) const; + + // G3.3: mark a channel whose plugin crashed inside processAudioBlock + // (SEH access violation). The channel is muted (real-time dispatch + + // renderAll skip it) until the next assign() — the plugin's state is + // undefined after a fault, so re-processing would fault again. + void markCrashed(uint32_t channel); + bool isCrashed(uint32_t channel) const; // Editor-open predicate: while any VST editor is attached, channels whose // plugin DLL path has an open editor count as quiet (same-plugin channels // too -- the worker pumps the editor's window proc inside the DLL while the @@ -146,6 +153,8 @@ private: std::vector scratchL_, scratchR_; // Channel-level reload flag (see isReloading) — indexed by MIDI channel. bool reloadingCh_[16] = {}; + // G3.3: channel muted after a plugin fault inside processAudioBlock. + bool crashedCh_[16] = {}; // True when the channel's plugin DLL has an attached editor (or its reload // is in progress): real-time code drops MIDI and skips rendering. Lowercased // compare inside -- Windows plugin paths are case-insensitive. diff --git a/native_bridge/src/NativeInstrumentEngine.cpp b/native_bridge/src/NativeInstrumentEngine.cpp index 983cd55..81c2203 100644 --- a/native_bridge/src/NativeInstrumentEngine.cpp +++ b/native_bridge/src/NativeInstrumentEngine.cpp @@ -9,6 +9,8 @@ #define FS_SYNTH (static_cast(synth)) #define FS_SETTINGS (static_cast(settings)) +#include + #include #include #include @@ -206,8 +208,9 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type, types_[channel] = type; // Fresh instance is by construction not reloading — clear the channel // flag so real-time MIDI dispatch (which drops reloading channels) - // flows to it again. + // flows to it again. G3.3: a reload also clears the crash-mute. reloadingCh_[channel] = false; + crashedCh_[channel] = false; } if (oldInst) { isBypassed_.store(true); @@ -300,11 +303,23 @@ bool InstrumentEngineManager::isReloading(uint32_t channel) const { return channel < 16 && reloadingCh_[channel]; } -// Quiet = reloading (mid-rebuild) only. G1.4: bo mute-when-editor-open — -// editor co the mo trong luc PLAY, audio loop van process() binh thuong. +void InstrumentEngineManager::markCrashed(uint32_t channel) { + std::lock_guard lock(mu_); + if (channel < 16) crashedCh_[channel] = true; +} + +bool InstrumentEngineManager::isCrashed(uint32_t channel) const { + std::lock_guard lock(mu_); + return channel < 16 && crashedCh_[channel]; +} + +// Quiet = reloading (mid-rebuild) OR crashed (G3.3). G1.4: bo +// mute-when-editor-open — editor co the mo trong luc PLAY, audio loop van +// process() binh thuong. G3.3: channel crash -> mute vi trang thai plugin +// khong xac dinh sau fault; xu ly tiep se fault lai. // Called with mu_ held by the real-time dispatch. bool InstrumentEngineManager::channelQuiet(uint32_t ch) const { - if (ch >= 16 || reloadingCh_[ch]) return true; + if (ch >= 16 || reloadingCh_[ch] || crashedCh_[ch]) return true; return false; } @@ -362,6 +377,20 @@ void InstrumentEngineManager::allNotesOff() { } } +// G3.3: SEH cannot live inside a function that needs C++ unwinding (C2712), +// so per-channel process() runs in this helper. A plugin access violation +// (0xC0000005) is caught here — the channel is muted instead of killing the +// whole bridge process. /EHa (set in CMakeLists) allows mixing with the +// outer C++ try/catch net in main.cpp. +static bool SafeProcessChannel(INativeInstrument* inst, float* outL, float* outR, uint32_t n) { + __try { + inst->processAudioBlock(outL, outR, n); + return true; + } __except (EXCEPTION_EXECUTE_HANDLER) { + return false; + } +} + void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t numSamples) { if (isBypassed_.load()) { std::memset(outputL, 0, numSamples * sizeof(float)); @@ -377,10 +406,19 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t scratchR_.resize(numSamples); } for (auto& [ch, inst] : channels_) { - if (channelQuiet(ch)) continue; // editor open on this plugin DLL: do not process + if (channelQuiet(ch)) continue; // editor open / crashed: do not process std::memset(scratchL_.data(), 0, numSamples * sizeof(float)); std::memset(scratchR_.data(), 0, numSamples * sizeof(float)); - inst->processAudioBlock(scratchL_.data(), scratchR_.data(), numSamples); + if (!SafeProcessChannel(inst.get(), scratchL_.data(), scratchR_.data(), numSamples)) { + // G3.3: plugin fault — mute this channel only, keep the bridge up. + if (!crashedCh_[ch]) { + crashedCh_[ch] = true; + std::cerr << "[NativeBridge] G3.3: channel " << ch + << " plugin crashed in processAudioBlock (SEH) — muted until reload" + << std::endl; + } + continue; + } for (uint32_t i = 0; i < numSamples; ++i) { outputL[i] += scratchL_[i]; outputR[i] += scratchR_[i]; diff --git a/native_bridge/tests/seh_channel_test.cpp b/native_bridge/tests/seh_channel_test.cpp new file mode 100644 index 0000000..be36770 --- /dev/null +++ b/native_bridge/tests/seh_channel_test.cpp @@ -0,0 +1,80 @@ +// native_bridge/tests/seh_channel_test.cpp +// G3.3: SEH per-channel crash isolation. A plugin whose processAudioBlock +// faults (access violation) must be caught by SafeProcessChannel -> renderAll +// mutes the channel instead of taking down the whole bridge process. +// Assert-based, no framework. Includes the engine .cpp (like shm_selfcheck). +#include "../include/NativeInstrumentEngine.h" +#include "../src/NativeInstrumentEngine.cpp" + +#include +#include +#include + +class CrashingStub : public INativeInstrument { +public: + bool init(double, uint32_t) override { return true; } + void selectProgram(uint32_t, uint32_t, uint32_t) override {} + void noteOn(uint32_t, uint32_t, float, uint32_t) override {} + void noteOff(uint32_t, uint32_t, uint32_t) override {} + void controlChange(uint32_t, uint32_t, uint32_t) override {} + void programChange(uint32_t, uint32_t) override {} + void pitchBend(uint32_t, uint32_t) override {} + bool openGUI(void*) override { return false; } + void closeGUI() override {} + void processAudioBlock(float*, float*, uint32_t) override { + volatile int* p = nullptr; + *p = 42; // deliberate access violation + } +}; + +class OkStub : public INativeInstrument { +public: + bool init(double, uint32_t) override { return true; } + void selectProgram(uint32_t, uint32_t, uint32_t) override {} + void noteOn(uint32_t, uint32_t, float, uint32_t) override {} + void noteOff(uint32_t, uint32_t, uint32_t) override {} + void controlChange(uint32_t, uint32_t, uint32_t) override {} + void programChange(uint32_t, uint32_t) override {} + void pitchBend(uint32_t, uint32_t) override {} + bool openGUI(void*) override { return false; } + void closeGUI() override {} + void processAudioBlock(float* outL, float* outR, uint32_t n) override { + for (uint32_t i = 0; i < n; ++i) { outL[i] = 1.0f; outR[i] = 2.0f; } + } +}; + +int main() { + // 1. Faulting plugin: SEH catches the AV, process survives, returns false. + { + float L[64] = {}, R[64] = {}; + CrashingStub stub; + bool ok = SafeProcessChannel(&stub, L, R, 64); + assert(!ok); + // Block left untouched (renderAll continues without this channel). + bool allZero = true; + for (float f : L) if (f != 0.0f) allZero = false; + assert(allZero); + } + // 2. Healthy plugin passes through unchanged. + { + float L[64] = {}, R[64] = {}; + OkStub stub; + bool ok = SafeProcessChannel(&stub, L, R, 64); + assert(ok); + assert(L[0] == 1.0f && R[0] == 2.0f); + } + // 3. Manager crash-mute flags: set, read, and renderAll skips a crashed + // channel (no exception escapes into the audio loop). + { + InstrumentEngineManager mgr; + assert(!mgr.isCrashed(3)); + mgr.markCrashed(3); + assert(mgr.isCrashed(3)); + assert(!mgr.isCrashed(4)); // other channels unaffected + float L[64] = {}, R[64] = {}; + mgr.renderAll(L, R, 64); // crashed + unassigned: no-op, no crash + assert(L[0] == 0.0f); + } + std::printf("SEH_CHANNEL_TEST PASS\n"); + return 0; +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2b4ab60..ee2c96d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,6 +23,7 @@ windows-sys = { version = "0.59", features = [ "Win32_Foundation", "Win32_Security", "Win32_System_Memory", + "Win32_System_Threading", ] } [profile.release] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4b28a42..5dd4cc2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -95,6 +95,21 @@ fn kill_process_tree(child: &CommandChild) -> bool { .unwrap_or(false) } +/// G3.2: true if a process with `pid` still exists (OpenProcess + exit code). +/// Used to classify a stalled heartbeat: pid alive = HANG, pid gone = CRASH. +fn process_alive(pid: u32) -> bool { + use windows_sys::Win32::Foundation::STILL_ACTIVE; + use windows_sys::Win32::System::Threading::{GetExitCodeProcess, OpenProcess}; + let handle = unsafe { OpenProcess(windows_sys::Win32::System::Threading::PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) }; + if handle.is_null() { + return false; // not found or no access -> treat as gone + } + let mut code: u32 = 0; + let ok = unsafe { GetExitCodeProcess(handle, &mut code) } != 0; + unsafe { windows_sys::Win32::Foundation::CloseHandle(handle) }; + ok && code == STILL_ACTIVE as u32 +} + /// Audio frame pushed to the WebView (bridge SHM -> `bridge-audio` event). #[derive(Clone, serde::Serialize)] struct AudioFrame { @@ -478,8 +493,24 @@ pub fn run() { let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into()); let log_path = std::path::Path::new(&log_dir) .join("SonicForgeDAW").join("logs").join("spawn.log"); - let mut line = String::from("[tauri] bridge heartbeat stalled 3s — restart attempt #1 -"); + // G3.2: phân loại stall — process còn sống = HANG + // (heartbeat thread kẹt), pid biến mất = CRASH. + let pid = pump_handle + .state::() + .0 + .lock() + .ok() + .and_then(|g| g.as_ref().map(|c| c.pid())); + let kind = match pid { + Some(p) if process_alive(p) => { + format!("HUNG (pid {p} alive, heartbeat stalled)") + } + Some(p) => format!("CRASHED (pid {p} gone)"), + None => "UNKNOWN (no tracked pid)".into(), + }; + let mut line = format!( + "[tauri] bridge heartbeat stalled 3s — {kind} — restart attempt #1\n" + ); // FIX: kill bridge cũ trước — không được để 2 bridge // cùng map SHM (race control queue / double load). kill_bridge(&pump_handle);