You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
t.Fields.Add(newFieldDoc("AvatarDeltaKeyframeMaxIntervalMs"," Ceiling for the adaptive keyframe stretch: while a sender's deltas stay tiny (idle avatar) the keyframe interval doubles up to this value; motion snaps it back to the base. Receivers that miss a keyframe request one on demand. 0 or <= base disables. int (ms); default 2000. "));
186
186
t.Fields.Add(newFieldDoc("StripAdditionalDataAtLowQuality"," Drop AdditionalAvatarData (face blendshapes, custom behaviour params) from the Low and VeryLow avatar tiers — unreadable at those distances; the reliable low-frequency behaviour channel still reaches everyone. High/Medium keep it. true|false; default true. "));
187
187
t.Fields.Add(newFieldDoc("EnableUplinkAvatarDelta"," Accept client-to-server avatar deltas and advertise support: clients upload a full keyframe every ~500 ms plus small deltas in between instead of full frames every packet (60-90% less avatar ingress). false = clients upload full keyframes only. true|false; default true. "));
188
+
t.Fields.Add(newFieldDoc("ImageShareEgressMegabitsPerSecond"," Server egress one sharing player may spend on image replication, in megabits per second. A shared image is relayed once per recipient who is not on a direct P2P link, so this budget divided by the fan-out is the rate the sharer actually uploads at - at the old client-side assumption of 4 Mb/s a twenty-player instance moved a picture at about 25 KB/s. Sized per sharer, so the worst case is this times the number of people sharing at once; divide it down on a small pipe and raise it on a large one. int (Mb/s); 0 leaves the client on its own conservative default; default 200. "));
188
189
t.Fields.Add(newFieldDoc("EnableBSRProfiling"," Emit Server Reduction System profiling output. true|false. "));
189
190
t.Fields.Add(newFieldDoc("BSRMaxSliceCount"," Furthest the Server Reduction System may slice its roster under load. int; 0 = scale with player count, which is recommended. At slice N each tick serves only 1/N of the receivers, so everyone's update rate drops uniformly - it is the last-resort lever, used only after stretching the tick period and shedding distant players. This cap decides how far the server may degrade before it stops degrading and simply starts missing its tick instead. It used to be a fixed 32, chosen when 2000 was a large instance; at 8000 players a cap of 32 still leaves 250 receivers per tick, so a struggling server reaches the ceiling with nowhere left to go. Automatic keeps the per-tick fan-out roughly flat as population grows. Set a positive value only to pin it. "));
190
191
t.Fields.Add(newFieldDoc("BSRMaxDegreeOfParallelism"," Worker cap for the Server Reduction System's parallel phases (send loop, message processing, distance sweep). int; 0 = automatic and recommended. Automatic scales the pool with the player count and caps it at the share of the machine the core allocator has granted this phase - a share whose ceiling is measured at runtime rather than assumed, so it already tracks the hardware. Setting a number here overrides all of that, including the measurement, and is clamped to the core count. The tick runs hundreds of times a second, so every worker costs dispatch and GC-poll traffic per tick; once the per-tick slice is large enough to keep them busy, extra workers cost more than they return. Set it only to hold the server down on a box shared with other services. Watch the 'send N/M workers' figures in the [CPU] log line to see what automatic is choosing. "));
t.Fields.Add(newFieldDoc("AllowPeerAddressChange"," Allow a peer's remote endpoint (IP/port) to change mid-session, e.g. mobile network roaming. true|false. "));
245
246
t.Fields.Add(newFieldDoc("MergeHoldMs"," How long, in milliseconds, a partly-filled packet-merge buffer may wait for more data before being sent. float; 0 = send on every logic pass (legacy). The logic loop runs hundreds of times a second, so flushing every pass emits many half-empty datagrams and the server pays full per-packet cost for each; holding a partial buffer briefly lets consecutive passes coalesce. A buffer that fills the MTU is always sent immediately, so this caps added latency rather than adding it — only small sends ever wait, and never longer than this value. 2-5 is a reasonable range; raise it to cut packet rate further, lower it if voice latency matters more than CPU. "));
246
247
t.Fields.Add(newFieldDoc("CompactMerged"," Frame merged unreliable traffic with the compact per-entry format. true|false; true is recommended. A merged message used to carry four bytes of framing (a two-byte nested length plus its own property and channel bytes); the compact form drops the property byte, since the datagram already says everything inside it is unreliable, and uses a one-byte length for payloads up to 255 - two bytes of framing, or three above 255. Different traffic still shares one MTU-sized datagram, so avatar updates and voice keep riding together. Measured at 500 players: 0.93% less total egress (~4.97 Mbit/s, about 2.24 GB/hour), 0.37% fewer UDP packets, no CPU change, no drops. This is a send-side setting and is safe to change on one end only, because both framings are always decoded; every client able to connect understands it, which the transport protocol id and the server version check together guarantee. Turn it off only to A/B the saving or to rule the framing out while diagnosing something else. "));
247
-
t.Fields.Add(newFieldDoc("MaxUnreliableQueuePerPeer"," Maximum unreliable packets queued per peer before the oldest are dropped. int; 0 = size automatically from player count and available memory, which is recommended. This is the backstop that keeps an overloaded server alive: with no bound at all, a server that cannot drain its send queue grows the backlog instead of shedding, and at 2000 players that backlog reached ~40 GB before every peer timed out. Oldest are dropped first because a newer position update supersedes them. WARNING: this used to be a fixed 256, which is too small to be only a backstop - at 2000 players it discarded roughly half of every avatar update produced, and because discarding is cheaper than sending, the reduction system read the resulting fast ticks as spare capacity and produced even more. Raising it to 4096 on identical load measured zero drops, 22% more delivered bytes and 21% less CPU. Automatic sizes it per box; set a positive value only to pin it for a reproducible measurement. "));
248
+
t.Fields.Add(newFieldDoc("MaxUnreliableQueuePerPeer"," Maximum unreliable packets queued per peer before the oldest are dropped. int; 0 = size automatically from player count and available memory, which is recommended. This is the backstop that keeps an overloaded server alive: with no bound at all, a server that cannot drain its send queue grows the backlog instead of shedding, and at 2000 players that backlog reached ~40 GB before every peer timed out. Oldest are dropped first because a newer position update supersedes them. WARNING: this used to be a fixed 256, which is too small to be only a backstop - at 2000 players it discarded roughly half of every avatar update produced, and because discarding is cheaper than sending, the reduction system read the resulting fast ticks as spare capacity and produced even more. Raising it to 4096 on identical load measured zero drops, 22% more delivered bytes and 21% less CPU. Automatic sizes it per box; set a positive value only to pin it for a reproducible measurement. Applies to bulk state traffic only - voice has its own queue and its own bound, see MaxPriorityUnreliableQueuePerPeer. "));
249
+
t.Fields.Add(newFieldDoc("MaxPriorityUnreliableQueuePerPeer"," Maximum voice packets queued per peer before the oldest are dropped. int; 0 = size automatically from player count and available memory, which is recommended. Voice is queued separately from bulk avatar traffic and drained first, so a backlog of position updates can neither delay it nor shed it. That separation is the fix for a real bug: the bulk queue drops oldest-first because a newer avatar update supersedes the one behind it, which is not true of audio, so voice sharing that queue was being destroyed at the bulk stream's drop rate and whatever survived arrived behind the backlog, too late to play. This queue is allowed to be DEEPER than the bulk one, which sounds backwards and is the whole point: bulk depth buys avatar frames that the next frame replaces anyway, while voice depth buys audio that has no replacement. Measured at 1000 clients on a deliberately starved server, moving budget from bulk to voice improved both at once - voice delivered 85.7% to 93.6%, peak RSS 7.8 GB to 4.6 GB. A flat 256 here delivered only 32.8%, because a receiver in a crowd takes a voice packet from every audible talker every frame period and 256 covers single-digit milliseconds of that. Watch droppedVoice on /health, which should stay flat. "));
248
250
t.Fields.Add(newFieldDoc("PeerUpdatePeersPerWorker"," Peers each worker in the transport's per-peer update pass is expected to service. int; 0 = 128. Lower means more workers for the same player count. This is the setting that decides how much of a large machine the server can actually use: the default was fitted to a 32-thread host, and it sizes the pool by population rather than by the machine, so at 4000 peers it picks 31 workers however many cores exist - a 128-core host then sits near a quarter utilisation. Halve it to double the workers. Tune it against the pass time in the [CPU] log line: above 25 ms with cores to spare means this is too high. Machines with many slower cores want a lower value than few fast ones, because each worker gets through fewer peers per pass. "));
249
251
t.Fields.Add(newFieldDoc("PeerUpdateParallelism"," Worker cap for the transport's per-peer update pass. int; 0 = automatic and recommended. Automatic sizes the pool from the peer count and holds it inside the share the core allocator has granted this pass, which moves with load - the pass widens while it is behind and gives the cores back when it recovers. Setting a number here pins the pool and opts out of that, and is clamped to the core count. The pass runs hundreds of times a second and does little work per peer, so letting it spread across every core costs more in thread wake-up and GC-poll traffic than it saves. Watch the 'peer-update N/M workers' figures and the pass time in the [CPU] log line before overriding. "));
250
252
t.Fields.Add(newFieldDoc("MaxSendSockets"," Ceiling on sockets the server may add at runtime when the network path is what limits it. int; 0 = auto (half the CPU cores, floored at 4, never above the core count). Linux only - needs SO_REUSEPORT. Each socket is both an extra send path and an extra receive thread. Sends: the send loop gets SLOWER with more threads on one socket - measured at 1000 players, 8 to 16 to 32 send workers took the update phase from 6.1 to 12.9 to 15.4 ms per tick while throughput fell from 497 to 393 MB/s - so another socket, not another core, is what adds capacity. Receives: one receive thread is one core's worth of syscall throughput, and past that the kernel discards inbound datagrams; this never appears as high CPU because the thread is pinned either way, so it is detected from the kernel's RcvbufErrors counter. On machines with many weak cores the useful range runs to 64 sockets, which is where the auto derivation lands on a 128-core host. Growth needs sustained pressure, except on receive drops which act immediately - and a socket added for drops is then checked: if the drop rate does not fall, growth stops, because the cause is not something more receive threads can fix (raise sysctl net.core.rmem_max, or the link is full). Grow-only. "));
0 commit comments