Expose runtime rolling window reads
This commit is contained in:
parent
2df40000e4
commit
caec14cbf2
7 changed files with 211 additions and 4 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue