- Rust 67.7%
- JavaScript 19.5%
- C 4.5%
- Shell 3.4%
- HTML 3.1%
- Other 1.7%
release.yml verifies the tag against Cargo.toml and fails the run if they disagree, so the version has to move with the tag. |
||
|---|---|---|
| .github/workflows | ||
| assets | ||
| crates | ||
| deploy | ||
| docs | ||
| scripts | ||
| tests | ||
| tools | ||
| web | ||
| .containerignore | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
| RELEASING.md | ||
| WEBCODECS-PLAN.md | ||
splitux together
Remote-Play-Together–style online co-op layered on top of splitux. Splitux already turns one machine into N isolated split-screen seats (gamescope + bubblewrap per player, per-instance controller isolation, Goldberg LAN emulation). splitux together adds the missing half: let a remote friend occupy one of those seats from a browser — receiving the game video over WebRTC and sending their gamepad back into a virtual controller that splitux assigns to that instance.
splitux ── launches ──► N × [ gamescope + bwrap + game ] (already exists)
│ │ each = a capturable surface
│ assigns uinput pad k ──────┘
▼
seat-streamer (one per seat):
pipewiresrc(gamescope-k) ! vulkanupload ! vulkanh264enc ! h264parse
! rtph264pay ! webrtcbin ─────────────────────────────┐ WebRTC
datachannel("input") ◄───── W3C Gamepad frames ───────────┤
└─► writes to uinput pad k (splitux binds it to instance k)│
▼
orchestrator: signalling broker + seat registry + static web host
browser: lists seats, click → <video> + Gamepad API → datachannel
Why this shape
- No Wolf, no Sunshine. Splitux is the multi-seat engine already; layering Wolf on top would mean two seat managers fighting. We only needed the streaming half.
- Vulkan HW encode, not x264. On RDNA4 (RX 9070) there is no VAAPI encode;
hardware encode is Vulkan Video (
vulkanh264enc). This is essential here because the box is simultaneously running N games — software (x264) encode would steal CPU from the games, while VCN (the encode block) is separate silicon that doesn't touch render performance. webrtcbin, notwebrtcsink. Each seat is 1 game → 1 player → 1 peer, so webrtcsink's multi-consumer fan-out is unused overhead, and it can't drivevulkanh264enc(novulkanuploadinsertion, no congestion-control bitrate support). Plainwebrtcbingives full pipeline control and HW encode.
Validated on this machine (RX 9070 / RADV GFX1201, gst 1.28)
vulkanh264encencodes H.264 (constrained-baseline — ideal for universal browser decode). ✔vulkanupload ! vulkanh264enc ! h264parse(config-interval=-1) ! rtph264payproduces browser-ready RTP (packetization-mode=1,profile-level-id=42c02a,sprop-parameter-sets) at 1080p60. ✔
Crates
| crate | what |
|---|---|
crates/seat-streamer |
one process per seat: webrtcbin + Vulkan pipeline + persistent uinput virtual gamepad. gst is the offerer; input datachannel → pad. |
crates/orchestrator |
axum signalling broker (/ws/producer, /ws/viewer), seat registry (GET /api/seats), serves web/. |
web/ |
plain-JS RTCPeerConnection answerer + Gamepad API client. |
Dev quickstart (local loopback, video = test pattern)
# 1. orchestrator (serves web + signalling) on :8080
cargo run -p orchestrator -- --web web
# 2. a seat (test source until pipewire capture is wired)
# needs access to /dev/uinput (see below)
cargo run -p seat-streamer -- --seat seat-1 --name "Player 1 — test" \
--signalling ws://127.0.0.1:8080/ws/producer
# 3. open http://localhost:8080 , click the seat, plug in a controller
Test benches
scripts/smoke.sh # headless full-loop regression (aiortc peer; no game)
scripts/bench-terraria.sh # POC: launch Terraria in gamescope-splitux + stream it
scripts/harness.sh # attach to a game you launched in splitux (1 seat per player)
bench-terraria.sh is the self-contained proof: it launches Terraria inside
gamescope-splitux the way splitux does on Wayland (default nested-Wayland
backend), reads the gamescope PipeWire node id from its log, and streams that
node to a browser seat. Open http://localhost:8080 and click "Terraria".
Video only for now — remote gamepad→game needs the seat→instance pad binding.
/dev/uinput access: add a udev rule or run the seat with group input.
KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"
Status / next
- gst webrtc stack installed, HW-encode + RTP chain validated headlessly
- seat-streamer (webrtcbin offerer, Vulkan, uinput pad)
- orchestrator (signalling + registry + static)
- browser client (answerer + gamepad)
- real capture: pipewire node of each gamescope surface (
--source pipewire --pw-node <id>) - splitux binding: pre-create pads, assign seat→instance deterministically
- VPS edge: nginx
game.gabeforge.com+ coturn TURN + signalling over wg0