Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
72f454b
feat(ariadne): headless avatar predict/reconcile/interpolate (H1 slic…
transfix Oct 22, 2026
2de2bb2
fix(ariadne): guard reconcile against stale/duplicate acks; pin tol +…
transfix Oct 22, 2026
6cbde4a
feat(ariadne): cvc::ariadne::holon — clock + avatar bundle on its own…
transfix Oct 22, 2026
de5b1ac
fix(ariadne): holon review fixes — peer sample guard, explicit non-mo…
transfix Oct 22, 2026
cb6c0a3
feat(ariadne): holon <-> state-tree sync over scalar keys (H1 slice 4…
transfix Oct 22, 2026
8ee30c6
feat(arisrv): server-authoritative holon loop + room-set-intent verb …
transfix Oct 22, 2026
4385c9e
Merge remote-tracking branch 'origin/master' into feat/h1-pose-reconcile
transfix Oct 22, 2026
54bf156
Merge branch 'feat/h1-pose-reconcile' into feat/h1-holon
transfix Oct 22, 2026
3eb973c
Merge branch 'feat/h1-holon' into feat/h1-sync
transfix Oct 22, 2026
27be9c2
Merge remote-tracking branch 'origin/master' into feat/h1-sync
transfix Oct 23, 2026
197035b
ci: run the master-targeted matrix (base retargeted from feat/h1-pose…
transfix Oct 23, 2026
0713e70
H1 slice 5: the shared room scene (conf.rooms.<room>.scene.*) + arisr…
transfix Oct 23, 2026
c6804d1
H1 S6 (core): client_pump, clock alignment, sparse-sample peer walk, …
transfix Oct 23, 2026
692ffe1
H1 S6 (client): the room in 3D in arigl — room_view, WASD avatar, fol…
transfix Oct 23, 2026
3a1231d
H1 S6: seed the room with the SceneGraph chrome off (live-test finding)
transfix Oct 23, 2026
6c7bd05
arisrv: reflow the seed comment to 100 columns
transfix Oct 23, 2026
04b2b92
cyprty: ?relay= and ?name= page parameters
transfix Oct 23, 2026
f328113
H1: native and browser clients leave the room alike; arisrv room readout
transfix Oct 23, 2026
a4b56a7
H1 S6: frame the room on arrival, gold-free peer colours, log who is …
transfix Oct 23, 2026
e700d92
arigl: a fresh conference node id per launch
transfix Oct 24, 2026
f24ce2f
arisrv: log the stack on a fatal signal (Linux/glibc)
transfix Oct 24, 2026
8c495b4
Merge remote-tracking branch 'origin/master' into feat/h1-scene
transfix Oct 24, 2026
dc43597
ci: run the master-targeted matrix (base retargeted to master after #…
transfix Oct 24, 2026
64afc1e
Merge branch 'feat/h1-scene' into feat/h1-client
transfix Oct 24, 2026
5025996
arigl: single space before the <random> include comment (changed-line…
transfix Oct 24, 2026
51c3248
Conference link stats: Ping/Pong per connection, background tabs stay…
transfix Oct 24, 2026
f93f614
Merge origin/master into feat/h1-client
transfix Oct 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 63 additions & 25 deletions examples/ariadne/documents/conference_roster.ari
Original file line number Diff line number Diff line change
Expand Up @@ -87,30 +87,58 @@ windows:
bind: (if (= (state-get "mic_on") "1") "● on" "muted")

# ── Users: the roster with per-peer local controls ──
# One compact ROW per participant, as a table of unit widgets: every row is its own grid with the
# SAME fixed column widths, so the rows line up as columns. ping / down / up are each person's
# link to the relay AS THEY MEASURE IT (published to the room once a second; "--" = not measured
# yet). cam / mic / vol are LOCAL controls (what YOU see and hear of them), never sent.
- window: Users
id: users
pos: [390, 40]
size: [380, 540]
size: [420, 260]
closable: true
children:
- text: "in the room"
bind: self.count
- separator
# You (first row): your name lights up GREEN while YOU talk.
- text: "you"
bind: conf.self
visible_when: (not (= (state-get "self.talking") "1"))
- text: "you"
bind: conf.self
color: [0.30, 1.0, 0.40]
visible_when: (= (state-get "self.talking") "1")
- separator
# Everyone else: one row per peer (peer.<i>). The on_tick fills peer.<i>.{pid,name,cam,talking}.
- type: group
id: users_head
layout: { kind: grid, col_widths: [118, 64, 50, 50, 34, 34, 0] }
children:
- text: "name"
- text: "ping"
- text: "down"
- text: "up"
- text: "cam"
- text: "mic"
- text: "vol"
# You: your name lights up GREEN while YOU talk (text colour is static, so it is a visibility
# swap: only one of the two is visible, so they share one cell).
- type: group
id: users_you
layout: { kind: grid, col_widths: [118, 64, 50, 50, 34, 34, 0] }
children:
- text: ""
bind: conf.self
visible_when: (not (= (state-get "self.talking") "1"))
- text: ""
bind: conf.self
color: [0.30, 1.0, 0.40]
visible_when: (= (state-get "self.talking") "1")
- text: ""
bind: self.ping
- text: ""
bind: self.down
- text: ""
bind: self.up
- text: ""
- text: ""
- text: "(you)"
# Everyone else: one row per peer (peer.<i>), filled by on_tick.
- type: group
repeat: (state-get "peer.count")
layout: { kind: grid, col_widths: [118, 64, 50, 50, 34, 34, 0] }
children:
# Name — click to open + raise this peer's camera window (cam_<i>). Two texts: the GREEN one
# shows only while they are talking (text colour is static, so "light up" is a visibility swap).
# Name: click to open + raise this peer's camera window (cam_<i>); GREEN while talking.
- text: ""
bind: peer.{i}.name
on_click: (begin (state-set "tree.cam_{i}.open" "1") (state-set "tree.cam_{i}.raise" "1"))
Expand All @@ -120,24 +148,30 @@ windows:
color: [0.30, 1.0, 0.40]
on_click: (begin (state-set "tree.cam_{i}.open" "1") (state-set "tree.cam_{i}.raise" "1"))
visible_when: (= (state-get "peer.{i}.talking") "1")
- text: ""
bind: peer.{i}.ping
- text: ""
bind: peer.{i}.down
- text: ""
bind: peer.{i}.up
# Local mute toggles (checked = on). Default on: the host treats von/aon as muted only when
# explicitly "0", so an untouched peer is shown + heard.
# explicitly "0", so an untouched peer is shown + heard. "##" hides the label (the header
# names the column).
- type: checkbox
label: "cam"
label: "##cam"
bind: peer.{i}.von
def: true
- type: checkbox
label: "mic"
label: "##mic"
bind: peer.{i}.aon
def: true
# Local volume in our mix (1 = unity, up to 2x boost).
- type: slider_float
label: "vol"
label: "##vol"
bind: peer.{i}.vol
lo: 0
hi: 2
def: 1
- separator

# ── One camera window per OTHER participant, titled by their name ──
- window: Peer
Expand All @@ -163,16 +197,16 @@ on_tick: |
; every cam_<i> declares the SAME authored pos:, so without this the per-peer windows land
; exactly on top of one another and look like a single window — the #1 reason a user reports
; "I can't see the other cameras". Tile them in a 2-column grid to the RIGHT of the Users
; window (which sits at x≈390–610): the roster is NEVER covered, and up to four cameras show
; window (which sits at x≈390–800): the roster is NEVER covered, and up to four cameras show
; side-by-side without overlapping on a laptop screen. Seeded ONCE here, before any peer window
; exists, so each window's first-use geometry seed lands at its own spot; the user's drag then
; persists and wins. Covers 6 peers; a 7th+ falls back to the authored pos.
(state-set "tree.cam_0.geometry" "612,66,330,290")
(state-set "tree.cam_1.geometry" "952,66,330,290")
(state-set "tree.cam_2.geometry" "612,364,330,290")
(state-set "tree.cam_3.geometry" "952,364,330,290")
(state-set "tree.cam_4.geometry" "612,662,330,290")
(state-set "tree.cam_5.geometry" "952,662,330,290")
(state-set "tree.cam_0.geometry" "806,66,330,290")
(state-set "tree.cam_1.geometry" "1146,66,330,290")
(state-set "tree.cam_2.geometry" "806,364,330,290")
(state-set "tree.cam_3.geometry" "1146,364,330,290")
(state-set "tree.cam_4.geometry" "806,662,330,290")
(state-set "tree.cam_5.geometry" "1146,662,330,290")
(state-set "cam_on" "1") ; on/off toggles start ON (streams open unmuted)
(state-set "mic_on" "1")
(room-join "" (state-get "conf.room") (dict "name" (state-get "conf.self")))
Expand Down Expand Up @@ -251,6 +285,10 @@ on_tick: |
(state-get (str-concat "media." pid ".camera")))
(state-set (str-concat "peer." idx ".talking")
(state-get (str-concat "media." pid ".talking")))
; their link quality, as THEY measure it (ping to the relay, bits/s down + up)
(state-set (str-concat "peer." idx ".ping") (get-attr p "ping"))
(state-set (str-concat "peer." idx ".down") (get-attr p "down"))
(state-set (str-concat "peer." idx ".up") (get-attr p "up"))
(set idx (+ idx 1))
(set total (+ total 1))))))
(state-set "peer.count" (str idx))
Expand Down
80 changes: 80 additions & 0 deletions inc/cvc/ariadne/conference/room_net.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
Copyright 2026 The University of Texas at Austin

This file is part of libcvc.

libcvc is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 2.1 as published by the Free Software Foundation.

libcvc is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*/

#ifndef __CVC_ARIADNE_CONFERENCE_ROOM_NET_H__
#define __CVC_ARIADNE_CONFERENCE_ROOM_NET_H__

#include <string>

namespace cvc {
class app;

namespace ariadne {
namespace conference {

// ----------------
// Network quality in the shared room tree
// ----------------
// What each end measures about its links (state_transport::link_reports: the Ping/Pong round trip
// and traffic both ways), published as scalar leaves beside the rest of the room so every
// participant can see everyone's connection quality:
//
// conf.rooms.<room>.participants.<pid>.net.{rtt_ms, in_kbps, out_kbps}
// the participant's OWN measurement of its link to the relay -- written by that client (like
// its .intent), shown next to its name in everyone's roster
// conf.rooms.<room>.participants.<pid>.net.relay.{rtt_ms, in_kbps, out_kbps}
// the RELAY's measurement of its link to that participant (in = what the relay receives from
// it) -- written by the relay, like .pose
// conf.rooms.<room>.server.net.{in_kbps, out_kbps, links}
// the relay's totals over all of its links
//
// Values are kilobits/second and milliseconds; negative = not measured yet.

struct net_sample {
double rtt_ms = -1.0;
double in_kbps = -1.0;
double out_kbps = -1.0;
};

// A participant's own measurement (client side).
void write_participant_net(cvc::app &app, const std::string &room, const std::string &pid,
const net_sample &s);
bool read_participant_net(cvc::app &app, const std::string &room, const std::string &pid,
net_sample &out);

// The relay's measurement of its link to a participant (server side).
void write_relay_net(cvc::app &app, const std::string &room, const std::string &pid,
const net_sample &s);
bool read_relay_net(cvc::app &app, const std::string &room, const std::string &pid,
net_sample &out);

// The relay's totals.
struct server_net {
double in_kbps = 0.0;
double out_kbps = 0.0;
int links = 0;
};
void write_server_net(cvc::app &app, const std::string &room, const server_net &s);
bool read_server_net(cvc::app &app, const std::string &room, server_net &out);

// Compact display text for a roster cell: "42 ms" / "--"; "850k" / "1.2M" / "--" (bits/s).
std::string format_rtt(double rtt_ms);
std::string format_rate(double kbps);

} // namespace conference
} // namespace ariadne
} // namespace cvc

#endif // __CVC_ARIADNE_CONFERENCE_ROOM_NET_H__
4 changes: 2 additions & 2 deletions inc/cvc/ariadne/conference/room_scene.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ namespace conference {
//
// conf.rooms.<room>.scene.rev bumped by every scene write
// conf.rooms.<room>.scene.env.background.{gradient, top.{r,g,b}, bottom.{r,g,b}}
// conf.rooms.<room>.scene.env.chrome the SceneGraph grid/axis chrome (1/0)
// conf.rooms.<room>.scene.env.chrome the SceneGraph diagnostic chrome (1/0)
// conf.rooms.<room>.scene.env.lights.<id>.{present, kind, rig, azimuth, elevation, intensity,
// cone, color.{r,g,b}, pos.{x,y,z}, target.{x,y,z}}
// conf.rooms.<room>.scene.objects.<id>.{present, source.{uri, primitive, plane_size,
Expand Down Expand Up @@ -97,7 +97,7 @@ std::uint64_t scene_revision(cvc::app &app, const std::string &room_id);
cvc::ariadne::Scene read_room_scene(cvc::app &app, const std::string &room_id);

// The MVP environment the server seeds a room with (§5g "ground + grid + a reference object"): a
// gradient background, the grid chrome on, a directional sun, a 20 m ground plane and a 1 m
// gradient background, the diagnostic chrome off, a directional sun, a 20 m ground plane and a 1 m
// reference box standing on it at the origin. Z is up, as in every .ari scene.
room_scene_env default_scene_env();
std::vector<cvc::ariadne::SceneNode> default_scene_objects();
Expand Down
41 changes: 38 additions & 3 deletions inc/cvc/ariadne/holon.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <cvc/physics/pose_tracker.h>
#include <map>
#include <string>
#include <vector>

namespace cvc {
namespace ariadne {
Expand Down Expand Up @@ -57,8 +58,18 @@ namespace ariadne {
enum class holon_role { server, client };

struct holon_config {
// A holon is advanced at its HOST's cadence, not a render loop's: arisrv pumps its server holon
// at ~5 Hz (24 quanta due per advance at 120 Hz) and a throttled browser tab may run at 10 fps.
// world_clock's default catch-up cap (8 quanta, sized for a 60+ Hz frame loop) would silently
// drop the rest -- running the room's avatars, and its tick, at a third of real time -- so a
// holon's clock allows 0.5 s of catch-up per advance.
holon_config() { clock.max_steps_per_advance = 60; }

cvc::world_clock::config clock; // the holon's own quantum / scale / mode
double reconcile_tol = 0.05; // metres; the client's snap threshold (slice 2)
// CLIENT: how far (in ticks) the replica's clock may drift from the server's tick before
// apply_authoritative re-aligns it. 60 = 0.5 s at the default 120 Hz. See apply_authoritative.
std::uint64_t max_skew_ticks = 60;
};

class holon {
Expand Down Expand Up @@ -88,6 +99,8 @@ class holon {
const cvc::physics::body_state &start = cvc::physics::body_state{});
void remove_participant(const std::string &pid);
bool has_participant(const std::string &pid) const;
// Every registered pid, in pid order.
std::vector<std::string> participants() const;

// Set a participant's latest intent. Server: the validated input it integrates
// into the authoritative pose. Client: only the LOCAL avatar's own input (a
Expand All @@ -98,6 +111,15 @@ class holon {
// arrived via replication. The local avatar reconciles to it (snap + replay on
// divergence); a peer takes it as its newest interpolation sample. No-op on a
// server holon or an unknown pid.
//
// Reconcile compares ticks, so the replica's clock must count the server's ticks.
// The two clocks start independently (a client joins a server that has run for
// hours), so a sample more than max_skew_ticks from the replica's tick -- the first
// one after joining, or one after a backgrounded browser tab stalled the frame
// loop -- re-aligns the replica: its clock seeks to server_tick and the local
// avatar's unacknowledged input log is dropped (its ticks no longer mean anything;
// its state is kept). Between re-alignments both clocks run from wall time at the
// same rate.
void apply_authoritative(const std::string &pid, const cvc::physics::body_state &auth,
std::uint64_t server_tick);

Expand All @@ -112,23 +134,36 @@ class holon {
// std::out_of_range for an unknown pid.
cvc::physics::body_state state(const std::string &pid) const;

// A participant's DISPLAY pose at the current frame's interpolation alpha:
// predicted for the local avatar, interpolated between the last two samples for a
// peer (never extrapolated), the integrated pose on a server.
// A participant's DISPLAY pose this frame: predicted for the local avatar, the
// integrated pose on a server, and for a peer an interpolation between its last
// two samples (never extrapolated). Samples arrive far less often than quanta (the
// relay publishes ~5 Hz against a 120 Hz clock), so a peer is shown ONE SAMPLE
// INTERVAL behind: the newest sample's arrival starts a walk from `prev` to
// `newest` that takes as long as the server took between the two (their
// server_tick gap), measured on this holon's own clock (tick + alpha).
cvc::physics::pose display_pose(const std::string &pid) const;

// CLIENT: the position jumps reconcile has applied to the local avatar `pid`
// (snap + replay on divergence) since the last call, summed, then cleared. A
// renderer eases this out so a correction reads as a glide rather than a jump; the
// holon itself stays exact. Zero for a peer, on a server, or for an unknown pid.
cvc::physics::vec3 take_correction(const std::string &pid);

private:
struct participant {
bool local = false;
avatar_sim sim; // server authoritative integrator
avatar_intent intent; // latest intent (server input / client-local input)
predicted_avatar predicted; // client-local prediction + reconcile
cvc::physics::vec3 correction{0.0, 0.0, 0.0}; // reconcile jumps not yet taken
// peer interpolation: the last two authoritative samples + a monotonic guard
// (mirroring predicted_avatar's stale/duplicate-ack rejection) so a reordered
// or duplicate peer sample never rewinds or freezes the interpolation window.
bool have_sample = false;
std::uint64_t last_sample_tick = 0;
cvc::physics::body_state prev, newest;
std::uint64_t newest_arrival_tick = 0; // this clock's tick when `newest` arrived
std::uint64_t sample_span = 0; // server ticks from prev to newest (0 = one sample)
};

void on_step(const cvc::clock_driver::clock_frame &f);
Expand Down
20 changes: 18 additions & 2 deletions inc/cvc/ariadne/holon_sync.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,28 @@ bool read_intent(cvc::app &app, const std::string &room, const std::string &pid,
// read its proposed .intent, feed it to the holon; advance the holon one frame;
// then write every participant's authoritative .pose + .server_tick back to the
// tree (which replicates to the clients). The caller supplies the server holon
// and calls this once per relay tick. A departed participant simply stops
// receiving intent/pose updates (reaping its holon slot is a follow-up). wall_dt
// and calls this once per relay tick. A participant who left (room_leave) is dropped
// from the holon, so a rejoin respawns on the spawn ring. wall_dt
// is forwarded to holon::advance: the default (-1) measures real elapsed wall time
// (what the relay wants); a test injects a fixed dt for a deterministic step count.
void server_pump(holon &h, cvc::app &app, const std::string &room, double wall_dt = -1.0);

// Drive one frame of a CLIENT holon against the replicated room tree (H1 slice 6), the mirror of
// server_pump: register every present participant (room_roster) -- SELF_PID as the local,
// predicted avatar, everyone else as an interpolated peer, each starting at its replicated pose
// when one exists -- drop the ones that left, apply each replicated authoritative .pose (the holon
// ignores a sample it has already seen, so re-reading an unchanged pose every frame is harmless),
// then advance the holon one frame. Read the result with h.display_pose(pid). wall_dt as in
// server_pump.
void client_pump(holon &h, cvc::app &app, const std::string &room, const std::string &self_pid,
double wall_dt = -1.0);

// Propose the local avatar's intent (client side): the client holon predicts with it at once and
// it is written to <self>.intent for the server to validate. Call it when the intent CHANGES (each
// call is a replicated write), not every frame.
void propose_intent(holon &h, cvc::app &app, const std::string &room, const std::string &self_pid,
const avatar_intent &intent);

} // namespace ariadne
} // namespace cvc

Expand Down
6 changes: 6 additions & 0 deletions inc/cvc/gl/CameraController.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ class CameraController : public cvc::state_object<CameraController> {
// camera would drift on its own.
void releaseHeldKeys();

// Whether `keySym` (a VTK key sym: "w", "Up", ...; a single letter matches either
// case) is held right now -- the same held set Fly navigation reads, so a host
// driving something else from the keys (an avatar in Orbit mode) sees exactly what
// the camera does, including the drop when a UI overlay takes the keyboard.
bool keyHeld(const std::string &keySym) const;

// Rebind a movement key (also settable via state "keys.<action>"). Actions:
// "forward","backward","strafe_left","strafe_right","up","down","toggle_mode",
// "sprint". Values are VTK key syms ("w","space","Control_L","Tab","Shift_L").
Expand Down
Loading
Loading