Skip to main content

sesh_sdk/
vec.rs

1//! Vector operations for batch audio processing.
2//!
3//! Each op has two code paths: an inline Rust fallback (always available) and a
4//! host-accelerated import (used when `sesh_vec_version() > 0`). The SDK selects
5//! the path at runtime. Plugin authors call the same functions regardless of platform.
6
7use std::sync::atomic::{AtomicU32, Ordering};
8
9// ---------------------------------------------------------------------------
10// Host capability detection
11// ---------------------------------------------------------------------------
12
13extern "C" {
14    fn sesh_vec_version() -> u32;
15}
16
17/// Cached host vec version. 0 = not yet queried, u32::MAX = stubs (web).
18static HOST_VEC_VERSION: AtomicU32 = AtomicU32::new(0);
19
20fn host_version() -> u32 {
21    let v = HOST_VEC_VERSION.load(Ordering::Relaxed);
22    if v != 0 {
23        return v;
24    }
25    let v = unsafe { sesh_vec_version() };
26    // Store non-zero so we don't re-query. If host returns 0, store a sentinel.
27    let store = if v == 0 { u32::MAX } else { v };
28    HOST_VEC_VERSION.store(store, Ordering::Relaxed);
29    v
30}
31
32#[inline]
33fn use_host_ops() -> bool {
34    host_version() > 0 && host_version() != u32::MAX
35}
36
37// ---------------------------------------------------------------------------
38// Host imports (C ABI, raw pointers)
39// ---------------------------------------------------------------------------
40
41extern "C" {
42    fn sesh_vec_copy_host(dst: *mut f32, src: *const f32, len: u32);
43    fn sesh_vec_fill_host(dst: *mut f32, value: f32, len: u32);
44    fn sesh_vec_add_host(dst: *mut f32, a: *const f32, b: *const f32, len: u32);
45    fn sesh_vec_add_scalar_host(dst: *mut f32, value: f32, len: u32);
46    fn sesh_vec_mul_host(dst: *mut f32, a: *const f32, b: *const f32, len: u32);
47    fn sesh_vec_mul_scalar_host(dst: *mut f32, value: f32, len: u32);
48    fn sesh_vec_mul_add_host(dst: *mut f32, src: *const f32, gain: f32, len: u32);
49    fn sesh_vec_clamp_host(dst: *mut f32, src: *const f32, min: f32, max: f32, len: u32);
50    fn sesh_vec_ring_write_host(
51        buf: *mut f32, buf_len: u32, pos: *mut u32, src: *const f32, len: u32,
52    );
53    fn sesh_vec_ring_read_host(
54        buf: *const f32, buf_len: u32, pos: u32, dst: *mut f32, offset: u32, len: u32,
55    );
56    fn sesh_vec_delay_read_host(
57        buf: *const f32, buf_len: u32, pos: u32, dst: *mut f32, time: *const f32, len: u32,
58    );
59    fn sesh_vec_osc_host(
60        phase: *mut f32, dst: *mut f32, freq: f32, waveform: u32, sample_rate: f32, len: u32,
61    );
62    fn sesh_vec_biquad_host(
63        state: *mut f32, dst: *mut f32, src: *const f32,
64        cutoff: *const f32, q: *const f32, gain: *const f32,
65        filter_type: u32, sample_rate: f32, len: u32,
66    );
67    fn sesh_vec_envelope_host(
68        state: *mut f32, dst: *mut f32, src: *const f32,
69        attack: *const f32, release: *const f32,
70        mode: u32, sample_rate: f32, len: u32,
71    );
72    fn sesh_vec_tanh_host(dst: *mut f32, src: *const f32, drive: *const f32, len: u32);
73    fn sesh_vec_hard_clip_host(dst: *mut f32, src: *const f32, threshold: *const f32, len: u32);
74    fn sesh_vec_abs_host(dst: *mut f32, src: *const f32, len: u32);
75    fn sesh_vec_neg_host(dst: *mut f32, src: *const f32, len: u32);
76    fn sesh_vec_sqrt_host(dst: *mut f32, src: *const f32, len: u32);
77    fn sesh_vec_recip_host(dst: *mut f32, src: *const f32, len: u32);
78    fn sesh_vec_div_host(dst: *mut f32, a: *const f32, b: *const f32, len: u32);
79    fn sesh_vec_pow_host(dst: *mut f32, src: *const f32, exp: *const f32, len: u32);
80    fn sesh_vec_schroeder_allpass_host(
81        buf: *mut f32, buf_len: u32, pos: *mut u32,
82        dst: *mut f32, src: *const f32,
83        delay: u32, g: f32, len: u32,
84    );
85}
86
87// ---------------------------------------------------------------------------
88// Enums and state types
89// ---------------------------------------------------------------------------
90
91/// Oscillator waveform shape.
92#[repr(u32)]
93#[derive(Clone, Copy)]
94pub enum Waveform {
95    Sine = 0,
96    Triangle = 1,
97    Saw = 2,
98    Square = 3,
99}
100
101/// Biquad filter type.
102#[repr(u32)]
103#[derive(Clone, Copy)]
104pub enum FilterType {
105    Lowpass = 0,
106    Highpass = 1,
107    Bandpass = 2,
108    Notch = 3,
109    /// Parametric EQ band — boost/cut at cutoff frequency.
110    Peak = 4,
111    /// Boost/cut below cutoff frequency.
112    LowShelf = 5,
113    /// Boost/cut above cutoff frequency.
114    HighShelf = 6,
115    /// Phase shift without changing amplitude — used in phasers.
116    Allpass = 7,
117}
118
119/// Internal state for a biquad filter (two-sample history).
120#[repr(C)]
121pub struct BiquadState {
122    pub x1: f32,
123    pub x2: f32,
124    pub y1: f32,
125    pub y2: f32,
126}
127
128impl BiquadState {
129    pub const fn new() -> Self {
130        Self { x1: 0.0, x2: 0.0, y1: 0.0, y2: 0.0 }
131    }
132}
133
134/// Envelope follower detection mode.
135#[repr(u32)]
136#[derive(Clone, Copy)]
137pub enum EnvelopeMode {
138    /// Track instantaneous peaks.
139    Peak = 0,
140    /// Track root-mean-square level.
141    Rms = 1,
142}
143
144/// Internal state for an envelope follower.
145#[repr(C)]
146pub struct EnvelopeState {
147    pub current: f32,
148}
149
150impl EnvelopeState {
151    pub const fn new() -> Self {
152        Self { current: 0.0 }
153    }
154}
155
156// ===========================================================================
157// Math ops
158// ===========================================================================
159
160/// Copy `src` into `dst`.
161pub fn vec_copy(dst: &mut [f32], src: &[f32]) {
162    let len = dst.len().min(src.len());
163    if use_host_ops() {
164        unsafe { sesh_vec_copy_host(dst.as_mut_ptr(), src.as_ptr(), len as u32) }
165    } else {
166        dst[..len].copy_from_slice(&src[..len]);
167    }
168}
169
170/// Fill `dst` with a constant value.
171pub fn vec_fill(dst: &mut [f32], value: f32) {
172    let len = dst.len();
173    if use_host_ops() {
174        unsafe { sesh_vec_fill_host(dst.as_mut_ptr(), value, len as u32) }
175    } else {
176        for s in dst.iter_mut() {
177            *s = value;
178        }
179    }
180}
181
182/// Element-wise addition: `dst[i] = a[i] + b[i]`.
183pub fn vec_add(dst: &mut [f32], a: &[f32], b: &[f32]) {
184    let len = dst.len().min(a.len()).min(b.len());
185    if use_host_ops() {
186        unsafe { sesh_vec_add_host(dst.as_mut_ptr(), a.as_ptr(), b.as_ptr(), len as u32) }
187    } else {
188        for i in 0..len {
189            dst[i] = a[i] + b[i];
190        }
191    }
192}
193
194/// Add scalar to every element: `dst[i] += value`.
195pub fn vec_add_scalar(dst: &mut [f32], value: f32) {
196    let len = dst.len();
197    if use_host_ops() {
198        unsafe { sesh_vec_add_scalar_host(dst.as_mut_ptr(), value, len as u32) }
199    } else {
200        for s in dst.iter_mut() {
201            *s += value;
202        }
203    }
204}
205
206/// Element-wise multiplication: `dst[i] = a[i] * b[i]`.
207pub fn vec_mul(dst: &mut [f32], a: &[f32], b: &[f32]) {
208    let len = dst.len().min(a.len()).min(b.len());
209    if use_host_ops() {
210        unsafe { sesh_vec_mul_host(dst.as_mut_ptr(), a.as_ptr(), b.as_ptr(), len as u32) }
211    } else {
212        for i in 0..len {
213            dst[i] = a[i] * b[i];
214        }
215    }
216}
217
218/// Multiply every element by scalar: `dst[i] *= value`.
219pub fn vec_mul_scalar(dst: &mut [f32], value: f32) {
220    let len = dst.len();
221    if use_host_ops() {
222        unsafe { sesh_vec_mul_scalar_host(dst.as_mut_ptr(), value, len as u32) }
223    } else {
224        for s in dst.iter_mut() {
225            *s *= value;
226        }
227    }
228}
229
230/// Multiply and accumulate: `dst[i] += src[i] * gain`.
231pub fn vec_mul_add(dst: &mut [f32], src: &[f32], gain: f32) {
232    let len = dst.len().min(src.len());
233    if use_host_ops() {
234        unsafe { sesh_vec_mul_add_host(dst.as_mut_ptr(), src.as_ptr(), gain, len as u32) }
235    } else {
236        for i in 0..len {
237            dst[i] += src[i] * gain;
238        }
239    }
240}
241
242/// Clamp: `dst[i] = clamp(src[i], min, max)`.
243pub fn vec_clamp(dst: &mut [f32], src: &[f32], min: f32, max: f32) {
244    let len = dst.len().min(src.len());
245    if use_host_ops() {
246        unsafe { sesh_vec_clamp_host(dst.as_mut_ptr(), src.as_ptr(), min, max, len as u32) }
247    } else {
248        for i in 0..len {
249            dst[i] = src[i].clamp(min, max);
250        }
251    }
252}
253
254/// In-place clamp: `dst[i] = clamp(dst[i], min, max)`.
255pub fn vec_clamp_assign(dst: &mut [f32], min: f32, max: f32) {
256    let len = dst.len();
257    if use_host_ops() {
258        unsafe { sesh_vec_clamp_host(dst.as_mut_ptr(), dst.as_ptr(), min, max, len as u32) }
259    } else {
260        for i in 0..len {
261            dst[i] = dst[i].clamp(min, max);
262        }
263    }
264}
265
266// ===========================================================================
267// Circular buffer ops
268// ===========================================================================
269
270/// Write `src` into circular buffer `buf` starting at `*pos`, wrapping at `buf.len()`.
271/// Advances `*pos` by `src.len()`.
272pub fn vec_ring_write(buf: &mut [f32], pos: &mut usize, src: &[f32]) {
273    let buf_len = buf.len();
274    let frames = src.len();
275    if use_host_ops() {
276        let mut pos32 = *pos as u32;
277        unsafe {
278            sesh_vec_ring_write_host(
279                buf.as_mut_ptr(), buf_len as u32, &mut pos32, src.as_ptr(), frames as u32,
280            );
281        }
282        *pos = pos32 as usize;
283    } else {
284        for i in 0..frames {
285            buf[(*pos + i) % buf_len] = src[i];
286        }
287        *pos = (*pos + frames) % buf_len;
288    }
289}
290
291/// Read `dst.len()` contiguous samples from circular buffer at `pos - offset`, wrapping.
292pub fn vec_ring_read(buf: &[f32], pos: usize, dst: &mut [f32], offset: usize) {
293    let buf_len = buf.len();
294    let frames = dst.len();
295    if use_host_ops() {
296        unsafe {
297            sesh_vec_ring_read_host(
298                buf.as_ptr(), buf_len as u32, pos as u32,
299                dst.as_mut_ptr(), offset as u32, frames as u32,
300            );
301        }
302    } else {
303        let start = (pos + buf_len - offset) % buf_len;
304        for i in 0..frames {
305            dst[i] = buf[(start + i) % buf_len];
306        }
307    }
308}
309
310// ===========================================================================
311// Delay op
312// ===========================================================================
313
314/// Per-sample modulated delay read with linear interpolation.
315///
316/// For each sample `i`, reads from circular buffer at a fractional offset
317/// `time[i]` samples behind where the write head was at sample `i`.
318/// `pos` should be the write head position *after* the most recent `vec_ring_write`.
319pub fn vec_delay_read(buf: &[f32], pos: usize, dst: &mut [f32], time: &[f32]) {
320    let buf_len = buf.len();
321    let frames = dst.len().min(time.len());
322    if use_host_ops() {
323        unsafe {
324            sesh_vec_delay_read_host(
325                buf.as_ptr(), buf_len as u32, pos as u32,
326                dst.as_mut_ptr(), time.as_ptr(), frames as u32,
327            );
328        }
329    } else {
330        for i in 0..frames {
331            // The write head was at (pos - frames + i) when sample i was written.
332            let write_pos_at_i = (pos + buf_len - frames + i) % buf_len;
333
334            let delay_int = time[i] as usize;
335            let delay_frac = time[i] - delay_int as f32;
336
337            let idx1 = (write_pos_at_i + buf_len - delay_int) % buf_len;
338            let idx2 = (idx1 + buf_len - 1) % buf_len;
339
340            dst[i] = buf[idx1] + delay_frac * (buf[idx2] - buf[idx1]);
341        }
342    }
343}
344
345// ===========================================================================
346// Schroeder allpass diffuser
347// ===========================================================================
348
349/// Schroeder allpass filter operating on a circular buffer.
350///
351/// Unity-gain allpass: smears transients without changing frequency balance.
352/// Used in series for reverb diffusion. Each allpass needs its own buffer and
353/// write position (like a delay line).
354///
355/// `g` is the allpass coefficient (typically 0.5–0.7). `delay` is in samples.
356pub fn vec_schroeder_allpass(
357    buf: &mut [f32],
358    pos: &mut usize,
359    dst: &mut [f32],
360    src: &[f32],
361    delay: usize,
362    g: f32,
363) {
364    let buf_len = buf.len();
365    let frames = dst.len().min(src.len());
366    if use_host_ops() {
367        let mut pos32 = *pos as u32;
368        unsafe {
369            sesh_vec_schroeder_allpass_host(
370                buf.as_mut_ptr(), buf_len as u32, &mut pos32,
371                dst.as_mut_ptr(), src.as_ptr(),
372                delay as u32, g, frames as u32,
373            );
374        }
375        *pos = pos32 as usize;
376    } else {
377        let mut wp = *pos;
378        for i in 0..frames {
379            let read_idx = (wp + buf_len - delay) % buf_len;
380            let buf_out = buf[read_idx];
381
382            let v = src[i] + g * buf_out;
383            dst[i] = buf_out - g * v;
384
385            buf[wp] = v;
386            wp = (wp + 1) % buf_len;
387        }
388        *pos = wp;
389    }
390}
391
392// ===========================================================================
393// Oscillator
394// ===========================================================================
395
396/// Fill `dst` with oscillator output. Advances `*phase`. `freq` is in Hz.
397pub fn vec_osc(
398    phase: &mut f32,
399    dst: &mut [f32],
400    freq: f32,
401    waveform: Waveform,
402    sample_rate: f32,
403) {
404    let frames = dst.len();
405    if use_host_ops() {
406        unsafe {
407            sesh_vec_osc_host(
408                phase as *mut f32, dst.as_mut_ptr(),
409                freq, waveform as u32, sample_rate, frames as u32,
410            );
411        }
412    } else {
413        let phase_inc = freq / sample_rate;
414        for i in 0..frames {
415            dst[i] = match waveform {
416                Waveform::Sine => (*phase * std::f32::consts::TAU).sin(),
417                Waveform::Triangle => 4.0 * (*phase - (*phase + 0.5).floor()).abs() - 1.0,
418                Waveform::Saw => 2.0 * (*phase - (*phase + 0.5).floor()),
419                Waveform::Square => if *phase % 1.0 < 0.5 { 1.0 } else { -1.0 },
420            };
421            *phase += phase_inc;
422            if *phase >= 1.0 {
423                *phase -= 1.0;
424            }
425        }
426    }
427}
428
429// ===========================================================================
430// Filter
431// ===========================================================================
432
433/// Biquad filter with per-sample modulation of cutoff, Q, and gain.
434///
435/// `cutoff` is in Hz, `q` is the Q factor, `gain` is in dB (used for Peak/Shelf types).
436/// Coefficients are recomputed each sample from the parameter buffers.
437pub fn vec_biquad(
438    state: &mut BiquadState,
439    dst: &mut [f32],
440    src: &[f32],
441    cutoff: &[f32],
442    q: &[f32],
443    gain: &[f32],
444    filter_type: FilterType,
445    sample_rate: f32,
446) {
447    let frames = dst.len().min(src.len()).min(cutoff.len()).min(q.len()).min(gain.len());
448    if use_host_ops() {
449        unsafe {
450            sesh_vec_biquad_host(
451                state as *mut BiquadState as *mut f32,
452                dst.as_mut_ptr(), src.as_ptr(),
453                cutoff.as_ptr(), q.as_ptr(), gain.as_ptr(),
454                filter_type as u32, sample_rate, frames as u32,
455            );
456        }
457    } else {
458        for i in 0..frames {
459            let w0 = std::f32::consts::TAU * cutoff[i] / sample_rate;
460            let cos_w0 = w0.cos();
461            let sin_w0 = w0.sin();
462            let alpha = sin_w0 / (2.0 * q[i]);
463            let a_db = gain[i];
464            let a_lin = 10.0f32.powf(a_db / 40.0);
465
466            let (b0, b1, b2, a0, a1, a2) = match filter_type {
467                FilterType::Lowpass => {
468                    let b1 = 1.0 - cos_w0;
469                    let b0 = b1 / 2.0;
470                    (b0, b1, b0, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
471                }
472                FilterType::Highpass => {
473                    let b1 = -(1.0 + cos_w0);
474                    let b0 = (1.0 + cos_w0) / 2.0;
475                    (b0, b1, b0, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
476                }
477                FilterType::Bandpass => {
478                    (alpha, 0.0, -alpha, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
479                }
480                FilterType::Notch => {
481                    (1.0, -2.0 * cos_w0, 1.0, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
482                }
483                FilterType::Peak => {
484                    (
485                        1.0 + alpha * a_lin,
486                        -2.0 * cos_w0,
487                        1.0 - alpha * a_lin,
488                        1.0 + alpha / a_lin,
489                        -2.0 * cos_w0,
490                        1.0 - alpha / a_lin,
491                    )
492                }
493                FilterType::LowShelf => {
494                    let two_sqrt_a_alpha = 2.0 * a_lin.sqrt() * alpha;
495                    (
496                        a_lin * ((a_lin + 1.0) - (a_lin - 1.0) * cos_w0 + two_sqrt_a_alpha),
497                        2.0 * a_lin * ((a_lin - 1.0) - (a_lin + 1.0) * cos_w0),
498                        a_lin * ((a_lin + 1.0) - (a_lin - 1.0) * cos_w0 - two_sqrt_a_alpha),
499                        (a_lin + 1.0) + (a_lin - 1.0) * cos_w0 + two_sqrt_a_alpha,
500                        -2.0 * ((a_lin - 1.0) + (a_lin + 1.0) * cos_w0),
501                        (a_lin + 1.0) + (a_lin - 1.0) * cos_w0 - two_sqrt_a_alpha,
502                    )
503                }
504                FilterType::HighShelf => {
505                    let two_sqrt_a_alpha = 2.0 * a_lin.sqrt() * alpha;
506                    (
507                        a_lin * ((a_lin + 1.0) + (a_lin - 1.0) * cos_w0 + two_sqrt_a_alpha),
508                        -2.0 * a_lin * ((a_lin - 1.0) + (a_lin + 1.0) * cos_w0),
509                        a_lin * ((a_lin + 1.0) + (a_lin - 1.0) * cos_w0 - two_sqrt_a_alpha),
510                        (a_lin + 1.0) - (a_lin - 1.0) * cos_w0 + two_sqrt_a_alpha,
511                        2.0 * ((a_lin - 1.0) - (a_lin + 1.0) * cos_w0),
512                        (a_lin + 1.0) - (a_lin - 1.0) * cos_w0 - two_sqrt_a_alpha,
513                    )
514                }
515                FilterType::Allpass => {
516                    (1.0 - alpha, -2.0 * cos_w0, 1.0 + alpha, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
517                }
518            };
519
520            // Normalize coefficients.
521            let b0 = b0 / a0;
522            let b1 = b1 / a0;
523            let b2 = b2 / a0;
524            let a1 = a1 / a0;
525            let a2 = a2 / a0;
526
527            let x0 = src[i];
528            let y0 = b0 * x0 + b1 * state.x1 + b2 * state.x2
529                - a1 * state.y1 - a2 * state.y2;
530
531            state.x2 = state.x1;
532            state.x1 = x0;
533            state.y2 = state.y1;
534            state.y1 = y0;
535
536            dst[i] = y0;
537        }
538    }
539}
540
541// ===========================================================================
542// Dynamics
543// ===========================================================================
544
545/// Envelope follower. Tracks amplitude of `src` with attack/release smoothing.
546///
547/// `attack` and `release` are in seconds (per-sample buffers for modulation).
548/// Output in `dst` is the smoothed envelope value.
549pub fn vec_envelope(
550    state: &mut EnvelopeState,
551    dst: &mut [f32],
552    src: &[f32],
553    attack: &[f32],
554    release: &[f32],
555    mode: EnvelopeMode,
556    sample_rate: f32,
557) {
558    let frames = dst.len().min(src.len()).min(attack.len()).min(release.len());
559    if use_host_ops() {
560        unsafe {
561            sesh_vec_envelope_host(
562                state as *mut EnvelopeState as *mut f32,
563                dst.as_mut_ptr(), src.as_ptr(),
564                attack.as_ptr(), release.as_ptr(),
565                mode as u32, sample_rate, frames as u32,
566            );
567        }
568    } else {
569        for i in 0..frames {
570            let input_level = match mode {
571                EnvelopeMode::Peak => src[i].abs(),
572                EnvelopeMode::Rms => src[i] * src[i],
573            };
574
575            let att_coeff = (-1.0 / (attack[i] * sample_rate)).exp();
576            let rel_coeff = (-1.0 / (release[i] * sample_rate)).exp();
577
578            let coeff = if input_level > state.current { att_coeff } else { rel_coeff };
579            state.current = coeff * state.current + (1.0 - coeff) * input_level;
580
581            dst[i] = match mode {
582                EnvelopeMode::Peak => state.current,
583                EnvelopeMode::Rms => state.current.sqrt(),
584            };
585        }
586    }
587}
588
589// ===========================================================================
590// Waveshaping
591// ===========================================================================
592
593/// Soft saturation: `dst[i] = tanh(src[i] * drive[i])`.
594pub fn vec_tanh(dst: &mut [f32], src: &[f32], drive: &[f32]) {
595    let len = dst.len().min(src.len()).min(drive.len());
596    if use_host_ops() {
597        unsafe { sesh_vec_tanh_host(dst.as_mut_ptr(), src.as_ptr(), drive.as_ptr(), len as u32) }
598    } else {
599        for i in 0..len {
600            dst[i] = (src[i] * drive[i]).tanh();
601        }
602    }
603}
604
605/// Hard clipping: clamp `src` to `±threshold[i]`.
606pub fn vec_hard_clip(dst: &mut [f32], src: &[f32], threshold: &[f32]) {
607    let len = dst.len().min(src.len()).min(threshold.len());
608    if use_host_ops() {
609        unsafe {
610            sesh_vec_hard_clip_host(dst.as_mut_ptr(), src.as_ptr(), threshold.as_ptr(), len as u32)
611        }
612    } else {
613        for i in 0..len {
614            dst[i] = src[i].clamp(-threshold[i], threshold[i]);
615        }
616    }
617}
618
619// ===========================================================================
620// Unary / additional math ops
621// ===========================================================================
622
623/// Absolute value: `dst[i] = |src[i]|`.
624pub fn vec_abs(dst: &mut [f32], src: &[f32]) {
625    let len = dst.len().min(src.len());
626    if use_host_ops() {
627        unsafe { sesh_vec_abs_host(dst.as_mut_ptr(), src.as_ptr(), len as u32) }
628    } else {
629        for i in 0..len {
630            dst[i] = src[i].abs();
631        }
632    }
633}
634
635/// Negate: `dst[i] = -src[i]`. Phase inversion.
636pub fn vec_neg(dst: &mut [f32], src: &[f32]) {
637    let len = dst.len().min(src.len());
638    if use_host_ops() {
639        unsafe { sesh_vec_neg_host(dst.as_mut_ptr(), src.as_ptr(), len as u32) }
640    } else {
641        for i in 0..len {
642            dst[i] = -src[i];
643        }
644    }
645}
646
647/// Square root: `dst[i] = sqrt(src[i])`.
648pub fn vec_sqrt(dst: &mut [f32], src: &[f32]) {
649    let len = dst.len().min(src.len());
650    if use_host_ops() {
651        unsafe { sesh_vec_sqrt_host(dst.as_mut_ptr(), src.as_ptr(), len as u32) }
652    } else {
653        for i in 0..len {
654            dst[i] = src[i].sqrt();
655        }
656    }
657}
658
659/// Reciprocal: `dst[i] = 1.0 / src[i]`.
660pub fn vec_recip(dst: &mut [f32], src: &[f32]) {
661    let len = dst.len().min(src.len());
662    if use_host_ops() {
663        unsafe { sesh_vec_recip_host(dst.as_mut_ptr(), src.as_ptr(), len as u32) }
664    } else {
665        for i in 0..len {
666            dst[i] = 1.0 / src[i];
667        }
668    }
669}
670
671/// Element-wise division: `dst[i] = a[i] / b[i]`.
672pub fn vec_div(dst: &mut [f32], a: &[f32], b: &[f32]) {
673    let len = dst.len().min(a.len()).min(b.len());
674    if use_host_ops() {
675        unsafe { sesh_vec_div_host(dst.as_mut_ptr(), a.as_ptr(), b.as_ptr(), len as u32) }
676    } else {
677        for i in 0..len {
678            dst[i] = a[i] / b[i];
679        }
680    }
681}
682
683/// Element-wise power: `dst[i] = src[i].powf(exp[i])`.
684pub fn vec_pow(dst: &mut [f32], src: &[f32], exp: &[f32]) {
685    let len = dst.len().min(src.len()).min(exp.len());
686    if use_host_ops() {
687        unsafe { sesh_vec_pow_host(dst.as_mut_ptr(), src.as_ptr(), exp.as_ptr(), len as u32) }
688    } else {
689        for i in 0..len {
690            dst[i] = src[i].powf(exp[i]);
691        }
692    }
693}
694
695// ===========================================================================
696// In-place (_assign) variants
697// ===========================================================================
698//
699// These are Rust convenience wrappers that call the same host imports with
700// dst aliased as src. Raw pointer aliasing is fine — this is purely a Rust
701// borrow-checker workaround. No additional C/host API surface.
702
703/// In-place element-wise addition: `dst[i] += src[i]`.
704pub fn vec_add_assign(dst: &mut [f32], src: &[f32]) {
705    let len = dst.len().min(src.len());
706    if use_host_ops() {
707        unsafe { sesh_vec_add_host(dst.as_mut_ptr(), dst.as_ptr(), src.as_ptr(), len as u32) }
708    } else {
709        for i in 0..len {
710            dst[i] += src[i];
711        }
712    }
713}
714
715/// In-place element-wise multiplication: `dst[i] *= src[i]`.
716pub fn vec_mul_assign(dst: &mut [f32], src: &[f32]) {
717    let len = dst.len().min(src.len());
718    if use_host_ops() {
719        unsafe { sesh_vec_mul_host(dst.as_mut_ptr(), dst.as_ptr(), src.as_ptr(), len as u32) }
720    } else {
721        for i in 0..len {
722            dst[i] *= src[i];
723        }
724    }
725}
726
727/// In-place soft saturation: `dst[i] = tanh(dst[i] * drive[i])`.
728pub fn vec_tanh_assign(dst: &mut [f32], drive: &[f32]) {
729    let len = dst.len().min(drive.len());
730    if use_host_ops() {
731        unsafe { sesh_vec_tanh_host(dst.as_mut_ptr(), dst.as_ptr(), drive.as_ptr(), len as u32) }
732    } else {
733        for i in 0..len {
734            dst[i] = (dst[i] * drive[i]).tanh();
735        }
736    }
737}
738
739/// In-place hard clipping: clamp `dst` to `±threshold[i]`.
740pub fn vec_hard_clip_assign(dst: &mut [f32], threshold: &[f32]) {
741    let len = dst.len().min(threshold.len());
742    if use_host_ops() {
743        unsafe { sesh_vec_hard_clip_host(dst.as_mut_ptr(), dst.as_ptr(), threshold.as_ptr(), len as u32) }
744    } else {
745        for i in 0..len {
746            dst[i] = dst[i].clamp(-threshold[i], threshold[i]);
747        }
748    }
749}
750
751/// In-place absolute value: `dst[i] = |dst[i]|`.
752pub fn vec_abs_assign(dst: &mut [f32]) {
753    let len = dst.len();
754    if use_host_ops() {
755        unsafe { sesh_vec_abs_host(dst.as_mut_ptr(), dst.as_ptr(), len as u32) }
756    } else {
757        for i in 0..len {
758            dst[i] = dst[i].abs();
759        }
760    }
761}
762
763/// In-place negate: `dst[i] = -dst[i]`.
764pub fn vec_neg_assign(dst: &mut [f32]) {
765    let len = dst.len();
766    if use_host_ops() {
767        unsafe { sesh_vec_neg_host(dst.as_mut_ptr(), dst.as_ptr(), len as u32) }
768    } else {
769        for i in 0..len {
770            dst[i] = -dst[i];
771        }
772    }
773}
774
775/// In-place square root: `dst[i] = sqrt(dst[i])`.
776pub fn vec_sqrt_assign(dst: &mut [f32]) {
777    let len = dst.len();
778    if use_host_ops() {
779        unsafe { sesh_vec_sqrt_host(dst.as_mut_ptr(), dst.as_ptr(), len as u32) }
780    } else {
781        for i in 0..len {
782            dst[i] = dst[i].sqrt();
783        }
784    }
785}
786
787/// In-place reciprocal: `dst[i] = 1.0 / dst[i]`.
788pub fn vec_recip_assign(dst: &mut [f32]) {
789    let len = dst.len();
790    if use_host_ops() {
791        unsafe { sesh_vec_recip_host(dst.as_mut_ptr(), dst.as_ptr(), len as u32) }
792    } else {
793        for i in 0..len {
794            dst[i] = 1.0 / dst[i];
795        }
796    }
797}
798
799/// In-place element-wise division: `dst[i] /= src[i]`.
800pub fn vec_div_assign(dst: &mut [f32], src: &[f32]) {
801    let len = dst.len().min(src.len());
802    if use_host_ops() {
803        unsafe { sesh_vec_div_host(dst.as_mut_ptr(), dst.as_ptr(), src.as_ptr(), len as u32) }
804    } else {
805        for i in 0..len {
806            dst[i] /= src[i];
807        }
808    }
809}
810
811/// In-place element-wise power: `dst[i] = dst[i].powf(exp[i])`.
812pub fn vec_pow_assign(dst: &mut [f32], exp: &[f32]) {
813    let len = dst.len().min(exp.len());
814    if use_host_ops() {
815        unsafe { sesh_vec_pow_host(dst.as_mut_ptr(), dst.as_ptr(), exp.as_ptr(), len as u32) }
816    } else {
817        for i in 0..len {
818            dst[i] = dst[i].powf(exp[i]);
819        }
820    }
821}