Lilith Metaverse · Features

Client Tiers, Fidelity & the Stadium Envelope

A focused page within the Lilith Metaverse Features documentation. The full map and every sibling page live in the Features hub.

5sections13 minread1diagram5tables

On this page
flowchart TD Request[Device network account room and accessibility needs] --> Eligible{Native UE eligible?} Eligible -->|yes| Native[Native UE5 tier] Eligible -->|no| Pixel{Pixel-streaming capacity and latency eligible?} Pixel -->|yes| Stream[Pixel-streamed premium tier] Pixel -->|no| Web[Locally rendered fallback web tier] Native --> Room[Same authoritative room and member identity] Stream --> Room Web --> Room Room --> Density[Audience density LOD and interest management] Density --> Feedback[Visible fidelity disclosure and recovery]

The ladder chooses realization, not membership or world truth. Capacity, latency, device, entitlement, comfort, and accessibility can change the tier; the member should remain in the same governed room job with honest fidelity disclosure.

V3 — the Lilith Metaverse — has to put a gaming PC, a Quest 3, an iPhone, a locked-down work laptop, and a Chromebook on hotel Wi‑Fi into the same yoga room or the same concert, and it has to do that without forking the world or quietly handing the weak device a second-class truth about who is present and what is safe. The premium experience — Lumen global illumination, Nanite geometry, MetaHuman avatars, Sequencer-driven concerts — can only run inside Unreal Engine 5, so the strategy is to build one UE5 project and reach every device through a tiered client ladder: native UE binaries at the top, the same binary server-rendered over Pixel Streaming for the browser, a locally-rendered three.js fallback for hardware that can do neither, and a static landing page as the floor. Each rung trades fidelity — fewer particles, baked light instead of Lumen, a procedural avatar instead of a MetaHuman — but never identity, presence, or safety: every tier speaks the same multiplayer wire protocol to the same authoritative Rust world server and authenticates through the same V1 BFF. This page is the feature-level tour of that ladder, the Tier-2 fidelity envelope (what a fallback visitor genuinely loses, labelled honestly against the code), and the stadium envelope — how a single concert seats up to 4,096 attendees by banding them across the render paths. It hands the enforcement mechanics — the typed launch contract, the two-layer router, the Pixel Streaming relay — to its architecture companion ../architecture/tier-routing-and-pixel-streaming.md, and the world/presence model it rides on to ./world-rooms-presence-multiplayer.md. The surface-by-surface orientation is its sibling ./product-surfaces.md. The feature map this page belongs to is ../V3_features.md.

What ships, honestly#

The routing decision is real, typed, and tested on both layers. A visitor crosses an availability gate in the Oshun shell BFF (resolveV3LilithLaunchDecision, apps/oshun/bff/src/routes/v3-lilith-launch.ts) that answers "is this region open and this tenant cleared here?", then a device-capability surface picker in the Lilith BFF (resolveLilithLaunchDecision, apps/lilith/bff/src/routes/v3-lilith-launch-route.ts) that answers "what can this device actually render?" and emits one of four surfacesnative-deep-link, pxstream-browser, fallback-browser, or static-landing — as a discriminated-union target so the dishonest combinations (a streaming target with no POP, a static landing that still claims a renderer) are unrepresentable. That picker is regression-locked by a weighted synthetic device-mix gate that asserts per-tier decision shares to within ±1 point.

The Pixel Streaming browser client is a real Epic bindingplayer.ts (@oshun/lilith-web-pxstream) imports @epicgames-ps/lib-pixelstreamingfrontend-ue5.5 and drives it through a LilithPixelStreamingClient state machine. The Tier-2 fallback's renderer, client-side physics, spatial audio, tenant parity model, and steady-FPS gate are genuine (@oshun/lilith-engine-web-fallback), exercised by Vitest and a pixel-reading Playwright suite.

Three honest qualifications keep this page from over-claiming, and each is verified below rather than asserted:

  • Tier-1 is a buildable engineering scaffold, not a content-complete game. The 17 UE modules, 11-platform cook registry, Game Feature plugin pipeline, and wire codec are real and tested; the authored content (levels, ability blueprints, Niagara, MetaSound graphs, avatar meshes) is largely declared in manifests but not committed. See ../architecture/tier1-ue5-client.md.
  • The Pixel Streaming worker fleet and Epic signaller are external infrastructure. The browser lib binds the real Epic frontend and the relay constructs Epic-protocol SDP, but the per-session headless UE process lives in the GPU fleet, not this repo.
  • Tier-2 has three labelled fidelity gaps beyond "reduced": VRM avatar rasterization, live multiplayer packets, and real binary asset transcoding are all planned, not present — the running fallback uses procedural primitive avatars, a simulated gateway handshake, and a bake that emits byte-budgeted placeholders. These are detailed under The Tier-2 fidelity envelope.

The four-tier client ladder#

The ladder is a strict descent: each rung is reached only when the rung above is unavailable, and the surface picker walks it as a fixed sequence (override → native → pixel-streaming → fallback → static).

Tier Surface Where it renders Real code anchor Fidelity
1 Native UE5 Local GPU, installed binary V3/ue/V3.uproject (17 modules, 11 cook profiles) Full UE5 (Lumen + Nanite)
1 Pixel Streaming Server GPU worker → WebRTC frames @oshun/lilith-web-pxstream (player.ts) Streamed UE5 (1080p H.264 / AV1)
2 WebGPU/WebGL2 fallback Local GPU via three.js @oshun/lilith-engine-web-fallback (renderer.ts) Reduced (baked light, no Nanite)
0 Static landing No 3D at all buildFallbackDecisionstatic-landing Refuses to fake a renderer

Tier 1 — the canonical UE5 client, native and server-side#

Tier 1 is the single V3/ue/ project cooked eleven ways. A native install on a capable device (the router's override: 'native' or an installed-client probe) emits a native-deep-link target (oshun://lilith/launch?...) and carries no backend GPU cost — the device renders locally at lowest latency and highest fidelity. The same binary, compiled headless by V3PixelStreamingWorker.Target.cs as a monolithic shipping-with-logging target, is what a GPU worker runs to serve the browser: it renders frames server-side and forwards multiplayer packets to the world server, so a browser attendee is — from the world server's perspective — just another Tier-1 client. The module split, cook-profile registry, and worker target are the subject of ../architecture/tier1-ue5-client.md.

The Pixel Streaming browser tier#

player.ts is the real Epic binding, and it is not a stub. createLilithPixelStreamingClient builds a LilithPixelStreamingClient that matches a worker (POSTing a launch request whose result is validated by parseLilithPixelStreamingWorkerMatch, which rejects any codec that is not H264 or AV1), constructs the Epic PixelStreaming client with a per-mode codec/bitrate envelope, mounts the returned video, and arms an 8,000 ms first-frame budget timer that fails the session loudly if no frame arrives in time (defaultFirstFrameBudgetMs = 8000). The three streaming modes are the bitrate envelope the relay's POP selection honours:

Mode Preferred codec Max bitrate Use
adaptive auto (worker) 12 Mbps Default; auto-codec
low-latency H.264 6 Mbps Constrained uplink, latency-sensitive
quality AV1 20 Mbps Modern Chrome/Edge, max fidelity

On videoInitialized the client records firstFrameMs and fires an input round-trip probe (oshun.v3.inputProbeinputProbeAck) over the Epic UI interaction channel so the surface can prove the data channel is clean, not just that pixels arrived. Beyond the player, the package descriptor (index.ts) advertises twelve capabilities — session-match:8ms-budget, webrtc-offer:16ms-budget, input-channel:24ms-budget, and nine accessibility control channels (reduced motion, color-vision palettes, one-handed mobile, photosensitive-safe strobe attenuation, flat-stereo, cognitive-load reduction, keyboard/single-switch, launch localization, avatar-anchored captions). The honest boundary: the worker process and Epic signaller the client negotiates with are external fleet — what ships here is the client shell and its match call.

Tier 2 — the no-one-locked-out local renderer#

When the device can neither install the native client nor sustain a Pixel Streaming WebRTC session — old laptops, locked-down machines, cellular tablets, Pixel-Streaming-restricted regions, or an explicit "Lite mode" — the picker emits a fallback-browser target and the browser renders the world locally through three.js. selectLilithFallbackRendererBackend (renderer.ts:176) prefers WebGPU, bridges to a webgl2-webgpu-fallback budget when WebGPU init fails, and throws when WebGL2 is absent rather than presenting a blank canvas. Two frozen budget profiles drive everything downstream (renderer.ts:142, mirrored in steadyFps.ts):

Backend Target FPS Max render cost Pixel-ratio cap
webgpu 60 16.7 ms 1.5
webgl2 30 33.4 ms 1.0

A WebGPU-requested device that quietly fell back is held to the webgl2 30 fps budget, not 60 — the budget follows the real backend, not the request. The validateLilithTier2SteadyFpsSample gate checks a 60-minute sustained sample against eight conditions (min and average FPS at target, p95 and p99 frame time within budget, dropped-frame rate ≤ 0.5 %, no thermal throttle), each failure a named reason. The static-landing floor is the picker's last resort: if the browser reports neither WebGPU nor WebGL2, it returns static-landing with local-rendering-unsupported — it refuses to claim a runnable fallback it cannot deliver.

The Tier-2 fidelity envelope (known gaps from canonical)#

The Tier-2 fallback is the no-one-locked-out path, not a feature-parity mirror of Tier 1. The table below is the customer/operator-facing envelope; the columns are grounded in parity.ts, which enumerates exactly four runtime profiles selected by selectLilithTier2TenantParityProfile.

Feature Tier-1 (UE5) Tier-2 (three.js)
Global illumination Lumen real-time GI Pre-baked lightmap atlases (3 for Commons, 4 for a Saraswati hall)
Geometry detail Nanite virtualized geometry LOD chains; ≤ 80 K triangles/avatar; ≤ 30 MB venue load budget
Niagara stage VFX Full Sequencer-driven Niagara Reduced particles (96 Commons) / cue sprites (≤ 384 hall)
Aja AR overlays World-space UMG anchored to student HUD text + 9-joint 2D pose guide; no world-space anchoring
Camera cinematics Sequencer-driven concert cameras Cue-event-driven cameras with simplified blending
Stadium interactivity Front + mezzanine Pixel Streaming Crowd-band Yemaya LL‑HLS preview + thin spatial seat audio

What Tier-2 actually substitutes — the four parity profiles#

lilithTier2TenantParityProfiles (parity.ts:72) is real data, not prose. Three of the four profiles are parityLevel: 'full' — the room is fully reachable at reduced fidelity — and only the stadium tier is degraded-preview:

  • commons-venue (full): Lumen GI → 3 baked lightmap atlases at a 30 MB venue budget; full Niagara → a 96-particle commons-soft-presence shader.
  • tara-live-class (full): world-space UMG Aja cues → HUD text plus a 9-joint screen-space pose guide (poseGuideOverlay.jointCount: 9, targetPose: 'warrior-two'), gated separately by validateAjaTier2CueUxReport for instructor + editorial sign-off, 80 % cohort clarity, WCAG-AA contrast, and no world-space anchoring.
  • saraswati-hall-concert (full): Sequencer Niagara → cue-event sprites capped at 384 particles across 4 baked stage atlases.
  • saraswati-stadium-preview (degraded-preview): the interactive stage collapses to a Yemaya LL‑HLS crowd-band video proxy plus thin seat audio.

Each profile carries a telemetrySignature, and the four join into one load-bearing parity string (tara:aja-hud:pose9|commons:baked-lightmap3:niagara96|saraswati:hall:cue-particles384|saraswati:stadium:yemaya-llhls-crowd) that the Playwright suite hard-asserts, so bake and runtime cannot drift apart without the signature diverging. Tara live classes, Tara on-demand, all Lilith Commons venues, and Saraswati class- and hall-tier concerts run end-to-end on Tier 2 with these caveats; only stadium-scale concert interactivity is genuinely reduced.

The honest gaps — what Tier-2 does not yet do#

Three monolith claims are not backed by this codebase, and a feature page that pretended otherwise would be lying to operators planning the envelope:

  • VRM avatar rasterization at "≤ 80 K triangles" is planned, not present. There is no GLTFLoader, no VRM importer, and no .glb/.vrm load path in the fallback app or engine — a grep returns zero. Avatars in the running scene are procedural primitive clusters (createAvatarCluster, renderer.ts:506: a capsule body plus a sphere head). The 80 K cap exists only as a constant inside the bake planner; nothing at runtime loads a baked avatar mesh today.
  • The "same multiplayer Protobuf packets, full presence/voice parity" claim does not hold for Tier-2 yet. The protocol library exists (libs/v3/multiplayer-protocol) but the fallback does not import it (grep returns zero). gateway.ts selects a transport (WebTransport over QUIC preferred, WebRTC data channel as fallback) and then returns a simulated handshake with a hard-coded RTT (24 ms / 42 ms) — a "linked" badge, not a live socket. The Rapier prop-pickup prediction in physics.ts is a deterministic reconciliation scenario (two parallel Rapier worlds), not a live netcode loop.
  • The asset bake emits placeholders, not binaries. The nightly bake is a real, gated planning and manifest stage — it decides LODs, byte budgets, S3 layout, and render feasibility, and proves all of it with a passing validator — but it writes Draco/KTX2/Opus artifacts as byte-budgeted text placeholders rather than invoking real transcoders.

These are the same gaps the architecture companion documents at ../architecture/tier2-fallback-web-client.md; they are honest seams in a real renderer, not stubs masquerading as features.

The stadium envelope — banding 4,096 across the ladder#

A Saraswati Stadium-tier concert targets up to 4,096 attendees per instance, which no single render path can serve interactively. The product answer is a banded seating model that routes each attendee to the render path matching their ticket and device — and it is worth being precise that the banding is a product model layered over what the code actually enforces, not a code struct of its own.

Band Capacity Render path Ticket gate
Front 256 (named) 256 Native UE or dedicated Pixel Streaming worker Front-row tier
Mezzanine (next 768) 768 Pixel Streaming worker, 1080p lower-fidelity preset Mezzanine tier
Crowd band (remainder) 3,072 Yemaya LL‑HLS multicast + thin spatial seat + chat/emoji Crowd tier (free/low-cost) / Tier-2

What the code actually enforces#

The banding rests on three real anchors, each at a different layer:

  • The 256 front band is the world server's interest cap, not a turnstile. The authoritative world server distinguishes only two capacity tiers (Class, Stadium), and they are interest and bandwidth budgets, not head-count admission limits. STADIUM_TIER_VISIBLE_ENTITY_CAP = 256 (apps/v3/lilith-world-server/src/lib.rs:507) is the number of nearest entities an rstar R-tree hands each viewer, pinned by stadium_interest_load_test_confirms_256_entity_cap (lib.rs:5783), which asserts visible.len() == 256. So the front 256 get authoritative transforms; everyone beyond the cap is rendered as the crowd shader, which is exactly why a 4,096-attendee room stays bandwidth-bounded (the world-rooms page covers the interest model in depth).
  • The mezzanine/front Pixel Streaming cost is a real per-node budget. The relay's fleet config carries class_sessions_per_gpu_node: 6 and stadium_sessions_per_gpu_node: 3 (apps/v3/lilith-pxstream-relay/src/lib.rs:437), so a stadium concert's interactive bands are explicitly the most expensive sessions a GPU node hosts — three per 24-vCPU node — which is why the front/mezzanine bands are ticket-gated and capacity-reserved rather than free walk-ups.
  • The crowd band is the Tier-2 stadium-preview profile. The saraswati-stadium-preview parity profile (parity.ts:260) is the crowd band's render path: mode: 'yemaya-ll-hls', crowdBand: true, and a proxyUrl that is honestly a placeholder endpoint (https://stream.oshun.example/...m3u8), not a live stream. A Tier-2 attendee defaults here; a native attendee with a front/mezzanine ticket renders locally.

The contract vocabulary, for completeness, is a third naming: V3CapacityTier Schema = ['class', 'salon', 'theater', 'stadium'] (libs/contracts/src/v3/primitives.ts:20). The "front 256 / mezzanine 768 / crowd 3,072" split itself is a product target met by these three mechanisms — it is not a single banding struct any one service owns.

Capacity, prewarm, and the tight-quota POPs#

Because a stadium concert lights up 4,096 attendees at a scheduled minute, the relay treats it as a capacity event rather than organic load. A scheduled concert (event_id: "concert:saraswati:full-moon-stadium" in the relay) pre-warms a POP worker pool ahead of doors, and the abuse-admission layer routes free-tier latecomers to the Tier-2 crowd band when the reserved interactive pool is full (Fallback → HTTP 503 with a banner) rather than starving a booked attendee. The hardest regions are codified: ap-south-1 and ap-northeast-1 are tight-quota POPs requiring a 90-day reservation lead for scheduled events (versus 30 days elsewhere) and a 60-minute prewarm window for stadium events. Capacity beyond 4,096 is handled by spinning up additional concert instances with a shared persona and setlist but separate audiences. The full POP-scoring, admission, prewarm, and multi-cloud-failover machinery — and the honest caveat that those drills run on deterministic simulate_* fixtures, not a live cloud cut-over — is the subject of ../architecture/tier-routing-and-pixel-streaming.md.

Where this connects#