ENetMultiplayerPeer.CreateServer(port, 256, 3) on 4.7.2 |
Inputs stalled about 350 ms at a time, 13 to 17 sequence gaps in 3 s, reliable RPCs included; (port, 256) had none. The source passes max_channels + 2 as the incoming bandwidth limit; Godot's master branch has the arguments in the right order |
| Headless Godot with no frame cap |
About 144 frames a second; Engine.MaxFps = 60 used 30 to 40% less CPU |
tc qdisc ... netem in this container |
Not available (the kernel lacks sch_netem); bandwidth shaping only |
A lag-simulating MultiplayerPeerExtension around ENet, server and 4 clients, 15 s |
RTT 0.25, 76, 132 and 297 ms at the four settings; 0 corrections with inputs repeated three times, 144 (up to two steps) with one copy at 5% loss; 0 checksum mismatches |
| A server and 50 clients as 50 processes (60 Hz inputs, 30 Hz snapshots, about 2 minutes) |
About 95% of one core in all with Engine.MaxFps = 60 (155% without); about 163 MB per process, 8 GB in all; 24 KB/s down and 2 KB/s up per client |
The same 50 clients in one process (a SceneMultiplayer per branch) |
16% of one core, 184 MB |
| 250 players, full-world snapshots |
Breaks: 4 KB snapshots fragment, reliable round trips reach 18 s. Full-world snapshots grow with the square of the players; past about 80 to 100, send only what each player needs (interest management) and deltas |
| The first seconds after 50 clients connect at once |
385 inputs lost in one 30-second run, round trips up to 2 s: keep the warm-up out of any assertion |
| One process, a server and 50 clients, 25 ± 10 ms each way and 1% loss, 5 minutes |
Memory 179 to 180 MB, about 47% of one core, 0 corrections, 0 checksum mismatches |
| Godot 4.7.2 mono quitting |
2 of about 60 processes crashed on exit (signal 11) and lost buffered output: write results to a file before quitting |
Godot 4.7.2 class reference (GodotSharp.xml), SceneReplicationConfig.ReplicationMode |
Always: "constantly sending updates using unreliable transfer mode"; OnChange: "sending updates using reliable transfer mode when its value changes"; visibility per peer, no priority or byte budget |
The C# example's TRIBES model (ReplicationTests): 80 objects, 4 state groups, 8 changes a tick, 30 packets a second of at most 400 bytes, 20% loss, 120 ± 30 ms, 900 ticks then 120 quiet |
212 of 1,020 packets lost; the client ends with exactly the in-scope objects and current values; 0 older values applied; 51,084 bytes against 1,005,540 for full snapshots of the scope (5.1%), both at 4 bytes a value; 1,366 lost values resent, 152 stale ones not |
| The same replicator, 40 objects with priorities 1 to 40, room for 5 a packet, all changing every packet, 992 packets |
Updates sent: priority 40, 241; priority 20, 124; priority 1, 6: bandwidth follows priority and nothing starves |
| The same replicator, an 8-player fight and 200 objects entering scope at once, 400-byte packets |
Uncapped: the opponent got no update for 9 packets in a row, all created within 10; MaxOverridesPerPacket 4: none missed, all created within 11 |
The C# example's event stream (EventStreamTests): 60 objects appearing over 600 ticks, spawn events posted a tick after the object enters scope, guaranteed hits and unguaranteed footsteps, 400-byte packets (100 for events), 20% loss, 120 ± 30 ms, 150 quiet ticks |
750 packets, 141 lost; every guaranteed event processed exactly once and in order, 85 resent, 163 unguaranteed lost and never resent; events ignoring their ghost (as a separate reliable channel would): 11 processed before the object existed on the client; waiting for the ghost's creation: 0 |
The C# example's bit packing (BitPackerTests) |
A position in -1024..1024 m at 1/1024 m takes 22 bits, worst error 1/2048 m; position, health 0..100, a 16-state animation and a flag: 56 bits, 7 bytes, against 20 at 4 bytes a value. GhostFormat sends a ghost's groups at those widths: its creation 14 bytes with the 4-byte header, against 27; a health change alone 8 against 12 |
LinkBudget (ReplicationTests): 120 objects changing every tick at 30 ticks a second, 20,000 bytes a second of uplink; the client asks for 20 packets of 300 bytes, then 10 of 120, then 60 of 2,000 |
Exactly 20, 10 and 30 packets every second; never a packet over the request; 5,940, 1,180 and 19,913 bytes a second (the last capped to 666-byte packets by the uplink) |
The C# example's netcode model at 50, 100 and 250 ms with jitter and 2% loss (NetcodeTests) |
Clients end exactly on the server's state; no input lost; interpolated steps of one tick; every rewound hit registers, none without rewinding |
The C# example's rollback session (RollbackTests, 3 + 7 frames, 3,600 ticks) at 50, 100 and 250 ms (jitter 5, 10, 15 ms, 2% loss) and 333 ms (none) |
No pause; every confirmed checkpoint equals the same match played offline; 0, 146, 295 and 300 rollbacks for 301 remote input changes, mean depth 0, 1.01, 4.92 and 7.00, never above 7 |
| The same, the first round trip that pauses (10 ms steps, no jitter or loss) |
340 ms for 3 + 7 frames (the window is 333 ms), 200 for 2 + 4 (200), 300 for 1 + 8 (300). At 340 ms 327 of 3,600 ticks paused; at 400 ± 20 ms with 2% loss, 829; both stay in sync |
| Time sync: 190 ± 4 ms, one peer started 4 frames ahead, 3,000 ticks after a 600-tick warm-up |
Without: the peer ahead re-simulates 7.00 frames a rollback, the other 0, and stays 4.00 frames ahead. With: 3.00 and 3.00, 0.00 ahead, after stretching 34 ticks by 2 ms (66.7 ms, 4 frames) |
| Save policy: re-simulated frames a tick, rollbacks (with nothing wrong), worst tick in steps + saves; every frame against confirmed frame |
150 ms: 0.17, 300 (0), 4 + 4 against 1.21, 735 (435), 8 + 1. 250 ms: 0.41, 298 (0), 7 + 7 against 2.40, 1,277 (979), 8 + 1. 333 ms: 0.58, 300 (0), 8 + 8 against 6.98, 3,590 (3,290), 8 + 1 |
A value refreshed every 8 frames into a field Save() misses (PoseCacheSim) |
SyncTest depth 1 passes 3,600 ticks; depth 7 reports tick 3, found while re-running ticks 2 to 8 after the refresh at tick 8; a rollback session at 250 ms desyncs from the offline match |
| The same at 250 ± 25 ms, 4 players, a 10 s storm of 4 packets in a row lost every half second, then calm |
Waiting server: 0 corrections, moving players left standing 930 times. Repeating the last input at a fixed rate: 3,441 repeated (887 in the calm), 291 corrections, about 0.15 inputs queued. With time dilation of 2 / 5 / 10%: 246 / 211 / 250 repeated (16 / 43 / 50 in the calm), 34 / 33 / 35 corrections, 3.0 / 3.2 / 3.3 inputs queued late in the storm, 1.1 / 2.1 / 2.8 in the calm; 1%: 345 repeated; 20%: 395, and 4.0 queued in the calm |
| 10 packets in a row lost 4 times at 250 ms, 2 players |
3 copies of each input: 64 inputs lost, 6 corrections. Every unacknowledged input (capped at 32): 0 lost, 0 corrections, 18.5 inputs a packet on average, 30 at most |
| A 250 ms rewind cap |
A 500 ms client's shots rewound 553 to 643 ms without it, all hits; with it 0 of 16 hit. A 50 ms client (150 ms back) is unaffected, 16 of 16 |
soak on tests/soak-fixture, a server and 4 clients for 30 s (standard 4.7.2) |
Each process about 95 MB, flat; under 2% CPU each; round trip about 7 ms |
soak with --leak (16 KB a tick never freed) and --crash-after=5 |
soak-leak at about 1700 MB/h; soak-failed naming the server's exit code 3 |
soak -JunkPps <n> on tests/soak-fixture, a junk-UDP flood against the server's own port on 127.0.0.1, 4 clients, 20 s (standard 4.7.2, 4 cores) |
None and 1,000 and 10,000 pps: input loss 0.1 to 0.6%, worst round trip 7 to 24 ms, all clients fine. 50,000 pps: input loss about 38%, round trips up to 5.9 s, a client dropped, soak-failed. 100,000 pps: input loss 50 to 57%, round trips 0.3 to 1.2 s, one or two clients dropped, soak-failed |
| Where the Godot 4.7.2 ENet server degrades (same run) |
The cliff is between 10,000 and 50,000 junk packets a second: one poll draining at most 256 datagrams per service call, on the main thread, copes with tens of thousands a second, then collapses. Between the talk's "naive 100 to 1,000" and "well-written ~250,000". Derived, this machine and fixture |