# Client Tiers, Fidelity & the Stadium Envelope

```mermaid
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](../architecture/tier-routing-and-pixel-streaming.md),
and the world/presence model it rides on to
[./world-rooms-presence-multiplayer.md](./world-rooms-presence-multiplayer.md).
The surface-by-surface orientation is its sibling
[./product-surfaces.md](./product-surfaces.md). The feature map this page
belongs to is [../V3_features.md](../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 surfaces** —
`native-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 binding** — `player.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](../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-tier-2-fidelity-envelope-known-gaps-from-canonical).

## 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                      | `buildFallbackDecision` → `static-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](../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.inputProbe` → `inputProbeAck`) 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](../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](./world-rooms-presence-multiplayer.md) 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](../architecture/tier-routing-and-pixel-streaming.md).

## Where this connects

- [./product-surfaces.md](./product-surfaces.md) — the surface-by-surface
  orientation: the UE5 client, the two web tiers, Studio, and the operator
  console, each named to its real code.
- [./world-rooms-presence-multiplayer.md](./world-rooms-presence-multiplayer.md)
  — the world/room/capacity model the tiers render, including the 256-entity
  interest cap the stadium front band rides on.
- [../architecture/tier-routing-and-pixel-streaming.md](../architecture/tier-routing-and-pixel-streaming.md)
  — the architecture companion: the typed launch contract, the two-layer router,
  the relay matchmaker, and the stadium capacity/prewarm model.
- [../architecture/tier1-ue5-client.md](../architecture/tier1-ue5-client.md) and
  [../architecture/tier2-fallback-web-client.md](../architecture/tier2-fallback-web-client.md)
  — the two render surfaces routing chooses between, in engineering depth.
- The feature hub: [../V3_features.md](../V3_features.md).
