Repository navigation
feat(core): clock_driver — unified per-frame time-slice fan-out; drive the conference pump through it - #766
Conversation
…lock
Introduce cvc::clock_driver (inc/cvc/core/clock_driver.h,
src/cvc/core/clock_driver.cpp): the host-side loop that advances a
world_clock exactly once per frame and fans the result out to ordered
observers that share a single clock_frame.
Two observer kinds mirror the advance()/for-each-step/present shape the
world_clock header sketches and that tick_scene already open-codes:
- step observers fire first, ascending `order`: "advance your own sim
by the quanta that fell due" (physics follow, a holon's simulation).
- frame observers fire after every step observer, ascending `order`:
"do the once-per-frame work / present at frame.alpha" (drain a
transport, forward media stamped with frame.t, interpolate a pose).
Every observer sees the SAME clock_frame computed once at the top of
advance(), so a media timestamp (frame.t) read by one observer is
bit-identical to the one read by another — what lets a receiver align
two streams stamped in the same frame. clock_frame also carries the raw
wall_dt for work that must track real time regardless of sim rate
(talk-hold decay, camera, fps), a monotonic frame index, and a `first`
flag; a measured first advance books no startup burst.
Optional world_clock_binding routing lets the sim.* control keys steer
the driver and publishes telemetry each frame. Observers may be removed
or added from inside the fan-out (a self-removing one-shot, deferred
adds); re-entering advance() throws.
This is the reusable spine for the holon tick model and for refactoring
the conference host's monolithic per-frame pump onto an ordered,
single-advance observer set.
13 gtest cases in src/cvc/tests/clock_driver_test.cpp cover ordering,
the shared frame, first-frame seeding, remove/add during fan-out,
re-entrancy, the follower-driven step path, and the binding route.
Replace ConferenceSession's monolithic eleven-step pump() and its hand-rolled media_clock_now() with a single cvc::clock_driver that owns the one per-frame advance of the per-app world_clock and fans the steps out as ordered observers. setup() now builds a clock_driver over app.world_clock() and calls install_observers(), which registers the eleven per-frame steps as frame observers at explicit ascending orders (kOrderPumpAll .. kOrderActivity) — the exact order the open-coded pump ran them in. pump() is now just driver_->advance(): it measures the wall delta, advances the clock once (the single media timestamp, frame.t, handed to forward_streams so a receiver can A/V-align a frame's camera and mic), and fires the observers. The device enumeration becomes a self-removing one-shot observer instead of a per-frame latched branch. Behavior is preserved: same step order, one world-clock reading per frame shared by every step, the first measured advance books no startup burst (as media_clock_now's last_wall_ < 0 skip did), now_wall_ stays a once-per-frame steady_clock reading for the frame-rate-independent talk-hold, and conf.pump still runs before the document on:tick (the deliberate one-frame lag read_peer_controls relies on). A plain local document has no driver and pump() no-ops, exactly as before. This removes the conference's second, hand-rolled world_clock advance path (the map's "never double-advance" hazard) and puts the host on the same one-advance/ordered-observer spine the holon simulation will use. Exercises both the run-mode input pump and the snapshot-mode direct pump. arigl builds clean; the no-relay path (arigl check) is green.
ConferenceSession had no destructor, so its members were destroyed in
reverse declaration order: media_mu_ and inbox_ (declared after the
transport) were torn down BEFORE ws_/grpc_, whose destructor is the only
thing that joins the transport reader thread. On every teardown path
that does not reach the run loop's explicit conf.stop() — snapshot,
check, info, a document-load failure, or an exception unwinding the run
loop — the still-running reader could invoke on_media and lock a
destroyed media_mu_ / push to a destroyed inbox_: a use-after-free and
data race. The snapshot-of-a-live-conference path is explicitly
supported, so this was reachable, not theoretical.
Add ~ConferenceSession() { stop(); }. The destructor body runs ahead of
member destruction, so stop() joins the reader (via stop_(), which the
transport makes idempotent with _running.exchange(false)) while
media_mu_/inbox_ are still alive. ConferenceSession::stop() is already
idempotent (clear/reset are no-ops the second time), so the run path's
explicit stop() followed by this one is safe. Single-threaded wasm has
no reader thread and is unaffected.
Pre-existing hazard (not introduced by the clock_driver refactor);
surfaced by an adversarial review of this branch and fixed here since it
lives in the same struct the refactor touches.
|
Ran a 3-lens adversarial review of this branch (framework correctness / refactor faithfulness vs the original pump / lifetime-threading-wasm), each finding independently verified. Faithfulness + framework: no confirmed issues — the observer order reproduces the original pump exactly, One confirmed finding (high), pre-existing — now fixed in 535f397: The review cleared the driver/app lifetime, the observers' captured |
What
Introduces
cvc::clock_driver— the reusable spine for the holon tick model — and puts the arigl conference host on it. This is the "time-slice framework" piece: one advance per clock per frame, fanned out to ordered observers that share a single reading of time.Slice 1 —
cvc::clock_driver(new)inc/cvc/core/clock_driver.h+src/cvc/core/clock_driver.cppThe host-side loop that
world_clockalways assumed but left to each caller to open-code (tick_sceneis this loop; the conference host was this loop as an 11-step pump). Factored out so there is exactly one advance per clock per frame and one place that owns the order.Two observer kinds mirror the
advance()/ for-each-step / present shape:order— "advance your own sim by the quanta that fell due" (a physicsfollow(), a holon's simulation; use the sharedclock_frame.step/ aclock_follower).order— "do the once-per-frame work / present atframe.alpha".Every observer sees the same
clock_framecomputed once at the top ofadvance(), so a media timestamp (frame.t) read by one observer is bit-identical to another's — what lets a receiver align two streams stamped in the same frame.clock_framealso carries rawwall_dt(for work that must track real time regardless of sim rate — talk-hold, camera, fps), a monotonicindex, and afirstflag; a measured first advance books no startup burst. Optionalworld_clock_bindingrouting lets thesim.*control keys steer the driver and publishes telemetry each frame. Observers may remove/add themselves from inside the fan-out (a self-removing one-shot); re-enteringadvance()throws.13 gtest cases (
src/cvc/tests/clock_driver_test.cpp) cover ordering, the shared frame, first-frame seeding, remove/add during the fan-out, re-entrancy, the follower-driven step path, and the binding route.Slice 2 — drive the conference pump through it
src/arigl/main.cppConferenceSession's monolithic 11-steppump()and its hand-rolledmedia_clock_now()are replaced by aclock_driveroverapp.world_clock().setup()builds the driver and callsinstall_observers(), which registers the 11 steps as frame observers at explicit ascending orders (kOrderPumpAll .. kOrderActivity) — the exact order the open-coded pump ran them.pump()is now justdriver_->advance(). Device enumeration becomes a self-removing one-shot observer.Behavior-preserving: same step order; one world-clock reading per frame shared by every step (
frame.t→forward_streams); the first measured advance books no startup burst (asmedia_clock_now'slast_wall_ < 0skip did);now_wall_stays a once-per-frame steady_clock reading for the frame-rate-independent talk-hold;conf.pumpstill runs before the documenton:tick(the deliberate one-frame lagread_peer_controlsrelies on); a plain local document has no driver andpump()no-ops. This also removes the conference's second, hand-rolledworld_clockadvance path (the "never double-advance" hazard).Why
The flagship holon model is
{world_clock + physics + scene/pose}, server-authoritative and client-replicable. A holon needs a single, ordered, one-advance-per-frame tick surface that both the server and each client replica run; the conference host needed to stop hand-advancing its own clock and join that surface.clock_driveris that surface, and the conference is the first consumer. (First step of the holon plan; H0 data()-replication and H1 holon bundle build on top.)Testing
clock_driver_test: 13/13 green.world_clock_test: 49/49 green (library healthy with the new TU).ariglbuilds clean; the no-relay path (arigl check conference_roster.ari) is green.Formatted with clang-format 18.1.8 (matches CI).