Expose runtime rolling window reads

This commit is contained in:
Meta Vens 2026-05-19 10:20:15 +01:00
commit caec14cbf2
7 changed files with 211 additions and 4 deletions

View file

@ -26,8 +26,8 @@ shape removes independent per-kind storage from the live foundation.
edge/window queries, ring counts, and latest sample lookup by sensor hash.
The ABI now exposes total rolling-buffer length and typed-view length.
- `LocalcastRuntime` wraps the current reservoir with typed native producer
calls. It is the intended live spine, but must be rebuilt on the single
rolling-buffer invariant before new runtime work.
calls and total/typed read functions. It is the intended live spine for
Aquarium/Faust bindings.
- Repo-local persistence machinery.
- First architecture map.
- Example config for one video source plus two audio sources.

View file

@ -91,6 +91,8 @@ sample. No stage owns a private history that can outlive the reservoir.
typed producer calls for each sample kind so camera capture, audio capture,
phase estimation, material fitting, event detection, and render planning all
append into one shared five-second window without crossing a Python file cache.
It also exposes total and typed-view reads so Aquarium/Faust can consume that
same window without building private histories.
## Visual Fusion
@ -181,7 +183,9 @@ in `native/reservoir/include/localcast_reservoir.h`:
- create/destroy an opaque `LocalcastRuntime`;
- append typed camera/audio/phase/event/render handles through producer calls;
- query a native runtime status struct with shared edge, window start, total
count, and typed-view counts.
count, and typed-view counts;
- read runtime samples by total rolling-buffer index, typed-view index, and
latest sensor hash.
The ABI carries `payload_handle`, not payload bytes. Aquarium owns image,
feature, surface, material, and GPU memory interpretation. Faust/native DSP owns

View file

@ -41,7 +41,9 @@ runtime path:
claims, and render packets;
- it exposes `LocalcastRuntimeStatus` so Aquarium/Faust can inspect the shared
edge, window start, total sample count, and typed-view counts without polling JSON or
MessagePack files.
MessagePack files;
- it exposes total and typed-view sample reads so Aquarium/Faust can consume the
same rolling window producers append to.
Payload handles are owned by the caller. Aquarium should treat them as handles
to GPU/native visual memory; Faust/native DSP should treat them as handles to

View file

@ -101,6 +101,29 @@ bool localcast_runtime_push_phase_claim(LocalcastRuntime *runtime, LocalcastSamp
bool localcast_runtime_push_event_claim(LocalcastRuntime *runtime, LocalcastSampleHandle sample);
bool localcast_runtime_push_render_packet(LocalcastRuntime *runtime, LocalcastSampleHandle sample);
bool localcast_runtime_status(const LocalcastRuntime *runtime, LocalcastRuntimeStatus *out_status);
size_t localcast_runtime_len(const LocalcastRuntime *runtime);
size_t localcast_runtime_view_len(const LocalcastRuntime *runtime, uint32_t sample_kind);
bool localcast_runtime_sample_at(
const LocalcastRuntime *runtime,
size_t index,
uint32_t *out_sample_kind,
LocalcastSampleHandle *out_sample
);
bool localcast_runtime_view_sample_at(
const LocalcastRuntime *runtime,
uint32_t sample_kind,
size_t index,
LocalcastSampleHandle *out_sample
);
bool localcast_runtime_latest_for_sensor(
const LocalcastRuntime *runtime,
uint32_t sample_kind,
uint64_t sensor_id_hash,
LocalcastSampleHandle *out_sample
);
#ifdef __cplusplus
}

View file

@ -31,6 +31,20 @@ impl SampleKind {
_ => None,
}
}
fn id(self) -> u32 {
match self {
Self::CameraFrame => 0,
Self::CameraFeature => 1,
Self::SceneRay => 2,
Self::SurfaceClaim => 3,
Self::MaterialClaim => 4,
Self::AudioBlock => 5,
Self::PhaseClaim => 6,
Self::EventClaim => 7,
Self::RenderPacket => 8,
}
}
}
#[derive(Debug, Clone, PartialEq)]
@ -520,6 +534,99 @@ pub extern "C" fn localcast_runtime_status(
true
}
#[unsafe(no_mangle)]
pub extern "C" fn localcast_runtime_len(ptr: *const LocalcastRuntime) -> usize {
let Some(runtime) = (unsafe { ptr.as_ref() }) else {
return 0;
};
runtime.reservoir.len()
}
#[unsafe(no_mangle)]
pub extern "C" fn localcast_runtime_view_len(
ptr: *const LocalcastRuntime,
sample_kind: u32,
) -> usize {
let Some(runtime) = (unsafe { ptr.as_ref() }) else {
return 0;
};
let Some(kind) = SampleKind::from_u32(sample_kind) else {
return 0;
};
runtime.reservoir.view_len(kind)
}
#[unsafe(no_mangle)]
pub extern "C" fn localcast_runtime_sample_at(
ptr: *const LocalcastRuntime,
index: usize,
out_sample_kind: *mut u32,
out_sample: *mut LocalcastSampleHandle,
) -> bool {
let Some(runtime) = (unsafe { ptr.as_ref() }) else {
return false;
};
let Some(kind_out) = (unsafe { out_sample_kind.as_mut() }) else {
return false;
};
let Some(sample_out) = (unsafe { out_sample.as_mut() }) else {
return false;
};
let Some(sample) = runtime.reservoir.sample_at(index) else {
return false;
};
*kind_out = sample.kind.id();
*sample_out = sample.payload;
true
}
#[unsafe(no_mangle)]
pub extern "C" fn localcast_runtime_view_sample_at(
ptr: *const LocalcastRuntime,
sample_kind: u32,
index: usize,
out_sample: *mut LocalcastSampleHandle,
) -> bool {
let Some(runtime) = (unsafe { ptr.as_ref() }) else {
return false;
};
let Some(kind) = SampleKind::from_u32(sample_kind) else {
return false;
};
let Some(sample_out) = (unsafe { out_sample.as_mut() }) else {
return false;
};
let Some(sample) = runtime.reservoir.view_sample_at(kind, index) else {
return false;
};
*sample_out = sample.payload;
true
}
#[unsafe(no_mangle)]
pub extern "C" fn localcast_runtime_latest_for_sensor(
ptr: *const LocalcastRuntime,
sample_kind: u32,
sensor_id_hash: u64,
out_sample: *mut LocalcastSampleHandle,
) -> bool {
let Some(runtime) = (unsafe { ptr.as_ref() }) else {
return false;
};
let Some(kind) = SampleKind::from_u32(sample_kind) else {
return false;
};
let Some(sample_out) = (unsafe { out_sample.as_mut() }) else {
return false;
};
let sensor_id = sensor_id_hash.to_string();
let Some(sample) = runtime.reservoir.latest_for_sensor(kind, &sensor_id) else {
return false;
};
*sample_out = sample.payload;
true
}
fn localcast_runtime_push_typed(
ptr: *mut LocalcastRuntime,
kind: SampleKind,
@ -899,6 +1006,51 @@ mod tests {
localcast_runtime_destroy(runtime);
}
#[test]
fn runtime_spine_exposes_total_and_typed_window_reads() {
let runtime = localcast_runtime_create(DEFAULT_RESERVOIR_NS);
assert!(localcast_runtime_push_camera_frame(
runtime,
handle(10, 10, 11, 1, 100),
));
assert!(localcast_runtime_push_audio_block(
runtime,
handle(20, 12, 13, 2, 200),
));
assert!(localcast_runtime_push_camera_frame(
runtime,
handle(10, 14, 15, 3, 300),
));
assert_eq!(3, localcast_runtime_len(runtime));
assert_eq!(2, localcast_runtime_view_len(runtime, 0));
assert_eq!(1, localcast_runtime_view_len(runtime, 5));
let mut kind = 99;
let mut sample = handle(0, 0, 0, 0, 0);
assert!(localcast_runtime_sample_at(
runtime,
1,
&mut kind,
&mut sample,
));
assert_eq!(5, kind);
assert_eq!(200, sample.payload_handle);
assert!(localcast_runtime_view_sample_at(runtime, 0, 1, &mut sample,));
assert_eq!(300, sample.payload_handle);
assert!(localcast_runtime_latest_for_sensor(
runtime,
0,
10,
&mut sample,
));
assert_eq!(300, sample.payload_handle);
localcast_runtime_destroy(runtime);
}
#[test]
fn runtime_spine_rejects_nulls() {
let sample = handle(1, 2, 3, 4, 5);
@ -909,8 +1061,28 @@ mod tests {
sample,
));
assert!(!localcast_runtime_status(std::ptr::null(), &mut status));
assert_eq!(0, localcast_runtime_len(std::ptr::null()));
assert_eq!(0, localcast_runtime_view_len(std::ptr::null(), 0));
let runtime = localcast_runtime_create(0);
assert!(!localcast_runtime_status(runtime, std::ptr::null_mut()));
let mut kind = 0;
let mut out = sample;
assert!(!localcast_runtime_sample_at(
runtime,
0,
std::ptr::null_mut(),
&mut out,
));
assert!(!localcast_runtime_sample_at(
runtime,
0,
&mut kind,
std::ptr::null_mut(),
));
assert!(!localcast_runtime_view_sample_at(runtime, 99, 0, &mut out,));
assert!(!localcast_runtime_latest_for_sensor(
runtime, 99, 1, &mut out,
));
localcast_runtime_destroy(runtime);
}

View file

@ -65,3 +65,4 @@
{"ts": "2026-05-19T10:20:00+01:00", "type": "implementation", "status": "ok", "note": "Diagnostic render math split away from the OpenGL Spout publisher: localcast.diagnostics.render_math owns pure budget/projection/brush/rasterization code and tests import it directly, while spout_output is reduced to frame sources, status writers, and the disposable OpenGL/Spout shell."}
{"ts": "2026-05-19T10:35:00+01:00", "type": "implementation", "status": "ok", "note": "Deleted the Python/OpenGL Spout publisher and diagnostic_stream_spout launcher. The only surviving visual render code in LocalCastBridge is pure diagnostic render_math plus typed visual cache adapters; production OBS publication is now exclusively an Aquarium responsibility."}
{"ts": "2026-05-19T10:48:00+01:00", "type": "implementation", "status": "ok", "note": "Removed the Python visual producer from the scripts command surface by moving it to localcast.diagnostics.visual_producer and updating tests/docs to import it as diagnostic code. It remains a fossil to delete after native/Aquarium equivalents own the behavior."}
{"ts": "2026-05-19T11:02:00+01:00", "type": "implementation", "status": "ok", "note": "LocalcastRuntime now exposes total and typed-view window reads through the C ABI: len, view_len, sample_at, view_sample_at, and latest_for_sensor. Aquarium/Faust consumers can walk the same rolling buffer that native producers append to instead of building private caches."}

View file

@ -72,6 +72,11 @@ class PerfectMachineContractTests(unittest.TestCase):
"localcast_runtime_push_event_claim",
"localcast_runtime_push_render_packet",
"localcast_runtime_status",
"localcast_runtime_len",
"localcast_runtime_view_len",
"localcast_runtime_sample_at",
"localcast_runtime_view_sample_at",
"localcast_runtime_latest_for_sensor",
]:
self.assertIn(function, header)