Skip to content

H1 slice 5: the shared room scene (conf.rooms.<room>.scene.*) + arisrv seed - #776

Merged
transfix merged 14 commits into
masterfrom
feat/h1-scene
Oct 24, 2026
Merged

transfix merged 14 commits into
masterfrom
feat/h1-scene

Conversation

@transfix

@transfix transfix commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Targets master now that #773 (slices 3+4) is merged; master is merged in (CMake test-list conflict resolved by keeping every entry).

H1 slice 5 adds the server-authoritative shared 3D scene that every client renders from its own camera (ARIADNE_CONFERENCE_ROADMAP.md §5g). The scene lives in the replicated room tree under conf.rooms.<room>.scene.* as scalar leaves, like S4's pose keys, so it travels over the existing "conf" interest filter with no transport changes.

What it adds

  • cvc::ariadne::conference::room_scene (room_scene.{h,cpp}).
    • Objects are SceneNodes: a URI or a plane/box/sphere primitive, plus transform, material and visible.
    • The environment is the background (solid or gradient), the grid/axis chrome, and the SceneLights.
    • Because these are the .ari scene's own types, read_room_scene() returns a cvc::ariadne::Scene. The client render (S6) is then realize_scene(sg, read_room_scene(app, room)), with no second schema.
  • scene.rev is written last by every scene write. A renderer re-realizes only when it changes, and a reader that sees the new revision also sees the content it covers.
  • Removal is soft (present = 0), for the same reason as room_leave: a hard delete gets resurrected by peers. Lights left out of a later write_scene_env are soft-removed too. Ids containing . throw std::invalid_argument.
  • arisrv seeds the MVP scene once: a gradient background, the grid chrome, a sun, a 20 m ground plane, and a 1 m reference box standing on it. The seed is idempotent and does nothing if the room already has a scene, so restarting the relay won't overwrite edits. The writes go out to clients on the first pass of the relay loop, and late joiners get them in the snapshot.

Tests

room_scene_test has 9 gtests. They cover round-trips for the environment and objects, soft removal and revision bumps, id validation, idempotent seeding, mapping to a Scene, and an end-to-end case: the seeded scene replicates to a client over two in-process shards, then the client follows a move and a removal. These, plus holon_sync_test (5) and holon_test (7), pass locally on Windows/MSVC (RelWithDebInfo, shared). The new files and the arisrv hunk are formatted with clang-format 18.1.8.

Not in this slice

  • S6 (the client side): arigl re-realizes the room scene on scene.rev changes, draws avatars from participants.<pid>.pose, and drives its own .intent from input.
  • The live two-client cyprty test comes after S6.
  • Server-sole-writer enforcement remains §5e/H2. Like .pose, it is loopback-trusted by convention for now.

…e 2)

The one genuinely new mechanism H1 needs on top of the clock_driver +
data() replication it already has: a participant's avatar is
server-authoritative, so a client must move its own avatar without a
round-trip (predict), correct when the authoritative state arrives
(reconcile), and show peers lag-free without guessing ahead
(interpolate).

New cvc::ariadne (headless, engine-free, no GL / state tree / transport
-- that plumbing is the holon, slice 3):
- avatar_sim: a deterministic linear kinematic integrator over a fixed
  dt (position += velocity*dt, pure double arithmetic, no trig), so the
  same intents reproduce bit-for-bit -- what makes replay sound. A stub
  for the physics::world save/restore stepping slice 3 swaps in behind
  the same shape.
- predicted_avatar: client prediction + server reconciliation. Applies
  local intents immediately, logs the unacknowledged ones; on an
  authoritative state for a past tick, keeps the prediction when it
  already matches within tolerance (no correction), else snaps to the
  authoritative state and REPLAYS the inputs issued after that tick, so
  in-flight player input is corrected-on-top, not dropped.
- interpolate_peer: a non-local peer between its last two authoritative
  samples, alpha clamped to [0,1) so it never extrapolates past newest.

avatar_sim_test (6 cases, all green): bit-reproducible stepping; a
perfect prediction reconciles with no correction; prediction ahead of
the server keeps inputs pending; a server override snaps + replays
in-flight ticks (and really moves the avatar); a matching ack after a
correction does not re-snap or jump backward; peer interpolation never
overshoots (and clamps negative/NaN alpha to the previous sample). All
comparisons are lib-step vs lib-step, so bit-equality holds without
-ffp-contract=off.

Branches off master (has #766 clock_driver + #767 H0). Independent of
#768 (the physics codec); the MVP pose rides scalar keys, so this does
not need the typed-object codec. Next: S3 wires this into a
cvc::ariadne::holon on its own clock_driver.
… integrator tests

Adversarial review of the predict/reconcile mechanism surfaced three
items, all addressed here:

- [med] predicted_avatar::reconcile had no guard against a non-monotonic
  server_tick. A reordered or duplicate ack (tick <= one already
  reconciled) failed the lookup, fell into the diverged branch, and
  snapped the avatar back to a stale authoritative state while dropping
  the inputs between the two acks (a visible backward rubber-band).
  Real datagram transports reorder/duplicate, so this was a live latent
  hazard for S3's caller. Fix: track the highest reconciled server_tick
  and no-op (return false, touch nothing) on any ack at or below it.
  Test: StaleOrDuplicateAckIsIgnored.

- [med] the held-vs-snap tolerance was never exercised at a nonzero
  distance — every "no correction" test had a bit-identical prediction
  (d==0), so a tol/tol2 or threshold-direction regression would ship
  green. Test: ReconcileHoldsWithinToleranceAndSnapsBeyond (0.5*tol held,
  2*tol snaps).

- [low] the determinism test was tautological (two in-process runs of a
  pure function). Replaced with IntegratesConstantVelocityToClosedForm:
  a closed-form n*v*dt check (EXPECT_NEAR, not a bit-exact golden, so FMA
  contraction across toolchains can't flip it) that catches a formula
  regression, plus a retained purity assertion.

Noted for S3 (dismissed as out-of-scope for the headless stub): the
tolerance is position-only (fine while the stub integrates no rotation;
revisit when physics::world adds orientation), and _log is unbounded if
a caller never acks (a real server acks every tick). avatar_sim_test now
8 cases, all green.
… clock_driver (H1 slice 3)

The holon object the roadmap §5h names: a server-authoritative,
client-replicable bundle of ONE world_clock and the participant avatars
on it. The server drives the clock and owns every avatar's authoritative
pose; a client runs a replica that predicts its own avatar (slice 2) and
reconciles to the authoritative state, and interpolates peers.

cvc::ariadne::holon (inc/cvc/ariadne/holon.h, src/cvc/ariadne/holon.cpp):
- Owns its world_clock AND cvc::clock_driver BY VALUE (not the app-global
  clock), so two holons in one process never fight over time.
- Drives avatar integration as a clock_driver STEP observer and records
  the presentation alpha as a FRAME observer — the same step-then-present
  order tick_scene uses, so a frame observer never reads a pose the step
  has not yet produced.
- Per participant: server integrates the latest intent into the
  authoritative pose over the quanta that fell due; a client-local avatar
  predicts + reconciles (slice 2's predicted_avatar); a peer is driven by
  authoritative samples and interpolated at the frame alpha. A client may
  set an intent only on its OWN avatar, never a peer — the proposed/
  authoritative split (the verb path that enforces it server-side is
  slice 4).

Deliberately the headless clock + avatar-pose core: the Jolt
cvc::physics::world (replacing the linear avatar_sim behind the same
step shape) and the cvcGL scene_realize / physics_scene_binding
rendering are later slices, so this stays deterministically testable
with no GL / state tree / transport.

holon_test (5 cases, all green): each holon owns a distinct clock
(advancing one never ticks another); a server + client holon reproduce
slice 2's convergence through the holon API (perfect prediction holds
bit-for-bit; a server +y override snaps the client off its wrong +x
prediction); a client cannot drive a peer; a peer interpolates through
the frame alpha and never overshoots.

Stacks on #770 (slice 2). The scalar-key state wiring that connects this
to replication lands with slice 4 (the intent/pose verb path).
…vable, replay/stale tests

Adversarial review of the holon surfaced six items; addressed here:

- [med] the peer interpolation path ignored server_tick while the local
  path guards against stale/duplicate/reordered acks. A reordered peer
  sample would rewind the interpolation window (prev <- newer, newest <-
  older) and jerk the peer backward; a duplicate would collapse it.
  apply_authoritative's peer branch now applies the same monotonic guard
  (drop a sample whose server_tick <= the newest applied). Test:
  PeerIgnoresStaleOrDuplicateSample.
- [med] the convergence tests always acked the client's latest tick, so
  reconcile drained the whole log and the replay-suffix path (and
  on_step's tick numbering) was never exercised — an off-by-one would
  pass. Added ClientReplaysInFlightInputsOnServerCorrection: a divergent
  authoritative state for a PAST tick snaps the client and replays its
  inputs after that tick; the result is checked against a reference, so a
  wrong tick/replay-suffix fails.
- [low/med] ClientCannotDriveAPeer passed vacuously (a peer is never
  intent-driven). Reframed as PeerIsSampleDrivenNotIntentDriven, stating
  what it actually proves; the set_intent guard stays (defensive).
- [low] the holon was only implicitly non-movable (via world_clock's
  mutex). Made it explicit (deleted copy/move) so a future movable-clock
  refactor can't silently re-enable a move and dangle the observers' this.
- [low] dropped the dead `have_prev` field; the peer window now tracks
  `have_sample` + `last_sample_tick`.
- [low] strengthened the peer no-overshoot assertion with a lower bound
  (advanced most of the way, not frozen).

No memory-safety/crash bugs were found. holon_test now 7 cases, green.
… core)

Wires cvc::ariadne::holon to the replicated conference room tree over
plain SCALAR leaf keys, under the participant subtree the conference
already uses (conf.rooms.<room>.participants.<pid>, via
room_participant_path):

  <participant>.pose.pos/orient/vel.{...} + .pose.server_tick  server-written
  <participant>.intent.move.{x,y,z}                            client-written

cvc::ariadne holon_sync.{h,cpp}: write_pose/read_pose and
write_intent/read_intent. Scalar leaves (not a typed body_state data()
object) are deliberate — they already journal + replicate through the
existing "conf" interest filter with zero transport changes, and
sidestep the data()-object work (§52). Each double is written
round-trip-safe (%.17g, portable across toolchains, no std::to_chars FP
dependency) so a reader recovers it exactly. Writes use the room tree's
touch-before-value idiom so a fresh node's first write is not lost to the
distributed-state attach race.

Authority (the proposed/authoritative path pair): a client writes only
its own .intent.*; the server reads every participant's .intent,
integrates it into the holon, and is the SOLE writer of .pose +
.server_tick; the client reads .pose read-only and reconciles. Writes go
through a C++ host (never a DSL state-set, since ui.docs.* is excluded
from replication). H1 is loopback-trusted sole-writer-by-convention; the
connection<->pid admission binding is the §5e/mesh increment.

holon_sync_test (4 cases, green): pose + intent scalar round-trip recover
binary64 exactly; read returns false when absent; and an END-TO-END over
a real two-shard state_transport_inproc — the client proposes intent, it
replicates to the server, the server holon integrates it and writes the
authoritative pose, that replicates back, and the client reconciles and
tracks the authoritative avatar bit-for-bit across 8 frames.

Stacks on #772 (slice 3). The remaining live-integration piece (a C++
room-set-intent verb + arisrv hosting the server holon + a per-room
client pump) is the follow-on that the live two-client cyprty test
exercises.
…(H1 slice 4b)

Wire the holon into the live relay so a participant's avatar becomes
server-authoritative across the wire.

- holon_sync: server_pump(holon, app, room, wall_dt=-1) — one authoritative
  frame against the replicated room tree: for every present participant
  (room_roster) add it to the holon if new, read its proposed .intent,
  advance the holon, and write each authoritative .pose + .server_tick.
  wall_dt defaults to measured real time (the relay); a test injects a
  fixed dt for a deterministic step count.
- arisrv: construct one server holon for the room and call server_pump
  each relay tick, right after room_relay_tick, so the authoritative pose
  writes are local shard mutations the existing drain_local block fans to
  every client on both transports. (~5 Hz relay cadence -> the holon
  catches up several quanta per tick and clients interpolate; a smoother
  authoritative rate is a later tuning.)
- room_intrinsics: a (room-set-intent HANDLE MOVE-X MOVE-Y MOVE-Z) C++
  verb so a client proposes only its OWN avatar's intent (pid from the
  room-join handle, so it cannot propose for a peer). Writes via the
  holon_sync intent keys; a C++ verb because the DSL chroot ui.docs.*
  is excluded from replication (same reason room-join is C++).

holon_sync_test gains ServerPumpIntegratesRosterFromIntents: two
participants join + propose intent on a client app, replicate, and
server_pump integrates each from its own intent and writes authoritative
poses that replicate back (A moves +x, B moves +y). 5 cases green; arisrv
builds + links.

Extends #773 (slice 4). The remaining client-side piece (arigl driving
its local avatar's intent from input + rendering peer avatars from the
replicated poses) is S5/S6 + the live two-client cyprty test.
# Conflicts:
#	src/cvc/tests/CMakeLists.txt
# Conflicts:
#	src/cvc/tests/CMakeLists.txt
# Conflicts:
#	src/cvc/CMakeLists.txt
#	src/cvc/tests/CMakeLists.txt
…v seed

The server-authoritative 3D scene every client renders from its own camera
(ARIADNE_CONFERENCE_ROADMAP.md §5g). It lives in the replicated room tree as
scalar leaves, like the S4 pose keys, so it rides the "conf" interest filter
with no transport change.

The schema is the .ari scene's own types: objects are SceneNodes (a URI or a
plane/box/sphere primitive, transform, material, visible), the environment is
background + chrome + SceneLights. read_room_scene() turns the live subtree
back into a cvc::ariadne::Scene, so the client render (S6) is realize_scene()
over it. scene.rev is written last by every write so a renderer re-realizes
only on change. Removal is soft (present = 0), as room_leave is.

arisrv seeds the MVP environment once (gradient background, grid chrome, a
sun, a 20 m ground plane, a 1 m reference box); seeding is a no-op when the
room already has a scene.

room_scene_test: 9 gtests, including the seeded scene replicating to a
client over two in-process shards and tracking a move + removal.
# Conflicts:
#	src/arisrv/main.cpp
#	src/cvc/CMakeLists.txt
#	src/cvc/tests/CMakeLists.txt
@transfix
transfix changed the base branch from feat/h1-sync to master October 24, 2026 00:49
@transfix
transfix merged commit 4fe58db into master Oct 24, 2026
24 of 25 checks passed
@transfix
transfix deleted the feat/h1-scene branch October 24, 2026 15:41
transfix added a commit that referenced this pull request Oct 24, 2026
Carries the master merge (#776 squash, #774, #775, #777, #780, #782,
#784, #785) up the H1 stack.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant