// src-tauri/src/shm.rs // Shared memory `SonicForge_DAW_IPC` — created by the DAW, opened by the // C++ bridge (native_bridge). Layout MUST match native_bridge/include/ // SharedMemoryIPC.h (verified by native_bridge/tests/shm_selfcheck.cpp). #![cfg_attr(not(windows), allow(dead_code))] use std::ffi::c_void; use std::ptr; use std::sync::Mutex; use windows_sys::Win32::Foundation::{CloseHandle, HANDLE}; use windows_sys::Win32::System::Memory::{ CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS, PAGE_READWRITE, }; // SHM name RIENG cho tung process app: 2 instance (vi du bản portable install\ // + bản MSI cài song song) dung chung ten -> 2 bridge cung map 1 SHM -> ca 2 // drain control/midi queue (double instrument load, double GUI attach) -> race // -> crash USER32 (Event Log 12:07:06 + 12:07:51: bridge MSI crash trong luc // bridge portable dang chay). App PID duy nhat moi instance. pub fn shm_name() -> String { format!("SonicForge_DAW_IPC_{}", std::process::id()) } pub const AUDIO_BLOCK_SIZE: usize = 256; pub const MIDI_QUEUE_CAP: usize = 64; pub const CONTROL_QUEUE_CAP: usize = 8; pub const CONTROL_PATH_MAX: usize = 1024; #[repr(C)] #[derive(Clone, Copy, serde::Serialize)] pub struct MidiEventIPC { pub command: u8, pub channel: u8, pub pitch: u8, pub velocity: u8, pub data2: u8, // CC value / program / PB LSB pub data3: u8, // PB MSB (0xE only) pub reserved: [u8; 2], // reserved[0]: live flag (1=live input, bypass transportStopped guard); reserved[1]: 0 pub sample_offset: u32, } #[repr(C)] #[derive(Clone, Copy)] pub struct ControlEventIPC { pub ctype: u32, pub arg0: u32, pub arg1: u32, pub channel: u32, // LOAD: MIDI channel to assign (A10) pub arg2: [u8; CONTROL_PATH_MAX], } #[repr(C)] pub struct SharedAudioBufferIPC { pub client_read_index: u32, pub bridge_write_index: u32, pub master_left: [f32; AUDIO_BLOCK_SIZE], pub master_right: [f32; AUDIO_BLOCK_SIZE], pub block_timestamp: u64, pub midi_queue: [MidiEventIPC; MIDI_QUEUE_CAP], pub midi_queue_count: u32, pub control_queue: [ControlEventIPC; CONTROL_QUEUE_CAP], pub control_queue_count: u32, // G0.3: bridge sets this while an openGUI attach job is in flight. // Append-only field — offsets of control_queue (2836) and // control_queue_count (11156) are unchanged. pub attach_in_flight: u32, // G3.1: dedicated heartbeat — incremented ~10Hz by a bridge thread // independent of the audio loop. Watchdog liveness keys on this. // Append-only field: offsets above are unchanged. pub heartbeat: u32, // VSTi crackle fix: multi-slot audio ring (appended after heartbeat — // offsets above are unchanged). Bridge renders block N into slot // (bridge_write_index & ring_mask), then increments bridge_write_index. // Pump drains EVERY slot between its last index and write_index, in order. pub ring_mask: u32, pub ring_left: [[f32; AUDIO_BLOCK_SIZE]; 8], pub ring_right: [[f32; AUDIO_BLOCK_SIZE]; 8], // A14: MIDI OUT from plugins (VST2 audioMaster opcode=8 VstEvents / VST3 // IEventList). Bridge writes, pump drains -> `bridge-midi-out` -> JS. // Append-only field: offsets above are unchanged. pub midi_out_queue: [MidiEventIPC; MIDI_QUEUE_CAP], pub midi_out_count: u32, } pub struct Shm { handle: HANDLE, view: *mut SharedAudioBufferIPC, } unsafe impl Send for Shm {} impl Shm { /// Create (or open) the mapping. Returns None on non-Windows (dev fallback). pub fn create() -> Option { if !cfg!(windows) { return None; } let name_wide: Vec = shm_name().encode_utf16().chain(std::iter::once(0)).collect(); let size = std::mem::size_of::() as u64; let handle = unsafe { CreateFileMappingW( HANDLE::default(), ptr::null(), PAGE_READWRITE, (size >> 32) as u32, size as u32, name_wide.as_ptr(), ) }; if handle == HANDLE::default() { return None; } let view = unsafe { MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0) }; if view.Value.is_null() { unsafe { CloseHandle(handle) }; return None; } Some(Shm { handle, view: view.Value as *mut SharedAudioBufferIPC, }) } fn ipc(&self) -> &mut SharedAudioBufferIPC { unsafe { &mut *self.view } } pub fn push_midi(&self, cmd: u8, channel: u8, pitch: u8, velocity: u8, data2: u8, data3: u8, sample_offset: u32, live: u8) -> bool { let ipc = self.ipc(); if ipc.midi_queue_count as usize >= MIDI_QUEUE_CAP { return false; } let i = ipc.midi_queue_count as usize; ipc.midi_queue[i] = MidiEventIPC { command: cmd, channel, pitch, velocity, data2, data3, reserved: [live, 0u8], sample_offset, }; ipc.midi_queue_count += 1; true } pub fn push_control(&self, ctype: u32, arg0: u32, arg1: u32, channel: u32, path: &str) -> bool { let ipc = self.ipc(); if ipc.control_queue_count as usize >= CONTROL_QUEUE_CAP { return false; } let mut ev = ControlEventIPC { ctype, arg0, arg1, channel, arg2: [0u8; CONTROL_PATH_MAX], }; let bytes = path.as_bytes(); let n = bytes.len().min(CONTROL_PATH_MAX - 1); ev.arg2[..n].copy_from_slice(&bytes[..n]); // LOAD relies on C++ side strnlen() — arg1 is free for TRANSPORT playhead. let i = ipc.control_queue_count as usize; ipc.control_queue[i] = ev; ipc.control_queue_count += 1; true } pub fn read_audio(&self) -> ([f32; AUDIO_BLOCK_SIZE], [f32; AUDIO_BLOCK_SIZE]) { let ipc = self.ipc(); (ipc.master_left, ipc.master_right) } /// Read ring slot for block index `idx` (slot = idx & ring_mask). pub fn read_ring_audio(&self, idx: u32) -> ([f32; AUDIO_BLOCK_SIZE], [f32; AUDIO_BLOCK_SIZE]) { let ipc = self.ipc(); let slot = (idx & ipc.ring_mask) as usize; (ipc.ring_left[slot], ipc.ring_right[slot]) } pub fn ring_mask(&self) -> u32 { self.ipc().ring_mask } pub fn write_index(&self) -> u32 { self.ipc().bridge_write_index } pub fn block_timestamp(&self) -> u64 { self.ipc().block_timestamp } pub fn attach_in_flight(&self) -> bool { self.ipc().attach_in_flight != 0 } pub fn heartbeat(&self) -> u32 { self.ipc().heartbeat } /// Drain all pending MIDI-out events (plugin-generated). Returns them and /// resets the count; empty vec when none. pub fn drain_midi_out(&self) -> Vec { let ipc = self.ipc(); let n = (ipc.midi_out_count as usize).min(MIDI_QUEUE_CAP); if n == 0 { return Vec::new(); } let mut out = Vec::with_capacity(n); for i in 0..n { out.push(ipc.midi_out_queue[i]); } ipc.midi_out_count = 0; out } } impl Drop for Shm { fn drop(&mut self) { if !self.view.is_null() { unsafe { UnmapViewOfFile(windows_sys::Win32::System::Memory::MEMORY_MAPPED_VIEW_ADDRESS { Value: self.view as *mut c_void }) }; } unsafe { CloseHandle(self.handle) }; } } /// Process-wide shared-memory state for Tauri. pub struct ShmState(pub Mutex>); #[cfg(test)] mod tests { use super::*; #[test] fn layout_matches_c_header() { // native_bridge/tests/shm_selfcheck.cpp asserts the same sizes. assert_eq!(std::mem::size_of::(), 12); assert_eq!(std::mem::size_of::(), 1040); // Last field (ring_right) ends at 27556; midi_out_queue appends // at 27556, +64*12 + 4 = 28328 (already 8-aligned — no tail pad). // Matches MSVC sizeof (verified against the C header). assert_eq!(std::mem::size_of::(), 28328); } }