No description
  • Rust 67.7%
  • JavaScript 19.5%
  • C 4.5%
  • Shell 3.4%
  • HTML 3.1%
  • Other 1.7%
Find a file
gabrielgad 235974db00
All checks were successful
CI / build-and-test (push) Successful in 2m24s
Release / build-and-test (push) Successful in 1m11s
Release / release-image (push) Successful in 48s
Bump workspace to 0.13.1
release.yml verifies the tag against Cargo.toml and fails the run if they
disagree, so the version has to move with the tag.
2026-08-27 08:52:54 -04:00
.github/workflows ci: parse the workflow files, and fix the one that did not 2026-08-08 15:43:54 -04:00
assets splitux-together: browser Remote-Play-Together over WebRTC 2026-06-12 08:54:12 -04:00
crates admin: a backlog panel for every multiplayer game we could support 2026-08-08 17:56:02 -04:00
deploy deploy: ship gadgetd with seat-streamer as one matched pair 2026-08-08 13:58:34 -04:00
docs seat-streamer: pin ICE to a fixed port range and record why a path was chosen 2026-08-07 07:17:14 -04:00
scripts pacing shim, adaptive-streaming design, bench tooling 2026-07-02 20:56:15 -04:00
tests seat-streamer: let the viewer declare when it stops watching, and give the client the host clock 2026-08-07 09:28:13 -04:00
tools seat-streamer: let the viewer declare when it stops watching, and give the client the host clock 2026-08-07 09:28:13 -04:00
web Lobby widget follows Element's light/dark theme 2026-08-27 08:45:47 -04:00
.containerignore single-URL invites, server-side TURN, and lab deploy 2026-06-12 14:39:24 -04:00
.gitignore gitignore the deploy script's isolated target dir 2026-08-07 09:37:06 -04:00
Cargo.lock Bump workspace to 0.13.1 2026-08-27 08:52:54 -04:00
Cargo.toml Bump workspace to 0.13.1 2026-08-27 08:52:54 -04:00
README.md splitux-together: browser Remote-Play-Together over WebRTC 2026-06-12 08:54:12 -04:00
RELEASING.md release: publish through Forgejo's API, and stop failing silently 2026-08-08 15:42:07 -04:00
WEBCODECS-PLAN.md webcodecs: keep decoding through a gap on an intra-refresh stream 2026-07-29 12:35:17 -04:00

splitux together

Remote-Play-Togetherstyle 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, not webrtcsink. Each seat is 1 game → 1 player → 1 peer, so webrtcsink's multi-consumer fan-out is unused overhead, and it can't drive vulkanh264enc (no vulkanupload insertion, no congestion-control bitrate support). Plain webrtcbin gives full pipeline control and HW encode.

Validated on this machine (RX 9070 / RADV GFX1201, gst 1.28)

  • vulkanh264enc encodes H.264 (constrained-baseline — ideal for universal browser decode). ✔
  • vulkanupload ! vulkanh264enc ! h264parse(config-interval=-1) ! rtph264pay produces 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