G3.2+G3.3: classify hang/crash vs audio stall + SEH per-channel plugin crash isolation (mute channel)
This commit is contained in:
@@ -101,3 +101,22 @@ endif()
|
|||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(gui_probe PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm)
|
target_link_libraries(gui_probe PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm)
|
||||||
endif()
|
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()
|
||||||
|
|||||||
@@ -119,6 +119,13 @@ public:
|
|||||||
// worker thread): real-time code drops MIDI for it. Channel-level flag —
|
// worker thread): real-time code drops MIDI for it. Channel-level flag —
|
||||||
// survives the instance swap (the per-instance flag dies with the object).
|
// survives the instance swap (the per-instance flag dies with the object).
|
||||||
bool isReloading(uint32_t channel) const;
|
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
|
// Editor-open predicate: while any VST editor is attached, channels whose
|
||||||
// plugin DLL path has an open editor count as quiet (same-plugin channels
|
// 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
|
// too -- the worker pumps the editor's window proc inside the DLL while the
|
||||||
@@ -146,6 +153,8 @@ private:
|
|||||||
std::vector<float> scratchL_, scratchR_;
|
std::vector<float> scratchL_, scratchR_;
|
||||||
// Channel-level reload flag (see isReloading) — indexed by MIDI channel.
|
// Channel-level reload flag (see isReloading) — indexed by MIDI channel.
|
||||||
bool reloadingCh_[16] = {};
|
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
|
// 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
|
// is in progress): real-time code drops MIDI and skips rendering. Lowercased
|
||||||
// compare inside -- Windows plugin paths are case-insensitive.
|
// compare inside -- Windows plugin paths are case-insensitive.
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#define FS_SYNTH (static_cast<fluid_synth_t*>(synth))
|
#define FS_SYNTH (static_cast<fluid_synth_t*>(synth))
|
||||||
#define FS_SETTINGS (static_cast<fluid_settings_t*>(settings))
|
#define FS_SETTINGS (static_cast<fluid_settings_t*>(settings))
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -206,8 +208,9 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
|
|||||||
types_[channel] = type;
|
types_[channel] = type;
|
||||||
// Fresh instance is by construction not reloading — clear the channel
|
// Fresh instance is by construction not reloading — clear the channel
|
||||||
// flag so real-time MIDI dispatch (which drops reloading channels)
|
// 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;
|
reloadingCh_[channel] = false;
|
||||||
|
crashedCh_[channel] = false;
|
||||||
}
|
}
|
||||||
if (oldInst) {
|
if (oldInst) {
|
||||||
isBypassed_.store(true);
|
isBypassed_.store(true);
|
||||||
@@ -300,11 +303,23 @@ bool InstrumentEngineManager::isReloading(uint32_t channel) const {
|
|||||||
return channel < 16 && reloadingCh_[channel];
|
return channel < 16 && reloadingCh_[channel];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quiet = reloading (mid-rebuild) only. G1.4: bo mute-when-editor-open —
|
void InstrumentEngineManager::markCrashed(uint32_t channel) {
|
||||||
// editor co the mo trong luc PLAY, audio loop van process() binh thuong.
|
std::lock_guard<std::mutex> lock(mu_);
|
||||||
|
if (channel < 16) crashedCh_[channel] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InstrumentEngineManager::isCrashed(uint32_t channel) const {
|
||||||
|
std::lock_guard<std::mutex> 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.
|
// Called with mu_ held by the real-time dispatch.
|
||||||
bool InstrumentEngineManager::channelQuiet(uint32_t ch) const {
|
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;
|
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) {
|
void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t numSamples) {
|
||||||
if (isBypassed_.load()) {
|
if (isBypassed_.load()) {
|
||||||
std::memset(outputL, 0, numSamples * sizeof(float));
|
std::memset(outputL, 0, numSamples * sizeof(float));
|
||||||
@@ -377,10 +406,19 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
|
|||||||
scratchR_.resize(numSamples);
|
scratchR_.resize(numSamples);
|
||||||
}
|
}
|
||||||
for (auto& [ch, inst] : channels_) {
|
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(scratchL_.data(), 0, numSamples * sizeof(float));
|
||||||
std::memset(scratchR_.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) {
|
for (uint32_t i = 0; i < numSamples; ++i) {
|
||||||
outputL[i] += scratchL_[i];
|
outputL[i] += scratchL_[i];
|
||||||
outputR[i] += scratchR_[i];
|
outputR[i] += scratchR_[i];
|
||||||
|
|||||||
@@ -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 <cassert>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ windows-sys = { version = "0.59", features = [
|
|||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_Security",
|
"Win32_Security",
|
||||||
"Win32_System_Memory",
|
"Win32_System_Memory",
|
||||||
|
"Win32_System_Threading",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
+33
-2
@@ -95,6 +95,21 @@ fn kill_process_tree(child: &CommandChild) -> bool {
|
|||||||
.unwrap_or(false)
|
.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).
|
/// Audio frame pushed to the WebView (bridge SHM -> `bridge-audio` event).
|
||||||
#[derive(Clone, serde::Serialize)]
|
#[derive(Clone, serde::Serialize)]
|
||||||
struct AudioFrame {
|
struct AudioFrame {
|
||||||
@@ -478,8 +493,24 @@ pub fn run() {
|
|||||||
let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
|
let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
|
||||||
let log_path = std::path::Path::new(&log_dir)
|
let log_path = std::path::Path::new(&log_dir)
|
||||||
.join("SonicForgeDAW").join("logs").join("spawn.log");
|
.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::<BridgeProcess>()
|
||||||
|
.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
|
// FIX: kill bridge cũ trước — không được để 2 bridge
|
||||||
// cùng map SHM (race control queue / double load).
|
// cùng map SHM (race control queue / double load).
|
||||||
kill_bridge(&pump_handle);
|
kill_bridge(&pump_handle);
|
||||||
|
|||||||
Reference in New Issue
Block a user