Skip to content

Repository files navigation

display

A reusable balena block that provides a hardware-accelerated Weston Wayland compositor, enabling any containerised app to render graphics on embedded Linux devices.

Supported Devices

  • Raspberry Pi 4
  • Raspberry Pi 5
  • Generic x86_64 (GPT)

Pre-built images are published to the balena registry for each supported architecture:

Image Architecture Devices
bh.cr/balenasolutions/display-aarch64 ARM 64-bit (aarch64) Raspberry Pi 4, Raspberry Pi 5
bh.cr/balenasolutions/display-amd64 x86-64 (amd64) Generic x86_64 (GPT)

How to Use This Block

Option 1 — Direct image in docker-compose.yml

Reference the architecture-specific image directly. Best when your fleet targets a single known architecture (e.g. aarch64 for Raspberry Pi 4/5):

docker-compose.yml

version: '2.1'

services:
  display:
    image: bh.cr/balenasolutions/display-aarch64
    privileged: true
    restart: always
    network_mode: host
    volumes:
      - display-socket:/run
    labels:
      io.balena.features.dbus: '1'

  your-app:
    build: ./your-app
    restart: always
    depends_on:
      - display
    volumes:
      - display-socket:/run
    devices:
      - /dev/dri:/dev/dri
    environment:
      - WAYLAND_DISPLAY=wayland-0
      - XDG_RUNTIME_DIR=/run/user/0

volumes:
  display-socket:

Option 2 — Dockerfile.template (multi-arch)

Use a Dockerfile.template so balena substitutes the correct architecture at build time. Best for when you deploy your app to fleets of different architectures (e.g aarch64 and amd64):

./display/Dockerfile.template

FROM bh.cr/balenasolutions/display-%%BALENA_ARCH%%

docker-compose.yml

version: '2.1'

services:
  display:
    build: ./display
    privileged: true
    restart: always
    network_mode: host
    volumes:
      - display-socket:/run
    labels:
      io.balena.features.dbus: '1'

  your-app:
    build: ./your-app
    restart: always
    depends_on:
      - display
    volumes:
      - display-socket:/run
    devices:
      - /dev/dri:/dev/dri
    environment:
      - WAYLAND_DISPLAY=wayland-0
      - XDG_RUNTIME_DIR=/run/user/0

volumes:
  display-socket:

In your app's entry script, wait for the socket before launching:

SOCKET="$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY"
while [ ! -e "$SOCKET" ]; do sleep 1; done
exec your-app

Configuration

You can configure Weston sections and keys using the following environment variables:

Variable Options Default Description
XDG_RUNTIME_DIR valid directory path /run/user/0 Directory where the Wayland socket is created.
SOCKET_NAME [String] (e.g., wayland-0, wayland-1) wayland-0 Name of the Wayland socket
WESTON_DEBUG true, false false Enable debug mode
WESTON_INI_PATH Absolute path to a custom weston.ini file - When this variable is set and the file is present, Weston will be launched directly using the provided file. Consequently any other DISPLAY_* environment variables defined for the container will be ignored.
DISPLAY_UX_MODE desktop, kiosk kiosk Sets the user interface mode. Use kiosk for a single full-screen application(ideal for embedded use cases), or desktop for a multi-window environment.
DISPLAY_IDLE_TIMEOUT [Integer] (seconds) 0 Time in seconds before the display enters an inactive mode and blanks the screen. 0 disables the idle timeout. Note: Only applicable when DISPLAY_UX_MODE=desktop.
DISPLAY_REQUIRE_INPUT true, false false Dictates whether an active input device is required to launch. false permits display-only deployments.
DISPLAY_ALLOW_LOCKING true, false false Enables or disables screen locking functionality.
DISPLAY_PANEL_POSITION top, bottom, left, right, none none Sets the location of the desktop panel. none disables the panel entirely, ensuring an unobstructed viewport.
DISPLAY_ROTATION 0, 90, 180, 270 0 Rotates the displayed image clockwise by this many degrees (e.g. 90 turns the picture 90° clockwise; its top edge moves to the right). Applied to the first connected display (see Display geometry & rotation).
DISPLAY_RESOLUTION WIDTHxHEIGHT or WIDTHxHEIGHT@REFRESH display's native mode Output resolution, e.g. 1920x1080 or 1920x1080@60 (refresh rate in Hz). Leave unset to use the display's native (preferred) mode.
DISPLAY_SCALE [Integer] ≥ 1 1 Integer output scaling factor (e.g. 2 for HiDPI displays).

Display geometry & rotation

DISPLAY_ROTATION, DISPLAY_RESOLUTION and DISPLAY_SCALE configure the output and are applied to the first connected display detected (the block auto-detects DRM connectors, so you do not need to know the platform-specific connector name like HDMI-A-1 or DSI-1). The detected connector names are logged on startup.

Rotation rotates the displayed content clockwise by the given number of degrees — DISPLAY_ROTATION=90 turns the picture 90° clockwise (the top edge moves to the right), the same way the rotation setting behaves on macOS/Windows and in the v2 block. It is expressed in degrees (not compositor-specific terms like left/right) so the interface stays independent of the underlying compositor. Touch input rotates automatically with the display — the compositor applies the matching transform to libinput touch devices, so there is no separate touch-calibration setting to configure.

When using DISPLAY_ROTATION=90 or 270, set DISPLAY_RESOLUTION to the panel's native (pre-rotation) mode explicitly (e.g. 1920x1080) rather than leaving it unset, as the compositor positions the output using the post-rotation width.

Example — a portrait kiosk on a 1080p panel rotated 90° clockwise:

services:
  display:
    image: bh.cr/balenalabs/display-<arch>
    privileged: true
    volumes:
      - display-socket:/run
    labels:
      io.balena.features.dbus: '1'
    environment:
      DISPLAY_ROTATION: 90
      DISPLAY_RESOLUTION: 1920x1080

Multiple displays are not yet supported. Only the first connected display is configured by these variables; see docs/todo-multi-display.md for the current status. For custom multi-output layouts today, provide your own weston.ini via WESTON_INI_PATH (see Advanced Configuration).

Advanced Configuration

If the configurations provided does not suit your use case such as complex output management—such as requiring complex output management, multiple independent displays, or custom launchers—you must provide a custom weston.ini via a Dockerfile override.

The following snippets demonstrate how to override the configuration by inheriting from the base image and copying a custom weston.ini. You must also configure WESTON_INI_PATH to the absolute path of your weston.ini file for it to take effect.

./display/Dockerfile.template

FROM bh.cr/balenasolutions/display-%%BALENA_ARCH%%

# Inject your custom Weston configuration
COPY weston.ini /etc/weston/weston.ini

Examples

GLXGears (examples/glxgears)

Renders a hardware-accelerated OpenGL ES spinning gears demo using eglgears_wayland from Mesa utils. Demonstrates EGL/OpenGL rendering over Wayland with a live FPS/CPU overlay via GALLIUM_HUD.

Touchscreen Demo (examples/touchscreen-demo)

Runs the GTK4 demo suite over Wayland, demonstrating interactive touch input. The specific demo can be configured via the DEMO environment variable (default: drawingarea).

Architecture

This project uses a block pattern: a single display container runs the Weston compositor and exposes a Wayland socket via a shared Docker volume. Any number of client containers can connect to it.

Display Block (Wayland Compositor)

  • Runs the Weston compositor, managing graphics hardware directly via DRM
  • Creates a Wayland socket at /run/user/0/wayland-0 for client connections
  • Handles GPU rendering through the DRM backend
  • Releases the Plymouth DRM lock on startup to ensure exclusive GPU access

Your App (Wayland Client)

  • Any Wayland-compatible application (GTK4, Qt, LVGL, OpenGL, etc.)
  • Connects to the display block via the shared Wayland socket
  • Renders UI through the Wayland protocol (hardware-accelerated)
  • Polls for the socket before attempting connection

Communication

Both containers share a Docker volume (display-socket) mounted at /run, making the Weston socket accessible to client containers.

How It Works

┌─────────────────────────────────────────────────────────────┐
│                      Docker Compose                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────────────┐      ┌────────────────────────┐   │
│  │   display (block)    │      │   your-app (client)    │   │
│  ├──────────────────────┤      ├────────────────────────┤   │
│  │ Weston Compositor    │      │ Wayland Client         │   │
│  │ DRM Backend (GPU)    │◄─────┤ (GTK4 / Qt / LVGL /    │   │
│  │                      │      │  OpenGL / anything)    │   │
│  │ /usr/bin/entry.sh    │      │                        │   │
│  └──────────┬───────────┘      └───────────┬────────────┘   │
│             │                              │                │
│             └──────────────────────────────┘                │
│                    Shared volume: /run                      │
│              (Wayland socket: wayland-0)                    │
└─────────────────────────────────────────────────────────────┘

Hardware Acceleration

GPU acceleration is enabled through:

  1. DRM Backend — Weston connects directly to the GPU via /dev/dri
  2. Mesa Graphics Libraries — Provides OpenGL/Vulkan drivers

Docker Privileges

The display container requires:

  • privileged: true — DRM master access
  • /dev/dri — GPU device access
  • io.balena.features.dbus: '1' — D-Bus access (for stopping Plymouth)

Debugging

Common issues:

  • wayland-0 socket not found → Weston failed to start; check display logs
  • failed to load drm driver → GPU drivers not installed or hardware not supported
  • [WARN] D-Bus socket not found → Missing io.balena.features.dbus: '1' label on display service; Plymouth may still hold the DRM lock

About

A blueprint for deploying hardware-accelerated display servers on balenaOS. Serves as a practical example of abstracting graphics stacks.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages