← Back

Getting Ripes to run on Wayland (The hard way)

June 14, 2026

I’m working through a foundational course on RISC-V assembly, and one of the tools mentioned is Ripes, a visual RISC-V processor simulator that lets you step through your assembly, watch the pipeline stages, inspect registers, the works. It’s a genuinely good tool for learning what actually happens when an instruction moves through a 5-stage pipeline.

The course material shows Ripes looking clean. Crisp text, sharp edges, properly rendered UI. When I launched it on my Ubuntu 26 machine, it did not look like that. The GUI appeared scaled inward, like its elements cramped, the pipeline diagram crowded into less space than it should occupy, fonts slightly fuzzy, things not quite landing on pixel boundaries. The kind of wrong that makes you stare at it for a few seconds before you trust your eyes.

XWayland render of Ripes

A note on the above screenshot: I no longer have a clean “before” state to screenshot. By the time I thought to document this, I had already installed all the Wayland support infrastructure on my system, which changes how XWayland itself renders Qt applications. The XWayland screenshot here was taken after all that work was done, so it doesn’t fully reproduce the original cramped look. What it does show is still instructive, for instance, look at the register value column in the XWayland render, the hex values truncate to 0x0000... because the column geometry is off. The native Wayland render shows the full 0x00000000 in the same space. The before/after difference is real, just not this dramatic as what I originally saw.

I knew immediately what this was. Ubuntu 26 runs Wayland by default. Ripes was rendering through XWayland, the compatibility layer that lets X11 applications run on a Wayland compositor by translating between the two protocols. XWayland works fine for most things, but it doesn’t do fractional scaling correctly, and on a display with any HiDPI configuration, the result is exactly that slight blurriness. The application thinks it’s talking to an X11 server, but it isn’t and I believe things get lost in translation.

The fix is straightforward in principle, force Ripes to use the Wayland backend directly instead of going through XWayland. Qt, which Ripes is built on, has a Wayland platform plugin that makes this possible. You set QT_QPA_PLATFORM=wayland and Qt loads libqwayland-egl.so or libqwayland-generic.so instead of libqxcb.so, talks to the Wayland compositor natively, and the rendering is correct.

In practice, it was not straightforward at all, and here I am to tell you what actually happened.

AppImages and bundled runtimes

Ripes ships as an AppImage. AppImage is a packaging format where the entire application — binaries, libraries, assets — lives inside a single self-contained squashfs filesystem image that gets mounted at runtime via FUSE. The application never touches your system’s package manager. It just mounts itself under /tmp/.mount_Ripes-XXXXXX/ and runs.

This self-containment is the point of AppImage. It means the application works regardless of what libraries your system has installed. But it also means the application is entirely dependent on whatever was bundled inside the image at build time.

When I ran:

QT_QPA_PLATFORM=wayland ./Ripes-v2.2.6-linux-x86_64.AppImage

I got:

qt.qpa.plugin: Could not find the Qt platform plugin "wayland" in ""
Available platform plugins are: xcb.
Aborted (core dumped)

The Ripes AppImage was built with only the xcb platform plugin bundled. Whoever packaged it included libqxcb.so and nothing else. There was no libqwayland-egl.so, no libqwayland-generic.so, no Wayland platform support at all. Setting QT_QPA_PLATFORM=wayland just told Qt to look for a plugin that didn’t exist inside the bundle.

The system’s Qt Wayland plugins can’t help here either. Qt’s plugin ABI encodes the exact version of Qt the plugin was built against into the binary metadata. If the version doesn’t match exactly, Qt rejects the plugin at load time with “uses incompatible Qt library.” To confirm this was the case, I first needed to know which Qt version Ripes bundled.

Inspecting the AppImage

AppImages support a --appimage-mount flag that mounts the squashfs and gives you a path to inspect without extracting anything:

./Ripes-v2.2.6-linux-x86_64.AppImage --appimage-mount &
sleep 2
MOUNT_DIR=$(ls -d /tmp/.mount_Ripes* 2>/dev/null | head -1)
strings "$MOUNT_DIR/usr/lib/libQt5Core.so.5" | grep -E "^5\.[0-9]+\.[0-9]+" | head -3

Output came back as 5.15.2

Ubuntu 26 carries qtwayland5 at version 5.15.18. That’s not the same minor release, and Qt’s plugin version check compares the full version integer embedded in the .so metadata, and 5.15.2 != 5.15.18. Path 1, pointing QT_QPA_PLATFORM_PLUGIN_PATH at the system’s plugin directory, was dead before it started.

My surgical extraction and injection

The only remaining option was to open the AppImage, inject the correct Wayland plugins, and repack it. The correct plugins meant ones built against exactly Qt 5.15.2.

Ubuntu Impish (21.10) shipped qtwayland5 at version 5.15.2-4. Impish is EOL and its package archive has moved to old-releases.ubuntu.com, but the packages are still there. I pulled two of them:

wget http://old-releases.ubuntu.com/ubuntu/pool/universe/q/qtwayland-opensource-src/qtwayland5_5.15.2-4_amd64.deb
wget http://old-releases.ubuntu.com/ubuntu/pool/universe/q/qtwayland-opensource-src/libqt5waylandclient5_5.15.2-4_amd64.deb

Neither gets installed to the system. dpkg-deb -x extracts the contents of a .deb into a directory without touching dpkg‘s package database:

mkdir -p /tmp/qtwayland-extract
dpkg-deb -x qtwayland5_5.15.2-4_amd64.deb /tmp/qtwayland-extract
dpkg-deb -x libqt5waylandclient5_5.15.2-4_amd64.deb /tmp/qtwayland-extract

Then extract the AppImage itself:

./Ripes-v2.2.6-linux-x86_64.AppImage --appimage-extract
# produces ./squashfs-root/

The platforms directory inside was exactly what the error message implied, the one file:

squashfs-root/usr/plugins/platforms/libqxcb.so

I copied in the Wayland platform plugins and the client library they link against:

cp /tmp/qtwayland-extract/usr/lib/x86_64-linux-gnu/qt5/plugins/platforms/libqwayland-egl.so \
   squashfs-root/usr/plugins/platforms/

cp /tmp/qtwayland-extract/usr/lib/x86_64-linux-gnu/qt5/plugins/platforms/libqwayland-generic.so \
   squashfs-root/usr/plugins/platforms/

cp /tmp/qtwayland-extract/usr/lib/x86_64-linux-gnu/libQt5WaylandClient.so.5* \
   squashfs-root/usr/lib/

Then the RPATH problem

Testing with QT_QPA_PLATFORM=wayland squashfs-root/AppRun produced a new error:

qt.qpa.plugin: Could not load the Qt platform plugin "wayland" in "" even though it was found.
Available platform plugins are: wayland-egl, wayland, xcb.

“Found but could not load” is a different failure mode from “not found.” The plugin binary exists and Qt can read its metadata, but when Qt tries to dlopen it, the dynamic linker fails. I ran ldd against the plugin to find out what it couldn’t resolve:

ldd squashfs-root/usr/plugins/platforms/libqwayland-generic.so | grep "not found"
libQt5WaylandClient.so.5 => not found
libQt5Gui.so.5 => not found
libQt5Core.so.5 => not found

This needs some explanation. ldd runs without any environment modifications, so it uses only the system’s default library search paths. The AppImage’s bundled libQt5Core.so.5 and friends are not on those paths. Normally AppRun sets LD_LIBRARY_PATH before exec’ing the application binary, which is how the main executable finds its bundled libraries. But libqwayland-generic.so is a plugin loaded at runtime via dlopen, not a direct dependency of the main binary. By the time Qt calls dlopen on the plugin, LD_LIBRARY_PATH is already set. But AppRun in this case was a compiled binary, not a shell script, so I couldn’t grep it to see what paths it was setting.

I checked where AppRun was actually injecting libraries:

./Ripes-v2.2.6-linux-x86_64.AppImage --appimage-mount &
sleep 2
MOUNT_DIR=$(ls -d /tmp/.mount_Ripes* 2>/dev/null | head -1)
ls "$MOUNT_DIR/"

The Qt libraries (libQt5Core.so.5, libQt5Gui.so.5, etc.) were sitting directly in the squashfs root, not in usr/lib/. AppRun was adding the root directory to LD_LIBRARY_PATH, not usr/lib/. I had copied libQt5WaylandClient.so.5 into usr/lib/, which was the wrong place.

Moving it to the root of squashfs-root/ still didn’t fix it. The dlopen path for a plugin uses the plugin’s own RPATH if one is set, falling back to LD_LIBRARY_PATH from the process environment. I checked the plugin’s RPATH:

objdump -x squashfs-root/usr/plugins/platforms/libqwayland-generic.so | grep -i rpath

No output. The plugin had no RPATH set at all. Whoever built qtwayland5 for Ubuntu Impish compiled it without embedding an RPATH, expecting the standard system library paths to contain everything it needed and which they would, if this plugin were installed system-wide on an Impish machine. Inside an AppImage, that assumption doesn’t hold.

Fixing the RPATH with patchelf

Now, patchelf is a tool for modifying ELF binaries post-compilation. Among other things, it can set or modify the RPATH stored in a binary’s dynamic section. I used it to embed an RPATH that uses $ORIGIN, a dynamic linker token that expands to the directory containing the .so file being loaded:

patchelf --set-rpath '$ORIGIN:$ORIGIN/../../:$ORIGIN/../../../' \
  squashfs-root/usr/plugins/platforms/libqwayland-generic.so

patchelf --set-rpath '$ORIGIN:$ORIGIN/../../:$ORIGIN/../../../' \
  squashfs-root/usr/plugins/platforms/libqwayland-egl.so

The three paths cover:

  • $ORIGIN — the platforms/ directory itself (where I had also placed a copy of the client lib)
  • $ORIGIN/../../usr/ (where usr/lib/ is a subdirectory, but this doesn’t reach it)
  • $ORIGIN/../../../ — the squashfs root, where the other Qt libraries live

The next missing layer

With the RPATH patched and the client lib in place, the wayland plugin finally loaded. But then:

qt.qpa.wayland: No shell integration named "xdg-shell" found
qt.qpa.wayland: No shell integration named "xdg-shell-v6" found
qt.qpa.wayland: No shell integration named "wl-shell" found
qt.qpa.wayland: Loading shell integration failed.

Shell integration plugins are a separate set of .so files that implement the Wayland shell protocol, that is the part that handles window creation, positioning, decoration, and focus negotiation between the client and the compositor. On GNOME Wayland, the relevant protocol is xdg-shell. Without libxdg-shell.so, Qt can connect to the Wayland socket but can’t actually create a window.

These plugins were also in the Impish package, just in a different subdirectory:

find /tmp/qtwayland-extract -name "*.so" | grep -v platforms | sort

I created the missing plugin directories inside squashfs-root and copied everything in:

mkdir -p squashfs-root/usr/plugins/wayland-shell-integration
mkdir -p squashfs-root/usr/plugins/wayland-graphics-integration-client
mkdir -p squashfs-root/usr/plugins/wayland-decoration-client

cp /tmp/qtwayland-extract/usr/lib/x86_64-linux-gnu/qt5/plugins/wayland-shell-integration/*.so \
   squashfs-root/usr/plugins/wayland-shell-integration/

cp /tmp/qtwayland-extract/usr/lib/x86_64-linux-gnu/qt5/plugins/wayland-graphics-integration-client/*.so \
   squashfs-root/usr/plugins/wayland-graphics-integration-client/

cp /tmp/qtwayland-extract/usr/lib/x86_64-linux-gnu/qt5/plugins/wayland-decoration-client/*.so \
   squashfs-root/usr/plugins/wayland-decoration-client/

Then it works, but the AppRun Wrapper doesn’t

Running from the extracted tree with an explicit LD_LIBRARY_PATH:

LD_LIBRARY_PATH=~/Downloads/squashfs-root:~/Downloads/squashfs-root/usr/lib \
QT_QPA_PLATFORM=wayland \
~/Downloads/squashfs-root/AppRun

Ripes launched with native Wayland, crisp rendering, exactly what I wanted.

Native Wayland render of Ripes

Repacking with appimagetool and running the resulting AppImage without the explicit LD_LIBRARY_PATH broke it again. The problem was that AppRun is a compiled binary that sets its own LD_LIBRARY_PATH at startup based on the squashfs mount point, but it does this before any plugin loading happens. The Wayland plugins, loaded later via dlopen, inherit that environment, but the RPATH patch should have made them self-sufficient. What was still going wrong?

Mounting the repacked AppImage and re-checking confirmed the RPATH survived repacking ($ORIGIN:$ORIGIN/../../:$ORIGIN/../../../ was there). The issue was subtler, libQt5WaylandClient.so.5 was present in three locations inside the image (the root, usr/lib/, and usr/plugins/platforms/), but the RPATH path $ORIGIN/../../../ from inside usr/plugins/platforms/ resolves to the squashfs mount root, and which at runtime is something like /tmp/.mount_Ripes-XXXXXX/. That path is correct, and the library is there. But the dynamic linker had already resolved libQt5WaylandClient.so.5 through a different path before the plugin was loaded, and something in the resolution order was causing a conflict.

Rather than chase that further, I wrote a small wrapper script that explicitly sets LD_LIBRARY_PATH using the actual mount point before invoking AppRun:

#!/bin/bash
APPIMAGE="/opt/ripes/Ripes-v2.2.6-wayland.AppImage"
"$APPIMAGE" --appimage-mount > /dev/null 2>&1 &
MOUNT_PID=$!
sleep 1
ACTUAL_MOUNT=$(ls -d /tmp/.mount_Ripes* 2>/dev/null | head -1)

LD_LIBRARY_PATH="$ACTUAL_MOUNT:$ACTUAL_MOUNT/usr/lib" \
QT_QPA_PLATFORM=wayland \
"$ACTUAL_MOUNT/AppRun" "$@"

kill $MOUNT_PID 2>/dev/null

This lives at /usr/local/bin/ripes and is what the .desktop entry calls.

What actually happened, summarized

The Ripes AppImage was built without Wayland support. The platform plugin, shell integration plugins, and client library were all missing. Injecting the correct ones required:

  • Sourcing Qt 5.15.2-exact builds from the Ubuntu Impish EOL archive
  • Extracting them without installing to the system via dpkg-deb -x
  • Patching the RPATH on the platform plugins with patchelf so they could find their dependencies at runtime
  • Copying the shell integration plugin tree into the AppImage
  • Working around AppRun‘s library path setup with a wrapper script

The wrapper script is inelegant. The correct fix is for the AppImage to be rebuilt with the Wayland plugins included and their RPATHs set at compile time. But this gets Ripes running on Wayland right now, on this machine, without waiting for an upstream release.

The course material looks the way it should.