A headless Rust client for the Basis VR platform that implements the Basis protocol using basis-protocol and basis-transport.
This is a headless client implementation that mimics the behavior of the Basis VR client. It connects to the Basis server and can simulate multiple clients with configurable behavior including movement, chat, and resource loading.
- UDP-based client using the same protocol as the Basis server
- Configurable via XML config file
- Multiple client simulation support
- Movement and camera simulation
- Chat support
- Resource loading simulation
- Opt-in voice simulation from Ogg Opus files
- Interactive console controls for voice, client scaling, and graceful shutdown
- Automatic reconnection
- v56 default with automatic v55 compatibility on a structured version rejection
- Validated MTU-probe replies on Windows and the Linux shared receiver
cargo run --releaseConfiguration is read from Config.xml by default. You can specify a different config and server address via command-line arguments:
cargo run --release -- --helpVoice simulation is disabled by default. By default, it requires ffmpeg on
PATH to re-encode input files into Unity-compatible voice packets. To enable
it, place .opus or .ogg Ogg Opus files in the configured audio folder, then run:
cargo run --release -- --voice --voice-audio-folder audio --voice-speaker-percent 10The client re-encodes each input file through ffmpeg into 48 kHz mono Opus with
20 ms frames, then sends those packets over the Basis voice channel. Speaking
clients send to peers within the configured hearing distance, which defaults to
25 meters.
Audio loading runs up to eight conversions in parallel, limited by the available CPU count. The console shows each file's encoded audio time and speed, plus the completed/total file count. One Ctrl+C cancels all active conversions, removes their temporary outputs, and stops queued files from starting before shutdown.
While running, the console accepts these commands:
voice enable voice simulation
add [count] add clients (defaults to 100)
quit [batch] [ms] disconnect clients in batches (defaults to 100 / 250 ms)
help show commands
Voice can therefore be enabled after startup with voice, and add grows the
population without restarting. Ctrl+C uses the same graceful batched shutdown.
The shutdown defaults can be changed with --quit-batch-size and
--quit-batch-delay-ms.
To bypass the ffmpeg re-encode/cache path and send packetized input Opus directly:
cargo run --release -- --voice --no-voice-reencodeThe client reads Config.xml relative to the current working directory by
default, or the path passed to --config (also relative to the working directory
unless absolute). The tracked Config.xml is a sample; use an ignored file such
as config/local.xml for local credentials and pass --config config/local.xml.
A missing config is generated from client defaults. Malformed XML warns and
falls back to all defaults; invalid/empty supplied scalar values fall back per
field. Use --strict-config to reject malformed XML and invalid supplied scalar
values instead of falling back; errors identify the file and field. Explicit
CLI --ip, --port, --clients and other supported runtime
switches override loaded values. Server PascalCase environment overrides and
BASIS_SERVER_PASSWORD do not configure the client; set its XML Password to
match the server. BASIS_CLIENT_TOKIO_WORKERS configures the client's runtime
worker count only. See --help for CLI options.
The client attempts Basis protocol v56 first. If the server rejects that
connection with an explicit, structured v55 version mismatch, the same client
retries once with the v55 connection layout (no ephemeral key), then signs the
original nonce-only DID challenge and sends unencrypted client packets. Other
rejections, including invalid passwords, do not trigger fallback. The retry
stays on the same UDP socket and preserves the client's identity and ready state.
Because the version rejection is unauthenticated and v55 lacks v56's encrypted
uplink, this automatic downgrade is intended only for trusted v55 servers.
Set BASIS_CLIENT_DISABLE_V55_FALLBACK=1 to require v56 and reject attempts
to downgrade the connection.
AvatarPassword, AvatarUrl and AvatarLoadMode are client values, not server
configuration fields. The server uses a separate schema/path and defaults.
Voice settings include VoiceEnabled, VoiceAudioFolder, VoiceSpeakerPercent,
VoiceHearingDistance, and VoiceFrameDurationMs.
For a measured Windows voice/avatar load test, build the release server and
client, then run python scripts/perf/run-windows-voice-workload.py --audio-folder <Ogg-Opus-folder> --output <new-capture-directory> from the
repository root. The default suite uses 1,000 local clients: a voice preflight,
an avatar-only baseline, 10% simultaneous speakers, then 100% speakers. It
prepares every source clip as 48 kHz mono/20 ms Opus, freezes the binaries,
records process/health counters, and verifies sampled received packets against
the encoded corpus and with an FFmpeg decode.
The lower-level Windows runner also accepts --voice-audio-folder,
--voice-speaker-percent, and --no-voice-reencode. It sets
BASIS_VOICE_DIAGNOSTIC_CSV to enable per-client voice counters and an observer
window synchronized with the avatar capture marker. Other clients allocate no
voice diagnostic counters. These measurements exercise headless packet delivery;
they do not emulate 1,000 audio playback engines or a physical network.
Voice runs default to a 24 GiB server working-set limit. The runner captures a
controlled early stop when that limit is reached, and starts measurement early
if warmup reaches half the limit. Override this with
--max-server-working-set-mib in the lower-level runner; 0 disables the limit.
cargo build --releaseWindows uses mimalloc by default; opt out with
cargo build --release --no-default-features. Linux retains its allocator.
At 1,000 clients, sampled peak working set fell from 85-86 MiB to 29-32 MiB; no overall speedup was established. Windows also replies to validated MTU probes to reduce packet-processing cost. See the performance summary for results.
The client workspace contains:
basis-rust-client— CLI parsing, interactive console input, runtime setup, and Windows allocator.basis-client-core— Configuration, connections, reliability, platform receivers, avatar simulation and observation, voice, population management, and command dispatch in focused modules.
The core uses the server's basis-protocol for message serialization, network
writers, avatar codecs, delta encoding, and DID responses. It uses
basis-transport for packet types, delivery methods, sequence comparison, wire
constants, and timestamps. Client socket handling and shared receivers remain
in the core because the server transport's connection API accepts server peers.
Other Rust callers can construct basis_client_core::ClientOptions with
Default and call basis_client_core::run on their Tokio runtime, supplying a
callback that starts a command channel. The console binary and build output
paths are unchanged.
Validate all client packages with:
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warningsPersistence simulation is opt-in. Each admitted, identity-verified client writes
an exact 16,384-byte changing binary value to its own load-test/state key on
channel 32, then reads it back and compares every byte. The data is deterministic
for verification and varies with the client, connection and write sequence.
Only one Set/Get chain is in flight per client. Initial sends are staggered;
the nominal interval defaults to 30 seconds. It works with randomized and
--sync-batching movement modes, including Linux shared receive.
Enable it in Config.xml:
<PersistenceUploadEnabled>true</PersistenceUploadEnabled>
<PersistenceUploadIntervalSeconds>30</PersistenceUploadIntervalSeconds>Or override XML from the command line:
cargo run --release -- --persistence-upload --persistence-interval-secs 10 \
--observe-persistence-csv persistence.csv
# Explicitly disable a workload enabled in XML:
cargo run --release -- --persistence-upload=falseThe server needs verified DID authentication and basis.storage.use permission.
Set StorageMaxBytesPerPlayer to 32768 (or at least 16404): the 16 KiB value
plus scope/key overhead exceeds the default 16 KiB player quota. A smaller quota
produces Full replies rather than silently shrinking the requested payload.
--observe-persistence-csv records operation counts, statuses, distinct writers,
readback failures, timeouts, backpressure, and Set/Get/complete-cycle p50/p95/p99
latencies. Latencies have 0.1 ms histogram resolution and bounded storage; the
CSV reports any histogram overflow. --observe-persistence-start-file PATH
excludes warmup sends until the marker appears. The observation window defaults
to 60 seconds (--observe-persistence-window-secs); uploads stop at its end so
pending readbacks can drain. Without CSV observation, uploads continue until
shutdown. Each operation has a 10-second reply timeout. Disconnects cancel old
connection requests; late replies cannot complete a replacement connection's work.
The CSV describes Set plus readback traffic, not write-only capacity. Server errors and mismatched data are counted explicitly. See the persistence benchmark report for measured workload settings and limitations.