Start native reservoir runtime

This commit is contained in:
Meta Vens 2026-05-19 07:36:00 +01:00
commit b4d50860df
9 changed files with 261 additions and 1 deletions

2
.gitignore vendored
View file

@ -9,3 +9,5 @@ calibration/runs/
tools/drivers/
tools/obs-spout2-plugin/
tools/src/
target/
native/**/target/

View file

@ -5,6 +5,7 @@
- Perfect Machine contract:
- `docs/perfect-machine.md` defines the target native machine: one five-second spatiotemporal reservoir, Aquarium-owned dense visual fusion/material/brush rendering, Faust-owned hot audio DSP, OBS-owned broadcast controls, and LocalCastBridge-owned calibration/config/status.
- `config/perfect-machine.example.json` declares the contract shape for six cameras, six microphones, reservoir rings, native authorities, outputs, and the demotion of bridge scripts to tooling.
- `native/reservoir` is the first native Rust crate. It implements shared-edge five-second reservoir rings and proves that all rings expire against the newest sample, not private per-source clocks.
- Repo-local persistence machinery.
- First architecture map.
- Example config for one video source plus two audio sources.
@ -74,7 +75,7 @@
## Next
1. Implement the native reservoir ABI declared by `config/perfect-machine.example.json`.
1. Add a C ABI and Aquarium-facing binding around `native/reservoir`.
2. Move camera ingest into native capture workers that append `camera_frame` samples to the reservoir.
3. Move feature extraction, flow, cross-view matching, LOD reconciliation, and material fitting into Aquarium GPU compute.
4. Move mic alignment, voice separation, room suppression, Ambisonic/HOA spatialization, and stem generation into Faust/native DSP.

View file

@ -147,3 +147,16 @@ Keep these as tooling:
6. Let Faust consume reservoir audio blocks and source controls directly.
7. Keep the old bridge only as a comparison and fallback surface.
## Native Work Started
`native/reservoir` is the first native crate. It implements the shared-edge
five-second reservoir invariant:
- append samples into named rings;
- advance the reservoir edge from the newest sample;
- evict every ring against the same edge;
- query the latest valid sample per sensor.
This is intentionally below Aquarium and Faust. The crate owns retention and
sample-window semantics; Aquarium owns GPU interpretation and rendering, Faust
owns DSP interpretation.

13
native/README.md Normal file
View file

@ -0,0 +1,13 @@
# Native Runtime
This directory is for the hot path.
LocalCastBridge Python remains useful for calibration, launch, status, contract
tests, and offline analysis. It does not own dense live fusion or DSP.
## Crates
- `reservoir`: the first native five-second spatiotemporal reservoir core. It
owns shared-edge retention and typed sample rings. Aquarium/Faust integration
should build on this invariant instead of copying the deadline bridge cache.

7
native/reservoir/Cargo.lock generated Normal file
View file

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "localcast-reservoir"
version = "0.1.0"

View file

@ -0,0 +1,10 @@
[package]
name = "localcast-reservoir"
version = "0.1.0"
edition = "2024"
description = "Native five-second spatiotemporal reservoir core for LocalCastBridge"
[lib]
crate-type = ["rlib"]
[dependencies]

211
native/reservoir/src/lib.rs Normal file
View file

@ -0,0 +1,211 @@
use std::collections::{BTreeMap, VecDeque};
pub const DEFAULT_RESERVOIR_NS: u64 = 5_000_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum RingKind {
CameraFrame,
CameraFeature,
SceneRay,
SurfaceClaim,
MaterialClaim,
AudioBlock,
PhaseClaim,
EventClaim,
RenderPacket,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Sample<T> {
pub sensor_id: String,
pub timestamp_ns: u64,
pub arrival_ns: u64,
pub sequence: u64,
pub payload: T,
}
impl<T> Sample<T> {
pub fn new(
sensor_id: impl Into<String>,
timestamp_ns: u64,
arrival_ns: u64,
sequence: u64,
payload: T,
) -> Self {
Self {
sensor_id: sensor_id.into(),
timestamp_ns,
arrival_ns,
sequence,
payload,
}
}
}
#[derive(Debug, Clone)]
pub struct ReservoirRing<T> {
duration_ns: u64,
edge_ns: u64,
samples: VecDeque<Sample<T>>,
}
impl<T> ReservoirRing<T> {
pub fn new(duration_ns: u64) -> Self {
Self {
duration_ns,
edge_ns: 0,
samples: VecDeque::new(),
}
}
pub fn push(&mut self, sample: Sample<T>) {
self.edge_ns = self.edge_ns.max(sample.timestamp_ns);
self.samples.push_back(sample);
self.evict_expired();
}
pub fn set_edge(&mut self, edge_ns: u64) {
self.edge_ns = self.edge_ns.max(edge_ns);
self.evict_expired();
}
pub fn edge_ns(&self) -> u64 {
self.edge_ns
}
pub fn window_start_ns(&self) -> u64 {
self.edge_ns.saturating_sub(self.duration_ns)
}
pub fn len(&self) -> usize {
self.samples.len()
}
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Sample<T>> {
self.samples.iter()
}
pub fn latest_for_sensor(&self, sensor_id: &str) -> Option<&Sample<T>> {
self.samples
.iter()
.rev()
.find(|sample| sample.sensor_id == sensor_id)
}
fn evict_expired(&mut self) {
let start_ns = self.window_start_ns();
self.samples
.retain(|sample| sample.timestamp_ns >= start_ns && sample.timestamp_ns <= self.edge_ns);
}
}
#[derive(Debug, Clone)]
pub struct Reservoir<T> {
duration_ns: u64,
edge_ns: u64,
rings: BTreeMap<RingKind, ReservoirRing<T>>,
}
impl<T> Reservoir<T> {
pub fn new(duration_ns: u64) -> Self {
Self {
duration_ns,
edge_ns: 0,
rings: BTreeMap::new(),
}
}
pub fn push(&mut self, kind: RingKind, sample: Sample<T>) {
self.edge_ns = self.edge_ns.max(sample.timestamp_ns);
for ring in self.rings.values_mut() {
ring.set_edge(self.edge_ns);
}
let ring = self
.rings
.entry(kind)
.or_insert_with(|| ReservoirRing::new(self.duration_ns));
ring.push(sample);
ring.set_edge(self.edge_ns);
}
pub fn set_edge(&mut self, edge_ns: u64) {
self.edge_ns = self.edge_ns.max(edge_ns);
for ring in self.rings.values_mut() {
ring.set_edge(self.edge_ns);
}
}
pub fn ring(&self, kind: RingKind) -> Option<&ReservoirRing<T>> {
self.rings.get(&kind)
}
pub fn edge_ns(&self) -> u64 {
self.edge_ns
}
pub fn window_start_ns(&self) -> u64 {
self.edge_ns.saturating_sub(self.duration_ns)
}
}
impl<T> Default for Reservoir<T> {
fn default() -> Self {
Self::new(DEFAULT_RESERVOIR_NS)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ring_expires_from_newest_shared_edge() {
let mut ring = ReservoirRing::new(DEFAULT_RESERVOIR_NS);
ring.push(Sample::new("cam-a", 1_000_000_000, 1_000_000_010, 0, 1));
ring.push(Sample::new("cam-b", 4_000_000_000, 4_000_000_010, 0, 2));
ring.push(Sample::new("cam-a", 7_100_000_000, 7_100_000_010, 1, 3));
let timestamps = ring
.iter()
.map(|sample| sample.timestamp_ns)
.collect::<Vec<_>>();
assert_eq!(vec![4_000_000_000, 7_100_000_000], timestamps);
assert_eq!(2_100_000_000, ring.window_start_ns());
}
#[test]
fn reservoir_edge_expires_all_rings() {
let mut reservoir = Reservoir::default();
reservoir.push(
RingKind::CameraFrame,
Sample::new("cam-a", 1_000_000_000, 1_000_000_010, 0, "old-camera"),
);
reservoir.push(
RingKind::AudioBlock,
Sample::new("audio", 7_000_000_001, 7_000_000_020, 0, "audio-edge"),
);
let camera_ring = reservoir.ring(RingKind::CameraFrame).unwrap();
assert!(camera_ring.is_empty());
assert_eq!(2_000_000_001, reservoir.window_start_ns());
}
#[test]
fn latest_for_sensor_ignores_other_sources() {
let mut ring = ReservoirRing::new(DEFAULT_RESERVOIR_NS);
ring.push(Sample::new("cam-a", 10, 11, 0, "a0"));
ring.push(Sample::new("cam-b", 12, 13, 0, "b0"));
ring.push(Sample::new("cam-a", 14, 15, 1, "a1"));
let latest = ring.latest_for_sensor("cam-a").unwrap();
assert_eq!("a1", latest.payload);
assert_eq!(1, latest.sequence);
}
}

View file

@ -27,6 +27,7 @@ Ownership:
- Faust owns hot audio DSP and program stem generation.
- OBS owns broadcast controls.
- Python remains tooling, not the stream hot path.
- `native/reservoir` is the first native crate. It owns shared-edge five-second sample retention for typed rings and is the seed of the Aquarium/Faust runtime boundary.
Invariant: the five-second spatiotemporal reservoir is the live authority. Producers append, optimizers refine, Aquarium/Faust sample. No private history outlives the reservoir.

View file

@ -33,6 +33,8 @@ invariants:
- Local secrets remain untracked.
accepted_design:
- `docs/perfect-machine.md` and `config/perfect-machine.example.json` define the native target: one five-second spatiotemporal reservoir, Aquarium GPU fusion/rendering, Faust DSP, OBS output controls, and LocalCastBridge tooling.
- `native/reservoir` is the first native Rust crate. It implements shared-edge reservoir rings so all sample types expire from the newest live sample rather than private source histories.
- `config/localcast.example.json` defines receiver address, base port, video encode settings, and audio sources.
- `scripts/sender-discover.ps1` lists FFmpeg devices, protocols, and encoders.
- `scripts/sender-start.ps1` launches one FFmpeg process per enabled source.