11 minute read

Everything that broke running Omarchy in VMware Workstation

Xwayland dies on vmwgfx and takes app launching and clipboard sync down with it. What actually causes that, how to fix it, and why GPU acceleration on a Hyprland guest is currently blocked upstream.

S

I run Omarchy in a VMware Workstation VM on a Windows host, and once it is set up properly it is a good way to work. Getting there took a couple of days, most of which I spent chasing what I was convinced were three separate problems.

The browser would stop launching, Discord along with it, and at some point the clipboard would stop syncing to Windows without any indication that it had. Logging out and back in brought everything back for a few hours.

All of it came from one dead process, and none of the error messages pointed anywhere near it. What follows is the chain, the fixes, and the handful of diagnostics that sent me the wrong way for hours at a stretch.

Everything here is Arch (Omarchy), Hyprland 0.56, open-vm-tools 13.1, on VMware Workstation with 3D acceleration turned on.

Xwayland dies on vmwgfx

Xwayland dies and Hyprland does not bring it back.

unknown object (36), message attach(?oii)
XWAYLAND: wl_display#1: error 1: invalid arguments for wl_surface#18.attach
(EE) failed to dispatch Wayland events: Invalid argument

I assumed this was some kind of race for a while, but it is not. Start a scratch Xwayland by hand with nothing connected to it and it dies at startup every single time. Glamor is what is doing it: the accelerated path allocates its buffers through GBM on vmwgfx, hands the compositor a wl_buffer, and the compositor refuses it. Since that counts as a protocol violation rather than a rendering glitch, the connection gets killed outright.

One variable in ~/.config/uwsm/env sorts it:

export XWAYLAND_NO_GLAMOR=1

Xwayland 24.1 honours it, and you can check your own build with:

strings /usr/bin/Xwayland | grep XWAYLAND_

You lose nothing by setting it. glxinfo reports the same llvmpipe GL 4.6 either way, since this guest was never getting accelerated GLX in the first place.

What cost me time here was assuming LIBGL_ALWAYS_SOFTWARE=1 already covered it. That variable moves glamor’s rendering onto llvmpipe while the buffers still come from GBM on vmwgfx, so Xwayland carries on dying with it set. It looks like it should be operating on the same layer, which is why I skipped past it for so long.

Apps that hang instead of failing

This is the same bug again, and it is the reason one dead process ends up looking like several unrelated ones.

When Xwayland dies, Hyprland carries on exporting DISPLAY=:0 and leaves /tmp/.X11-unix/X0 sitting in place, so X11 clients never get an error. They connect to a socket that nobody is accepting on and block forever in unix_wait_for_peer.

Hanging like that is a lot harder to debug than a clean failure, because there is nothing to go and read. Both omarchy-launch-browser and omarchy-launch-webapp start with xdg-settings get default-web-browser, whose desktop detection shells out to xprop -root _DT_SAVE_MODE. That call blocks, the launcher never reaches the line that starts the app, and every keypress leaves another stuck process behind. There were ten of them by the time I went looking.

One command settles it:

cat /proc/<pid>/task/*/wchan     # unix_wait_for_peer = dead Xwayland, live DISPLAY

Discord goes the same way, since on Omarchy it is a Chromium web app running through that same launcher, which is how two apps with nothing to do with each other end up broken by one blocked probe.

If you are going to build something here, build a dead-man switch that notices Xwayland is gone and unlinks the stale socket so X11 connections fail immediately. It does not fix anything, but it turns “my desktop is broken” into “X11 apps are unavailable”, and the launcher probe drops from an infinite hang to 0.12 seconds.

The clipboard reports itself healthy while carrying nothing

Copy and paste between Windows and the guest has more places to break than anything else here, and every one of them breaks silently.

Diagram: the Windows host clipboard reaches vmtoolsd over the VMX backdoor, lands on the guest's X11 CLIPBOARD selection, and stops, because Hyprland does not sync X11 and Wayland. A bridge daemon carries text, images and file URIs across the gap.

There are three problems stacked on top of each other, roughly in the order you will meet them.

The plugin never loads. libdndcp.so links gtkmm3, which Arch ships as an optional dependency of open-vm-tools, so a stock install does not have it:

Opening plugin 'libdndcp.so' failed: libgtkmm-3.0.so.1: cannot open shared object file

Install gtkmm3 and it loads.

The plugin is X11-only. It calls gdk_x11_* and XOpenDisplay, while Omarchy exports GDK_BACKEND="wayland,x11,*", so GTK initialises on Wayland and every X11 call inside the plugin fails silently. A systemd user drop-in forcing GDK_BACKEND=x11 puts it back on Xwayland where it expects to be.

Hyprland does not sync the X11 and Wayland clipboards. VMware’s data lands on the X11 CLIPBOARD and stops there, invisible to every native app. Closing that gap needs a daemon that is an X11 client and a Wayland client simultaneously, copying whichever side changed most recently.

Do not trust systemctl is-active while you are working on this. When Xwayland dies the clipboard units exit, systemd restarts them straight back into the same dead display, and they will happily report active while nothing at all is moving.

The image support everyone says is missing

Common wisdom says open-vm-tools cannot carry images, and the test everyone reaches for agrees:

strings /usr/lib/open-vm-tools/plugins/vmusr/libdndcp.so | grep -cE '^image/'   # 0

I believed that for most of a day. The zero is real, but GTK registers image/png, image/bmp and the rest at runtime out of GdkPixbuf’s format list, so they are never in the binary as literal strings for grep to find. The symbol table tells a different story:

nm -D --undefined-only /usr/lib/open-vm-tools/plugins/vmusr/libdndcp.so | grep -i image

Back come Gtk::Clipboard::set_image(), wait_for_image(), Gdk::PixbufLoader and Pixbuf::save_to_buffer(), which is the entire image clipboard API in both directions. open-vm-tools 13 negotiates CopyPaste v4 with the host, and that version carries PNG. Images had been arriving on the guest’s X11 clipboard the whole time I was convinced they could not, and the only thing missing was something to carry them the last hop to Wayland.

It is a bad test in general, not just here. Any MIME type a toolkit assembles at runtime is invisible to grep.

Two things that will shape the bridge if you write one:

  • Do not read the clipboard image on every poll just to spot changes, because each read makes vmtoolsd drag the whole payload across the backdoor from the host. Hash the first 64 KB and only do a full read when that hash moves.
  • Re-encoding a PNG is not byte-stable. After a guest to host push, the bytes X11 hands back do not match the ones you supplied, so the image looks new and starts bouncing between the two clipboards indefinitely. Keep hashes of both forms.

GPU acceleration is blocked upstream

Your VM’s 3D acceleration is probably fine. Mine reports the full feature set:

vmwgfx: Capabilities: ... 3D, ... gbobject, dx
vmwgfx: Available shader model: SM_5_1X
EGL on GBM: SVGA3D, OpenGL 4.3

Everything still renders on the CPU, and the workaround everybody recommends is what is causing it. LIBGL_ALWAYS_SOFTWARE=1 does more than pick a software rasteriser, it hides the DRM node from EGL completely:

LIBGL_ALWAYS_SOFTWAREEGL devicesDRM node
unset2/dev/dri/card0 + renderD128, VMware
=11none

Aquamarine matches EGL devices against /dev/dri/card0, so with that variable set it finds nothing at all and Hyprland comes up without a GL renderer:

CDRMRenderer(drm): Can't create renderer, no matching devices found
drm: Failed to initialize renderer state for /dev/dri/card0

A compositor with no renderer cannot import a GPU buffer from anything, so it rejects every GPU client, which happens to be the exact symptom people set the variable to cure. It also retries the renderer indefinitely, and my hyprland.log got to 181 MB in 14 hours inside a 790 MB tmpfs, which is to say in RAM.

Removing it gets the compositor onto the GPU cleanly, but clients still die, including a plain GTK4 window, so it is not a Qt or Quickshell problem. A WAYLAND_DEBUG capture shows where it ends:

-> zwp_linux_buffer_params_v1#50.add(fd 15, 0, 0, 7680, 0, 0)
-> zwp_linux_buffer_params_v1#50.create_immed(new id wl_buffer#51, 1920, 1080, XR24, 0)
   discarded [unknown]#50.[event 1]          <- buffer_params.failed
   wl_display#1.error("invalid arguments for wl_surface#31.attach")

Hyprland advertises 116 format and modifier pairs, the client picks one off that list, and then the compositor refuses to import it. Nothing gets logged, even with AQ_TRACE=1 and HYPRLAND_TRACE=1 set.

To rule the driver out I wrote four small GBM/EGL programs that allocate a buffer and import it straight back through eglCreateImageKHR, and every combination works: implicit and explicit LINEAR modifiers, with and without modifier attributes, 1920x1080 at the exact stride the client asks for, across separate device fds, and into a card0 display. They all import cleanly, which puts the fault somewhere above Mesa.

It is filed as aquamarine#360. Until that lands, a Hyprland guest on VMware renders in software. Both hyprland and aquamarine come out of Arch’s extra repo, so whenever it is fixed it will arrive through an ordinary pacman -Syu without waiting on an Omarchy release.

There is one more trap in that error text. I spent hours reading unknown object (46), message attach(?oii) as the compositor rejecting a malformed buffer. What it is telling you is that the buffer does not exist: libwayland resolves the target object before it prints the method name and signature, so the object it cannot find is the wl_buffer argument rather than the surface. The compositor had already refused to create it, and once I read it that way there was enough to file.

Screen recording, and a simpler blocker

Omarchy binds ALT + PRINT to gpu-screen-recorder, which refuses to start on a VMware guest in two different ways depending on your config:

with LIBGL_ALWAYS_SOFTWARE=1
  "It's using llvmpipe (software rendering)... failed to load opengl"

without it
  "unknown gpu vendor: VMware, Inc."

The second one is a hard vendor whitelist covering AMD, NVIDIA and Intel, so it would refuse on this hardware even with acceleration working properly, and -fallback-cpu-encoding yes does not get you past it, because that flag only covers the encoder rather than the check itself.

wf-recorder does the job instead. It captures through wlr-screencopy, which is the same path grim already uses successfully here, and encodes x264 on the CPU. Full screen holds a steady 30.1 fps with no dropped frames at roughly 25% of one core, which is plenty for demos and bug reports.

Playing the result back crashed mpv, for the same underlying reason. It comes up on Wayland, builds itself a working GL 4.6 context, notices the renderer is llvmpipe and throws that context away as too slow, then falls through its probe list to x11egl and aborts:

[vo/gpu-next/wayland] GL_RENDERER='llvmpipe (LLVM 22.1.8, 256 bits)'
[vo/gpu-next/opengl]  Suspected software renderer or indirect context.
[vo/gpu-next/opengl]  Initializing GPU context 'x11egl'
mpv: video/out/x11_common.c:679: vo_x11_init: Assertion `!vo->x11' failed.

Telling it to accept the software renderer keeps it on the context that was already working. In ~/.config/mpv/mpv.conf:

gpu-sw=yes

That pattern turned up more than once. Anything that refuses software rendering on your behalf goes looking for a better path, and on this guest there is not one, so it finds a crash instead.

If you are setting this up

Check that Xwayland is actually alive before you diagnose anything else, because one dead process here shows up as several unrelated bugs and you can burn a lot of time treating them separately.

Most of the difficulty in this stack comes from calls that block forever instead of returning an error, so if something is unresponsive rather than broken, go looking for a hang before you go looking for a bug.

systemctl is-active will tell you a unit is healthy when it has restarted cleanly into a dead world, so do not let a green status talk you out of investigating.

And re-test whatever you wrote down earlier. Three notes I had been working from (images cannot cross, LIBGL fixes Xwayland, screenshots do not work here) turned out to be wrong when I finally checked them properly, and each one had sent me somewhere worse than the original bug.

Where it ended up: Xwayland stays up, text and images move both ways across the clipboard, and screenshots and recording both work. GPU acceleration is the one thing still outstanding, and it is sitting with upstream now rather than with me.