Linux host software for the WavePI S6 USB display boards (Allwinner T113-S3, 7" 1024x600 LCD with a capacitive touch panel, firmware in key2/wavepi-s6). Each board is a USB device that decodes an H.264 stream in hardware and sends its touch panel and host-port keyboard/mouse back on the same interface. This repository turns up to eight of them into ordinary screens of one machine:
wavepi.ko: a kernel module (DRM/KMS + evdev) that exposes every board as a connector of one virtual DRM device (with an EDID carrying the board's serial), its touch panel and USB input devices as evdev devices, and/dev/wavepiNfor frame export (dma-buf) and bitstream input (write()).wavepid: a daemon that encodes each screen's frames with the Rockchip MPP hardware encoder (RK3588) or x264 (any machine) and writes the H.264 access units back to the driver; keepalive for static content, status socket (wavepi-ctl status).wavepi-wm: a small wlroots compositor: one fullscreen client per screen, one seat per screen (the board's touch and host-port devices), screens identified by serial, clients survive unplug and re-attach on re-plug, Xwayland for X11 programs, a pointer cursor for mice.apps/imgui-demo: a Dear ImGui client used as the display-path test (60 fps, wall clock overlay), one instance per screen or one process with a window per screen (--windows A,B,C);tools/: test and bench scripts.
Developed on and for a Firefly ROC-RK3588S-PC (Ubuntu Rockchip 24.04, kernel 6.1 BSP), with the module also built and tested on an x86 PC (kernel 6.14). Three boards run on the bench at 59.8 fps each, touch per board, GPU-rendered clients, ~1.7 ms from compositor commit to encoded frame.
compositor (wavepi-wm, wlroots) --KMS commit--> wavepi.ko (one connector per board)
| DQBUF/QBUF: frames as dma-bufs
wavepid --MPP/x264--> write(/dev/wavepiN) --USB bulk OUT--> board (decode, LCD)
wl_seat per screen <-- evdev (touch, keyboard, mouse) <-- wavepi.ko <--USB bulk IN (16-byte records)-- board
Phases 0-3 of the plan (STUDY.md section 8) are complete on the hardware; Phase 4 (Debian packages, a reproducible image) is next. The engineering log with every measured number and 56 lessons is docs/HANDOVER.md; the architecture and the decisions (with the evidence) are in STUDY.md.
Boards need firmware wavepi-s6 commit 3d352d2 or later (protocol 1.1 with
unique serials, and the live-display decoder pipeline: older firmware shows
at most 30 fps and lags behind a 60 fps stream).
| Path | Content |
|---|---|
kernel/wavepi/ |
the module: wavepi_drv.c (platform + DRM device), wavepi_kms.c (CRTCs/planes/connectors, emulated vblank, frame export state machine), wavepi_usb.c (USB class driver, input records -> evdev), wavepi_cdev.c (/dev/wavepiN), wavepi_edid.c, compat.h (kernel 6.1 ... 6.14), uapi/wavepi.h (ioctls), proto/wavepi-proto.h (the wire protocol, shared with the firmware), 72-wavepi.rules, dkms.conf |
wavepid/ |
the encoder daemon (wavepid.c), backends enc_mpp.c / enc_x264.c, wavepi-ctl.c, systemd unit (meson) |
wavepi-wm/ |
the compositor (main.c, config.c), example screens.toml, lab configs in examples/, systemd unit (meson) |
apps/imgui-demo/ |
Dear ImGui demo client (SDL2 + OpenGL ES 3), make fetch gets ImGui into third_party/ |
tools/ |
wavepi-grab.c (frame export test client), integ-kmod.sh (byte-exact transport test), soak.sh, kmod-cycle.sh, wm-pc.sh / cage-wavepi-*.sh (compositors on the virtual connectors), mpp-dmabuf-enc.c (MPP import bench), flash-sdwire.sh (write an image to an SDWire3 card), stream/latency helpers |
image/ |
cloud-init template for the Ubuntu Rockchip image, a DTB with the SD slot capped at 50 MHz |
docs/HANDOVER.md, STUDY.md, prompt.md |
engineering log, study, the running task statement |
Debian/Ubuntu (the RK3588 runs Ubuntu 24.04 "Ubuntu Rockchip"; the PC runs Ubuntu 25.04):
sudo apt install build-essential git pkg-config meson ninja-build dkms linux-headers-$(uname -r) \
libdrm-dev libudev-dev libinput-dev libxkbcommon-dev libpixman-1-dev libwayland-dev wayland-protocols \
libgbm-dev libegl-dev libgles-dev libsdl2-dev libx264-dev libdrm-tests evtest wayland-utils
# RK3588 only (hardware encoder): sudo apt install librockchip-mpp-dev rockchip-mpp-demos
# wlroots 0.18 with Xwayland: Debian trixie / Ubuntu 25.04: sudo apt install libwlroots-0.18-dev xwayland
# Ubuntu 24.04 (noble ships 0.17): build wayland 1.23 + wlroots 0.18.2 from source
# into /usr/local (see docs/HANDOVER.md 4.7 and lesson 30)git clone https://github.com/key2/wavepi-host.git && cd wavepi-host
make # module against the running kernel, wavepid, wavepi-wm, tools
make demo # optional: the ImGui client (fetches Dear ImGui v1.91.9b)
sudo make install # binaries (PREFIX=/usr/local), units, example config (share/wavepi/), udev rule
sudo make dkms # module registered with dkms, loaded at boot
sudo wavepi-adduser # system user "wavepi" (groups video, render) the services run as
sudo systemctl enable --now seatd wavepid wavepi-wmOn Ubuntu 24.04 with wlroots in /usr/local, configure the compositor with
PKG_CONFIG_PATH=/usr/local/lib/aarch64-linux-gnu/pkgconfig (the Makefile
passes the environment through to meson). The module also builds by hand:
make -C kernel/wavepi KDIR=/lib/modules/$(uname -r)/build; a fake board for
machines without hardware: sudo insmod kernel/wavepi/wavepi.ko fake=1.
/etc/wavepi/screens.toml maps a board (by USB serial, or by connector name)
to the command that runs on it; everything else has defaults. Without it the
compositor uses the installed example (share/wavepi/screens.toml: a test
client per connector), so copy and edit that one:
[general]
xkb_layout = "us"
keyboard_policy = "last-touch" # host keyboards follow the screen touched last ("fixed": keyboard_screen)
xwayland = true
[[screen]]
name = "left"
serial = "c07730024beef" # lsusb -v: iSerial; or connector = "USB-1"
exec = "wavepi-imgui-demo" # /bin/sh -c, its own process group; windows of that group land here
[[screen]]
name = "right"
serial = "e206d96448d91"
exec = 'xterm -fa DejaVuSansMono -fs 14 -e sh -c "exec sh -i"' # X11 through XwaylandA client that exits is restarted (restart = true, restart_delay_ms); when a
board is unplugged its client keeps running and is shown again when a board
with the same serial comes back. Each client sees exactly one wl_seat (its
screen's), which keeps single-seat toolkits such as SDL2 and GLFW working.
One application with a window on several screens is configured as an
[[app]] instead; it runs once and chooses the screen of each window:
[general]
seat_model = "single" # one seat for everything (desktop semantics): touch is routed per
# surface, so an SDL2 app gets each panel's touches in the right window
[[screen]]
name = "A"
serial = "c07730024beef"
[[screen]]
name = "B"
serial = "e206d96448d91"
[[app]]
name = "dashboard"
exec = "wavepi-imgui-demo" # started once with WAVEPI_SCREENS=A,B
screens = ["A", "B"]The compositor names its outputs after the screens (wl_output name A,
description A: WavePI S6 <serial> on USB-n), so a window created fullscreen
on the SDL display whose name starts with A (xdg_toplevel.set_fullscreen
on that output) lands on board A; a [wavepi:A] tag in the title or app_id
does the same for toolkits that cannot pick outputs, X11 windows are placed by
their requested x position (the X root spans the screens at x = 4096·n), and
untagged windows are spread over the app's screens in creation order. The
demo does this with --windows A,B,C (one SDL window, GL context and ImGui
context per screen, shared state across them). With the default
seat_model = "per-screen" an app sees all seats and needs a multi-seat
toolkit (SDL3, GTK4, Qt).
sudo modetest -M wavepi # 8 CRTCs, connectors USB-1..8; connected ones carry the board's EDID
wavepi-ctl status # per screen: streaming/idle, fps, encode ms, latency ms, errors
wavepi-ctl info # the driver's view of /dev/wavepiN (serial, size, bind count)
sudo cat /sys/kernel/debug/dri/*/wavepi # frame export state: pending/busy/scanout fb, held flips, readers
journalctl -u wavepi-wm -b # outputs, clients, input devices and the screen they went toThe display-path test is the ImGui demo: let it run at full rate for a few minutes and compare its clock with a watch (a growing lag means something buffers; see HANDOVER lesson 53).
- Frames. The compositor renders into GPU buffers (Panfrost/Mali on the
RK3588, any Mesa driver elsewhere) and commits them to a connector of the
virtual DRM device. The module emulates vblank at the panel's refresh
(59.76 Hz), keeps the committed framebuffer pending and hands it to the
daemon as a dma-buf (
DQBUF); the daemon imports it into the encoder without copying, writes the access unit to/dev/wavepiNand returns the buffer (QBUF). The flip of the following frame completes only when the held one is returned, so the encoder never races the compositor; a 200 ms watchdog releases a held flip if the daemon disappears. A new reader gets the frame on screen right away, so a daemon restart does not blank anything. - Keepalive. The board returns to its splash 3 s after the last byte, so static screens are re-sent at 2 fps (tiny P-frames); every access unit ends with an AUD NAL so the board shows it without waiting for the next one.
- Input. The board sends 16-byte records (touch slots, relative motion,
keys, device hot-plug) on the bulk IN endpoint; the module turns them into
evdev devices under the board's USB interface, which is how the compositor
knows which screen a device belongs to (
/sys/class/wavepi/wavepiN/usb_path). - Identity. Boards are matched by the USB serial in the EDID; the slot (connector) a board gets is kept for the same serial across re-plugs.
tools/soak.sh: hours of compositor + daemon with a board unbind/rebind every 30 s, kernel warnings and slab counted (60 min / 112 cycles clean).tools/kmod-cycle.sh: module robustness (1000 bind/unbind cycles clean).tools/integ-kmod.sh: byte-exact bulk OUT through/dev/wavepiNwith a board and its serial console on the PC (3 MB, slow consumer; 3/3 exact).tools/wavepi-grab: dequeue frames, checksum, dump PPM, simulate a slow encoder (-w ms) to see the backpressure.tools/wm-pc.sh: the compositor on a PC with the fake board (sudo, one compositor per card); screenshots withwavepi-grab -o shot.ppm.
The kernel module (kernel/wavepi/) is GPL-2.0-only; its uAPI header is
dual GPL-2.0 / MIT; everything else is MIT. See LICENSE.