# Fixing KRFB (KDE Desktop Sharing) on Kubuntu 26.04 / Plasma 6 Wayland A practical guide to making **KRFB** work unattended on a **Wayland** Plasma 6 session — fixing the two bugs that cause a **black screen** (or `Protocol error: bad desktop size 0x0`) when connecting with any VNC viewer (RealVNC, TigerVNC, Remmina, etc.). Tested on: - **Kubuntu 26.04 LTS** (codename *resolute*) - **KDE Plasma 6** / **KWin Wayland** - **krfb** `4:25.12.3-0ubuntu1` - **libkpipewire6** `6.6.4-0ubuntu1` - **xdg-desktop-portal-kde** `6.6.5-0ubuntu0.1` - Intel iGPU (N150) — but the DMA-BUF issue affects other GPUs too The fix attaches VNC to the **existing** Wayland desktop session (the one you're already logged into). It does **not** create a virtual session and does **not** use X11. --- ## TL;DR — what was wrong KRFB on Wayland captures the screen through `xdg-desktop-portal` (ScreenCast + RemoteDesktop portal interfaces → PipeWire). Two separate bugs break it when running unattended: 1. **Portal consent is never granted.** The RemoteDesktop portal shows an interactive "Remote Control Requested" dialog on every fresh session. With no one at the physical console to click "Share", no screencast stream is created → black screen. 2. **DMA-BUF frame download fails on the negotiated Intel-tiled modifier.** Even after the portal consent is bypassed, KPipeWire negotiates a DMA-BUF format with an Intel tiled modifier that KRFB's `DmaBufHandler` cannot download → KRFB receives empty (cursor-only) buffers → still black screen (or `bad desktop size 0x0`). 3. **Red/blue colour swap.** Once frames finally flow, KRFB's `PWFrameBuffer` doesn't set the R/G/B bit shifts in the VNC pixel format, so libvncserver defaults to RGB byte order while PipeWire delivers BGRx → red and blue are swapped on the client. --- ## Attempts — what worked and what didn't ### ❌ Restarting KRFB Killing and relaunching KRFB (via `systemctl --user restart`, `systemd-run --user`, or the autostart generator) made no difference. The screencast node was never created because the portal consent dialog still needed a click. ### ❌ `KDE_APPLICATIONS_AS_SCOPE=1` / clean KDE session env Launching KRFB with a full KDE session environment (`KDE_FULL_SESSION=true`, `XDG_CURRENT_DESKTOP=KDE`, `KDE_APPLICATIONS_AS_SCOPE=1`, etc.) did **not** fix the `Failed to register with host portal … Connection already associated with an application ID` error. KRFB always sends `app_id: ""` (empty) to the portal regardless. ### ❌ `wayvnc` `wayvnc` is in the Ubuntu repos but is for **wlroots-based** compositors (Sway, Hyprland, Cage). It does not work with KWin/Plasma. Not an option. ### ❌ RealVNC Server / TigerVNC Server / x11vnc These either create a **virtual X11** session (failing the "existing session + no X11" requirement) or don't support Wayland at all. ### ❌ Manually linking the PipeWire nodes (`pw-link`) `pw-link kwin_wayland:output_1 krfb:input_1` failed with `Operation not permitted` — portal-managed screencast streams cannot be linked manually. ### ❌ `QT_QPA_PLATFORM=offscreen` to disable EGL/DMA-BUF This disabled DMA-BUF but also broke the portal screencast entirely — KRFB never created a screencast session and didn't listen on port 5900. The portal needs a real Wayland platform connection. ### ❌ `LIBGL_ALWAYS_SOFTWARE=1` Did not disable KPipeWire's DMA-BUF path — `eglChooseConfig` still succeeded and KRFB still negotiated the Intel-tiled DMA-BUF modifier → still empty buffers. ### ✅ Granting portal "mega-authorization" for the empty app-id The `xdg-desktop-portal-kde` source (`src/remotedesktop.cpp`) has an `isAppMegaAuthorized(app_id)` check that does `permissionStore.Lookup("kde-authorized", "remote-desktop", …)`. If the app-id has a `"yes"` permission, the consent dialog is **skipped** and the screencast starts unattended. The source explicitly supports authorizing the **empty** app-id (`""`) for host applications ("the user may authorize the empty app_id to cover generic host applications"). Since KRFB always sends `app_id: ""`, granting `"" → ["yes"]` is the sanctioned unattended path. **This fixed bug #1** (the portal consent dialog). ### ✅ `LD_PRELOAD` shim to force shm frames instead of DMA-BUF There's no env var or config option in `libkpipewire6` 6.6.4 to disable DMA-BUF. The fix is a tiny `LD_PRELOAD` shared library that stubs out the EGL DMA-BUF format/modifier query functions (`eglQueryDmaBufFormatsEXT`, `eglQueryDmaBufModifiersEXT`) and `epoxy_has_egl_extension("EGL_EXT_image_dma_buf_import")` to return empty/false. This makes KPipeWire's `createFormatsParams()` offer **only** shared-memory formats (no modifier), so KWin negotiates plain `BGRx` over `SPA_DATA_MemPtr` and KRFB receives real image data via `frame.image` → `memcpy` into the VNC framebuffer. **This fixed bug #2** (empty buffers). ### ✅ Same shim also fixes the R/B colour swap The shim intercepts `rfbGetScreen()` and `rfbNewFramebuffer()` (libvncserver) and sets the server pixel format shifts to match BGRx memory layout (`redShift=16, greenShift=8, blueShift=0`). **This fixed bug #3** (swapped colours). ### ❌ Intercepting only `rfbNewFramebuffer` (without `rfbGetScreen`) The first version of the colour fix only intercepted `rfbNewFramebuffer`. It didn't work because KRFB's initial pixel format is set via `rfbGetScreen()` → `getServerFormat()`, and the shifts defaulted to RGB. Both functions needed interception. --- ## Symptoms & how to confirm which bug you're hitting ### Bug #1 — portal consent (no screencast stream at all) ```bash # As the desktop user, with XDG_RUNTIME_DIR set: pw-cli ls Node | grep krfb # → nothing (no krfb Stream/Input/Video node) journalctl -t krfb --user -b | grep "no outputs" # → "qt.qpa.wayland: There are no outputs - creating placeholder screen" journalctl -t xdg-desktop-portal-kde --user -b | grep MegaAuth # → "MegaAuth: Failed to lookup permissions: No entry for remote-desktop" ``` ### Bug #2 — DMA-BUF empty buffers (screencast node exists, but black) ```bash # Enable KRFB pipewire debug logging first (see below), restart krfb, then: journalctl -t krfb --user -b | grep "empty buffer" # → "Got empty buffer. The buffer possibly carried only information about the mouse cursor." # (repeated many times = bug #2) # Check the negotiated PipeWire format on the kwin screencast node: pw-dump | grep -A5 '"Format"' # → "format": "BGRx", "modifier": 72057594037927944 (= 0x0100000000000008, Intel tiled) # A modifier present = DMA-BUF was negotiated = bug #2 ``` To enable KRFB debug logging temporarily: ```bash mkdir -p ~/.config/systemd/user/krfb.service.d cat > ~/.config/systemd/user/krfb.service.d/debug.conf <) ############################################################################# # 2. Build the LD_PRELOAD shim (forces shm frames + fixes R/B colour swap). ############################################################################# sudo apt-get update sudo apt-get install -y gcc libegl-dev libvncserver-dev cat > ~/krfb-nodmabuf-shim.c <<'SHIM' #define _GNU_SOURCE #include #include #include #include #include /* --- 1. Force PipeWire/KPipeWire to use shared-memory (shm) frames instead of * DMA-BUF. kwin's monitor screencast negotiates an Intel-tiled DMA-BUF * modifier that krfb's DmaBufHandler cannot download on this iGPU, * producing empty (cursor-only) buffers -> black screen. * By defeating the EGL dmabuf-format/modifier queries, KPipeWire's * createFormatsParams() offers only shm formats and kwin falls back to * sending real BGRx/BGRA image data. */ static EGLBoolean fail_formats(EGLDisplay d, EGLint max, EGLint *fmts, EGLint *num) { if (num) *num = 0; return EGL_TRUE; } static EGLBoolean fail_mods(EGLDisplay d, EGLint fmt, EGLint max, EGLuint64KHR *mods, EGLBoolean *ext, EGLint *num) { if (num) *num = 0; return EGL_TRUE; } EGLBoolean eglQueryDmaBufFormatsEXT(EGLDisplay d, EGLint max, EGLint *f, EGLint *n) { return fail_formats(d, max, f, n); } EGLBoolean eglQueryDmaBufModifiersEXT(EGLDisplay d, EGLint fmt, EGLint max, EGLuint64KHR *mods, EGLBoolean *ext, EGLint *n) { return fail_mods(d, fmt, max, mods, ext, n); } /* Make KPipeWire think EGL_EXT_image_dma_buf_import is unavailable so * queryDmaBufModifiers() returns empty modifier lists (shm-only offers). */ int epoxy_has_egl_extension(EGLDisplay d, const char *ext) { if (ext && strstr(ext, "EXT_image_dma_buf_import")) return 0; static int (*real)(EGLDisplay, const char *); if (!real) real = (int (*)(EGLDisplay, const char *))dlsym(RTLD_NEXT, "epoxy_has_egl_extension"); return real ? real(d, ext) : 0; } /* --- 2. Fix the R/B colour swap. PipeWire delivers BGRx/BGRA in memory * ([B,G,R,x]) but krfb's PWFrameBuffer::getServerFormat() does not set * the R/G/B shifts, so libvncserver defaults to RGB byte order and the * client sees red and blue swapped. We intercept rfbNewFramebuffer() * and rfbGetScreen() and set the server format to match the BGRx memory * layout: redShift=16 greenShift=8 blueShift=0. */ void rfbNewFramebuffer(rfbScreenInfoPtr screen, char *framebuffer, int width, int height, int bitsPerSample, int samplesPerPixel, int bytesPerPixel) { static void (*real)(rfbScreenInfoPtr, char *, int, int, int, int, int); if (!real) real = (void (*)(rfbScreenInfoPtr, char *, int, int, int, int, int)) dlsym(RTLD_NEXT, "rfbNewFramebuffer"); if (real) real(screen, framebuffer, width, height, bitsPerSample, samplesPerPixel, bytesPerPixel); if (screen && screen->serverFormat.bitsPerPixel == 32) { screen->serverFormat.redShift = 16; screen->serverFormat.greenShift = 8; screen->serverFormat.blueShift = 0; screen->serverFormat.redMax = 255; screen->serverFormat.greenMax = 255; screen->serverFormat.blueMax = 255; screen->serverFormat.trueColour = 1; screen->serverFormat.bigEndian = 0; } } rfbScreenInfoPtr rfbGetScreen(int *argc, char **argv, int width, int height, int bitsPerSample, int samplesPerPixel, int bytesPerPixel) { static rfbScreenInfoPtr (*real)(int*, char**, int, int, int, int, int); if (!real) real = (rfbScreenInfoPtr (*)(int*, char**, int, int, int, int, int)) dlsym(RTLD_NEXT, "rfbGetScreen"); rfbScreenInfoPtr screen = real ? real(argc, argv, width, height, bitsPerSample, samplesPerPixel, bytesPerPixel) : NULL; if (screen && screen->serverFormat.bitsPerPixel == 32) { screen->serverFormat.redShift = 16; screen->serverFormat.greenShift = 8; screen->serverFormat.blueShift = 0; screen->serverFormat.redMax = 255; screen->serverFormat.greenMax = 255; screen->serverFormat.blueMax = 255; screen->serverFormat.trueColour = 1; screen->serverFormat.bigEndian = 0; } return screen; } SHIM gcc -shared -fPIC -o ~/krfb-nodmabuf-shim.so ~/krfb-nodmabuf-shim.c -ldl ############################################################################# # 3. Replace the autostart entry with a proper systemd user service so KRFB # starts after the Plasma workspace, auto-restarts on failure, and loads # the shim via LD_PRELOAD. ############################################################################# # Disable the old autostart desktop entry: rm -f ~/.config/autostart/krfb.desktop # Create the systemd user service: cat > ~/.config/systemd/user/krfb.service <<'UNIT' [Unit] Description=Krfb VNC Server (Wayland, existing session) After=plasma-workspace.target plasma-xdg-desktop-portal-kde.service [Service] Type=simple ExecStart=/usr/bin/krfb -qwindowtitle Krfb Restart=on-failure RestartSec=3 Environment=QT_QPA_PLATFORM=wayland Environment=KDE_APPLICATIONS_AS_SCOPE=1 Environment=KDE_FULL_SESSION=true Environment=KDE_SESSION_VERSION=6 Environment=XDG_CURRENT_DESKTOP=KDE Environment=XDG_SESSION_DESKTOP=KDE Environment=XDG_SESSION_TYPE=wayland [Install] WantedBy=plasma-workspace.target UNIT # Create the drop-in that loads the shim: mkdir -p ~/.config/systemd/user/krfb.service.d cat > ~/.config/systemd/user/krfb.service.d/shim.conf <<'DROPIN' [Service] Environment=LD_PRELOAD=%h/krfb-nodmabuf-shim.so DROPIN systemctl --user daemon-reload systemctl --user enable --now krfb.service ############################################################################# # 4. Verify it works. ############################################################################# sleep 5 echo "krfb active: $(systemctl --user is-active krfb.service)" echo "5900 listen: $(ss -tlnp 2>/dev/null | grep -c 5900) listener(s)" echo "pw link: $(pw-link -l 2>/dev/null | grep -c 'krfb:input_1') link(s)" echo "empty frames: $(journalctl -t krfb --user -b --no-pager 2>/dev/null | grep -c 'empty buffer')" # Expected: active / 1+ / 1+ / 0 ``` After this, connect with any VNC viewer to `:5900`. You should see the live Plasma Wayland desktop with correct colours. --- ## What persists across reboots | Component | Where it's stored | Persists? | |---|---|---| | Portal mega-auth (`"" → ["yes"]`) | `~/.local/share/flatpak/db/kde-authorized` (via `xdg-permission-store.service`) | ✅ Yes | | KRFB systemd service | `~/.config/systemd/user/krfb.service` (enabled) | ✅ Yes | | LD_PRELOAD shim drop-in | `~/.config/systemd/user/krfb.service.d/shim.conf` | ✅ Yes | | Shim binary | `~/krfb-nodmabuf-shim.so` (built from `~/krfb-nodmabuf-shim.c`) | ✅ Yes | | Old autostart entry | removed (`~/.config/autostart/krfb.desktop`) | ✅ N/A | The `krfb.service` starts after `plasma-workspace.target`, so it launches automatically on login. `Restart=on-failure` brings it back if it crashes. --- ## After a kernel/mesa/KPipeWire upgrade If a future update fixes the DMA-BUF download path natively, you can remove the shim: ```bash rm ~/.config/systemd/user/krfb.service.d/shim.conf systemctl --user daemon-reload systemctl --user restart krfb.service ``` If colours break again after removing the shim, the R/B-swap part of the shim is still needed (that's a KRFB bug, not a DMA-BUF issue) — keep the `rfbGetScreen`/`rfbNewFramebuffer` interception and only drop the EGL stubs. --- ## KRFB password KRFB's VNC password is stored in `~/.config/krfbrc` under `[Security] UninvitedConnectionsPassword`. To change it, edit that file and restart `krfb.service`. Make sure `AllowUninvitedConnections=true` and `allowUnattendedAccess=true` are set for unattended access.