Expose rolling reservoir view reads
This commit is contained in:
parent
94e76e09be
commit
d58d0bb097
6 changed files with 195 additions and 2 deletions
|
|
@ -174,6 +174,7 @@ in `native/reservoir/include/localcast_reservoir.h`:
|
|||
- push timestamped sample metadata into a typed view of the rolling buffer;
|
||||
- advance/query the shared edge and window start;
|
||||
- query total length and typed-view length;
|
||||
- read samples by total rolling-buffer index or typed-view index;
|
||||
- query the latest sample for a sensor hash.
|
||||
- create/destroy an opaque `LocalcastRuntime`;
|
||||
- append typed camera/audio/phase/event/render handles through producer calls;
|
||||
|
|
|
|||
|
|
@ -24,8 +24,9 @@ The reservoir ABI is intentionally small:
|
|||
camera frame, camera feature, scene ray, surface claim, material claim, audio
|
||||
block, phase claim, event claim, and render packet.
|
||||
- The reservoir can create/destroy, push, set/query edge, query window start,
|
||||
count the whole rolling buffer, count a typed view, and fetch the latest
|
||||
sample for a sensor hash inside a typed view.
|
||||
count the whole rolling buffer, count a typed view, read samples by total
|
||||
buffer index or typed-view index, and fetch the latest sample for a sensor
|
||||
hash inside a typed view.
|
||||
|
||||
## Runtime ABI
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,20 @@ uint64_t localcast_reservoir_window_start_ns(const LocalcastReservoir *reservoir
|
|||
size_t localcast_reservoir_len(const LocalcastReservoir *reservoir);
|
||||
size_t localcast_reservoir_view_len(const LocalcastReservoir *reservoir, uint32_t sample_kind);
|
||||
|
||||
bool localcast_reservoir_sample_at(
|
||||
const LocalcastReservoir *reservoir,
|
||||
size_t index,
|
||||
uint32_t *out_sample_kind,
|
||||
LocalcastSampleHandle *out_sample
|
||||
);
|
||||
|
||||
bool localcast_reservoir_view_sample_at(
|
||||
const LocalcastReservoir *reservoir,
|
||||
uint32_t sample_kind,
|
||||
size_t index,
|
||||
LocalcastSampleHandle *out_sample
|
||||
);
|
||||
|
||||
bool localcast_reservoir_latest_for_sensor(
|
||||
const LocalcastReservoir *reservoir,
|
||||
uint32_t sample_kind,
|
||||
|
|
|
|||
|
|
@ -113,6 +113,10 @@ impl<T> RollingReservoir<T> {
|
|||
self.samples.iter()
|
||||
}
|
||||
|
||||
pub fn sample_at(&self, index: usize) -> Option<&ReservoirSample<T>> {
|
||||
self.samples.get(index)
|
||||
}
|
||||
|
||||
pub fn view(&self, kind: SampleKind) -> impl Iterator<Item = &ReservoirSample<T>> {
|
||||
self.samples
|
||||
.iter()
|
||||
|
|
@ -123,6 +127,10 @@ impl<T> RollingReservoir<T> {
|
|||
self.view(kind).count()
|
||||
}
|
||||
|
||||
pub fn view_sample_at(&self, kind: SampleKind, index: usize) -> Option<&ReservoirSample<T>> {
|
||||
self.view(kind).nth(index)
|
||||
}
|
||||
|
||||
pub fn latest_for_sensor(
|
||||
&self,
|
||||
kind: SampleKind,
|
||||
|
|
@ -329,6 +337,53 @@ pub extern "C" fn localcast_reservoir_view_len(
|
|||
reservoir.inner.view_len(kind)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn localcast_reservoir_sample_at(
|
||||
ptr: *const LocalcastReservoir,
|
||||
index: usize,
|
||||
out_sample_kind: *mut u32,
|
||||
out_sample: *mut LocalcastSampleHandle,
|
||||
) -> bool {
|
||||
let Some(reservoir) = (unsafe { ptr.as_ref() }) else {
|
||||
return false;
|
||||
};
|
||||
let Some(out_kind) = (unsafe { out_sample_kind.as_mut() }) else {
|
||||
return false;
|
||||
};
|
||||
let Some(out) = (unsafe { out_sample.as_mut() }) else {
|
||||
return false;
|
||||
};
|
||||
let Some(sample) = reservoir.inner.sample_at(index) else {
|
||||
return false;
|
||||
};
|
||||
*out_kind = sample.kind as u32;
|
||||
*out = sample.payload;
|
||||
true
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn localcast_reservoir_view_sample_at(
|
||||
ptr: *const LocalcastReservoir,
|
||||
sample_kind: u32,
|
||||
index: usize,
|
||||
out_sample: *mut LocalcastSampleHandle,
|
||||
) -> bool {
|
||||
let Some(reservoir) = (unsafe { ptr.as_ref() }) else {
|
||||
return false;
|
||||
};
|
||||
let Some(kind) = SampleKind::from_u32(sample_kind) else {
|
||||
return false;
|
||||
};
|
||||
let Some(out) = (unsafe { out_sample.as_mut() }) else {
|
||||
return false;
|
||||
};
|
||||
let Some(sample) = reservoir.inner.view_sample_at(kind, index) else {
|
||||
return false;
|
||||
};
|
||||
*out = sample.payload;
|
||||
true
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn localcast_reservoir_latest_for_sensor(
|
||||
ptr: *const LocalcastReservoir,
|
||||
|
|
@ -539,6 +594,49 @@ mod tests {
|
|||
assert_eq!(3, reservoir.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexed_reads_expose_total_buffer_and_typed_views() {
|
||||
let mut reservoir = RollingReservoir::new(DEFAULT_RESERVOIR_NS);
|
||||
reservoir.push(ReservoirSample::new(
|
||||
SampleKind::CameraFrame,
|
||||
"cam-a",
|
||||
10,
|
||||
11,
|
||||
0,
|
||||
"frame-a",
|
||||
));
|
||||
reservoir.push(ReservoirSample::new(
|
||||
SampleKind::AudioBlock,
|
||||
"mic",
|
||||
12,
|
||||
13,
|
||||
0,
|
||||
"audio",
|
||||
));
|
||||
reservoir.push(ReservoirSample::new(
|
||||
SampleKind::CameraFrame,
|
||||
"cam-b",
|
||||
14,
|
||||
15,
|
||||
0,
|
||||
"frame-b",
|
||||
));
|
||||
|
||||
assert_eq!("audio", reservoir.sample_at(1).unwrap().payload);
|
||||
assert_eq!(
|
||||
"frame-b",
|
||||
reservoir
|
||||
.view_sample_at(SampleKind::CameraFrame, 1)
|
||||
.unwrap()
|
||||
.payload
|
||||
);
|
||||
assert!(
|
||||
reservoir
|
||||
.view_sample_at(SampleKind::PhaseClaim, 0)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_for_sensor_is_scoped_to_kind_and_sensor() {
|
||||
let mut reservoir = RollingReservoir::new(DEFAULT_RESERVOIR_NS);
|
||||
|
|
@ -635,6 +733,82 @@ mod tests {
|
|||
localcast_reservoir_destroy(reservoir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_abi_reads_total_buffer_and_view_samples_by_index() {
|
||||
let reservoir = localcast_reservoir_create(DEFAULT_RESERVOIR_NS);
|
||||
assert!(localcast_reservoir_push(
|
||||
reservoir,
|
||||
0,
|
||||
LocalcastSampleHandle {
|
||||
sensor_id_hash: 10,
|
||||
timestamp_ns: 10,
|
||||
arrival_ns: 11,
|
||||
sequence: 0,
|
||||
payload_handle: 100,
|
||||
},
|
||||
));
|
||||
assert!(localcast_reservoir_push(
|
||||
reservoir,
|
||||
5,
|
||||
LocalcastSampleHandle {
|
||||
sensor_id_hash: 20,
|
||||
timestamp_ns: 12,
|
||||
arrival_ns: 13,
|
||||
sequence: 0,
|
||||
payload_handle: 200,
|
||||
},
|
||||
));
|
||||
assert!(localcast_reservoir_push(
|
||||
reservoir,
|
||||
0,
|
||||
LocalcastSampleHandle {
|
||||
sensor_id_hash: 30,
|
||||
timestamp_ns: 14,
|
||||
arrival_ns: 15,
|
||||
sequence: 0,
|
||||
payload_handle: 300,
|
||||
},
|
||||
));
|
||||
|
||||
let mut kind = 99;
|
||||
let mut sample = LocalcastSampleHandle {
|
||||
sensor_id_hash: 0,
|
||||
timestamp_ns: 0,
|
||||
arrival_ns: 0,
|
||||
sequence: 0,
|
||||
payload_handle: 0,
|
||||
};
|
||||
|
||||
assert!(localcast_reservoir_sample_at(
|
||||
reservoir,
|
||||
1,
|
||||
&mut kind,
|
||||
&mut sample,
|
||||
));
|
||||
assert_eq!(5, kind);
|
||||
assert_eq!(200, sample.payload_handle);
|
||||
assert!(localcast_reservoir_view_sample_at(
|
||||
reservoir,
|
||||
0,
|
||||
1,
|
||||
&mut sample,
|
||||
));
|
||||
assert_eq!(300, sample.payload_handle);
|
||||
assert!(!localcast_reservoir_sample_at(
|
||||
reservoir,
|
||||
99,
|
||||
&mut kind,
|
||||
&mut sample,
|
||||
));
|
||||
assert!(!localcast_reservoir_view_sample_at(
|
||||
reservoir,
|
||||
99,
|
||||
0,
|
||||
&mut sample,
|
||||
));
|
||||
localcast_reservoir_destroy(reservoir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_abi_rejects_nulls_and_unknown_sample_kinds() {
|
||||
let sample = LocalcastSampleHandle {
|
||||
|
|
|
|||
|
|
@ -56,3 +56,4 @@
|
|||
{"ts": "2026-05-19T07:57:59+00:00", "type": "implementation", "status": "ok", "note": "OpenGL Spout sink quarantined: localcast.sensor_fusion no longer exports spout_output surfaces; the old SpoutGL renderer now lives under localcast.diagnostics.spout_output and scripts/stream_spout imports it as diagnostic/migration code only."}
|
||||
{"ts": "2026-05-19T08:01:04+00:00", "type": "implementation", "status": "ok", "note": "Python visual producer quarantined by name: scripts/live_sensor_fusion.py is now scripts/diagnostic_live_sensor_fusion.py, tests/docs import the diagnostic path, and future native/Aquarium work should delete or split its pure pieces rather than extend it."}
|
||||
{"ts": "2026-05-19T08:03:24+00:00", "type": "implementation", "status": "ok", "note": "Diagnostic Spout launcher renamed: scripts/stream_spout.py is now scripts/diagnostic_stream_spout.py, keeping the OpenGL bridge visibly outside the production path while Aquarium Spout publication remains the target."}
|
||||
{"ts": "2026-05-19T08:07:19+00:00", "type": "implementation", "status": "ok", "note": "Native reservoir ABI now exposes indexed reads over the total rolling buffer and typed views via localcast_reservoir_sample_at and localcast_reservoir_view_sample_at, allowing Aquarium/Faust consumers to walk the live window without owning private histories."}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ class PerfectMachineContractTests(unittest.TestCase):
|
|||
self.assertIn("enum LocalcastSampleKind", header)
|
||||
self.assertIn("size_t localcast_reservoir_len", header)
|
||||
self.assertIn("size_t localcast_reservoir_view_len", header)
|
||||
self.assertIn("bool localcast_reservoir_sample_at", header)
|
||||
self.assertIn("bool localcast_reservoir_view_sample_at", header)
|
||||
self.assertIn("bool localcast_reservoir_latest_for_sensor", header)
|
||||
|
||||
def test_native_runtime_header_exposes_typed_producer_spine(self):
|
||||
|
|
|
|||
Loading…
Reference in a new issue