550c98f275
Reorganización física de crates/: - core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/ - shared/ (3 crates) se redistribuye en protocol/ e init/ - lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/ Renames de proyectos: - shipote → shuma (runtime de sandboxes) - nouser → akasha (explorador de Mónadas) - yahweh → nahual (motor GPUI, antes ui_engine/) - lapaloma → pineal (data-viz agnóstica) Fraccionamiento UI → core agnóstico: - vista-core (DeckState + snap, 175 LOC, 5 tests verdes) - barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes) - vista-web y barra-web ahora son thin DOM bindings Documentación nueva: - 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat + 10 módulos + apps/ - docs/STATUS.md con cifras reales por proyecto - docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas) - CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets) Automatización: - scripts/reorg.py — script idempotente que: git mv directorios, renombra package names, recomputa path = refs, reescribe imports rust, actualiza workspace Cargo.toml. Soporta --dry-run. - scripts/split-changelog.py — particiona CHANGELOG por componente. Validación: - cargo check --workspace pasa (124 crates + 2 nuevos cores). - 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
217 lines
6.6 KiB
Rust
217 lines
6.6 KiB
Rust
//! `RingBuffer` — buffer circular de samples para streaming tipo
|
|
//! osciloscopio.
|
|
//!
|
|
//! Capacidad fija. `push(v)` hace dos writes (uno a `values`, uno
|
|
//! a `coords[head*2+1]`) y un increment de head + revision. El
|
|
//! buffer **nunca se reasigna**; el painter consume slices del
|
|
//! mismo backing memory frame tras frame.
|
|
//!
|
|
//! Convención: `x_norm` se pre-computa una vez en construcción
|
|
//! (modo sweep). El painter aplica el escalado a píxeles via su
|
|
//! propio transform — el buffer no rota X entre frames.
|
|
//!
|
|
//! ## Trampa del pre-fill (1.0.2 fix del Flutter)
|
|
//!
|
|
//! Antes que `count >= capacity`, los slots `[head, capacity)`
|
|
//! contienen ceros iniciales. Si el painter dibuja toda la
|
|
//! ringa, aparece una línea plana sobre la mitad derecha. La
|
|
//! API expone [`RingBuffer::filled_len`] que devuelve `head` en
|
|
//! ese caso, y `capacity` después — el painter clipea a eso.
|
|
|
|
/// Ring buffer en modo sweep (x_norm de cada slot es fijo).
|
|
///
|
|
/// Para modo scroll el painter aplica un translate adicional por
|
|
/// frame; la estructura de datos es la misma.
|
|
#[derive(Debug, Clone)]
|
|
pub struct RingBuffer {
|
|
/// Sample raw por slot.
|
|
values: Vec<f32>,
|
|
/// `[x_norm, y_value]` por slot. `x_norm = slot / (cap - 1)`,
|
|
/// fijo. `y_value` = `values[slot]`.
|
|
coords: Vec<f32>,
|
|
capacity: usize,
|
|
/// Próximo slot a escribir.
|
|
head: usize,
|
|
/// Monotonic, sobrevive wraparound. Útil para anclar
|
|
/// anotaciones por sample index absoluto.
|
|
count: u64,
|
|
revision: u64,
|
|
}
|
|
|
|
impl RingBuffer {
|
|
/// Asume `capacity >= 2` para que `x_norm` no divida por cero.
|
|
pub fn new(capacity: usize) -> Self {
|
|
assert!(capacity >= 2, "RingBuffer requiere capacity >= 2");
|
|
let mut coords = vec![0.0; capacity * 2];
|
|
let denom = (capacity - 1) as f32;
|
|
for slot in 0..capacity {
|
|
coords[slot * 2] = slot as f32 / denom;
|
|
}
|
|
Self {
|
|
values: vec![0.0; capacity],
|
|
coords,
|
|
capacity,
|
|
head: 0,
|
|
count: 0,
|
|
revision: 0,
|
|
}
|
|
}
|
|
|
|
pub fn push(&mut self, v: f32) {
|
|
self.values[self.head] = v;
|
|
self.coords[self.head * 2 + 1] = v;
|
|
self.head = (self.head + 1) % self.capacity;
|
|
self.count = self.count.wrapping_add(1);
|
|
self.revision = self.revision.wrapping_add(1);
|
|
}
|
|
|
|
/// Inserción en batch con dos memcpys (cola + wrap-around).
|
|
/// Para batches > capacity se queda con los últimos `capacity`
|
|
/// samples (los anteriores se sobreescribirían igual).
|
|
pub fn push_all(&mut self, batch: &[f32]) {
|
|
if batch.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let cap = self.capacity;
|
|
let src = if batch.len() > cap {
|
|
&batch[batch.len() - cap..]
|
|
} else {
|
|
batch
|
|
};
|
|
|
|
let tail = cap - self.head;
|
|
if src.len() <= tail {
|
|
self.values[self.head..self.head + src.len()].copy_from_slice(src);
|
|
for (i, v) in src.iter().enumerate() {
|
|
self.coords[(self.head + i) * 2 + 1] = *v;
|
|
}
|
|
self.head = (self.head + src.len()) % cap;
|
|
} else {
|
|
let (a, b) = src.split_at(tail);
|
|
self.values[self.head..].copy_from_slice(a);
|
|
for (i, v) in a.iter().enumerate() {
|
|
self.coords[(self.head + i) * 2 + 1] = *v;
|
|
}
|
|
self.values[..b.len()].copy_from_slice(b);
|
|
for (i, v) in b.iter().enumerate() {
|
|
self.coords[i * 2 + 1] = *v;
|
|
}
|
|
self.head = b.len();
|
|
}
|
|
|
|
self.count = self.count.wrapping_add(src.len() as u64);
|
|
self.revision = self.revision.wrapping_add(1);
|
|
}
|
|
|
|
pub fn capacity(&self) -> usize {
|
|
self.capacity
|
|
}
|
|
|
|
pub fn head(&self) -> usize {
|
|
self.head
|
|
}
|
|
|
|
pub fn count(&self) -> u64 {
|
|
self.count
|
|
}
|
|
|
|
pub fn revision(&self) -> u64 {
|
|
self.revision
|
|
}
|
|
|
|
pub fn is_full(&self) -> bool {
|
|
self.count >= self.capacity as u64
|
|
}
|
|
|
|
/// Cantidad de slots con datos reales. Antes del fill es
|
|
/// `head`; después es `capacity`. El painter clipea a este
|
|
/// valor para evitar el flicker del pre-fill.
|
|
pub fn filled_len(&self) -> usize {
|
|
if self.is_full() {
|
|
self.capacity
|
|
} else {
|
|
self.head
|
|
}
|
|
}
|
|
|
|
/// Slice interleaved de `[x_norm, y]`. Para render en dos
|
|
/// segmentos: `&coords()[..head*2]` y `&coords()[head*2..]`
|
|
/// (cuando is_full).
|
|
pub fn coords(&self) -> &[f32] {
|
|
&self.coords
|
|
}
|
|
|
|
/// Slice plana de samples raw — útil para downsample envelope
|
|
/// min/max sin pasar por coords.
|
|
pub fn values(&self) -> &[f32] {
|
|
&self.values
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn x_norm_precomputado() {
|
|
let r = RingBuffer::new(4);
|
|
// x_norm en slots 0, 1, 2, 3 = 0.0, 1/3, 2/3, 1.0
|
|
assert!((r.coords()[0] - 0.0).abs() < 1e-6);
|
|
assert!((r.coords()[2] - 1.0 / 3.0).abs() < 1e-6);
|
|
assert!((r.coords()[6] - 1.0).abs() < 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn push_actualiza_y_no_x() {
|
|
let mut r = RingBuffer::new(4);
|
|
r.push(5.0);
|
|
r.push(7.0);
|
|
// slot 0 → y=5, slot 1 → y=7, x quedó igual
|
|
assert_eq!(r.coords()[1], 5.0);
|
|
assert_eq!(r.coords()[3], 7.0);
|
|
assert!((r.coords()[2] - 1.0 / 3.0).abs() < 1e-6);
|
|
assert_eq!(r.head(), 2);
|
|
assert_eq!(r.count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn filled_len_bloquea_prefill() {
|
|
let mut r = RingBuffer::new(4);
|
|
assert_eq!(r.filled_len(), 0);
|
|
r.push(1.0);
|
|
r.push(2.0);
|
|
assert_eq!(r.filled_len(), 2);
|
|
r.push(3.0);
|
|
r.push(4.0);
|
|
assert_eq!(r.filled_len(), 4);
|
|
r.push(5.0); // wrap
|
|
assert_eq!(r.filled_len(), 4);
|
|
assert!(r.is_full());
|
|
}
|
|
|
|
#[test]
|
|
fn push_all_wrap_around() {
|
|
let mut r = RingBuffer::new(4);
|
|
r.push_all(&[1.0, 2.0, 3.0]); // head=3
|
|
r.push_all(&[4.0, 5.0, 6.0]); // wrap: 4 en slot 3, 5 en slot 0, 6 en slot 1
|
|
assert_eq!(r.values()[3], 4.0);
|
|
assert_eq!(r.values()[0], 5.0);
|
|
assert_eq!(r.values()[1], 6.0);
|
|
assert_eq!(r.head(), 2);
|
|
assert_eq!(r.count(), 6);
|
|
}
|
|
|
|
#[test]
|
|
fn push_all_oversized_se_queda_con_la_cola() {
|
|
let mut r = RingBuffer::new(4);
|
|
r.push_all(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
|
|
// Sólo los últimos 4 importan: [6,7,8,9]
|
|
assert_eq!(r.values()[0], 6.0);
|
|
assert_eq!(r.values()[1], 7.0);
|
|
assert_eq!(r.values()[2], 8.0);
|
|
assert_eq!(r.values()[3], 9.0);
|
|
assert!(r.is_full());
|
|
}
|
|
}
|