Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,48 @@ You can configure Weston sections and keys using the following environment varia
| `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-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:

```yaml
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`](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)).

## Advanced Configuration

Expand Down
65 changes: 65 additions & 0 deletions docs/todo-multi-display.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# TODO: Multi-display support (deferred)

The display block currently configures a **single output** — the geometry env vars
(`DISPLAY_ROTATION`, `DISPLAY_RESOLUTION`, `DISPLAY_SCALE`) are applied to the **first connected
display detected**. This note records the exploration already done so we don't re-investigate from
scratch when we pick multi-head up.

## What we found

### Output/connector names are concrete and platform-specific

Weston's `[output]` section keys on the DRM connector name (`HDMI-A-1`, `HDMI-A-2`, `DP-1`, `eDP-1`,
`DSI-1`, `VGA-1`, …). These names:

- come from DRM/KMS and vary by GPU, driver and platform (a Pi's `DSI-1` vs an x86 box's `DP-1`);
- have **no platform-agnostic "display-1 / display-2" abstraction** at the compositor-config layer —
Weston, wlroots/Sway, etc. all key on the concrete connector string.

The block detects connected connectors by reading `/sys/class/drm/card*-*/status` (see
`detect_connected_outputs()` in `entry.sh`), excluding virtual connectors like `Writeback-*`.

### Why not role-based targeting

We considered exposing role-based vars (`PRIMARY_*` / `SECONDARY_*`) that auto-resolve to detected
connectors. **Rejected as too non-deterministic**: the mapping of a "primary" role to a physical
port is not stable/predictable enough to be a reliable interface. When multi-head is implemented, the
user should target a **specific, concrete connector name** instead (e.g. `DISPLAY_ROTATION_HDMI_A_1`
or similar). To make that usable, the block should report detected connector names (it already logs
them on startup).

### The hard part: kiosk shell + single-client sidecar architecture

This is the main unknown to research before committing to a design:

- Weston's `kiosk-shell` is effectively **one fullscreen app per output**.
- A client block like `browser` is a **single Chromium instance = a single Wayland surface**, which
the compositor places on one output.

So "drive two displays" is **not** just adding a second `[output]` section. It likely needs one or
more of: multiple client instances (e.g. two browser containers), explicit surface→output placement,
or a different shell. The interaction between the block pattern (one client container, one socket)
and multi-output kiosk needs to be designed deliberately.

## Prior art

A proof-of-concept exists at
`balena-solutions/display@rahul/dual-screen:examples/multiscreen`. It:

- auto-detects connected connectors from `/sys/class/drm/card*-*/status`, treats the first as primary
(positioned at `0,0`) and the second to its right;
- exposes `PRIMARY_DISPLAY` / `SECONDARY_DISPLAY` (output names, auto-detected if unset),
`PRIMARY_MODE` / `SECONDARY_MODE`, per-connector `ROTATION_<OUTPUT_NAME>`, and `MULTISCREEN_ENABLED`;
- notes that 90°/270° rotation requires setting the post-rotation `*_MODE` explicitly, because Weston
positions using the post-rotation width.

## Available escape hatch today

Advanced users who need a multi-output layout now can supply a full custom `weston.ini` via
`WESTON_INI_PATH` (see "Advanced Configuration" in the README), which bypasses the block's dynamic
config generation entirely.

## Decision

Implement multi-display only if there is enough demand, rather than asking users to bring their own
`weston.ini`. Until then the block stays single-display by design.
39 changes: 39 additions & 0 deletions entry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,40 @@ WESTON_DEBUG="${WESTON_DEBUG:-false}"
# Load Weston configuration defaults from weston-env-defaults.sh
source /etc/weston/weston-env-defaults.sh

# ==============================================================================
# DEFAULT OUTPUT DETECTION
# ==============================================================================
# Weston's [output] section keys on the connector name (e.g. HDMI-A-1, DSI-1),
# which differs per platform/GPU. To keep the public DISPLAY_* geometry vars
# generic, we auto-detect the connected connector(s) and apply the config to the
# FIRST connected one. Multi-display targeting is intentionally out of scope for
# now (see docs/todo-multi-display.md).
detect_connected_outputs() { # echoes Weston output names, one per line
for status in /sys/class/drm/card*-*/status; do
[ -e "$status" ] || continue
[ "$(cat "$status")" = "connected" ] || continue
conn=$(basename "$(dirname "$status")") # e.g. card0-HDMI-A-1
name="${conn#card*-}" # -> HDMI-A-1 (Weston output name)
case "$name" in Writeback-*) continue ;; esac # skip virtual connectors
echo "$name"
done
}

CONNECTED_OUTPUTS=$(detect_connected_outputs)
if [ -n "$CONNECTED_OUTPUTS" ]; then
echo "Connected display outputs detected:"
echo "$CONNECTED_OUTPUTS" | sed 's/^/ - /'
export WESTON_INI_OUTPUT_NAME=$(echo "$CONNECTED_OUTPUTS" | head -n1)
echo "Applying display geometry (rotation/resolution/scale) to: $WESTON_INI_OUTPUT_NAME"
if [ "$(echo "$CONNECTED_OUTPUTS" | wc -l)" -gt 1 ]; then
echo "[NOTE] Multiple displays connected. Only '$WESTON_INI_OUTPUT_NAME' is configured;"
echo " For custom multi-output layouts, provide your own weston.ini via WESTON_INI_PATH."
fi
else
echo "[WARN] No connected display outputs detected; skipping [output] configuration."
export WESTON_INI_OUTPUT_NAME=""
fi

# ==============================================================================
# STOP PLYMOUTH (Release DRM Master)
# ==============================================================================
Expand Down Expand Up @@ -88,6 +122,11 @@ else
# We iterate explicitly to ensure a trailing newline separates each fragment,
# preventing syntax errors if a template file lacks an EOF newline.
for template in /etc/weston/templates/*.ini.template; do
# The [output] fragment requires a connector name; skip it when no display
# was detected (headless) so Weston doesn't reject an empty 'name='.
if [ "$(basename "$template")" = "04-output.ini.template" ] && [ -z "$WESTON_INI_OUTPUT_NAME" ]; then
continue
fi
cat "$template"
echo ""
done | envsubst > "$CONFIG_FILE"
Expand Down
32 changes: 31 additions & 1 deletion weston-env-defaults.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,34 @@ export WESTON_INI_CORE_REQUIRE_INPUT="${DISPLAY_REQUIRE_INPUT:-false}"
export WESTON_INI_SHELL_LOCKING="${DISPLAY_ALLOW_LOCKING:-false}"
export WESTON_INI_SHELL_PANEL_POSITION="${DISPLAY_PANEL_POSITION:-none}"

# [libinput] is explicitly hardcoded in its template and requires no variables.
# [libinput] is explicitly hardcoded in its template and requires no variables.

# [output]
# DISPLAY_ROTATION is the public, compositor-agnostic knob: it rotates the displayed
# CONTENT by N degrees CLOCKWISE (0/90/180/270). This matches what every end-user
# display setting does (macOS, Windows, GNOME) and the v2 block's xrandr behavior.
#
# IMPORTANT — direction inversion (do not "fix" this back):
# Weston's `transform` does NOT rotate content. Per `man weston.ini`, it describes
# "how you have rotated your MONITOR from its normal orientation", and Weston then
# counter-rotates the framebuffer to compensate. So `transform=rotate-90` ("monitor
# turned 90° CW") makes the visible content appear rotated 90° COUNTER-clockwise.
# (Verified on device; the `wl_output.transform` protocol that sway/wlroots also use
# shares this panel-orientation convention, so the inversion is not Weston-specific.)
# To honor our content-clockwise contract we therefore map to the OPPOSITE token:
# DISPLAY_ROTATION=90 (content CW) -> rotate-270
# DISPLAY_ROTATION=270 (content CCW) -> rotate-90
# 0 and 180 are direction-agnostic and map straight through.
case "${DISPLAY_ROTATION:-0}" in
0) export WESTON_INI_OUTPUT_TRANSFORM="normal" ;;
90) export WESTON_INI_OUTPUT_TRANSFORM="rotate-270" ;;
180) export WESTON_INI_OUTPUT_TRANSFORM="rotate-180" ;;
270) export WESTON_INI_OUTPUT_TRANSFORM="rotate-90" ;;
*) echo "[WARN] Invalid DISPLAY_ROTATION='${DISPLAY_ROTATION}' (expected 0/90/180/270); using 0"
export WESTON_INI_OUTPUT_TRANSFORM="normal" ;;
esac
# DISPLAY_RESOLUTION is the generic public form (WIDTHxHEIGHT). When unset we fall
# back to Weston's "preferred" keyword (the EDID-preferred mode) internally — that
# keyword is a Weston detail and is intentionally kept out of the public interface.
export WESTON_INI_OUTPUT_MODE="${DISPLAY_RESOLUTION:-preferred}"
export WESTON_INI_OUTPUT_SCALE="${DISPLAY_SCALE:-1}"
5 changes: 5 additions & 0 deletions weston-templates/04-output.ini.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[output]
name=${WESTON_INI_OUTPUT_NAME}
transform=${WESTON_INI_OUTPUT_TRANSFORM}
mode=${WESTON_INI_OUTPUT_MODE}
scale=${WESTON_INI_OUTPUT_SCALE}