Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 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
ef78731
Conference link stats: Ping/Pong probes, per-link traffic, room net s…
transfix Oct 24, 2026
5cca3d8
state_transport_grpc_test: leave only after the relay has read the Hello
transfix Oct 24, 2026
5025996
arigl: single space before the <random> include comment (changed-line…
transfix Oct 24, 2026
feeca61
Merge branch 'feat/h1-client' into feat/conf-link-stats
transfix Oct 24, 2026
a4e5e15
conference_roster: widen the ping column (a congested link reads 4 di…
transfix Oct 24, 2026
e0e774a
Transports: a peer that stops reading can no longer hang the relay; R…
transfix Oct 24, 2026
a279d01
Room: click-to-face, name labels, stamped intents and a client clock …
transfix Oct 24, 2026
fd70013
Room: name labels are a scene annotation, drawn even with the panels …
transfix Oct 24, 2026
76ff312
Room: the conference document declares the light rig and real-time sh…
transfix Oct 24, 2026
a10692b
Room: rigid-body avatars in a Jolt world
transfix Oct 24, 2026
15ad0e1
arigl: propose the user's intent only when they change it; follow the…
transfix Oct 24, 2026
ecb22a4
arigl: until the user clicks to face, W walks into the screen
transfix Oct 24, 2026
51c3248
Conference link stats: Ping/Pong per connection, background tabs stay…
transfix Oct 24, 2026
9ffad69
Merge branch 'feat/h1-client' into feat/h1-facing
transfix Oct 24, 2026
f93f614
Merge origin/master into feat/h1-client
transfix Oct 24, 2026
1ad8bdf
Merge feat/h1-client into feat/h1-facing
transfix Oct 24, 2026
3dc9b1f
Merge origin/master into feat/h1-facing
transfix Oct 24, 2026
e5fe0eb
Holon authority: the server vetoes intents and can override avatars (…
transfix Oct 24, 2026
f25586d
S7: relay-side avatar interest, ghost-free clients, opt-in mesh LOD (…
transfix Oct 25, 2026
591cc28
clang-format the S7 lines #783's changed-lines check flags
transfix Oct 25, 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
306 changes: 287 additions & 19 deletions docs/roadmap/ARIADNE_CONFERENCE_ROADMAP.md

Large diffs are not rendered by default.

120 changes: 117 additions & 3 deletions docs/roadmap/DISTRIBUTED_STATE_ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Distributed State Synchronization Roadmap

Date: May 22, 2026 (status snapshot corrected 2026-08-11)
Date: May 22, 2026 (status snapshot corrected 2026-08-11; conference-relay primitives added 2026-10-24)

## Status Snapshot

Expand Down Expand Up @@ -49,6 +49,109 @@ later that week. Also present on `master` today:
- **Fork-based multi-process IPC integration tests**, exercising real
cross-process replication rather than in-process fan-out.

## Landed for the conference relay (S7, 2026-10)

The Ariadne conference relay's spatial interest (`ARIADNE_CONFERENCE_ROADMAP.md`
§5h.1) needed per-peer, outbound filtering in this layer. Each piece below is
opt-in, or a no-op until a caller installs something; the policy (who gets
what) stays with the caller, here `room_aoi_relay`. None of it is access
control: a peer with no filter still receives everything.

- **Per-peer delivery predicate** (`state_peer_registry.h`). A
`state_delivery_filter` (`bool deliver(const std::string &path) const
noexcept`) is installed per known peer with `set_filter(node_id, filter)`
(false for an unknown peer; `nullptr` clears). `should_deliver` now passes
only when the peer's subscription prefixes match **and** its filter
delivers; an unknown peer still delivers everything (the back-compat
default). The `shared_ptr` is copied under the registry lock and evaluated
outside it, and a displaced filter is released after the lock drops.
`view_of(node_id)` returns a `delivery_view` (`known`, `subscriptions`,
`filter`, `deliver()`, `filtered()`) so a batch sender takes one lock per
connection, not per entry. `add_peer` on an existing id keeps its filter;
`remove_peer` drops it. Filters must be immutable once installed and must
never call back into the registry or a transport. The conference filter is
**deny-only and fails open** — a new leaf family can never be dropped
silently, which a prefix whitelist would do — and that is the recommended
shape.
- **Write-once connection identity** (gRPC + WS). The reader thread writes a
connection's peer id once, at its first Hello, and publishes it with a
release/acquire `identified` flag; every off-thread reader goes through
`peer_id()` (`nullptr` until then, or when that Hello named no node:
"unknown, deliver"), and a later Hello is ignored. (On gRPC a Hello with an
empty node id does not count as the first.) This fixes a data race: the
reader thread wrote `remote_node_id` while `publish()` read it unlocked.
- **The registry on every room-state send.** Before S7 only a transport's own
publish paths (`publish()`, `publish_message()`) consulted it, and the
`set_relay` forwards ignored it. Now so do both `set_relay` forward loops
(and so the bridge leg, which goes through the destination transport's
`publish()`), the gRPC snapshot responder (by the requester's view), and
the WebSocket ~1 Hz `broadcast_snapshot` (each connection gets only the
entries its view passes; since then the broadcast is also a per-connection
diff, see `state_transport_ws.h`). A skip is counted by
`note_delivery_filtered`. The admit
snapshot (`send_snapshot_locked`) stays unfiltered by design: it goes out
before the peer's Hello. On the client side, unsolicited-snapshot ingest
now skips entries a shard with enforced interest would reject, so a
relay's whole-tree snapshot no longer writes outside the client's interest.
- **Targeted snapshot push** — `state_transport::push_snapshot_to(node_id,
entries)`. gRPC and WS write one final, `request_id` 0 `SnapshotResponse`
holding exactly `entries` to every live connection identified as
`node_id` and return the number of connections written (0, with nothing
encoded, when none match or `entries` is empty; WS also skips a connection
with more than 2 MB of send backlog). The registry is not consulted: the
caller already decided. Called from the same thread as `publish()`, a push
lands ahead of later live writes on that connection. inproc/ipc keep the
base default (0). Clients apply it as any unsolicited snapshot, which never
blocks later last-writer-wins writes.
- **Relay interest gate** — `set_relay_requires_interest(bool)` on both
transports (default off), with `total_relay_withheld()`. When on, a
forwarded mutation or message that no local same-cluster shard `admits()`
/ `admits_message()` (the ingest gate, exposed on `state_cluster_shard`:
a delegation op always passes, anything else when its path is of interest)
is withheld from **both** the relay fan and the bridge. Ingest is
unchanged, and with no matching local shard everything is forwarded.
- **Journal trim and O(new) replay.** `state_change_journal::replay_after`
is a binary search (sequences are strictly increasing because `append`
assigns them under the lock), and `trim_through(sequence)` erases a
prefix while numbering continues. `state_cluster_shard::trim_published()`
trims through the publish cursor, so nothing undrained is lost. It is for a
node whose joiners bootstrap from the snapshot instead of a journal backfill
— set `set_backfill_on_connect(false)` with it, or late joiners miss the
trimmed writes; a rewind into the trimmed range replays only what survived.
arisrv does both on its relay (`--relay-journal trim`); clients are not
trimmed.
- **Rooted client shard.** The fourth `state_cluster_shard` constructor
argument, `root_path`, scopes what the adapter journals to one subtree.
arigl now passes `"conf"` (`CVC_ARIGL_REPLICATE_ROOT` overrides it; empty or
`*` is the whole tree), so per-document `ui.docs.*` state is never
journaled, drained or sent. Interest enforcement still filters what comes
in.
- **Media gate and gRPC stall drop** (`media_endpoint.h`, both transports).
`set_media_gate(media_gate_fn)`, where `media_gate_fn` is
`std::function<bool(const media_frame &, const std::string
&receiver_node_id)>`, is asked once per identified receiver per frame on
the router fan-out (`fan_media`); an unidentified receiver always gets the
frame, a node's own uplink (`publish_media_frame`) is never gated, and an
empty gate is the old full fan-out. Denials count in `total_media_gated()`.
The gate runs on the publisher's reader thread with no lock held, so it
must be cheap and must not throw. gRPC adds `set_media_stall_drop(ms)`
(0 = off, the library default): a receiver whose in-flight media write
started longer ago than that, or whose stream a frame cannot get within
it (`write_mu` is a timed mutex), is skipped (`total_media_stall_dropped()`),
the counterpart of the WS leg's backlog drop. That drop is now
`set_media_backlog_drop(bytes)` (32 KB by default, 0 = off), and its skips
count in `total_media_backlog_dropped()`. arisrv's 5 s AOI line prints the
count as `backlog`. The gate is asked only once the stream is held, so a
dropped frame spends no thumbnail. The relay loopback tests turn both drops
off, so they don't shed a frame on a loaded runner.
- **Accepted-stream lifetime (gRPC).** A server handler's stream dies when
`Channel()` returns, while publish, relay forwards and the link probe write
to it from other threads. The handler now cancels the stream and takes
`write_mu` once before returning; every write runs under `write_mu` and
checks `alive` there, so none is in flight or starts after the stream goes
(it was a use-after-free on every disconnect, seen as crashes in
`CompletionQueue::Pluck` under parallel load on Windows).

## Known gaps in the "landed" surface

These are implemented on the receive side or in isolation, and are easy to
Expand All @@ -64,7 +167,14 @@ mistake for finished:
guard, the legacy oversized-`value()` resolver).
- **Backpressure and slow-peer isolation are `inproc`-only.** The bounded
outbox, drop policies and quarantine live in `state_transport_inproc`; the
IPC and gRPC transports publish synchronously with no bounded queue.
IPC and gRPC transports publish synchronously with no bounded queue. (The
gRPC media stall drop above covers media frames only. The WebSocket leg's
backlog skips read `SIOCOUTQ`, which is Linux-only, so they never fire on a
Windows-hosted relay.)
- **Snapshot coverage differs by transport.** WebSocket has no
`request_snapshot` (`WS_STATE_TRANSPORT.md` Phase 2), and gRPC has no
counterpart of the WS ~1 Hz `broadcast_snapshot`, so a gRPC client has no
periodic recovery backstop (it can still call `request_snapshot`).
- **`distributed_state_config::blob_store_path` is not read.** `join()`
unconditionally constructs a `memory_state_blob_store`, so the field — which
`USAGE.md` documents as selecting filesystem persistence — currently has no
Expand All @@ -81,7 +191,11 @@ mistake for finished:
to `master`.

The journal is also **in-memory only** (`std::vector<state_mutation>`), so a
process restart loses all replay history; there is no durable/WAL log.
process restart loses all replay history; there is no durable/WAL log. Since
S7 a node can trim it (`trim_published()`, above), but only a node that turns
backfill off may. A client's journal is not trimmed and still grows with its
own writes: its dial-time backfill re-sends what it wrote while disconnected,
so client compaction has to preserve that and is still open.

## Related: state_exec DSL Engine

Expand Down
13 changes: 12 additions & 1 deletion docs/roadmap/VISIBILITY-AND-LOD-ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Visibility, Culling & LOD — Design & Roadmap

**Module:** `cvc::vis` (headless, in `inc/cvc/vis/` + `src/cvc/vis/`) plus a thin VTK-facing adapter in `cvcGL`.
**Status:** **LANDED (PR #249+).** The geometry/scene LOD described here is implemented — `inc/cvc/lod/select.h` (rung selection + hysteresis + the budget `solve()`), `lod/pyramid.h`, `cvc/geometry/simplify.h` (native QEM), `lod/tiles.h` (partition + tiled pyramids), `lod/store.h` (HDF5 `scene.cvch5`), the cvcGL `LodGraphicsNode`, and `cvc::vis` culling. The design-notes framing below predates the implementation — read it as rationale, not a TODO. **Still open:** physics LOD (none yet) and the runtime residency/paging driver (HLOD §4.2) — tracked in `ARIADNE_CONFERENCE_ROADMAP.md` §5h (H2/H3).
**Status:** **LANDED (PR #249+).** The geometry/scene LOD described here is implemented — `inc/cvc/lod/select.h` (rung selection + hysteresis + the budget `solve()`), `lod/pyramid.h`, `cvc/geometry/simplify.h` (native QEM), `lod/tiles.h` (partition + tiled pyramids), `lod/store.h` (HDF5 `scene.cvch5`), the cvcGL `LodGraphicsNode`, and `cvc::vis` culling. The design-notes framing below predates the implementation — read it as rationale, not a TODO; the §4.1 status note lists which `cvc::vis` and cvcGL files actually exist, and how the Ariadne room uses LOD. **Still open:** physics LOD (none yet) and the runtime residency/paging driver (HLOD §4.2) — tracked in `ARIADNE_CONFERENCE_ROADMAP.md` §5h (H2/H3).
**Provenance markers used throughout:** **[M]** measured on the target machine (Release, 32-core CUDA box, GTX 1650, 1280×800, vsync **off**), **[D]** derived arithmetically from measured values, **[E]** estimated.

---
Expand Down Expand Up @@ -967,6 +967,17 @@ src/cvcGL/examples/

`src/cvcGL/CMakeLists.txt` uses `file(GLOB CVCGL_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp")` (verified, line 34), so **every cvcGL-side source file lands with no CMake edit at all** — which keeps us off line 24 (`CVCGL_VTK_COMPONENTS`) and lines 281–292 (the examples block), both hot for PR #223.

**Status note (2026-10-24, verified against the tree after the conference's S7).** The manifest above is the design, not the tree:

- **`cvc::vis`** has `types.h`, `scene_view.h`, `view_params.h`, `visible_set.h`, `grid_index.h` and `reference.h` (plus `mesh_tiler.h`, which the manifest does not list), with sources for `types`, `grid_index`, `reference` and `mesh_tiler` and the `vis_frustum` / `vis_grid` / `vis_oracle` / `vis_tiler` tests. The cull pipeline, stages and seeds, `spatial_index` / `linear_index` / `quadtree_index`, `terrain_field`, `portal_graph`, `lod.h` / `hlod.h`, `regime.h`, `registry.h` and `manifest.h` are not there.
- **The cvcGL adapter does not exist**: no `ViewParams`, `VisCuller`, `CullAction`, `BatchedScene` or `SwayShader`, and no cvcGL source includes `cvc/vis/`. What cvcGL has is LOD *selection* on the `cvc::lod` side: `LodGraphicsNode` and `SceneGraph::selectLOD`, fed by `make_view_params(renderer, …)` in `LodGraphicsNode.h` (a `cvc::lod::view_params`, not a `cvc::vis` one), and, new in S7, `LodPyramidCache` (`inc/cvc/gl/LodPyramidCache.h`) plus `realize_scene`'s `RealizeOptions`.

**How the Ariadne room uses it** (S7; `ARIADNE_CONFERENCE_ROADMAP.md` §5h.1). `room_view` owns one `LodPyramidCache` for its lifetime, so a whole-scene re-realize re-attaches a cached ladder with no QEM pass. The cache key is `lod::content_hash` at 1 mm mixed with the mesh-build `pyramid_params`; a miss draws the mesh as rung 0 at once and builds one ladder per key on a single `async_lane` with `build_mesh_pyramid(…, pool = nullptr)` — never a `thread_pool` (its `std::thread` aborts on non-pthread wasm) and never `store.h` (HDF5, absent on wasm). Without threads the lane is deferred, one build per `poll()` on the render thread, so the build cap is 60k triangles there (2M threaded): the 69,473-triangle bunny stays at rung 0 on the deployed single-threaded cyprty, and the refusal is logged once. `realize_scene(…, RealizeOptions{&cache})` makes a node a `LodGraphicsNode` only for a file/URI mesh of at least `lod_min_tris` (4096) with no texture, shader, clip, body or children, styling every rung with `apply_node_material`. Each frame `room_view` runs `selectLOD` from its single camera (`preset_view(balanced)` natively, `aggressive` under `__EMSCRIPTEN__`) and invalidates the shadow bake when a rung changes; `room_view::set_lod(false, …)` (arigl: `CVC_ARIGL_LOD=0`) pins rung 0. Physics never sees a rung. The default room's plane (2 triangles) and box (12) get no LOD node at all; `arisrv --scene large` seeds 24 bunnies for it.

**Not used by the room, and why.** `RenderView` / `VisibilityMask` (`src/cvcGL/RenderView.cpp`) drive the separate `cvc::gl::Node` graph (`nodes.h` / `traversal.h`), not `SceneGraph`, and arigl has one view and one camera; the conference roadmap's claim that H1 uses them is corrected there, and per-camera views wait for a PiP or minimap (`CVCGL-UI-DSL-ROADMAP.md` §9.5, stage C). No `cvc::vis` culling runs in the room: at N ≤ 64 avatars VTK's own per-prop frustum culling is enough.

**Follow-ups owned here:** the cvcGL `ViewParams` adapter (`vtkRenderer` → `cvc::vis::view_params`), avatar frustum / screen-size culling and nearest-*K* label caps (all inert at room scale); and a world-scale fix in `LodGraphicsNode::select`, which compares a ladder's mesh-unit errors with world distances, so a transform-scaled node coarsens at the wrong distance (the large demo scene sizes its bunnies with a replicated fit instead). The `lod::solve` budget glue (`preset_budget`) over `LodGraphicsNode`s goes to H3 with the residency driver.

### 4.2 Core headers

```cpp
Expand Down
21 changes: 21 additions & 0 deletions examples/ariadne/documents/conference_roster.ari
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ meta:
author: cvc
min_libcvc: 3.6.0

# The room's LOOK. The relay owns what is IN the room (conf.rooms.<room>.scene: the ground, the
# reference box; the avatars), and every client draws it under this document's lighting, so the
# rig lives here, not in the server. A three-point rig staged over the 20 m floor: the key spot casts
# real-time shadows from the scene objects and every avatar (re-baked each frame, so a walking
# avatar's shadow walks with it); fill, back and environment keep the shaded sides readable.
scene:
lights:
- light: room_rig
rig: three_point
stage: { center: [0, 0, 1], radius: 10 }
key: { intensity: 0.9, azimuth: -35, elevation: 55, cone: 48 }
fill: 0.25
back: 0.25
warmth: 0.25
environment: 0.15
ambient: 0.12
shadows:
enabled: true
resolution: 2048
update_interval: 1

menubar:
- menu: Room
items:
Expand Down
Loading
Loading