From caec14cbf2afb86a3f13ed6242ade73eb986ec19 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 19 May 2026 10:20:15 +0100 Subject: [PATCH] Expose runtime rolling window reads --- docs/implementation-plan.md | 4 +- docs/perfect-machine.md | 6 +- native/README.md | 4 +- .../reservoir/include/localcast_reservoir.h | 23 +++ native/reservoir/src/lib.rs | 172 ++++++++++++++++++ state/evidence.jsonl | 1 + tests/test_perfect_machine_contract.py | 5 + 7 files changed, 211 insertions(+), 4 deletions(-) diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index b099505..c31af4e 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -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. diff --git a/docs/perfect-machine.md b/docs/perfect-machine.md index b014157..c6e9e3e 100644 --- a/docs/perfect-machine.md +++ b/docs/perfect-machine.md @@ -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 diff --git a/native/README.md b/native/README.md index a6c5da2..115dc05 100644 --- a/native/README.md +++ b/native/README.md @@ -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 diff --git a/native/reservoir/include/localcast_reservoir.h b/native/reservoir/include/localcast_reservoir.h index b61f39f..0987ccf 100644 --- a/native/reservoir/include/localcast_reservoir.h +++ b/native/reservoir/include/localcast_reservoir.h @@ -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 } diff --git a/native/reservoir/src/lib.rs b/native/reservoir/src/lib.rs index 4d47c5e..7e006c7 100644 --- a/native/reservoir/src/lib.rs +++ b/native/reservoir/src/lib.rs @@ -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); } diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 8ae6f15..3390c05 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -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."} diff --git a/tests/test_perfect_machine_contract.py b/tests/test_perfect_machine_contract.py index da9189c..fefc98a 100644 --- a/tests/test_perfect_machine_contract.py +++ b/tests/test_perfect_machine_contract.py @@ -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)