V3 is Lilith's embodied tier: the place where a meditator, a concert performer, an operator on shift, and a low-end browser visitor all stand in the same room and share one truth about who is where, who touched what, and who may hear whom. This page is the feature-level tour of the spatial substrate that makes that possible — the world model (the realm → venue → room → cell hierarchy that names every space), the rooms / sharding / capacity model (how a single venue fans out into session-scoped instances spread across server shards), the presence layer (embodiment, activity, and the visibility bands that decide who appears to whom), and the real-time multiplayer that keeps thousands of avatars coherent without the server melting. It deliberately hands two adjacent concerns to siblings: how an avatar is rendered, named, and heard lives in Avatars, Nameplates & Spatial Audio, and which client surface a given visitor lands on (native UE5, the Pixel Streaming worker, the WebGPU fallback, and how a 4,096-seat stadium is banded across them) lives in Client Tiers, Fidelity & the Stadium. The deep engine treatment — the authoritative tick, the QUIC gateway, JWT rotation, the voice SFU, durable persistence — is the architecture companion, ../architecture/world-server-and-gateway.md, with prediction and physics in ../architecture/netcode-protocol-and-physics.md. For the full feature scope this slots into, start at the hub: ../V3_features.md.
What ships, honestly#
The split is worth stating up front, because the description and the code do not fully agree, and this page follows the code.
- Real and test-covered. The spatial model is genuine Rust in
apps/v3/lilith-world-server/src/lib.rs(~6,300 lines, 28 in-file#[test]/#[tokio::test]cases plus seven integration files undertests/). Rooms, the consistent-hash shard ring,rstarR-tree interest management, the per-tier visible-entity caps, the 60-second reconnect retention, the presence visibility-band authority, and the scheduled-class admission gate are all implemented, deterministic, and covered. The wire contract (libs/v3/multiplayer-protocol/) carries presence, transforms, and capacity as first-class fields and is asserted byte-identical across Rust, TypeScript, and hand-rolled UE C++. - Library-grade, not a running daemon. The world server's
run_service()(lib.rs:4564) binds an axum router that exposes only/healthz,/readyz, and/metrics(build_router,lib.rs:4516). There is no accept loop feeding live client packets into a tickingRoomRegistry— the simulation core is exercised by tests, not yet by a client-serving socket. Treat presence and multiplayer as a verified library, not a deployed cluster. - Aspirational in the monolith / corrected here. The monolith's five-level
hierarchy and four numbered capacity tiers are a product model; the code is
thinner and the vocabulary has drifted across three layers (see below). The UE
V3Worldmodule is a scene/launch validator, not aUV3WorldSubsystemowning a live mirror, and "cross-shard chat via the V1 event bus" is an in-processVec(CrossShardEventBus,lib.rs:1120).
The world model: realms, venues, rooms, cells#
The monolith describes a five-level hierarchy: Realm (a top-level theme — Tara, Saraswati, Commons), District (a region within a realm), Venue (a named addressable space), Room (a session-scoped instance of a venue holding live participants, props, and music), and Cell (a break-out sub-room — a small yoga circle, an after-concert green room). Two of those five are load-bearing in code; the rest are naming convention the authoring side carries.
The unit the world server actually keys on is the room. RoomKey
(lib.rs:64) is a (venue_id, instance_id) pair whose room_id() renders as
venue:instance — exactly the shape the UE side ships as its default,
commons-atrium:morning (V3World.h:43). A live room is a RoomState
(lib.rs:316): its key, the shard it lives on, a capacity_tier, an optional
scene_revision, a RoomLifecycle (Active / Shutdown), the participant
map, and a broadcast queue. So "Venue" and "Room" are real, distinct keys;
"District" and "Cell" do not appear as code structures — they are product-model
groupings that authoring and routing impose on top of the (venue, instance)
namespace.
"Realm" survives in three different forms, which is itself an honest finding. On
the UE client a scene carries a RealmId FName (V3World.h:63, default
Lilith.Atrium); in the V3 contracts V3RealmSchema
(libs/contracts/src/v3/primitives.ts:17) is an environment enum
(production | preview | training | private), not a theme; and the theme realms
(Tara / Saraswati / Commons) live as the tenant vocabulary V3TenantSchema
(primitives.ts:9 — lilith-commons, tara-studio, saraswati-stage). The
monolith's tidy "Realm → District → Venue" stack is, in practice, a tenant-owned
theme plus a (venue, instance) room key plus an environment promotion lane.
Scenes and promotion#
A room renders a scene: a published level plus its assets. The UE
FV3AtriumPlaceholderSceneSpec (V3World.h:60) is the concrete shape — a level
asset path, a RealmId, a RoomId, a one-line VisualThesis, and a list of
required FV3AtriumSceneElements. BuildDefaultSceneSpec (V3World.cpp:121)
authors six of them for the Commons Atrium: the level, a walkable floor, a
MetaSound ambient bed, Niagara light shafts, the V3 HUD overlay, and a PIE
player start, each with a real asset path and a bRequiredForPie flag.
Validate refuses a spec missing any required element. On the server side,
scenes reach a live shard without a redeploy through the hot-reload poller
described in the
world-server architecture companion;
RoomState carries the SceneRevision it was spawned at so an instance restart
can pick up a re-published scene. Authoring, signing (Isis provenance), and the
dev → preview → prod promotion gates are covered by the content-pipeline pages.
Rooms, sharding, and capacity#
Capacity: a four-name product model over two budget tiers#
This is the sharpest description-vs-code gap, so it is worth being exact. The
monolith names four capacity tiers — Intimate (≤ 12), Class (≤ 64),
Hall (≤ 256), Stadium (≤ 4,096). The V3 contracts name a different
four — class | salon | theater | stadium (V3CapacityTierSchema,
primitives.ts:20). But the authoritative world server only distinguishes
two: CapacityTier::Class and CapacityTier::Stadium (the wire enum
CapacityTier, multiplayer.proto:5). Those two are not head-count admission
limits at all — they are interest and bandwidth budgets. visible_entity_cap
(lib.rs:2523) maps the tier to CLASS_TIER_VISIBLE_ENTITY_CAP = 32 or
STADIUM_TIER_VISIBLE_ENTITY_CAP = 256 (lib.rs:506), and the bandwidth
validator gates the same two tiers at ≤ 32 kbps and ≤ 256 kbps. So the
"4,096-seat" figure is a product target met by the
stadium banding model, not a number any
admission check in the world server enforces.
In fact the generic join path enforces no head-count cap at all. join_room
(lib.rs:3792) rejects only a duplicate session_id
(ParticipantAlreadyJoined); it does not count the room against a tier limit.
The one place a hard cap is enforced is the scheduled path:
spawn_tara_live_class_at_start (lib.rs:3435) requires the supplied roster to
exactly match schedule.capacity_limit (RosterCapacityMismatch,
lib.rs:3453), reject an empty roster, reject duplicate sessions, and require
the named instructor to be present (MissingInstructor). That is the real
capacity contract: a booked class admits its exact roster; an open Commons room
relies on the interest/bandwidth budget rather than a turnstile.
Sharding by consistent hash#
Rooms above the intimate scale are distributed across world-server nodes by a
consistent-hash ring. ConsistentHashRing (lib.rs:1078) builds a
BTreeMap<u64, shard_id> seeded with virtual_nodes replicas per ShardNode
(so load spreads evenly rather than clumping), and route_room hashes a
RoomKey::room_id() and walks to the next ring position (wrapping at the end).
ShardedRoomDirectory (lib.rs:1160) wraps the ring and a spawn_routed_room
helper that routes a key to its shard and spawns the room there. The even-spread
property is a real test:
consistent_hash_routes_one_thousand_sessions_evenly_across_three_shards
(lib.rs:6265) routes a thousand rooms across three shards and asserts the
distribution is balanced, and
sharded_directory_spawns_routed_rooms_and_propagates_cross_shard_chat
(lib.rs:6292) walks the spawn-and-chat path. The matching gateway-side ring
(GatewayConsistentHashRing) pins each session to the same shard so a client
and its room agree on a home node — that edge logic is in the
world-server-and-gateway companion.
One honest correction: cross-shard chat and presence do not ride V1's event
bus as the monolith implies. CrossShardEventBus (lib.rs:1120) is an
in-process Vec<CrossShardChatEvent> filtered by target room — a correct local
model of the fan-out, not a distributed bus integration.
Reconnect retention#
A transient drop does not evict a participant. retain_session_for_reconnect
(lib.rs:3819) parks a departing participant's full ParticipantState for
RECONNECT_SESSION_RETENTION_MS = 60_000; resume_retained_session
(lib.rs:3839) rejoins them if they return inside the window and refuses (and
reaps) an expired one; purge_expired_retained_sessions sweeps the rest. The
test world_server_retains_session_state_for_thirty_second_reconnect
(lib.rs:5545) proves a 30-second round trip lands the participant back intact.
The gateway honours the same 60-second window, so a reconnecting client is
re-pinned to the shard that still holds its retained state.
Presence and visibility bands#
Embodiment and activity#
Every connected user is a ParticipantState (lib.rs:83): a session_id, an
avatar_id, an EntityTransform, and a join timestamp. The transform
(multiplayer.proto:85) carries quantized position/rotation/velocity (integer
millimetres and milli-degrees), an embedded AvatarExpression (an expression id
plus intensity in basis points), a lod band, and a last_input_sequence. On
the wire, a PresencePacket (multiplayer.proto:115) adds an activity_state
and a visibility_band. The protocol's ActivityState enum is IDLE,
WALKING, PRACTICING, PERFORMING, OPERATOR (multiplayer.proto:11) —
narrower than the monolith's
present | practicing | watching | speaking | away | do-not-disturb, an honest
divergence to expect when reading the wire format. Expression and the
status-ring rendering of these states are the avatar page's concern; see
Avatars, Nameplates & Spatial Audio.
The visibility-band authority#
This is the strongest presence piece in code, and it matches the monolith
exactly. VisibilityBand (lib.rs:91) is
Public | Tenant | Cohort | Invited | Invisible, and
PresenceVisibilityAuthority (lib.rs:221) decides, for every presence
broadcast, who is allowed to receive it. subject_visible_to_viewer
(lib.rs:277) encodes the rules precisely:
- you always see yourself;
- operators always see operator peers
(
subject.is_operator && viewer.is_operator), so a moderation console is never blinded; Public→ everyone;Tenant→ same tenant only;Cohort→ same tenant and same non-empty cohort;Invited→ only sessions on the subject's invite list;Invisible→ no one.
recipients_for_presence (lib.rs:240) walks every viewer in the room through
that predicate and returns a sorted recipient set, failing loud if the subject
isn't present, a profile is missing, or the band would empty the recipient set
entirely (EmptyRecipientSet). broadcast_presence_with_visibility
(lib.rs:3909) then enqueues a presence.visibility broadcast addressed to
only those recipients and reports how many were filtered_session_count. The
integration test tests/visibility_band.rs stands up a twelve-session room
spanning every band — public, tenant, cohort peer, other-cohort, invited,
uninvited, invisible, two operators, and a foreign tenant — and asserts each
band resolves to the correct audience. This is the presence-visibility-band
capability advertised in SERVICE_DESCRIPTOR (lib.rs:39).
Presence cadence is a budgeted contract. The protocol's capability descriptor
(libs/v3/multiplayer-protocol/src/index.ts) publishes
presence-packet:8ms-budget, snapshot-delta:16ms-budget, and
gateway-pin:24ms-budget, and a session carries a LilithGatewayPin
(libs/contracts/src/v3/lilith.ts:33) — gatewayId, shardId, expiresAt —
once the edge has bound it to a shard. The monolith's "≤ 20 Hz transforms, ≤ 60
Hz expression, event-driven activity" maps onto the tick config below.
Real-time multiplayer#
Server-authoritative, no rollback#
Lilith multiplayer is client-server with server-authoritative state and no
rollback — a deliberate choice, because V3 is a contemplative/performance
space, not a twitch fighter. The simulation clock is TickLoopConfig
(lib.rs:336), a single generic struct defaulting to 50 Hz internal tick / 20
Hz transform broadcast / 60 Hz expression interpolation (lib.rs:343). The
per-mode rate table in the monolith (20 Hz yoga, 10/20 Hz concert audience, …)
is the intended configuration for each room type; the code is the
parameterized engine that consumes it, not a set of hard-coded presets.
Snapshots go out as deltas — only entities that changed since a named
base_sequence are sent — and a client chains base_sequences to detect a
dropped or reordered delta rather than silently corrupting its mirror.
Prediction is light dead reckoning clamped to a 0–250 ms extrapolation window;
reconciliation accepts a single sub-perceptual frame of divergence before
snapping to server truth. The prediction/physics detail is the
netcode companion.
Interest management keeps a stadium bounded#
A viewer never receives every avatar. InterestManager::visible_entities
(lib.rs:1310) loads all other participants into an rstar R-tree and
returns the nearest N, where N is the tier cap (32 at class, 256 at
stadium), tie- broken by id for deterministic output. This is a genuine spatial
index, pinned by two load tests:
interest_manager_caps_class_visibility_at_32_nearest_entities (lib.rs:5734)
and stadium_interest_load_test_confirms_256_entity_cap (lib.rs:5783). It is
what lets a 4,096-attendee stadium room hand each viewer a bounded snapshot —
the front band gets authoritative transforms, everyone beyond the cap is the
crowd shader described on the
client-tiers page. Bandwidth is then a
tested budget, not a hope: the class tier proves 256 moving avatars stay under
32 kbps on deltas alone, which is why no zstd was ever needed.
Interaction primitives — validated, owned, consented#
Multiplayer interaction is server-validated so a client can never assert state
the simulation rejects. Three Rust authorities carry it: pickup/place
(InteractionAuthority — cross-room pickup and double-grab are refused, covered
by tests/pickup_place.rs), the asana-lock consent state machine
(Requested → Consented → Locked → Released, where only the student may
consent, covered by tests/asana_lock.rs), and a parallel physical-adjustment
consent gate (tests/tara_physical_adjustment_consent.rs). A rapier3d
anti-cheat rejects any transform implying more than 12 m/s of motion. These are
the contractual backbone behind Tara live classes; the consent flows are
detailed in the netcode and Tara companions.
A join, end to end#
Assembled from the real functions above (the serving daemon does not yet wire them together), a join looks like this:
The UE side participates as a scene/launch validator, not a net subsystem.
FV3AtriumPlaceholderSceneBuilder::BuildColdJoinValidationReport
(V3World.cpp:187) runs 96 samples over six deterministic phase fixtures —
tap_to_room_ticket (500 ms budget) through
avatar_spawn_to_fully_rendered_frame (900 ms) — against a 5-second
join-to-rendered budget, and ConnectLocalWorldServerStack validates the
lilith-world-server:43101 service config (V3World.h:37) rather than opening
a live socket. These are honest synthetic budget checks, covered by UE
automation, not measurements of real joins.
Where this connects#
- Avatars, Nameplates & Spatial Audio — how the embodiment, activity-state ring, nameplate LOD, and voice that ride these presence packets are rendered and heard.
- Client Tiers, Fidelity & the Stadium — which surface a visitor lands on, the Tier-2 fidelity envelope, and the 4,096-seat stadium banding the capacity tiers feed.
- ../architecture/world-server-and-gateway.md — the authoritative tick, QUIC gateway, JWT rotation, voice SFU, and durable persistence behind this model.
- ../architecture/netcode-protocol-and-physics.md — the wire protocol, prediction/reconciliation, and the physics/consent authorities in depth.
- The feature hub: ../V3_features.md.