---
title: "DRM leasing: VR for Wayland"
layout: post
tags: [wayland]
---
As those who read my [status updates](/2019/07/15/Status-update-July-2019.html)
have been aware, recently I've been working on bringing VR to Wayland (and vice
versa). The deepest and most technical part of this work is *DRM leasing*
(Direct Rendering Manager, *not* Digital Restrictions Management), and I think
it'd be good to write in detail about what's involved in this part of the
effort. This work has been sponsored by [Status.im](https://status.im/), as part
of an effort to build a comprehensive Wayland-driven VR workspace. When we got
started, most of the plumbing was missing for VR headsets to be useful on
Wayland, so this has been my focus for a while. The result of this work is
summed up in this crappy handheld video:
Keith Packard, a long time Linux graphics developer, [wrote several blog posts
documenting his work implementing this feature for
X11](https://keithp.com/blogs/DRM-lease/). My journey was somewhat similar,
though thanks to his work I was able to save a lot of time. The rub of this idea
is that the Wayland compositor, the DRM (Direct Rendering Manager) master, can
"lease" some of its resources to a client so they can drive your display
directly. DRM is the kernel subsystem we use for enumerating and setting modes,
allocating pixel buffers, and presenting them in sync with the display's refresh
rate. For a number of reasons, minimizing latency being an important one, VR
applications prefer to do these tasks directly rather than be routed through the
display server like most applications are. The main tasks for implementing this
for Wayland were:
1. Draft a [protocol extension][wl-ext] for issuing DRM leases
1. Write implementations for [wlroots][wlr-pr] and [sway][sway-pr]
1. Get a [simple test client][kmscube] working
1. Draft a Vulkan extension for leasing via Wayland
1. Write an implementation for [Mesa's Vulkan WSI implementation][wsi]
1. Get a more complex [Vulkan test client][xrgears] working
1. Add support to [Xwayland][xwayland]
[wl-ext]: https://lists.freedesktop.org/archives/wayland-devel/2019-July/040768.html
[wlr-pr]: https://github.com/swaywm/wlroots/pull/1730
[sway-pr]: https://github.com/swaywm/sway/pull/4289
[kmscube]: https://git.sr.ht/~sircmpwn/kmscube
[wsi]: https://gitlab.freedesktop.org/mesa/mesa/merge_requests/1509
[xrgears]: https://git.sr.ht/~sircmpwn/xrgears
[xwayland]: https://gitlab.freedesktop.org/xorg/xserver/merge_requests/248
Let's break down exactly what was necessary for each of these steps.
## Wayland protocol extension
Writing a protocol extension was the first order of business. There was an
[earlier attempt][original proposal] which petered off in January. I started
with this, by cleaning it up based on my prior experience writing protocols,
normalizing much of the terminology and style, and cleaning up the state
management. After some initial rounds of review, there were some questions to
answer. The most important ones were:
- How do we identify the display? Should we send the EDID, which may be
bigger than the maximum size of a Wayland message?
- Are there security concerns? Could malicious clients read from framebuffers
they weren't given a lease for?
The EDID I ended up sending in a side channel (file descriptor to shared
memory), and the latter was proven to be a non-issue by writing a malicious
client and demonstrating that the kernel rejects its attempts to do evil.
```xml
The compositor may send this event once the connector is created to
provide a file descriptor which may be memory-mapped to read the
connector's EDID, to assist in selecting the correct connectors
for lease. The fd must be mapped with MAP_PRIVATE by the recipient.
Note that not all displays have an EDID, and this event will not be
sent in such cases.
```
A few more changes would happen to this protocol in the following weeks, but
this was good enough to move on to...
[original proposal]: https://lists.freedesktop.org/archives/wayland-devel/2018-January/036652.html
## wlroots & sway implementation
After a chat with Scott Anderson (the maintainer of DRM support in wlroots) and
thanks to his timely refactoring efforts, the stage was well set for introducing
this feature to wlroots. I had a good idea of how it would take shape. [Half of
the work][state machine] - the state machine which maintains the server-side
view of the protocol - is well trodden ground and was fairly easy to put
together. Despite being a well-understood problem in the wlroots codebase, these
state machines are always a bit tedious to implement correctly, and I was still
to flushing out bugs well into the remainder of this workstream.
[state machine]: https://github.com/swaywm/wlroots/pull/1730/files#diff-77b17feac8a8af251811a20e5b9bbdd1
The other half of this work was in [the DRM subsystem][drm subsystem]. We
decided that we'd have leased connectors appear "destroyed" to the compositor,
and thus the compositor would have an opportunity to clean it up and stop using
them, similar to the behavior of when an output is hotplugged. Further changes
were necessary to have the DRM internals elegantly carry around some state for
the leased connector and avoid using the connector itself, as well as dealing
with the termination of the lease (either by the client or by the compositor).
With all of this in place, it's a [simple matter][lease issuance] to enumerate
the DRM object IDs for all of the resources we intend to lease and issue the
lease itself.
```c
int nobjects = 0;
for (int i = 0; i < nconns; ++i) {
struct wlr_drm_connector *conn = conns[i];
assert(conn->state != WLR_DRM_CONN_LEASED);
nobjects += 0
+ 1 /* connector */
+ 1 /* crtc */
+ 1 /* primary plane */
+ (conn->crtc->cursor != NULL ? 1 : 0) /* cursor plane */
+ conn->crtc->num_overlays; /* overlay planes */
}
if (nobjects <= 0) {
wlr_log(WLR_ERROR, "Attempted DRM lease with <= 0 objects");
return -1;
}
wlr_log(WLR_DEBUG, "Issuing DRM lease with the %d objects:", nobjects);
uint32_t objects[nobjects + 1];
for (int i = 0, j = 0; i < nconns; ++i) {
struct wlr_drm_connector *conn = conns[i];
objects[j++] = conn->id;
objects[j++] = conn->crtc->id;
objects[j++] = conn->crtc->primary->id;
wlr_log(WLR_DEBUG, "connector: %d crtc: %d primary plane: %d",
conn->id, conn->crtc->id, conn->crtc->primary->id);
if (conn->crtc->cursor) {
wlr_log(WLR_DEBUG, "cursor plane: %d", conn->crtc->cursor->id);
objects[j++] = conn->crtc->cursor->id;
}
if (conn->crtc->num_overlays > 0) {
wlr_log(WLR_DEBUG, "+%zd overlay planes:", conn->crtc->num_overlays);
}
for (size_t k = 0; k < conn->crtc->num_overlays; ++k) {
objects[j++] = conn->crtc->overlays[k];
wlr_log(WLR_DEBUG, "\toverlay plane: %d", conn->crtc->overlays[k]);
}
}
int lease_fd = drmModeCreateLease(backend->fd,
objects, nobjects, 0, lessee_id);
if (lease_fd < 0) {
return lease_fd;
}
wlr_log(WLR_DEBUG, "Issued DRM lease %d", *lessee_id);
for (int i = 0; i < nconns; ++i) {
struct wlr_drm_connector *conn = conns[i];
conn->lessee_id = *lessee_id;
conn->crtc->lessee_id = *lessee_id;
conn->state = WLR_DRM_CONN_LEASED;
conn->lease_terminated_cb = lease_terminated_cb;
conn->lease_terminated_data = lease_terminated_data;
wlr_output_destroy(&conn->output);
}
return lease_fd;
```
[drm subsystem]: https://github.com/swaywm/wlroots/pull/1730/files#diff-8b05a774317ee8e87d51422170f82d4b
[lease issuance]: https://github.com/swaywm/wlroots/pull/1730/files#diff-8b05a774317ee8e87d51422170f82d4bR1601
The [sway implementation][sway-pr] is very simple. I added a note in wlroots
which exposes whether or not an output is considered "non-desktop" (a property
which is set for most VR headsets), then sway just rigs up the lease manager and
offers all non-desktop outputs for lease.
## kmscube
Testing all of this required the use of a simple test client. During his earlier
work, Keith wrote some patches on top of
[kmscube](https://gitlab.freedesktop.org/mesa/kmscube/), a simple Mesa demo
which renders a spinning cube directly via DRM/KMS/GBM. A [few simple
tweaks][kmscube patch] was suitable to get this working through my protocol
extension, and for the first time I saw something rendered on my headset through
sway!
[kmscube patch]: https://git.sr.ht/~sircmpwn/kmscube/commit/60d89ef1d9304427a1289174d9a311ab06e39b44
## Vulkan
Vulkan has a subsystem called WSI - Window System Integration - which handles
the linkage between Vulkan's rendering process and the underlying window system,
such as Wayland, X11, or win32. Keith added an extension to this system called
[VK_EXT_acquire_xlib_display][VK_EXT_acquire_xlib_display], which lives on top
of [VK_EXT_direct_mode_display][VK_EXT_direct_mode_display], a system for
driving displays directly with Vulkan. As the name implies, this system is
especially X11-specific, so I've drafted my own VK extension for Wayland:
VK_EXT_acquire_wl_display. This is the crux of it:
[VK_EXT_acquire_xlib_display]: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VK_EXT_acquire_xlib_display
[VK_EXT_direct_mode_display]: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VK_EXT_direct_mode_display
```xml
VkResult vkAcquireWaylandDisplayEXT
VkPhysicalDevice physicalDevice
struct wl_display* display
struct zwp_drm_lease_manager_v1* manager
int nConnectors
VkWaylandLeaseConnectorEXT* pConnectors
```
I chose to leave it up to the user to enumerate the leasable connectors from the
Wayland protocol, then populate these structs with references to the connectors
they want to lease:
```xml
struct zwp_drm_lease_connector_v1* pConnectorIn
VkDisplayKHR displayOut
```
Again, this was the result of some iteration and design discussions with other
folks knowledgable in these topics. I owe special thanks to Daniel Stone for
sitting down with me (figuratively, on IRC) and going over ideas for how to
design the Vulkan API. Armed with this specification, I now needed a Vulkan
driver which supported it.
## Implementing the VK extension in Mesa
[Mesa](https://www.mesa3d.org/) is the premier free software graphics suite
powering graphics on Linux and other operating systems. It includes an
implementation of OpenGL and Vulkan for several GPU vendors, and is the home of
the userspace end of AMDGPU, Intel, nouveau, and other graphics drivers. A
specification is nothing without its implementation, so I set out to
implementing this extension for Mesa. In the end, it turned out to be much
simpler than the corresponding X version. This is the complete code for the WSI
part of this feature:
```c
static void drm_lease_handle_lease_fd(
void *data,
struct zwp_drm_lease_v1 *zwp_drm_lease_v1,
int32_t leased_fd)
{
struct wsi_display *wsi = data;
wsi->fd = leased_fd;
}
static void drm_lease_handle_finished(
void *data,
struct zwp_drm_lease_v1 *zwp_drm_lease_v1)
{
struct wsi_display *wsi = data;
if (wsi->fd > 0) {
close(wsi->fd);
wsi->fd = -1;
}
}
static const struct zwp_drm_lease_v1_listener drm_lease_listener = {
drm_lease_handle_lease_fd,
drm_lease_handle_finished,
};
/* VK_EXT_acquire_wl_display */
VkResult
wsi_acquire_wl_display(VkPhysicalDevice physical_device,
struct wsi_device *wsi_device,
struct wl_display *display,
struct zwp_drm_lease_manager_v1 *manager,
int nConnectors,
VkWaylandLeaseConnectorEXT *connectors)
{
struct wsi_display *wsi =
(struct wsi_display *) wsi_device->wsi[VK_ICD_WSI_PLATFORM_DISPLAY];
/* XXX no support for mulitple leases yet */
if (wsi->fd >= 0)
return VK_ERROR_INITIALIZATION_FAILED;
/* XXX no support for mulitple connectors yet */
/* The solution will eventually involve adding a listener to each
* connector, round tripping, and matching EDIDs once the lease is
* granted. */
if (nConnectors > 1)
return VK_ERROR_INITIALIZATION_FAILED;
struct zwp_drm_lease_request_v1 *lease_request =
zwp_drm_lease_manager_v1_create_lease_request(manager);
for (int i = 0; i < nConnectors; ++i) {
zwp_drm_lease_request_v1_request_connector(lease_request,
connectors[i].pConnectorIn);
}
struct zwp_drm_lease_v1 *drm_lease =
zwp_drm_lease_request_v1_submit(lease_request);
zwp_drm_lease_request_v1_destroy(lease_request);
zwp_drm_lease_v1_add_listener(drm_lease, &drm_lease_listener, wsi);
wl_display_roundtrip(display);
if (wsi->fd < 0)
return VK_ERROR_INITIALIZATION_FAILED;
int nconn = 0;
drmModeResPtr res = drmModeGetResources(wsi->fd);
drmModeObjectListPtr lease = drmModeGetLease(wsi->fd);
for (uint32_t i = 0; i < res->count_connectors; ++i) {
for (uint32_t j = 0; j < lease->count; ++j) {
if (res->connectors[i] != lease->objects[j]) {
continue;
}
struct wsi_display_connector *connector =
wsi_display_get_connector(wsi_device, res->connectors[i]);
/* TODO: Match EDID with requested connector */
connectors[nconn].displayOut =
wsi_display_connector_to_handle(connector);
++nconn;
}
}
drmModeFreeResources(res);
return VK_SUCCESS;
}
```
Rigging it up to each driver's WSI shim is pretty straightforward from this
point. I only did it for radv - AMD's Vulkan driver (cause that's the hardware I
was using at the time) - but the rest should be trivial to add. Equipped with a
driver in hand, it's time to make a Real VR Application work on Wayland.
## xrgears
[xrgears](https://gitlab.com/lubosz/xrgears) is another simple demo application
like kmscube - but designed to render a VR scene. It leverages Vulkan and
[OpenHMD](http://www.openhmd.net/) (Open Head Mounted Display) to display this
scene and stick the camera to your head. With the Vulkan extension implemented,
it was a fairly simple matter to [rig up a Wayland backend][xrgears-patch]. The
result:
[xrgears-patch]: https://git.sr.ht/~sircmpwn/xrgears/commit/41ef1d1dfe3e56766d1f8b72b335567eb7842d04
## Xwayland
The final step was to integrate this extension with Xwayland, so that X
applications which took advantage of Keith's work would work via Xwayland. This
ended up being more difficult than I expected for one reason in particular:
modes. Keith's Vulkan extension is designed in two steps:
1. Convert an RandR output into a VkDisplayKHR
2. Acquire a lease for a set of VkDisplayKHRs
Between these steps, you can query the modes (available resolutions and refresh
rates) of the display. However, the Wayland protocol I designed does not let you
query modes until *after* you get the DRM handle, at which point you should
query them through DRM, thus reducing the number of sources of truth and
simplifying things considerably. This is arguably a design misstep in the
original Vulkan extension, but it's shipped in a lot of software and is beyond
fixing. So how do we deal with it?
One way (which was suggested at one point) would be to change the protocol to
include the relevant mode information, so that Xwayland could populate the RandR
modes from it. I found this distasteful, because it was making the protocol more
complex for the sake of a legacy system. Another option would be to make a
second protocol which includes this extra information especially for Xwayland,
but this also seemed like a compromise that compositors would rather not make.
Yet another option would be to have Xwayland request a lease with zero objects
and scan connectors itself, but zero-object leases are not possible.
The option I ended up going with is to have Xwayland open the DRM device itself
and scan connectors there. This is less palatable because (1) we can't be sure
which DRM device is correct, and (2) we can't be sure Xwayland will have
permission to read it. We're still not sure how best to solve this in the long
term. As it stands, this approach is sufficient to get it working in the common
case. The code looks something like this:
```c
static RRModePtr *
xwl_get_rrmodes_from_connector_id(int32_t connector_id, int *nmode, int *npref)
{
drmDevicePtr devices[1];
drmModeConnectorPtr conn;
drmModeModeInfoPtr kmode;
RRModePtr *rrmodes;
int drm;
int pref, i;
*nmode = *npref = 0;
/* TODO: replace with zero-object lease once kernel supports them */
if (drmGetDevices2(DRM_NODE_PRIMARY, devices, 1) < 1
|| !*devices[0]->nodes[0]) {
ErrorF("Failed to enumerate DRM devices");
return NULL;
}
drm = open(devices[0]->nodes[0], O_RDONLY);
drmFreeDevices(devices, 1);
conn = drmModeGetConnector(drm, connector_id);
if (!conn) {
close(drm);
ErrorF("drmModeGetConnector failed");
return NULL;
}
rrmodes = xallocarray(conn->count_modes, sizeof(RRModePtr));
if (!rrmodes) {
close(drm);
ErrorF("Failed to allocate connector modes");
return NULL;
}
/* This spaghetti brought to you courtesey of xf86RandrR12.c
* It adds preferred modes first, then non-preferred modes */
for (pref = 1; pref >= 0; pref--) {
for (i = 0; i < conn->count_modes; ++i) {
kmode = &conn->modes[i];
if ((pref != 0) == ((kmode->type & DRM_MODE_TYPE_PREFERRED) != 0)) {
xRRModeInfo modeInfo;
RRModePtr rrmode;
modeInfo.nameLength = strlen(kmode->name);
modeInfo.width = kmode->hdisplay;
modeInfo.dotClock = kmode->clock * 1000;
modeInfo.hSyncStart = kmode->hsync_start;
modeInfo.hSyncEnd = kmode->hsync_end;
modeInfo.hTotal = kmode->htotal;
modeInfo.hSkew = kmode->hskew;
modeInfo.height = kmode->vdisplay;
modeInfo.vSyncStart = kmode->vsync_start;
modeInfo.vSyncEnd = kmode->vsync_end;
modeInfo.vTotal = kmode->vtotal;
modeInfo.modeFlags = kmode->flags;
rrmode = RRModeGet(&modeInfo, kmode->name);
if (rrmode) {
rrmodes[*nmode] = rrmode;
*nmode = *nmode + 1;
*npref = *npref + pref;
}
}
}
}
close(drm);
return rrmodes;
}
```
A simple update to the Wayland protocol was necessary to add the `CONNECTOR_ID`
atom to the RandR output, which is used by Mesa's Xlib WSI code for acquiring
the display, and was reused here to line up a connector offered by the Wayland
compositor with a connector found in the kernel. The [rest of the
changes][xwayland] were pretty simple, and the result is that SteamVR works,
capping everything off nicely: