From 75e2584fefa30ffc0f744ccb397398168b72b71a Mon Sep 17 00:00:00 2001 From: Rahul Thakoor Date: Thu, 18 Jun 2026 14:53:12 +0400 Subject: [PATCH] Add output geometry config: rotation, resolution, scale New `DISPLAY_ROTATION`, `DISPLAY_RESOLUTION` and `DISPLAY_SCALE` env vars configure the Weston output via a new 04-output.ini.template fragment. - Auto-detect the first connected DRM connector and apply the config to it; skip the [output] section when no display is connected. - `DISPLAY_ROTATION` is clockwise degrees (0/90/180/270). Weston's transform describes panel orientation, not content, so the mapping is inverted (90 -> rotate-270, 270 -> rotate-90) to rotate content clockwise as documented. - `DISPLAY_RESOLUTION` is `WIDTHxHEIGHT[@refresh]`; unset uses the native mode. `DISPLAY_SCALE` is an integer factor. - Document the new vars in README; record multi-display as out of scope in docs/todo-multi-display.md. Change-type: minor Signed-off-by: rahul-thakoor <13220877+rahul-thakoor@users.noreply.github.com> --- README.md | 42 ++++++++++++++++ docs/todo-multi-display.md | 65 +++++++++++++++++++++++++ entry.sh | 39 +++++++++++++++ weston-env-defaults.sh | 32 +++++++++++- weston-templates/04-output.ini.template | 5 ++ 5 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 docs/todo-multi-display.md create mode 100644 weston-templates/04-output.ini.template diff --git a/README.md b/README.md index cf56d9c..21c1208 100644 --- a/README.md +++ b/README.md @@ -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- + 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 diff --git a/docs/todo-multi-display.md b/docs/todo-multi-display.md new file mode 100644 index 0000000..3d221f5 --- /dev/null +++ b/docs/todo-multi-display.md @@ -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_`, 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. diff --git a/entry.sh b/entry.sh index 77506a2..3cdcbb4 100644 --- a/entry.sh +++ b/entry.sh @@ -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) # ============================================================================== @@ -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" diff --git a/weston-env-defaults.sh b/weston-env-defaults.sh index 2cd6639..9c1e62d 100644 --- a/weston-env-defaults.sh +++ b/weston-env-defaults.sh @@ -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. \ No newline at end of file +# [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}" \ No newline at end of file diff --git a/weston-templates/04-output.ini.template b/weston-templates/04-output.ini.template new file mode 100644 index 0000000..9becc78 --- /dev/null +++ b/weston-templates/04-output.ini.template @@ -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}