Skip to content

H1 slice 6: the room in 3D — avatars, WASD + third-person camera, live-timing holon fixes - #779

Merged
transfix merged 27 commits into
masterfrom
feat/h1-client
Oct 24, 2026
Merged

transfix merged 27 commits into
masterfrom
feat/h1-client

Conversation

@transfix

@transfix transfix commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Targets master (#773 is merged). It also carries #776 (slice 5) until that merges, so merge #776 first; after that, this PR's diff is slice 6 alone.

H1 slice 6 is the client side of the shared room (ARIADNE_CONFERENCE_ROADMAP.md §5g/§5h). arigl now shows the room in 3D behind the conference windows. WASD or the arrow keys walk your avatar relative to the camera, and the camera orbits your avatar in third person.

Fixes in the holon, found by running the headless loop at relay timing

I ran the existing pieces end to end the way the relay runs them: the server pumped every 24 quanta (about 5 Hz) and the clients every quantum. Three things were broken:

  • The server holon ran at a third of real time. arisrv advances its holon at about 5 Hz, so 24 quanta are due each time, but world_clock's default catch-up cap is 8 and it silently dropped the rest. holon_config now allows 60 quanta (0.5 s) per advance. This affects H1 slices 3+4: cvc::ariadne::holon + holon ↔ state-tree sync over scalar keys #773's live path too.
  • A client's clock didn't count the server's ticks. Reconcile compares ticks, but a client joins a server that's already running. A client ahead of the server replayed its whole input history on every acknowledgement; one behind discarded its predicted motion. A sample more than max_skew_ticks (60) away now re-aligns the replica: it seeks the clock and drops the stale input log.
  • Peers flickered. They were interpolated with the per-quantum alpha, so with 5 Hz samples a peer jumped between its last two samples 120 times a second. A peer now walks from where it's shown to the newest sample over the server's own interval between them. Re-alignment shifts the walk instead of restarting it.

Also in this slice:

  • holon::take_correction(pid) reports reconcile jumps for the renderer to ease out.
  • client_pump mirrors server_pump: it registers you as the local avatar and everyone else as peers (starting from their replicated pose), drops departures, applies samples, and advances one frame. propose_intent sends your intent.
  • server_pump spawns newcomers on a 4 m golden-angle ring instead of inside the reference box.

The 3D view

  • cvc::gl::ariadne::room_view. It realizes the room scene into the window's SceneGraph (ids prefixed room_) and re-realizes it when scene.rev changes. It also draws one marker per participant, posed each frame at display_pose. Your avatar is gold and peers get a stable colour per pid. A correction to your own avatar is eased out over 0.12 s.
  • intent_from_keys. Held keys become an intent relative to the camera's heading on the Z-up ground. Diagonals are normalized and opposing keys cancel.
  • arigl. ConferenceSession::attach_view adds one ordered frame observer right after the transport pump. It turns keys into an intent (orbit mode only, since in fly mode the same keys move the camera), proposes the intent only when it changes, runs client_pump, updates the view, and keeps the orbit centre on your avatar. The camera's held keys are dropped while ImGui has the keyboard, so typing a name never walks.
  • CameraController::keyHeld exposes the held-key set the camera already keeps, on native and wasm.

Tests (Windows/MSVC, local)

  • holon_test 10: a peer walking across sparse samples, re-alignment including walk continuity, and take_correction.
  • holon_sync_test 6: client_pump end to end at relay timing over three in-process shards. A peer never steps back, your avatar stays within one relay interval of the authority, both replicas count the server's ticks, departures are dropped, and spawns land on the ring.
  • room_scene_test 9.
  • cvcgl_room_view (offscreen, 22 checks).
  • Live, end to end (Windows, gRPC, 2026-10-23): arisrv plus two arigl clients on loopback. alice ran the real conference_roster.ari with a synthetic camera. bob ran a scripted document that joins and calls (room-set-intent h 0.4 0 0). Each client wrote a snapshot of its own view. alice's view shows the seeded room (ground, reference box), her own gold avatar and bob's peer marker walking past her. So intent goes client → relay → server holon, and the authoritative pose goes server → relay → peer holon → room_view. The first run also found the debug-chrome problem fixed below.
    • The locally prebuilt static gRPC can't link with MSVC 14.44 (its STL is newer). The catalog's shared Windows gRPC 1.68.2+cvc.5 links and runs: cvcpkg install grpc protobuf --prefix <p> --link shared. The system VC++ runtime is 14.51.
  • Live-test finding, fixed here: the seeded room had the SceneGraph's diagnostic chrome on (bounding box, axis gizmo, "Max:" label). That's a debug overlay, so the default room now has it off.

Browser (WebSocket) and native (gRPC) clients in one room — verified live

The test setup was a Linux arisrv (both planes, built on catx-03 with glibc 2.35, the production relay's floor), the cyprty wasm client in a browser, and two native arigl clients over gRPC. With the relay's new room readout:

  • Presence and avatars cross the bridge in both directions. The browser draws the native clients, the native clients draw the browser, and the relay holds all of them. The page footer logs room: 3 avatar(s): alice_…, bob_…, webby2.
  • Browser input drives the server holon. Held W moved the browser's avatar 4.7 m in 2.5 s at 2 m/s, relative to the camera.
  • Departures work the same on both planes. They're retracted, and their intent is zeroed.
  • Soak: 8 relaunch cycles (16 native joins and leaves) plus a connected browser, with no relay faults.

Found by that run and fixed here

  • state_transport_grpc had no connection-closed hook, so a native client that exited stayed in the room forever and the server holon kept integrating its last intent. Added set_on_connection_closed (the WS contract; test ConnectionClosedReportsThePeersNodeId). arisrv now retracts departures from both planes through one queue, and server_pump drops departed participants.
  • Rejoins under the same id were invisible. Replication drops any (origin node id, sequence) it has already seen, and a restarted client (reloaded tab, relaunched native client, two people both named guest) restarts its sequence. arigl now adds a random per-launch suffix to its node id. Verified: a same-name reload leaves cleanly and rejoins visibly.
  • The first frame always proposes the intent (no stale walk on rejoin). The camera frames ±8 m on arrival so you see who's here. Peer colours avoid the gold that means "you". ?relay= and ?name= page parameters were added.
  • arisrv logs its stack on a fatal signal (Linux/glibc). One relay abort (malloc(): unsorted double linked list corrupted) happened during the test, before the same-id fix. It didn't reproduce under ASan or the soak, so its root cause is still unknown. The trace is there so a recurrence explains itself.
  • CVC_ARISRV_ROOM_DEBUG=1 logs each present participant's authoritative position and intent every ~5 s.

Known, not fixed here

  • The relay reaps a WebSocket that sends nothing for 10 s (to clear tabs that navigated away). A hidden or backgrounded browser tab is throttled enough to trip that, and the client doesn't reconnect, so switching tabs for a while drops you out of the room.
  • On Windows, native arigl can't be built with CVC_ENABLE_WS=ON: cvc.dll exceeds the 65,535-export DLL limit (LNK1189). gRPC is fine. No Windows CI lane builds WS, so this is a pre-existing gap.

Not done here

  • The browser path (WebSocket + wasm on cyprty) has not been exercised live. This run used native gRPC.
  • Known H1 limitation: your intent reaches the server one RTT plus up to one relay interval after you predicted it. While walking, your own avatar takes small, eased corrections. The real fix is H2: tick-stamped inputs plus a client lead estimate, and a faster authoritative rate.
  • Not built yet: name labels over avatars, and avatars facing their direction of motion (the intent is still linear).

…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.
…5 Hz server fix

What the live client needs from the holon, found by timing the headless loop like
the relay (server pumped every 24 quanta, clients every quantum):

- The server holon ran at a third of real time. arisrv advances it at ~5 Hz, 24
  quanta due each time, and world_clock's default catch-up cap is 8, so 2/3 of
  every interval was dropped. holon_config now allows 60 quanta (0.5 s) per
  advance, sized for a relay or a throttled tab rather than a render loop.
- A client replica's clock is aligned to the server's ticks. Reconcile compares
  ticks, and a client joins a server that has been running, so a sample more
  than max_skew_ticks away (60) seeks the replica's clock and drops the local
  avatar's stale input log. Without it, every ack replayed the whole input
  history (client ahead) or discarded predicted motion (client behind).
- Peers walk across sparse samples. They were interpolated by the per-quantum
  alpha, so with 5 Hz samples a peer flicked between its last two samples 120
  times a second. A peer now walks from where it is shown to the newest sample
  over the server's own interval between samples, timed on the replica's clock;
  re-alignment shifts the walk instead of restarting it.
- holon::take_correction reports reconcile jumps once, for a renderer to ease.
- client_pump (the mirror of server_pump: roster -> local/peer registration from
  the replicated pose, departures dropped, authoritative samples applied, one
  frame advanced) and propose_intent (predict + write .intent).
- server_pump spawns newcomers on a 4 m golden-angle ring around the origin
  instead of inside the reference object.

Tests: holon_test 10 (sparse-sample walk, re-alignment incl. walk continuity,
take_correction), holon_sync_test 6 (client_pump end to end at relay timing:
peer never steps back, self within a relay interval of the authority, replicas
count the server's ticks, departures dropped; spawn ring).
…low camera

cvc::gl::ariadne::room_view draws the conference room into the window's
SceneGraph: the server's shared scene (read_room_scene -> realize_scene, ids
prefixed room_, re-realized when scene.rev changes, background applied as a
view property) and one marker per participant of a client holon, posed each
frame at holon::display_pose. The local avatar's reconcile jumps
(holon::take_correction) are eased out over 0.12 s, so a server correction
glides instead of jumping. intent_from_keys maps held WASD / arrow keys to an
intent relative to the camera's heading on the Z-up ground (diagonals
normalized, opposing keys cancel).

arigl's ConferenceSession gains attach_view(view, cam) and one ordered frame
observer right after the transport pump. Each frame it reads the keys (Orbit
mode only, since in Fly the same keys move the camera; the camera's held set is
dropped while ImGui has the keyboard, so typing a name never walks), proposes
the intent only when it changes, runs client_pump, updates the room view, and
keeps the camera's orbit centre on our avatar (framed once at first sight).
The conference windows stay ImGui windows over the 3D view.

CameraController::keyHeld exposes the held set the camera already keeps
(native interactor and wasm alike).

cvcgl_room_view (offscreen, 22 checks): key -> intent, keyHeld, scene realize
+ re-realize on removal + teardown, avatar markers follow the holon, a 3 m
correction moves the marker one frame's share then glides onto the pose,
departures and teardown remove markers.
The first live run (arisrv + two arigl clients over gRPC on Windows) rendered the
seeded room with the SceneGraph's diagnostic chrome: the yellow bounding box, the
axis gizmo and the "Max: (...)" label. Chrome is a debug overlay, not a floor grid,
so the default room scene now leaves it off; the ground plane and the reference
box carry the space.
?relay=ws://host:port (or wss://) points the browser client at another arisrv -- a
staging or local relay -- instead of the page origin's /cyprty-ws; anything that is
not a ws(s):// URL is ignored. ?name= presets the display name (trimmed, at most 32
characters) instead of a random guest-xxxxx.
Running a native (gRPC) and a browser (WS) client in one room showed a native
client that exits is never retracted: only the WebSocket transport reported
closed connections, so the roster kept the ghost and the server holon kept
integrating its last intent (a walker drifted ~240 m over ten minutes).

- state_transport_grpc::set_on_connection_closed, the WS transport's contract:
  fired with the peer's node id (from its Hello) when its stream ends, never
  during stop(). Test: ConnectionClosedReportsThePeersNodeId.
- arisrv queues departures from BOTH planes through one handler and applies
  them on the main loop for both (the apply step was WS-only), zeroing the
  departed participant's intent so a rejoin under the same id does not resume
  an old walk. Hooks are now registered before each transport starts.
- server_pump drops holon participants who left the roster (the mirror of
  client_pump), so a rejoin respawns on the ring.
- arigl proposes its intent on the first frame even when standing still.
- CVC_ARISRV_ROOM_DEBUG=1 logs the room as the server holds it every ~5 s
  (each present participant's authoritative position and proposed intent).
…drawn

From the mixed browser + native run:
- The first-sight camera framed +-4 m around you, so in a narrow browser
  window the other participants (spawned 4-7 m away) were off-screen. Frame
  +-8 m: arriving at a party you should see who is there.
- Peer colours skip the 30-75 degree hues, so no peer is drawn in a yellow
  that reads as your own gold avatar.
- arigl logs "room: N avatar(s): ..." whenever the drawn set changes (the
  browser page shows stderr in its footer and console), via the new
  room_view::avatar_pids().
Replication's loop detection drops any mutation whose (origin node id, sequence)
it has already seen. arigl derived its node id from the display name alone, so a
client that restarted under the same name (a reloaded tab, a relaunched native
client, or two people who both kept "guest") restarted its sequence at 1 and
every write it made was dropped as a duplicate: it joined a room in which nobody,
including the relay's roster, could see it, until the relay restarted. Found
with a reloaded browser (?name=webby) and relaunched native clients.

The id is now the sanitized name plus a random six-character suffix per launch,
so each session is a new participant; the roster still shows the display name
(profile.name), and the document identifies "me" by self.pid, which follows.
The relay runs unattended, and during the mixed-transport test it died once with
"malloc(): unsorted double linked list corrupted" and nothing else to go on (no
gdb on the builder, apport ignores unpackaged binaries, and an ASan build did not
reproduce it). On SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL it now writes the stack to
stderr with backtrace_symbols_fd (no allocation; backtrace() is warmed up at
startup so the handler never lazy-loads libgcc on a corrupted heap), then
re-raises with the default action.
# Conflicts:
#	src/arisrv/main.cpp
#	src/cvc/CMakeLists.txt
#	src/cvc/tests/CMakeLists.txt
@transfix
transfix changed the base branch from feat/h1-scene to master October 24, 2026 00:49
…, compact roster with ping + bandwidth (#781)

* Conference link stats: Ping/Pong probes, per-link traffic, room net stats, compact roster

Link probe + stats (both transports, every connection):
- state_transport.proto: Ping/Pong frames. Each end pings every 2 s
  (state_transport::set_ping_interval); the other end answers at once and the
  sender measures the round trip on its own clock. Link-level only: never
  replicated, relayed or bridged.
- state_link_stats (lock-free per-connection bytes/frames both ways, RTT EWMA /
  last / min) + state_link_rate_meter (cumulative reports -> bytes/s);
  state_transport::link_reports() on state_transport_ws and _grpc, so a relay's
  links to clients, a client's link to its relay and a server's links to peer
  (delegation) servers are all measured the same way.

Background tabs no longer drop out: a browser answers the relay's Ping from its
WebSocket onmessage event, which a backgrounded tab still receives, so the idle
reaper keeps it; a ghost that cannot answer is still reaped. And arigl now
redials a lost relay link every 3 s and re-asserts its presence afterwards.

Room stats (room_net.h): each client publishes its own link quality as
participants.<pid>.net.{rtt_ms,in_kbps,out_kbps}; the relay publishes its view
of each participant (net.relay.*) and its totals (server.net.*) and logs them
(per link every ~5 s with CVC_ARISRV_ROOM_DEBUG, a totals line each minute).
(room-roster) entries carry the numbers and roster-cell text.

conference_roster.ari: the Users window is a compact table -- one row per
participant (name | ping | down | up | cam | mic | vol), each row a grid with
the same fixed column widths, so the rows line up as columns; your own row
shows your measured link. Peer camera tiles moved right of the wider window.

Tests: state_link_stats_test (3), StateTransportWsTest.LinkProbeMeasures-
RoundTripAtBothEnds + QuietClientAnsweringPingsIsNotReaped, StateTransportGrpc-
Test.LinkProbeMeasuresRoundTripAtBothEnds; RelayReapsIdleConnection turns the
probe off to keep testing a silent client.

* state_transport_grpc_test: leave only after the relay has read the Hello

ConnectionClosedReportsThePeersNodeId stopped the client as soon as both ends
counted a connection. Cancelling a gRPC stream drops frames the relay has not
read yet, so the relay sometimes never saw the client's Hello -- and a stream
that never identified itself is, by contract, not reported. The link probe's
extra threads shifted the timing enough to expose it (about 1 run in 5 after
ThreeEndpointFanOut). Wait until the relay's link report names the client.
20/20 repeats of the reproducing sequence pass.

* conference_roster: widen the ping column (a congested link reads 4 digits)

* Transports: a peer that stops reading can no longer hang the relay; RTT median

Production stalled for everyone after a browser tab froze: a write to that
client blocked once its TCP buffers filled, while holding the connection's write
lock, so the relay's main loop (and the idle reaper that would have dropped the
client, which runs on it) waited on the lock forever. The backlog thresholds
only make that less likely; nothing bounded it.

- Write watchdog (WS + gRPC): each connection records when a write starts; the
  probe thread closes a connection whose write has been blocked > 3 s (socket
  shutdown / stream TryCancel), so the write fails and the lock is freed. The
  client redials (arigl reconnect) and gets a fresh snapshot.
- The probe never waits: Pings use try_write (skip a busy connection) and skip
  one with more than 64 KB unsent.
- gRPC: the server handler's cancel hook is cleared under a mutex before its
  ServerContext goes away.
- state_link_stats: rtt_ms is now the median of the last 8 round trips; the
  moving average carried a 14.8 s probe (answered after a frozen tab woke) for
  minutes.

Tests: StateTransportWsTest.WriteToAPeerThatStopsReadingIsAbortedNotHung (raw
peer that handshakes and never reads; 32 MB media frames), StateLinkStatsTest.
MedianShrugsOffAStall.
#776 (feat/h1-scene) landed on master as a squash, so its room_scene files,
arisrv/main.cpp and both CMakeLists came back as add/add and content
conflicts against the H1 slice-5 commits this branch already carries.
Master's copies of those files are identical to #776's head, which is an
ancestor here, so this branch's versions stand. src/cvc/tests/CMakeLists.txt
also takes master's magick_bootstrap_test (#774) and nav_sim_world_test (#775).
@transfix
transfix merged commit fac62a9 into master Oct 24, 2026
24 checks passed
@transfix
transfix deleted the feat/h1-client branch October 24, 2026 19:51
transfix added a commit that referenced this pull request Oct 24, 2026
Carries the master merge (#779 squash, #786, #787) 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