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-wlrto 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.
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_dsidriver) - 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.
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.
Patch xdg-desktop-portal-wlr v0.7.1 to:
- Rotate SHM buffer pixels in-place before enqueuing them to pipewire
- Swap width/height in the pipewire format negotiation for 90°/270° transforms
- Adjust stride to match the rotated dimensions
- 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.
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.
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 gitgit 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.1Save 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.patchmeson setup build
ninja -C build# 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-wlrsystemctl --user status xdg-desktop-portal-wlrShould show active (running). The "Could not find render node" log messages are non-fatal (DMABUF path — SHM works fine).
wlr-randr --output DSI-2 --transform 270 &[screencast]
output_name=DSI-2
max_fps=30
chooser_type=nonediff --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);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_270→dst[(src_w - 1 - sx) * src_h + sy] = src[...](90° CCW pixel rotation)WL_OUTPUT_TRANSFORM_90→dst[sx * src_h + (src_h - 1 - sy)] = src[...](270° CCW pixel rotation)
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.
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.
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.
For 180° transforms, dimensions are not swapped (they're the same), but pixels are flipped in-place. Stride remains unchanged.
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- Base uConsole CM5 setup: ak-rex/ClockworkPi-apt
- Patched software: emersion/xdg-desktop-portal-wlr v0.7.1