Skip to content

About

Linux host for WavePI S6 USB touch displays (T113-S3): kernel DRM/evdev driver, MPP/x264 encoder daemon, wlroots multi-screen compositor. Built for the RK3588 (Firefly ROC-RK3588S-PC), also runs on x86.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

31 Commits

Folders and files

Repository files navigation

WavePI host: several USB touch displays on one Linux box

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/wavepiN for 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

Status

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).

Repository layout

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

Building from scratch

Dependencies

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)

Build and install

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-wm

On 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.

Configure the screens

/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 Xwayland

A 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).

Check that it works

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 to

The 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).

How it works, briefly

  • 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/wavepiN and 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.

Testing and bench tools

  • 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/wavepiN with 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 with wavepi-grab -o shot.ppm.

License

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.

About

Linux host for WavePI S6 USB touch displays (T113-S3): kernel DRM/evdev driver, MPP/x264 encoder daemon, wlroots multi-screen compositor. Built for the RK3588 (Firefly ROC-RK3588S-PC), also runs on x86.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages