# W3CS wire protocol, version 1

All integers use little-endian byte order. A WebRTC DataChannel message holds
one complete envelope. The relay does not combine envelopes. Ordered reliable
resources, ordered reliable recovery frames, unordered expiring live deltas,
and input use separate SCTP streams. This preserves complete geometry epochs
without making disposable live frames wait for retransmission.

The live-delta stream is unordered with `maxPacketLifeTime=250` ms. The relay
keeps its buffered amount between a 256 KiB minimum and a 1 MiB maximum, with
the active limit derived from measured bandwidth and RTT. Input messages use a
separate DataChannel and an independent signaling-WebSocket copy. A monotonic
input identifier makes the two paths idempotent.

## Envelope

| Offset | Size | Field |
| ---: | ---: | --- |
| 0 | 4 | ASCII magic `W3CS` |
| 4 | 1 | protocol version (`1`) |
| 5 | 1 | message kind |
| 6 | 2 | flags |
| 8 | 4 | session identifier |
| 12 | 4 | monotonically increasing sequence |
| 16 | 4 | frame identifier, or zero for non-frame messages |
| 20 | 2 | fragment index |
| 22 | 2 | fragment count |
| 24 | 4 | payload byte count |
| 28 | 4 | CRC-32 of the transmitted payload |

The fixed header is 32 bytes. `fragment_count` is in the range 1-4096.
`fragment_index` is less than `fragment_count`.

Message kinds are `HELLO` (1), `RESOURCE` (2), `FRAME` (3), `REPAIR` (4),
`ACK` (5), and `ERROR` (6).

Flags are `COMPRESSED` (bit 0), `KEYFRAME` (bit 1), `LAST` (bit 2), and
`GEOMETRY_ANCHOR` (bit 3). The compressed payload starts with a 32-bit
uncompressed size, an 8-bit codec, and three zero padding bytes. Codec 1 is
raw DEFLATE and remains for the Python reference relay. Codec 2 is Zstandard.
Codec 3 is Zstandard with a frame dictionary: the header carries one extra
32-bit frame identifier, and the decoder uses that frame's decoded payload as
the dictionary. The browser decodes codecs 2 and 3 with the vendored
`web/vendor/zstd-wasm` build. A decoder rejects an unknown codec and rejects
a size above the kind-specific limit before it allocates memory.

An `ERROR` message carries a UTF-8 JSON payload on the reliable channel. The
recorder sends `{"error":"capture-violation","method":"..."}` when the game
uses a D3D9 path the command stream cannot represent. The browser then fails
closed into the video fallback. Silent divergence is not permitted.

`HELLO` currently carries only the producer identity JSON. The required-
opcode negotiation described below is not implemented yet.

## Implemented record opcodes

| Opcode | Record |
| ---: | --- |
| 1 | complete small frame-state snapshot |
| 2 | create buffer |
| 4 | create texture |
| 5 | update texture |
| 6 | destroy resource |
| 7 | define raw content blob |
| 8 | define full XOR delta blob |
| 9 | define compact XOR-mask delta blob |
| 10 | reset content-blob cache |
| 16-29 | fixed-function state changes and clear |
| 32 | non-indexed draw |
| 33 | indexed draw |

## Record stream

A reassembled payload contains zero or more records. Each record starts with:

| Size | Field |
| ---: | --- |
| 1 | opcode |
| 1 | flags |
| 2 | reserved, must be zero |
| 4 | record payload byte count |

Unknown opcodes can be skipped by length. Required opcodes are declared in
`HELLO`. A client rejects a session when it does not implement a required
opcode.

Resource records use a 32-bit resource identifier and 32-bit generation.
Draw records reference both values. The browser does not render a frame that
references an absent generation. It sends `REPAIR`, and the relay sends a
reliable resource snapshot.

Each `FRAME` payload starts with a complete snapshot of the small fixed-
function state. This includes the selected render states, transforms, FVF,
stream bindings, index binding, texture binding, material, active directional
light, viewport, and clear color. Later records contain changes and draws.
This makes the next complete frame usable after an earlier frame is lost.

After decompression, a relayed `FRAME` payload starts with a 32-bit reliable
resource-sequence dependency. The record stream follows that value. The
browser does not parse or render the frame until it has processed the ordered
resource channel through that sequence. It drops the frame after 100 ms if
the dependency has not arrived. This prevents cross-DataChannel reordering
from exposing a draw before its texture or content blob.

## Limits

The browser and relay enforce these version-1 limits:

| Item | Limit |
| --- | ---: |
| transmitted fragment payload | 60 KiB |
| reassembled frame | 4 MiB |
| reassembled resource message | 64 MiB |
| records per frame | 16,384 |
| draws per frame | 4,096 |
| live resources | 16,384 |
| one buffer | 64 MiB |
| one texture dimension | 8,192 pixels |
| decoded texture storage | 256 MiB |
| incomplete frame reassembly lifetime | 500 ms |

A limit violation closes command mode and activates the existing video
fallback. It does not close the game session.
