Networking

Aether's QUIC-based transport layer with delta compression, interest management, voice channels, and client-side prediction reconciliation.

Aether's networking layer provides low-latency, encrypted communication between clients and servers. Built on QUIC via the quinn crate, it offers multiplexed reliable streams and unreliable datagrams in a single connection, with built-in TLS 1.3 encryption and connection migration. This guide covers the transport architecture, state synchronization, interest management, and voice system.

Overview

The networking system is split across two crates:

  • aether-network -- Policy abstractions for interest management, delta coding, prediction, voice, and the RuntimeTransport trait.
  • QUIC transport backend -- A concrete RuntimeTransport implementation using quinn for socket I/O.

This separation means the engine's networking logic is transport-agnostic. You can swap in a different transport (e.g., InMemoryTransport for testing) without changing the synchronization or interest code.

QUIC Transport

Architecture

The transport is organized into composable components:

use aether_network::quic::{QuicTransport, QuicConfig};

let config = QuicConfig::from_env();
let transport = QuicTransport::new(config).await?;

Internally, QuicTransport manages:

  • QuicServer -- Accepts incoming client connections and maintains a connection map.
  • QuicClient -- Connects to a server and holds a single connection.
  • QuicConnection -- Wraps a quinn::Connection with methods for reliable and unreliable messaging.
  • TLS certificate handling (self-signed for development, file-based for production).

Handshake Protocol

Every connection begins with a handshake that authenticates the client:

Client -> Server: [MAGIC "AETH"] [VERSION 1] [CLIENT_ID 8 bytes] [TOKEN_LEN 2 bytes] [TOKEN N bytes]
Server -> Client: [MAGIC "AETH"] [VERSION 1] [STATUS 1 byte] [SERVER_TICK 8 bytes]

Status codes: 0 = OK, 1 = Rejected, 2 = Version Mismatch.

The handshake completes before any game data flows, ensuring both sides agree on protocol version and authentication.

Connection Lifecycle

Connections progress through a state machine: Disconnected -> Connecting -> Handshaking -> Connected. From Connected, a lost connection enters Reconnecting, which either retries within the timeout or drops to Disconnected.

Data Channels

Messages are sent through two delivery modes:

use aether_network::transport::{Reliability, RuntimeTransport};

// Reliable ordered delivery via QUIC bi-directional stream
transport.send(client_id, &rpc_bytes, Reliability::ReliableOrdered).await?;

// Unreliable delivery via QUIC datagram
transport.send(client_id, &state_bytes, Reliability::UnreliableDatagram).await?;

Reliable messages use length-prefixed bi-directional QUIC streams. Unreliable messages use the QUIC datagram extension, with automatic fallback to an unreliable stream if datagrams are not supported or the message exceeds MAX_DATAGRAM_SIZE (1,200 bytes).

Delta Compression

To minimize bandwidth, Aether sends only the changes between successive state snapshots rather than full entity state each tick.

XOR-Based Diffing

The delta codec computes a byte-level XOR diff between the previous and current state:

use aether_network::delta::DeltaCodec;

let codec = DeltaCodec::new();
let delta = codec.encode_delta(&previous_state, &current_state);

// On the receiving end
let reconstructed = codec.apply_delta(&previous_state, &delta);
assert_eq!(reconstructed, current_state);

Quantized Encoding

Position and rotation values are quantized to reduce payload size:

DataEncodingPrecision
PositionFixed-point, 1 mm stepSub-millimeter
Rotation10 bits per component~0.35 degree resolution

This quantization is applied before delta encoding, further reducing the diff size since small movements produce minimal byte changes.

Interest Management

Not every client needs every entity update. The interest management system assigns entities to distance-based priority buckets relative to each client:

BucketUpdate RateDescription
CriticalEvery tickEntities the player is directly interacting with
HighEvery 2 ticksNearby entities within close range
MediumEvery 4 ticksEntities at moderate distance
LowEvery 8 ticksDistant but visible entities
DormantNo updatesOut of range, no data sent
use aether_network::interest::{InterestManager, BandwidthBudget};

let mut interest = InterestManager::new();
let budget = BandwidthBudget::new(64_000); // 64 KB/s per client

// Each tick, compute priorities for a client
let priorities = interest.compute_priorities(client_id, &entity_positions);

// Select the top-N entities that fit within the bandwidth budget
let selected = interest.select_within_budget(&priorities, &budget);

The system uses top-N prioritization: after assigning buckets, it selects the most important entities that fit within the client's bandwidth budget.

Voice Channels

Aether supports spatial voice communication using the unreliable datagram channel:

use aether_network::voice::{VoiceConfig, JitterBuffer};

let config = VoiceConfig::from_env();
let mut jitter_buffer = JitterBuffer::new(config);

// Incoming voice datagrams are buffered to smooth out jitter
jitter_buffer.push(voice_datagram);

// The audio system reads smoothed samples
if let Some(frame) = jitter_buffer.pop() {
    audio_output.play(frame);
}

Voice datagrams carry metadata including the sender ID, sequence number, and spatial position for 3D audio spatialization. The jitter buffer reorders packets and fills gaps to produce smooth audio output.

Client-Side Prediction

The prediction system works with the world runtime's InterpolationBuffer to hide latency:

use aether_network::prediction::{PredictionQueue, ReconciliationStatus};

let mut prediction = PredictionQueue::new();

// Client predicts the result of local input
prediction.push_predicted(tick, input, predicted_state);

// When the server confirms a tick
match prediction.reconcile(server_tick, &server_state) {
    ReconciliationStatus::Consistent => {
        // Prediction was correct, discard old entries
    }
    ReconciliationStatus::Corrected { delta } => {
        // Apply correction and replay subsequent inputs
    }
}

The reconciliation flow:

  1. The client applies inputs locally and stores predicted states.
  2. The server processes the same inputs authoritatively and broadcasts results.
  3. The client compares server state to its prediction for that tick.
  4. If they match, old predictions are pruned.
  5. If they diverge, the client rewinds to the server state and replays all inputs since that tick.

Configuration

All networking parameters are controlled via environment variables:

VariableDefaultDescription
AETHER_NET_BIND_ADDR0.0.0.0:4433Server bind address
AETHER_NET_SERVER_ADDR127.0.0.1:4433Client connect address
AETHER_NET_CERT_PATH(self-signed)TLS certificate PEM path
AETHER_NET_KEY_PATH(self-signed)TLS private key PEM path
AETHER_NET_CONNECT_TIMEOUT_SECS10Connection timeout
AETHER_NET_IDLE_TIMEOUT_SECS30Idle connection timeout
AETHER_NET_RECONNECT_TIMEOUT_SECS30Reconnect window

Key Types

TypeDescription
QuicTransportQUIC-based RuntimeTransport implementation
QuicConfigConnection settings loaded from environment variables
QuicConnectionWrapper around quinn::Connection with send/recv methods
RuntimeTransportTrait abstracting reliable and unreliable message delivery
DeltaCodecXOR-based delta compression encoder/decoder
InterestManagerDistance-based entity priority bucketing
BandwidthBudgetPer-client bandwidth allocation for interest selection
PredictionQueueClient-side prediction state with reconciliation
JitterBufferVoice datagram reordering and smoothing buffer
ReliabilityEnum: ReliableOrdered or UnreliableDatagram