Latency

Why Dish feels like a wire.

Latency is the one metric that matters in a controller. Dish was built around it on day one.

The latency budget

A wired Xbox controller polls at 250 Hz on Windows. That means your game can read a button press up to 4 ms after it happened, just from the polling rate. Stack a 60 fps frame on top of that and worst-case finger-to-pixel on a wired pad lands around 20 ms.

Dish was built to add almost nothing to that budget on a normal Wi-Fi 6 LAN. Here's where every millisecond goes:

Stage Typical time
Touch / button event surfaces in OS < 1 ms
Dish builds + encrypts 12-byte packet < 0.1 ms
UDP send + Wi-Fi airtime + UDP recv 1–4 ms (5 GHz)
Satellite verifies + injects via ViGEmBus < 0.5 ms
Game polls the new pad state 0–4 ms (depends on game)
Frame rendered to monitor 0–16 ms (depends on fps)
End-to-end (typical) ~6–25 ms

For reference, Bluetooth pads usually add 8 to 15 ms on top of their wired siblings. Dish's worst case on a healthy LAN is roughly the best case of a Bluetooth pad.

How we keep it tight

  • No queue, no async runtime. The input event handler calls sendto inline. No producer/consumer queue, no event loop hop, no Combine, no Kotlin coroutine. The input thread is the network thread.
  • Raw UDP. Not TCP, not WebSockets, not gRPC, not QUIC, not even NWConnection on Apple platforms. Raw POSIX sockets so we can set IP_TOS ourselves.
  • DSCP EF marking. Outbound packets are tagged DSCP class EF (0xB8). QoS-aware routers and access points jump them ahead of bulk traffic.
  • Tiny packets. 12 bytes of payload, around 50 bytes on the wire once UDP, IP, and 802.11 headers are in. One Wi-Fi frame.
  • Zero allocations on the hot path. Buffers are stack or pre-allocated. No GC pause can stretch a packet send.
  • Direct kernel injection. Satellite calls DeviceIoControl straight into ViGEmBus. No DLL marshalling, no IPC, no service round-trip. The receiver hot path is three syscalls with zero allocations: recvfrom()memcpy()DeviceIoControl().
  • Time-critical receive thread. Satellite pins its UDP receive thread to THREAD_PRIORITY_TIME_CRITICAL on Windows and to core 0 affinity, so a runaway browser tab can't starve your inputs.

What eats latency in practice

Every "Dish feels laggy" report we've seen traces back to one of these. None are Dish's fault, all have a fix:

  • 2.4 GHz Wi-Fi. Microwave interference, neighbor congestion, lower bandwidth ceiling. Fix: put both your phone and your gaming PC on 5 GHz or 6 GHz.
  • Wi-Fi power-save on Android. Some Android phones delay outbound packets by 30 to 100 ms once the screen sits still. Dish keeps a heartbeat every 2 seconds: small enough to be free on the radio, frequent enough to stop the OS from putting the Wi-Fi chip to sleep.
  • Mesh repeaters. Each hop adds 1 to 3 ms. Park Satellite on a node that's wired into the main router if you can.
  • USB-C dock with Ethernet on a Thunderbolt hub. Some cheap docks buffer. Use the laptop's built-in Wi-Fi or a passthrough adapter instead.
  • 120 Hz vs 60 Hz. A 120 Hz monitor halves the worst frame-time slice of the budget. Free 8 ms.

Want to measure it yourself?

Satellite's web UI at localhost:9877 shows live RTT per connection, the last loop microseconds, the peak loop microseconds, submit success and failure counters, and replay drop counts. The /debug page is the receiver's full performance read-out. If you see anything above 10 ms RTT on the same Wi-Fi, something else on your network is acting up. Open an issue with a screenshot of the debug page and we'll help you dig in.