ODSC - Batch to Stream workshop - integration of Apache Spark, Cassandra, Pos...
Networking Architecture of Warframe
1.
2. Outline
• Replication system overview
• Case study: Lunaro ball throw
• Replication: going wide
• Congestion control
• Dedicated servers
3. References/thanks
• David Aldridge, I Shot You First! (Gameplay Networking in
Halo: Reach) [GDC2011]
• Timothy Ford, Overwatch Gameplay Architecture and
Netcode [GDC2017]
• Philip Orwig, Replay Technology in Overwatch [GDC2017]
4. Warframe
• A cooperative third person online action game
• 3 platforms (PC, PS4 (launch title), XB1)
• ~32 millions accounts
• Own technology stack (everything from low-level socket code to
matchmaking, all 3 platforms)
• Mostly P2P, but we support “volunteer PVP servers”
7. Replication models in games
• Deterministic Lockstep (input only)
• Snapshot Interpolation (complete world state for all clients)
• State Replication (individual, prioritized chunks for every client)
8. Replication (host to client)
• Properties per object,
not reliable, unordered,
objects sorted by priority
(different for every client)
• Events – optionally reliable,
optionally ordered
9. Properties
• (Network) property: a network relevant data field of a replicated
object
• Two ends of the spectrum: single group of properties per object
(better perf) vs N individual properties (better for bandwidth)
• Our version: somewhere in-between, dirty bit + value. Implemented
as a template: TNet<T>. If a group of properties changes together
– encapsulate and associate with a single bit
• Dynamic arrays (replicated): convenient, but can of worms (bit
record structure no longer static, so merge/mask operations get
more complicated)
10. Property priorities
• Property priority = replication frequency
• More important properties (position, health) replicated more frequently
• Perfect conditions, no throttling: exactly as frequently as defined (data-driven)
• Part of the congestion control system – if throttled – all properties replicated
less frequent
11. Object prioritization
• Motivation: in case we can’t send all the objects this frame, sort them by a
perceived importance
• Per replicated object and per client (so 2 clients can end up with a very
different set of priorities)
• Broad per-type priorities + custom code logic for special types (like avatars)
• Supports inter-object dependencies (A needs to be replicated before B, mostly
for creation messages, e.g. avatar and his weapons)
13. High/low frequency lists
• Problem: thousands of object to update, only fraction
actually important. Created a system to split into two lists
(high and low frequency) automatically
• Objects start on high frequency list by default
(+associated with a timer)
• High frequency objects tested ever frame. If not dirty for X
seconds – moved to a low frequency list. If dirty – timer
bumped slightly.
• Low frequency list traversed over the course of multiple
frames, round-robin style. If dirty – moved to a high
frequency list and grace period extended
18. Ball throw – hybrid solution
• Request ball throw in advance,
at our future hand position
• No visible lag as long as ping
less than time to release event
(~160ms in Lunaro)
• Perfect for instigator, a little off
for others
19.
20. Takeaways
• No silver bullet (perfect prediction would be the closest), every solution comes
with a different set of problems
• Human players much more concerned with themselves - “favor the shooter”
• NPCs don’t complain (“favor the human” in PVE)
• Choose wisely depending on situation (responsiveness vs ‘correctness’)
• Not so hard to predict the future if your horizon is 100-200ms
21.
22. Replication jobs - “traditional” approach
• Work item: N objects (any client)
• The most natural approach, tempting to try it first
• Good load balancing
• Lock hell, prone to races (any job can read/write to from
client’s internal structures)
23. Replication jobs – our approach
• Work item: all objects for single client
• 100% lock and wait-free, only touching
own structures
• Pre-allocated buffers to avoid contention
on memory manager
• A little bit worse load-balancing, but
we try to fill the bubbles with other jobs
25. Congestion control
• UDP has no congestion control (unlike TCP)
• Existing approaches- first idea – “let’s do what TCP is doing!”
• Bad: not just 1 approach, dozens, good: well documented, source codes exist
(e.g. Linux kernel),
• Not directly applicable – takes too much time to converge, tries to maximize
bandwidth in the long run (steady transfer), has to be very generic (as opposed
to fine-tuned for just 1 game), transport layer only
26. Congestion control – our version
• Quickly realized a “TCP approach” not going to cut it (limited to transport
layer, very generic), still got/validated some ideas (e.g. BIC)
• Very small search space (~10-80kbytes/s), majority of logic in the replication
layer (as it has more information). Very application specific, controlled by ~30-
40 parameters. Uses both RTT & packet loss as connection quality metrics
• Start reasonably high, decrease if can’t handle, only try to increase if definitely
necessary (probing). Distinguish between upstream/downstream limitations
• Track both current and allowed maximum, rebalance periodically
• Two-tier throttling: a) sending properties less frequently, b) limiting # of
updated objects (sorted by priority)
27. Dedicated servers
• Decent starting point – game/engine code split into server and client layers.
P2P host/single player: running both layers, P2P client: only client
• Dedicated server – running only server layer, no need for custom
binaries/removing code (game process with extra arguments)
• Problem: version not used/maintained (P2P only for the last few years), easy to
introduce non-obvious bugs (changing net properties from client code, works
OK in P2P (both layers), breaks for the DS (no client layer, code doesn’t run))
• Added “DS validation” mode: TNet triggers an error if modified from the client
code. Catches majority of mistakes, works even in single-player
31. Compression
• Network compression - a very special case (tiny packets, performance very
important)
• We’ve tried LZF and different Huffman variants (N trees, ‘best’ tree chosen
based on data characteristics)
• Couldn’t justify spending too much time here versus buying Oodle
• Oodle worked out of the box, gave us very good results (1.4:1 or better), the
only time consuming part is training, but can be automated to some extent
32. Multithreading – pushing it further
• Handling packet delivery information from clients (acks/nacks)
• Not very expensive (< 1ms), but was trivial to offload, so why not
• Job per-client again, less urgent, can span frame boundaries
33. • For complex types (avatars) visiting all the individual properties can get
expensive
• Solution: split into groups (components), skip entire groups if empty
• Component/controller split not always ideal for dirty masks, so had to split it
based on how frequently they change rather than gameplay structure.
Editor's Notes
High-level overview
Layers that typically build a game networking system
A little bit of history, a short taxonomy of replication models in games.
Lockstep – RTSes, wait for all clients, JIP problems, determinism. Mention For Honor (P2P)
“Quake model” – good for perf, bad for BW as games get more complex
More details in Philip Orwig’s talk
Events, not state, typically reliable & ordered
If throttled – frequencies scaled down by s=bw_av/bw_needed
If throttled – priority preserved to avoid starvation, if you’ve not been replicated for a long time you’ll eventually bubble up to the top.
Master object: current frame dirty mask
Bits corresponding to suppressed properties left enabled
Combined with mask for each client = final mask for this frame
Converges fairly quick, typically around 150 objects left on the high frequency list
Unexpected benefit: diagnostics (“why is this object hiqh freq?”)
[Aim around 14-15m]
Lunaro: elevator pitch – futuristic handball/Speedball
Rules (brief)
Throw specifically
Problem: how to hide throw latency
Non-starter: host authoritative anim+throw (send request, start the whole thing on response)
Sequence diagram – a nice way to visualize message flow, makes it immediately obvious where the lag is. Vertical line = time
200ms lag +/- 20ms jitter
Subtle artifacts, more obvious if watching frame-by-frame (e.g. AvsPmod)
Inconsistencies, warp, etc
Reconciliation – what’s the worst case scenario?
Gameplay consequences for input (two diff buttons)
Questions: where/how do we hide the lag, how do we cheat.
Originally single threaded, but less objects/4p
PS4, 2013, had to be quick (launch title). 8p, thousands of objects, single thread no longer acceptable
Only one shot, could not afford going up a blind alley
Seems ‘embarrassingly parallel’ at a first glance
Cache coherence
Parent job W1 (setup): dirty masks for master objects (work: X objects), calculate priorities, sort, spawn children jobs
Children jobs: replication
Tree-structure (root+children)
https://www.cs.helsinki.fi/research/iwtcp/papers/linuxtcp.pdf (Congestion Control in Linux TCP),
Some algorithms very dated (pre-Wifi), not dealing with ‘long fat’ networks too good, not taking RTT into account.
1st – Tahoe/Reno (packet loss). Vegas (RTT), Veno (Vegas+Reno, Wifi, random or congestion packet loss?), BIC, BBR (Google)
80k = way more than needed, but can help with v. good connection+initial spike
Transport layer – connection quality queries + per-connection limits
Priority preserved between frames to avoid starvation
Binary search when growing, rapid drop (to the nearest 16k)
No manpower for #ifdefs, DS team=1 programmer)
DS validation excludes local player/avatar (do not exist on DS, would invalidate prediction)