Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
148 changes: 148 additions & 0 deletions inc/cvc/ariadne/holon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
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_HOLON_H__
#define __CVC_ARIADNE_HOLON_H__

#include <cstdint>
#include <cvc/ariadne/avatar_sim.h>
#include <cvc/core/clock_driver.h>
#include <cvc/core/namespace.h>
#include <cvc/core/world_clock.h>
#include <cvc/physics/body.h>
#include <cvc/physics/pose_tracker.h>
#include <map>
#include <string>

namespace cvc {
namespace ariadne {

// ----------------
// cvc::ariadne::holon (H1 slice 3)
// ----------------
// A holon is a server-authoritative, client-replicable bundle of ONE world_clock
// and the participant avatars that live on it. It is the object the roadmap's §5h
// names: the server drives the clock and owns every avatar's authoritative pose; a
// client runs a replica -- it PREDICTS its own avatar locally (slice 2) and
// RECONCILES to the authoritative state when it arrives, and INTERPOLATES peers.
//
// This slice is the headless clock + avatar-pose core. The holon owns its own
// world_clock and cvc::clock_driver BY VALUE (never the app-global clock, so two
// holons in one process never fight over time), and it drives avatar integration
// as a clock_driver STEP observer and presentation as a FRAME observer -- the same
// step-then-present shape tick_scene uses. The shared SCENE geometry, the Jolt
// cvc::physics::world (replacing the linear avatar_sim behind the same shape), and
// the cvcGL rendering of all this layer on top in later slices; none of that is
// here, so the model stays headless and deterministically testable.
//
// Authority (slice 4 adds the verb path): a client NEVER writes another avatar's
// pose; it writes only its own intent, the server validates/vetoes/overrides and
// is the sole writer of the authoritative pose, and the client respects that on
// reconcile. This class embodies that split -- set_intent on a client only drives
// its own (local) avatar's prediction; apply_authoritative is how the server's
// word reaches the client.

enum class holon_role { server, client };

struct holon_config {
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)
};

class holon {
public:
explicit holon(holon_role role, holon_config cfg = holon_config{});

// Non-copyable and non-movable: the clock_driver observers capture `this` and
// _driver holds a reference to _clock, so a moved holon would dangle both. This
// is already implicit (world_clock's mutex deletes the defaults), but the
// explicit deletes pin the invariant against a future refactor that makes the
// clock movable.
holon(const holon &) = delete;
holon &operator=(const holon &) = delete;
holon(holon &&) = delete;
holon &operator=(holon &&) = delete;

holon_role role() const noexcept { return _role; }
cvc::world_clock &clock() noexcept { return _clock; }
const cvc::world_clock &clock() const noexcept { return _clock; }
cvc::clock_driver &driver() noexcept { return _driver; }
std::uint64_t tick() const { return _clock.tick(); }

// Register a participant. `local` marks the CLIENT's own avatar (predicted); on a
// server every participant is authoritative, and on a client a non-local
// participant is an interpolated peer.
void add_participant(const std::string &pid, bool local = false,
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;

// 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
// non-local pid is ignored -- a client cannot drive a peer).
void set_intent(const std::string &pid, const avatar_intent &in);

// CLIENT ONLY: an authoritative state for `pid`, valid as of `server_tick`,
// 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.
void apply_authoritative(const std::string &pid, const cvc::physics::body_state &auth,
std::uint64_t server_tick);

// Advance the holon one frame. SERVER: drives its clock from wall time (wall_dt<0)
// or an injected dt, then integrates every authoritative avatar over the quanta
// that fell due. CLIENT: drives its clock to predict its own avatar over the due
// quanta; peers are sample-driven (apply_authoritative) and only re-interpolated.
void advance(double wall_dt = -1.0);

// A participant's authoritative body_state: the server's integrated state, the
// client-local avatar's current prediction, or a peer's newest sample. Throws
// 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.
cvc::physics::pose display_pose(const std::string &pid) const;

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
// 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;
};

void on_step(const cvc::clock_driver::clock_frame &f);
void on_present(const cvc::clock_driver::clock_frame &f);

holon_role _role;
holon_config _cfg;
cvc::world_clock _clock; // declared before _driver: _driver references it
cvc::clock_driver _driver;
std::map<std::string, participant> _participants;
double _alpha = 0.0; // the last frame's interpolation alpha, for display_pose
};

} // namespace ariadne
} // namespace cvc

#endif // __CVC_ARIADNE_HOLON_H__
96 changes: 96 additions & 0 deletions inc/cvc/ariadne/holon_sync.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
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_HOLON_SYNC_H__
#define __CVC_ARIADNE_HOLON_SYNC_H__

#include <cstdint>
#include <cvc/ariadne/avatar_sim.h>
#include <cvc/core/namespace.h>
#include <cvc/physics/body.h>
#include <string>

namespace cvc {

class app;

namespace ariadne {

class holon;

// ----------------
// cvc::ariadne — holon <-> state-tree sync over SCALAR keys (H1 slice 4)
// ----------------
// Connects a 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.{x,y,z} authoritative, SERVER-written
// <participant>.pose.orient.{w,x,y,z}
// <participant>.pose.vel.lin.{x,y,z}
// <participant>.pose.vel.ang.{x,y,z}
// <participant>.pose.server_tick the tick the pose is valid at
// <participant>.intent.move.{x,y,z} proposed, CLIENT-written
//
// Scalar leaves (not a typed body_state data() object) are used on purpose: they
// already journal and replicate through the existing "conf" interest filter with
// zero transport changes, and they sidestep the data()-object replication work
// (DISTRIBUTED_STATE §52). Each double is written round-trip-safe (17 significant
// digits), so a reader recovers it to within far less than the reconcile
// tolerance.
//
// 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. A client reads .pose
// read-only and reconciles. Writes go through a C++ host (these helpers / a verb),
// never a DSL (state-set), because the document chroot ui.docs.* is excluded from
// replication -- the same reason room-join is a C++ verb. The connection<->pid
// admission binding that would make "sole writer" cryptographically enforced is
// the §5e/mesh increment (H2); H1 is loopback-trusted sole-writer-by-convention.

// Write a participant's AUTHORITATIVE pose (server side): the body_state's pose +
// velocity leaves and the server_tick, each replicated (touch-before-value).
void write_pose(cvc::app &app, const std::string &room, const std::string &pid,
const cvc::physics::body_state &state, std::uint64_t server_tick);

// Read a participant's authoritative pose. Returns false (leaving out_* untouched)
// if the pose subtree is absent or malformed; true with out_state / out_server_tick
// filled otherwise.
bool read_pose(cvc::app &app, const std::string &room, const std::string &pid,
cvc::physics::body_state &out_state, std::uint64_t &out_server_tick);

// Write a participant's PROPOSED intent (client side -- its OWN avatar only).
void write_intent(cvc::app &app, const std::string &room, const std::string &pid,
const avatar_intent &intent);

// Read a participant's proposed intent (server side). Returns false if absent.
bool read_intent(cvc::app &app, const std::string &room, const std::string &pid,
avatar_intent &out_intent);

// Drive one authoritative frame of a SERVER holon against the replicated room
// tree: for every present participant (room_roster), add it to the holon if new,
// 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
// 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);

} // namespace ariadne
} // namespace cvc

#endif // __CVC_ARIADNE_HOLON_SYNC_H__
12 changes: 12 additions & 0 deletions src/arisrv/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
#include <cstring>
#include <cvc/ariadne/conference/room_intrinsics.h> // room_leave (retract a departed peer)
#include <cvc/ariadne/conference/room_media_intrinsics.h> // room_relay_tick
#include <cvc/ariadne/holon.h> // server-authoritative avatar holon (H1)
#include <cvc/ariadne/holon_sync.h> // server_pump: intent -> holon -> authoritative pose
#include <cvc/ariadne/stream/stream_relay.h>
#include <cvc/core/app.h>
#include <cvc/media/media_router.h> // protocol-agnostic media hub (WS + gRPC + future protocols)
Expand Down Expand Up @@ -150,6 +152,13 @@ int main(int argc, char **argv) {
#if CVC_ENABLE_WS
int reapticks = 0; // cadence for reaping ghost (navigated-away) connections
#endif
// The server-authoritative avatar holon for this room (H1): each tick it reads
// every participant's proposed intent from the replicated tree, integrates it on
// its own clock, and writes the authoritative pose back (fanned to clients by the
// drain_local block below). The relay loop paces ~5 Hz, so the holon catches up
// several quanta per tick and clients interpolate between the ~5 Hz pose samples;
// a smoother authoritative rate is a later tuning (tick the holon on its own).
cvc::ariadne::holon server_holon(cvc::ariadne::holon_role::server);
while (!g_stop.load()) {
#if CVC_ENABLE_WS
// Reap ghosts (~every second): a tab that navigated or reloaded away without a clean WebSocket
Expand Down Expand Up @@ -182,6 +191,9 @@ int main(int argc, char **argv) {
}
#endif
cvc::ariadne::conference::room_relay_tick(app, room, relay, relayed);
// Advance the authoritative avatar holon and write each participant's pose. The
// pose writes are local shard mutations, fanned to clients by the drain below.
cvc::ariadne::server_pump(server_holon, app, room);
#if CVC_ENABLE_WS
// Fan the relay's OWN room-tree writes — the room_leave retractions queued above and
// room_relay_tick's per-stream "ready" markers — out to EVERY client on BOTH transports. The
Expand Down
4 changes: 3 additions & 1 deletion src/cvc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1308,12 +1308,14 @@ list(APPEND INCLUDE_FILES ../../inc/cvc/net/http_client.h)
# the retained Widget tree, the Runtime + reconcile boundary, and direct
# cvc::state binding, walking a pluggable Backend. It touches NO VTK/ImGui/GL —
# the ImGui-over-VTK backend lives in cvcGL (src/cvcGL/ariadne). Roadmap §16.1.
list(APPEND SOURCE_FILES ariadne/ariadne.cpp ariadne/loader.cpp ariadne/uri.cpp ariadne/uri_state.cpp ariadne/uri_http.cpp ariadne/uri_http_cache.cpp ariadne/net_intrinsics.cpp ariadne/stream_intrinsics.cpp ariadne/conference/room_intrinsics.cpp ariadne/state_io.cpp ariadne/ftxui_backend.cpp ariadne/term_graphics.cpp ariadne/tvision_backend.cpp ariadne/app_runtime.cpp ariadne/doc_args.cpp ariadne/host_cli.cpp ariadne/host_signals.cpp ariadne/requires.cpp ariadne/host_info.cpp ariadne/avatar_sim.cpp)
list(APPEND SOURCE_FILES ariadne/ariadne.cpp ariadne/loader.cpp ariadne/uri.cpp ariadne/uri_state.cpp ariadne/uri_http.cpp ariadne/uri_http_cache.cpp ariadne/net_intrinsics.cpp ariadne/stream_intrinsics.cpp ariadne/conference/room_intrinsics.cpp ariadne/state_io.cpp ariadne/ftxui_backend.cpp ariadne/term_graphics.cpp ariadne/tvision_backend.cpp ariadne/app_runtime.cpp ariadne/doc_args.cpp ariadne/host_cli.cpp ariadne/host_signals.cpp ariadne/requires.cpp ariadne/host_info.cpp ariadne/avatar_sim.cpp ariadne/holon.cpp ariadne/holon_sync.cpp)
list(APPEND INCLUDE_FILES
../../inc/cvc/ariadne/widget.h
../../inc/cvc/ariadne/backend.h
../../inc/cvc/ariadne/ariadne.h
../../inc/cvc/ariadne/avatar_sim.h
../../inc/cvc/ariadne/holon.h
../../inc/cvc/ariadne/holon_sync.h
../../inc/cvc/ariadne/app_runtime.h
../../inc/cvc/ariadne/doc_args.h
../../inc/cvc/ariadne/host_cli.h
Expand Down
Loading
Loading