Skip to content

Repository files navigation

wlstream

Wire format for streaming remote Wayland sessions. Ship semantic events (surface create/destroy, pool data, commits, cursor), not pixels. LZ4-compressed, damage-aware, transport-agnostic.

License: MITSpec: v1.0Rust


Why?

Pixel streaming (VNC, SPICE, MJPEG) sends pre-rendered frames. For text-heavy UIs (terminals, editors, file managers), this wastes 50–100× the bandwidth. wlstream ships the same events the compositor uses internally — the client re-renders locally.

ScenarioPixel streamingwlstream
Idle terminal~250 MB/s0 (no event sent)
Cursor blink~250 MB/s~130 B
Text typed~250 MB/s~50 KB
Full redraw~250 MB/s~4 MB (LZ4-compressed)
Video (1080p 60fps)~250 MB/s~250 MB/s (no win)

Comparison with alternatives

FeatureVNCSPICEXprawlstream
Display serverAnyQEMUX11Wayland
ApproachPixel copyPixel copyX11 forwardEvent streaming
Bandwidth (terminal)HighHighMediumVery low
Client re-renderingNoNoLimitedFull
Damage trackingRegion-basedRegion-basedRegion-basedRect in COMMIT
CompressionTight/ZRLEQuic/GLZLZ4/zstdLZ4
Scale at render timeNoNoPartialYes
Text accessibilityLostLostPreservedPreserved
TransportRDPSPICESSH/TCPAny (socket/TCP/Zenoh)

What's in this repo?

  • spec/WAYLAND_STREAM_PROTOCOL.md — wire format specification (7 event types)
  • spec/wlstream.schema.json — machine-readable JSON Schema
  • src/ — Rust crate (wlstream):
    • parser — decode wire bytes into typed events
    • sender — encode events into wire bytes
    • compositor — state machine: events → RGBA frame
    • protocol — damage rect primitives (Rect, clamp_damage, merge_damage)
    • lz4 — thin wrapper over lz4_flex
  • docs/DESIGN.md — design rationale, alternatives rejected, threat model
  • docs/ROADMAP.md — v1.1+ plans (Zig module, reference sender/receiver, v2 protocol)

Quick start

As a Rust dependency

[dependencies]
wlstream = "1.0"
use wlstream::{Encoder,Compositor, parse_events,StreamEvent};// Sender side: encode eventsletmut enc = Encoder::new();
enc.surface_create(1);
enc.pool_data(1,800,600,3200,0,1920000,&lz4_compressed_bytes);
enc.surface_commit(1,1,0,800,600,3200,0,0,0,800,600);let wire_bytes = enc.finish();// Receiver side: decode + compositeletmut comp = Compositor::new();for event inparse_events(&wire_bytes){match event.unwrap(){StreamEvent::PoolData{ pool_id, width, height, stride, format, raw_len, lz4_data } => {
comp.handle_pool_data(pool_id, width, height, stride, format, raw_len, lz4_data);}StreamEvent::SurfaceCommit{ surface_id, pool_id, offset, buf_width, buf_height,
buf_stride, format, damage_x, damage_y, damage_w, damage_h } => {
comp.handle_surface_commit(surface_id, pool_id, offset, buf_width, buf_height,
buf_stride, format, damage_x, damage_y, damage_w, damage_h);}
_ => {}}}if comp.dirty{println!("frame: {}x{} ({} bytes, damage: {:?})",
comp.frame.width, comp.frame.height,
comp.frame.data.len(), comp.last_damage);}

Running tests

git clone https://github.com/bzdOS/wlstream
cd wlstream
cargo test

Event types

IDEventPurpose
0x01SURFACE_CREATENew wl_surface
0x02SURFACE_DESTROYSurface removed
0x03POOL_DATASHM pool contents (LZ4-compressed)
0x04SURFACE_COMMITAttach pool region + damage rect
0x05CURSOR_MOVECursor position update
0xFESESSION_RESETClear all caches (compositor restart)
0xFFERRORNon-fatal error with code + message

License

MIT. See LICENSE.

Origin

Developed as part of bsdOS — a privacy-first mobile OS on FreeBSD. Extracted as a standalone protocol

  • crate in June 2026.

About

Wire format for streaming remote Wayland sessions - semantic events (surface lifecycle, pool data, commits, cursor) with LZ4 compression and damage rects. Rust crate + JSON Schema

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages