Skip to content

Instantly share code, notes, and snippets.

@bachkukkik
Last active May 21, 2026 07:21
Show Gist options
  • Select an option

  • Save bachkukkik/050501bcaf3331632894506ba08d2e49 to your computer and use it in GitHub Desktop.

Select an option

Save bachkukkik/050501bcaf3331632894506ba08d2e49 to your computer and use it in GitHub Desktop.
Fix screen sharing orientation on ClockworkPi uConsole (CM5, Wayland/labwc) by patching xdg-desktop-portal-wlr to rotate SHM buffers

Fix: ClockworkPi uConsole Screen Sharing Orientation (Wayland/labwc)

TL;DR — On the ClockworkPi uConsole (CM5, Debian, Wayland/labwc), screen sharing in Chromium shows vertically flipped or incorrectly rotated. This patch fixes it by modifying xdg-desktop-portal-wlr to rotate captured SHM buffers before sending them to pipewire. Chromium ignores pipewire's VideoTransform metadata, so the rotation must be applied to the pixel data itself.

Quick fix: Apply the patch below, rebuild xdg-desktop-portal-wlr, install, and restart portal services.


Problem

On the ClockworkPi uConsole with:

  • Compute Module 5 Lite
  • Debian Trixie (testing)
  • Wayland session with labwc 0.9.2 compositor (wlroots 0.19)
  • Built-in CWU50 DSI panel (drm_rp1_dsi driver)
  • Chromium with --ozone-platform=wayland

The built-in panel displays correctly, but screen sharing (e.g., Google Meet via Chromium) and HDMI output show the image flipped or with incorrect orientation.

Root Cause

The CWU50 DSI panel is physically mounted in portrait orientation. A device tree overlay sets rotation = <0x5a> (90°), which the kernel uses to report panel_orientation=right via DRM. The compositor (labwc/wlroots) then applies a Transform: 270 at the output level to correct the display.

However, xdg-desktop-portal-wlr captures the raw framebuffer (720×1280 portrait) and sends it through pipewire with a VideoTransform metadata tag indicating the rotation. Chromium ignores this metadata and displays the raw buffer as-is, resulting in a flipped/rotated screen share.

Solution

Patch xdg-desktop-portal-wlr v0.7.1 to:

  1. Rotate SHM buffer pixels in-place before enqueuing them to pipewire
  2. Swap width/height in the pipewire format negotiation for 90°/270° transforms
  3. Adjust stride to match the rotated dimensions
  4. Set VideoTransform to identity (none) since the pixels are now pre-rotated

DMABUF capture is left unchanged — the transform metadata is still sent in case the consumer handles it.

Why only SHM?

Chromium uses SHM buffers for screen capture via pipewire. The total buffer size is the same before and after rotation (720×1280×4 = 1280×720×4), so no buffer reallocation is needed.


Prerequisites

Install build dependencies:

sudo apt install build-essential meson ninja-build \
  libwlroots-dev libpipewire-0.3-dev libwayland-dev \
  wayland-protocols libdrm-dev libgbm-dev libegl-dev \
  libgl-dev libxkbcommon-dev libsystemd-dev libinih-dev \
  pkg-config git

Step-by-Step Instructions

1. Download the source

git clone https://github.com/emersion/xdg-desktop-portal-wlr.git /tmp/xdg-desktop-portal-wlr
cd /tmp/xdg-desktop-portal-wlr
git checkout v0.7.1

2. Apply the patch

Save the full patch below to /tmp/portal-wlr-shm-rotation.patch, then:

cd /tmp/xdg-desktop-portal-wlr
git apply /tmp/portal-wlr-shm-rotation.patch

3. Build

meson setup build
ninja -C build

4. Install (replaces system binary)

# Back up the original
sudo cp /usr/libexec/xdg-desktop-portal-wlr /usr/libexec/xdg-desktop-portal-wlr.bak

# Stop services before replacing
systemctl --user stop xdg-desktop-portal-wlr xdg-desktop-portal

# Install the patched binary
sudo cp build/xdg-desktop-portal-wlr /usr/libexec/xdg-desktop-portal-wlr

# Restart services
systemctl --user start xdg-desktop-portal
sleep 1
systemctl --user start xdg-desktop-portal-wlr

5. Verify

systemctl --user status xdg-desktop-portal-wlr

Should show active (running). The "Could not find render node" log messages are non-fatal (DMABUF path — SHM works fine).

Configuration Files

labwc autostart (~/.config/labwc/autostart)

wlr-randr --output DSI-2 --transform 270 &

portal-wlr config (~/.config/xdg-desktop-portal-wlr/config)

[screencast]
output_name=DSI-2
max_fps=30
chooser_type=none

Full Patch

diff --git a/src/screencast/pipewire_screencast.c b/src/screencast/pipewire_screencast.c
index 41c91ab..66fde68 100644
--- a/src/screencast/pipewire_screencast.c
+++ b/src/screencast/pipewire_screencast.c
@@ -10,12 +10,50 @@
 #include <sys/mman.h>
 #include <unistd.h>
 #include <assert.h>
+#include <stdlib.h>
+#include <string.h>
 #include <libdrm/drm_fourcc.h>
 
 #include "wlr_screencast.h"
 #include "xdpw.h"
 #include "logger.h"
 
+static bool transform_needs_swap(enum wl_output_transform t) {
+	return t == WL_OUTPUT_TRANSFORM_90 || t == WL_OUTPUT_TRANSFORM_270;
+}
+
+static bool transform_needs_rotation(enum wl_output_transform t) {
+	return t == WL_OUTPUT_TRANSFORM_90 || t == WL_OUTPUT_TRANSFORM_180 ||
+		t == WL_OUTPUT_TRANSFORM_270;
+}
+
+static void rotate_shm(uint32_t *dst, const uint32_t *src,
+		uint32_t src_w, uint32_t src_h, uint32_t src_stride_el,
+		enum wl_output_transform transform) {
+	if (transform == WL_OUTPUT_TRANSFORM_90) {
+		for (uint32_t sy = 0; sy < src_h; sy++) {
+			for (uint32_t sx = 0; sx < src_w; sx++) {
+				dst[sx * src_h + (src_h - 1 - sy)] =
+					src[sy * src_stride_el + sx];
+			}
+		}
+	} else if (transform == WL_OUTPUT_TRANSFORM_180) {
+		for (uint32_t sy = 0; sy < src_h; sy++) {
+			for (uint32_t sx = 0; sx < src_w; sx++) {
+				dst[(src_h - 1 - sy) * src_stride_el + (src_w - 1 - sx)] =
+					src[sy * src_stride_el + sx];
+			}
+		}
+	} else if (transform == WL_OUTPUT_TRANSFORM_270) {
+		for (uint32_t sy = 0; sy < src_h; sy++) {
+			for (uint32_t sx = 0; sx < src_w; sx++) {
+				dst[(src_w - 1 - sx) * src_h + sy] =
+					src[sy * src_stride_el + sx];
+			}
+		}
+	}
+}
+
 static struct spa_pod *build_buffer(struct spa_pod_builder *b, uint32_t blocks, uint32_t size,
 		uint32_t stride, uint32_t datatype) {
 	assert(blocks > 0);
@@ -147,6 +185,13 @@ static uint32_t build_formats(struct spa_pod_builder *b[static 2], struct xdpw_s
 	uint32_t modifier_count;
 	uint64_t *modifiers = NULL;
 
+	enum wl_output_transform transform = cast->target->output->transformation;
+	bool swap = transform_needs_swap(transform);
+
+	uint32_t shm_w = cast->screencopy_frame_info[WL_SHM].width;
+	uint32_t shm_h = cast->screencopy_frame_info[WL_SHM].height;
+	if (swap) { uint32_t tmp = shm_w; shm_w = shm_h; shm_h = tmp; }
+
 	if (!cast->avoid_dmabufs &&
 			build_modifierlist(cast, cast->screencopy_frame_info[DMABUF].format, &modifiers, &modifier_count) && modifier_count > 0) {
 		param_count = 2;
@@ -155,13 +200,13 @@ static uint32_t build_formats(struct spa_pod_builder *b[static 2], struct xdpw_s
 				modifiers, modifier_count);
 		assert(params[0] != NULL);
 		params[1] = build_format(b[1], xdpw_format_pw_from_drm_fourcc(cast->screencopy_frame_info[WL_SHM].format),
-				cast->screencopy_frame_info[WL_SHM].width, cast->screencopy_frame_info[WL_SHM].height, cast->framerate,
+				shm_w, shm_h, cast->framerate,
 				NULL, 0);
 		assert(params[1] != NULL);
 	} else {
 		param_count = 1;
 		params[0] = build_format(b[0], xdpw_format_pw_from_drm_fourcc(cast->screencopy_frame_info[WL_SHM].format),
-				cast->screencopy_frame_info[WL_SHM].width, cast->screencopy_frame_info[WL_SHM].height, cast->framerate,
+				shm_w, shm_h, cast->framerate,
 				NULL, 0);
 		assert(params[0] != NULL);
 	}
@@ -309,8 +354,15 @@ fixate_format:
 	logprint(DEBUG, "pipewire: size: (%u, %u)", cast->pwr_format.size.width, cast->pwr_format.size.height);
 	logprint(DEBUG, "pipewire: max_framerate: (%u / %u)", cast->pwr_format.max_framerate.num, cast->pwr_format.max_framerate.denom);
 
-	params[0] = build_buffer(&b[0].b, blocks, cast->screencopy_frame_info[cast->buffer_type].size,
-			cast->screencopy_frame_info[cast->buffer_type].stride, data_type);
+	uint32_t buf_size = cast->screencopy_frame_info[cast->buffer_type].size;
+	uint32_t buf_stride = cast->screencopy_frame_info[cast->buffer_type].stride;
+	if (cast->buffer_type == WL_SHM && transform_needs_swap(cast->target->output->transformation)) {
+		buf_stride = cast->screencopy_frame_info[WL_SHM].height * 4;
+		buf_size = buf_stride * cast->screencopy_frame_info[WL_SHM].width;
+	}
+
+	params[0] = build_buffer(&b[0].b, blocks, buf_size,
+			buf_stride, data_type);
 
 	params[1] = spa_pod_builder_add_object(&b[1].b,
 		SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
@@ -380,6 +432,9 @@ static void pwr_handle_stream_add_buffer(void *data, struct pw_buffer *buffer) {
 		d[plane].flags = 0;
 		d[plane].fd = xdpw_buffer->fd[plane];
 		d[plane].data = NULL;
+		if (cast->buffer_type == WL_SHM && transform_needs_swap(cast->target->output->transformation)) {
+			d[plane].chunk->stride = cast->screencopy_frame_info[WL_SHM].height * 4;
+		}
 		// clients have implemented to check chunk->size if the buffer is valid instead
 		// of using the flags. Until they are patched we should use some arbitrary value.
 		if (xdpw_buffer->buffer_type == DMABUF && d[plane].chunk->size == 0) {
@@ -445,6 +500,22 @@ void xdpw_pwr_enqueue_buffer(struct xdpw_screencast_instance *cast) {
 		cast->err = 1;
 	}
 
+	enum wl_output_transform transform = cast->target->output->transformation;
+	if (!buffer_corrupt && cast->buffer_type == WL_SHM && transform_needs_rotation(transform)) {
+		struct xdpw_buffer *xdpw_buf = cast->current_frame.xdpw_buffer;
+		void *data = mmap(NULL, xdpw_buf->size[0], PROT_READ | PROT_WRITE, MAP_SHARED, xdpw_buf->fd[0], 0);
+		if (data != MAP_FAILED) {
+			void *tmp = malloc(xdpw_buf->size[0]);
+			if (tmp) {
+				rotate_shm(tmp, data, xdpw_buf->width, xdpw_buf->height,
+					xdpw_buf->stride[0] / 4, transform);
+				memcpy(data, tmp, xdpw_buf->size[0]);
+				free(tmp);
+			}
+			munmap(data, xdpw_buf->size[0]);
+		}
+	}
+
 	logprint(TRACE, "********************");
 	struct spa_meta_header *h;
 	if ((h = spa_buffer_find_meta_data(spa_buf, SPA_META_Header, sizeof(*h)))) {
@@ -457,7 +528,11 @@ void xdpw_pwr_enqueue_buffer(struct xdpw_screencast_instance *cast) {
 
 	struct spa_meta_videotransform *vt;
 	if ((vt = spa_buffer_find_meta_data(spa_buf, SPA_META_VideoTransform, sizeof(*vt)))) {
-		vt->transform = cast->target->output->transformation;
+		if (cast->buffer_type == WL_SHM && transform_needs_rotation(transform)) {
+			vt->transform = SPA_META_TRANSFORMATION_None;
+		} else {
+			vt->transform = cast->target->output->transformation;
+		}
 		logprint(TRACE, "pipewire: transformation %u", vt->transform);
 	}
 
diff --git a/src/screencast/wlr_screencast.c b/src/screencast/wlr_screencast.c
index c1fae8e..5e6c86d 100644
--- a/src/screencast/wlr_screencast.c
+++ b/src/screencast/wlr_screencast.c
@@ -140,10 +140,17 @@ static void wlr_frame_buffer_done(void *data,
 	}
 
 	// Check if announced screencopy information is compatible with pipewire meta
+	uint32_t expected_w = cast->screencopy_frame_info[cast->buffer_type].width;
+	uint32_t expected_h = cast->screencopy_frame_info[cast->buffer_type].height;
+	if (cast->buffer_type == WL_SHM && (
+			cast->target->output->transformation == WL_OUTPUT_TRANSFORM_90 ||
+			cast->target->output->transformation == WL_OUTPUT_TRANSFORM_270)) {
+		uint32_t tmp = expected_w; expected_w = expected_h; expected_h = tmp;
+	}
 	if ((cast->pwr_format.format != xdpw_format_pw_from_drm_fourcc(cast->screencopy_frame_info[cast->buffer_type].format) &&
 			cast->pwr_format.format != xdpw_format_pw_strip_alpha(xdpw_format_pw_from_drm_fourcc(cast->screencopy_frame_info[cast->buffer_type].format))) ||
-			cast->pwr_format.size.width != cast->screencopy_frame_info[cast->buffer_type].width ||
-			cast->pwr_format.size.height != cast->screencopy_frame_info[cast->buffer_type].height) {
+			cast->pwr_format.size.width != expected_w ||
+			cast->pwr_format.size.height != expected_h) {
 		logprint(DEBUG, "wlroots: pipewire and wlroots metadata are incompatible. Renegotiate stream");
 		cast->frame_state = XDPW_FRAME_STATE_RENEG;
 		xdpw_wlr_frame_finish(cast);

Technical Details

Inverse Transform

The compositor applies an output transform to correct the physical panel orientation. For the uConsole, the output reports WL_OUTPUT_TRANSFORM_270 (270° CCW). The raw framebuffer captured by screencopy is in the panel's native portrait orientation (720×1280).

To produce the same correctly-oriented image that appears on the panel, we apply the same transform value to the pixel data — but since rotate_shm() rotates the buffer rather than transforming the coordinate system, the pixel-level rotation for WL_OUTPUT_TRANSFORM_270 maps to a 90° CCW pixel rotation, and WL_OUTPUT_TRANSFORM_90 maps to a 270° CCW pixel rotation. The formulas in the patch reflect this mapping:

  • WL_OUTPUT_TRANSFORM_270dst[(src_w - 1 - sx) * src_h + sy] = src[...] (90° CCW pixel rotation)
  • WL_OUTPUT_TRANSFORM_90dst[sx * src_h + (src_h - 1 - sy)] = src[...] (270° CCW pixel rotation)

Buffer Size Invariance

The CWU50 panel reports a framebuffer of 720×1280 (portrait). After a 90° or 270° rotation, the dimensions become 1280×720 (landscape). The total buffer size is unchanged:

720 × 1280 × 4 bytes/pixel = 3,686,400 bytes
1280 × 720 × 4 bytes/pixel = 3,686,400 bytes

This means no buffer reallocation is needed — the pixels are rotated in-place using a temporary buffer.

Dimension Swap

For 90° and 270° transforms, width and height are swapped in the pipewire format negotiation so the consumer (Chromium) receives a 1280×720 stream instead of 720×1280. The stride is adjusted from src_w × 4 to src_h × 4.

Compatibility Check Fix

wlr_screencast.c checks if pipewire's negotiated format matches the screencopy frame info. Since we swap dimensions for SHM, the compatibility check must also swap its expected values to avoid infinite renegotiation loops.

180° Rotation

For 180° transforms, dimensions are not swapped (they're the same), but pixels are flipped in-place. Stride remains unchanged.


Rollback

To revert to the original binary:

systemctl --user stop xdg-desktop-portal-wlr xdg-desktop-portal
sudo cp /usr/libexec/xdg-desktop-portal-wlr.bak /usr/libexec/xdg-desktop-portal-wlr
systemctl --user start xdg-desktop-portal
sleep 1
systemctl --user start xdg-desktop-portal-wlr

Credits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment