logo

drewdevault.com

[mirror] blog and personal website of Drew DeVault git clone https://hacktivis.me/git/mirror/drewdevault.com.git

DRM-leasing-and-VR-for-Wayland.md (20439B)


  1. ---
  2. date: 2019-08-09
  3. title: "DRM leasing: VR for Wayland"
  4. layout: post
  5. tags: [wayland]
  6. ---
  7. As those who read my [status updates](/2019/07/15/Status-update-July-2019.html)
  8. have been aware, recently I've been working on bringing VR to Wayland (and vice
  9. versa). The deepest and most technical part of this work is *DRM leasing*
  10. (Direct Rendering Manager, *not* Digital Restrictions Management), and I think
  11. it'd be good to write in detail about what's involved in this part of the
  12. effort. This work has been sponsored by [Status.im](https://status.im/), as part
  13. of an effort to build a comprehensive Wayland-driven VR workspace. When we got
  14. started, most of the plumbing was missing for VR headsets to be useful on
  15. Wayland, so this has been my focus for a while. The result of this work is
  16. summed up in this crappy handheld video:
  17. <video src="https://yukari.sr.ht/steamvr.webm" controls>
  18. Your web browser does not support the webm video codec. Please consider using
  19. web browsers that support free and open standards.
  20. </video>
  21. Keith Packard, a long time Linux graphics developer, [wrote several blog posts
  22. documenting his work implementing this feature for
  23. X11](https://keithp.com/blogs/DRM-lease/). My journey was somewhat similar,
  24. though thanks to his work I was able to save a lot of time. The rub of this idea
  25. is that the Wayland compositor, the DRM (Direct Rendering Manager) master, can
  26. "lease" some of its resources to a client so they can drive your display
  27. directly. DRM is the kernel subsystem we use for enumerating and setting modes,
  28. allocating pixel buffers, and presenting them in sync with the display's refresh
  29. rate. For a number of reasons, minimizing latency being an important one, VR
  30. applications prefer to do these tasks directly rather than be routed through the
  31. display server like most applications are. The main tasks for implementing this
  32. for Wayland were:
  33. 1. Draft a [protocol extension][wl-ext] for issuing DRM leases
  34. 1. Write implementations for [wlroots][wlr-pr] and [sway][sway-pr]
  35. 1. Get a [simple test client][kmscube] working
  36. 1. Draft a Vulkan extension for leasing via Wayland
  37. 1. Write an implementation for [Mesa's Vulkan WSI implementation][wsi]
  38. 1. Get a more complex [Vulkan test client][xrgears] working
  39. 1. Add support to [Xwayland][xwayland]
  40. [wl-ext]: https://lists.freedesktop.org/archives/wayland-devel/2019-July/040768.html
  41. [wlr-pr]: https://github.com/swaywm/wlroots/pull/1730
  42. [sway-pr]: https://github.com/swaywm/sway/pull/4289
  43. [kmscube]: https://git.sr.ht/~sircmpwn/kmscube
  44. [wsi]: https://gitlab.freedesktop.org/mesa/mesa/merge_requests/1509
  45. [xrgears]: https://git.sr.ht/~sircmpwn/xrgears
  46. [xwayland]: https://gitlab.freedesktop.org/xorg/xserver/merge_requests/248
  47. Let's break down exactly what was necessary for each of these steps.
  48. ## Wayland protocol extension
  49. Writing a protocol extension was the first order of business. There was an
  50. [earlier attempt][original proposal] which petered off in January. I started
  51. with this, by cleaning it up based on my prior experience writing protocols,
  52. normalizing much of the terminology and style, and cleaning up the state
  53. management. After some initial rounds of review, there were some questions to
  54. answer. The most important ones were:
  55. - How do we identify the display? Should we send the EDID, which may be
  56. bigger than the maximum size of a Wayland message?
  57. - Are there security concerns? Could malicious clients read from framebuffers
  58. they weren't given a lease for?
  59. The EDID I ended up sending in a side channel (file descriptor to shared
  60. memory), and the latter was proven to be a non-issue by writing a malicious
  61. client and demonstrating that the kernel rejects its attempts to do evil.
  62. ```xml
  63. <event name="edid">
  64. <description summary="edid">
  65. The compositor may send this event once the connector is created to
  66. provide a file descriptor which may be memory-mapped to read the
  67. connector's EDID, to assist in selecting the correct connectors
  68. for lease. The fd must be mapped with MAP_PRIVATE by the recipient.
  69. Note that not all displays have an EDID, and this event will not be
  70. sent in such cases.
  71. </description>
  72. <arg name="edid" type="fd" summary="EDID file descriptor" />
  73. <arg name="size" type="uint" summary="EDID size, in bytes"/>
  74. </event>
  75. ```
  76. A few more changes would happen to this protocol in the following weeks, but
  77. this was good enough to move on to...
  78. [original proposal]: https://lists.freedesktop.org/archives/wayland-devel/2018-January/036652.html
  79. ## wlroots & sway implementation
  80. After a chat with Scott Anderson (the maintainer of DRM support in wlroots) and
  81. thanks to his timely refactoring efforts, the stage was well set for introducing
  82. this feature to wlroots. I had a good idea of how it would take shape. [Half of
  83. the work][state machine] - the state machine which maintains the server-side
  84. view of the protocol - is well trodden ground and was fairly easy to put
  85. together. Despite being a well-understood problem in the wlroots codebase, these
  86. state machines are always a bit tedious to implement correctly, and I was still
  87. to flushing out bugs well into the remainder of this workstream.
  88. [state machine]: https://github.com/swaywm/wlroots/pull/1730/files#diff-77b17feac8a8af251811a20e5b9bbdd1
  89. The other half of this work was in [the DRM subsystem][drm subsystem]. We
  90. decided that we'd have leased connectors appear "destroyed" to the compositor,
  91. and thus the compositor would have an opportunity to clean it up and stop using
  92. them, similar to the behavior of when an output is hotplugged. Further changes
  93. were necessary to have the DRM internals elegantly carry around some state for
  94. the leased connector and avoid using the connector itself, as well as dealing
  95. with the termination of the lease (either by the client or by the compositor).
  96. With all of this in place, it's a [simple matter][lease issuance] to enumerate
  97. the DRM object IDs for all of the resources we intend to lease and issue the
  98. lease itself.
  99. ```c
  100. int nobjects = 0;
  101. for (int i = 0; i < nconns; ++i) {
  102. struct wlr_drm_connector *conn = conns[i];
  103. assert(conn->state != WLR_DRM_CONN_LEASED);
  104. nobjects += 0
  105. + 1 /* connector */
  106. + 1 /* crtc */
  107. + 1 /* primary plane */
  108. + (conn->crtc->cursor != NULL ? 1 : 0) /* cursor plane */
  109. + conn->crtc->num_overlays; /* overlay planes */
  110. }
  111. if (nobjects <= 0) {
  112. wlr_log(WLR_ERROR, "Attempted DRM lease with <= 0 objects");
  113. return -1;
  114. }
  115. wlr_log(WLR_DEBUG, "Issuing DRM lease with the %d objects:", nobjects);
  116. uint32_t objects[nobjects + 1];
  117. for (int i = 0, j = 0; i < nconns; ++i) {
  118. struct wlr_drm_connector *conn = conns[i];
  119. objects[j++] = conn->id;
  120. objects[j++] = conn->crtc->id;
  121. objects[j++] = conn->crtc->primary->id;
  122. wlr_log(WLR_DEBUG, "connector: %d crtc: %d primary plane: %d",
  123. conn->id, conn->crtc->id, conn->crtc->primary->id);
  124. if (conn->crtc->cursor) {
  125. wlr_log(WLR_DEBUG, "cursor plane: %d", conn->crtc->cursor->id);
  126. objects[j++] = conn->crtc->cursor->id;
  127. }
  128. if (conn->crtc->num_overlays > 0) {
  129. wlr_log(WLR_DEBUG, "+%zd overlay planes:", conn->crtc->num_overlays);
  130. }
  131. for (size_t k = 0; k < conn->crtc->num_overlays; ++k) {
  132. objects[j++] = conn->crtc->overlays[k];
  133. wlr_log(WLR_DEBUG, "\toverlay plane: %d", conn->crtc->overlays[k]);
  134. }
  135. }
  136. int lease_fd = drmModeCreateLease(backend->fd,
  137. objects, nobjects, 0, lessee_id);
  138. if (lease_fd < 0) {
  139. return lease_fd;
  140. }
  141. wlr_log(WLR_DEBUG, "Issued DRM lease %d", *lessee_id);
  142. for (int i = 0; i < nconns; ++i) {
  143. struct wlr_drm_connector *conn = conns[i];
  144. conn->lessee_id = *lessee_id;
  145. conn->crtc->lessee_id = *lessee_id;
  146. conn->state = WLR_DRM_CONN_LEASED;
  147. conn->lease_terminated_cb = lease_terminated_cb;
  148. conn->lease_terminated_data = lease_terminated_data;
  149. wlr_output_destroy(&conn->output);
  150. }
  151. return lease_fd;
  152. ```
  153. [drm subsystem]: https://github.com/swaywm/wlroots/pull/1730/files#diff-8b05a774317ee8e87d51422170f82d4b
  154. [lease issuance]: https://github.com/swaywm/wlroots/pull/1730/files#diff-8b05a774317ee8e87d51422170f82d4bR1601
  155. The [sway implementation][sway-pr] is very simple. I added a note in wlroots
  156. which exposes whether or not an output is considered "non-desktop" (a property
  157. which is set for most VR headsets), then sway just rigs up the lease manager and
  158. offers all non-desktop outputs for lease.
  159. ## kmscube
  160. Testing all of this required the use of a simple test client. During his earlier
  161. work, Keith wrote some patches on top of
  162. [kmscube](https://gitlab.freedesktop.org/mesa/kmscube/), a simple Mesa demo
  163. which renders a spinning cube directly via DRM/KMS/GBM. A [few simple
  164. tweaks][kmscube patch] was suitable to get this working through my protocol
  165. extension, and for the first time I saw something rendered on my headset through
  166. sway!
  167. <video src="https://yukari.sr.ht/vr.webm" controls>
  168. Your web browser does not support the webm video codec. Please consider using
  169. web browsers that support free and open standards.
  170. </video>
  171. [kmscube patch]: https://git.sr.ht/~sircmpwn/kmscube/commit/60d89ef1d9304427a1289174d9a311ab06e39b44
  172. ## Vulkan
  173. Vulkan has a subsystem called WSI - Window System Integration - which handles
  174. the linkage between Vulkan's rendering process and the underlying window system,
  175. such as Wayland, X11, or win32. Keith added an extension to this system called
  176. [VK_EXT_acquire_xlib_display][VK_EXT_acquire_xlib_display], which lives on top
  177. of [VK_EXT_direct_mode_display][VK_EXT_direct_mode_display], a system for
  178. driving displays directly with Vulkan. As the name implies, this system is
  179. especially X11-specific, so I've drafted my own VK extension for Wayland:
  180. VK_EXT_acquire_wl_display. This is the crux of it:
  181. [VK_EXT_acquire_xlib_display]: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VK_EXT_acquire_xlib_display
  182. [VK_EXT_direct_mode_display]: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VK_EXT_direct_mode_display
  183. ```xml
  184. <command successcodes="VK_SUCCESS" errorcodes="VK_ERROR_INITIALIZATION_FAILED">
  185. <proto><type>VkResult</type> <name>vkAcquireWaylandDisplayEXT</name></proto>
  186. <param><type>VkPhysicalDevice</type> <name>physicalDevice</name></param>
  187. <param>struct <type>wl_display</type>* <name>display</name></param>
  188. <param>struct <type>zwp_drm_lease_manager_v1</type>* <name>manager</name></param>
  189. <param><type>int</type> <name>nConnectors</name></param>
  190. <param><type>VkWaylandLeaseConnectorEXT</type>* <name>pConnectors</name></param>
  191. </command>
  192. ```
  193. I chose to leave it up to the user to enumerate the leasable connectors from the
  194. Wayland protocol, then populate these structs with references to the connectors
  195. they want to lease:
  196. ```xml
  197. <type category="struct" name="VkWaylandLeaseConnectorEXT">
  198. <member>struct <type>zwp_drm_lease_connector_v1</type>* <name>pConnectorIn</name></member>
  199. <member><type>VkDisplayKHR</type> <name>displayOut</name></member>
  200. </type>
  201. ```
  202. Again, this was the result of some iteration and design discussions with other
  203. folks knowledgable in these topics. I owe special thanks to Daniel Stone for
  204. sitting down with me (figuratively, on IRC) and going over ideas for how to
  205. design the Vulkan API. Armed with this specification, I now needed a Vulkan
  206. driver which supported it.
  207. ## Implementing the VK extension in Mesa
  208. [Mesa](https://www.mesa3d.org/) is the premier free software graphics suite
  209. powering graphics on Linux and other operating systems. It includes an
  210. implementation of OpenGL and Vulkan for several GPU vendors, and is the home of
  211. the userspace end of AMDGPU, Intel, nouveau, and other graphics drivers. A
  212. specification is nothing without its implementation, so I set out to
  213. implementing this extension for Mesa. In the end, it turned out to be much
  214. simpler than the corresponding X version. This is the complete code for the WSI
  215. part of this feature:
  216. ```c
  217. static void drm_lease_handle_lease_fd(
  218. void *data,
  219. struct zwp_drm_lease_v1 *zwp_drm_lease_v1,
  220. int32_t leased_fd)
  221. {
  222. struct wsi_display *wsi = data;
  223. wsi->fd = leased_fd;
  224. }
  225. static void drm_lease_handle_finished(
  226. void *data,
  227. struct zwp_drm_lease_v1 *zwp_drm_lease_v1)
  228. {
  229. struct wsi_display *wsi = data;
  230. if (wsi->fd > 0) {
  231. close(wsi->fd);
  232. wsi->fd = -1;
  233. }
  234. }
  235. static const struct zwp_drm_lease_v1_listener drm_lease_listener = {
  236. drm_lease_handle_lease_fd,
  237. drm_lease_handle_finished,
  238. };
  239. /* VK_EXT_acquire_wl_display */
  240. VkResult
  241. wsi_acquire_wl_display(VkPhysicalDevice physical_device,
  242. struct wsi_device *wsi_device,
  243. struct wl_display *display,
  244. struct zwp_drm_lease_manager_v1 *manager,
  245. int nConnectors,
  246. VkWaylandLeaseConnectorEXT *connectors)
  247. {
  248. struct wsi_display *wsi =
  249. (struct wsi_display *) wsi_device->wsi[VK_ICD_WSI_PLATFORM_DISPLAY];
  250. /* XXX no support for mulitple leases yet */
  251. if (wsi->fd >= 0)
  252. return VK_ERROR_INITIALIZATION_FAILED;
  253. /* XXX no support for mulitple connectors yet */
  254. /* The solution will eventually involve adding a listener to each
  255. * connector, round tripping, and matching EDIDs once the lease is
  256. * granted. */
  257. if (nConnectors > 1)
  258. return VK_ERROR_INITIALIZATION_FAILED;
  259. struct zwp_drm_lease_request_v1 *lease_request =
  260. zwp_drm_lease_manager_v1_create_lease_request(manager);
  261. for (int i = 0; i < nConnectors; ++i) {
  262. zwp_drm_lease_request_v1_request_connector(lease_request,
  263. connectors[i].pConnectorIn);
  264. }
  265. struct zwp_drm_lease_v1 *drm_lease =
  266. zwp_drm_lease_request_v1_submit(lease_request);
  267. zwp_drm_lease_request_v1_destroy(lease_request);
  268. zwp_drm_lease_v1_add_listener(drm_lease, &drm_lease_listener, wsi);
  269. wl_display_roundtrip(display);
  270. if (wsi->fd < 0)
  271. return VK_ERROR_INITIALIZATION_FAILED;
  272. int nconn = 0;
  273. drmModeResPtr res = drmModeGetResources(wsi->fd);
  274. drmModeObjectListPtr lease = drmModeGetLease(wsi->fd);
  275. for (uint32_t i = 0; i < res->count_connectors; ++i) {
  276. for (uint32_t j = 0; j < lease->count; ++j) {
  277. if (res->connectors[i] != lease->objects[j]) {
  278. continue;
  279. }
  280. struct wsi_display_connector *connector =
  281. wsi_display_get_connector(wsi_device, res->connectors[i]);
  282. /* TODO: Match EDID with requested connector */
  283. connectors[nconn].displayOut =
  284. wsi_display_connector_to_handle(connector);
  285. ++nconn;
  286. }
  287. }
  288. drmModeFreeResources(res);
  289. return VK_SUCCESS;
  290. }
  291. ```
  292. Rigging it up to each driver's WSI shim is pretty straightforward from this
  293. point. I only did it for radv - AMD's Vulkan driver (cause that's the hardware I
  294. was using at the time) - but the rest should be trivial to add. Equipped with a
  295. driver in hand, it's time to make a Real VR Application work on Wayland.
  296. ## xrgears
  297. [xrgears](https://gitlab.com/lubosz/xrgears) is another simple demo application
  298. like kmscube - but designed to render a VR scene. It leverages Vulkan and
  299. [OpenHMD](http://www.openhmd.net/) (Open Head Mounted Display) to display this
  300. scene and stick the camera to your head. With the Vulkan extension implemented,
  301. it was a fairly simple matter to [rig up a Wayland backend][xrgears-patch]. The
  302. result:
  303. [xrgears-patch]: https://git.sr.ht/~sircmpwn/xrgears/commit/41ef1d1dfe3e56766d1f8b72b335567eb7842d04
  304. <video src="https://yukari.sr.ht/xrgears.webm" controls>
  305. Your web browser does not support the webm video codec. Please consider using
  306. web browsers that support free and open standards.
  307. </video>
  308. ## Xwayland
  309. The final step was to integrate this extension with Xwayland, so that X
  310. applications which took advantage of Keith's work would work via Xwayland. This
  311. ended up being more difficult than I expected for one reason in particular:
  312. modes. Keith's Vulkan extension is designed in two steps:
  313. 1. Convert an RandR output into a VkDisplayKHR
  314. 2. Acquire a lease for a set of VkDisplayKHRs
  315. Between these steps, you can query the modes (available resolutions and refresh
  316. rates) of the display. However, the Wayland protocol I designed does not let you
  317. query modes until *after* you get the DRM handle, at which point you should
  318. query them through DRM, thus reducing the number of sources of truth and
  319. simplifying things considerably. This is arguably a design misstep in the
  320. original Vulkan extension, but it's shipped in a lot of software and is beyond
  321. fixing. So how do we deal with it?
  322. One way (which was suggested at one point) would be to change the protocol to
  323. include the relevant mode information, so that Xwayland could populate the RandR
  324. modes from it. I found this distasteful, because it was making the protocol more
  325. complex for the sake of a legacy system. Another option would be to make a
  326. second protocol which includes this extra information especially for Xwayland,
  327. but this also seemed like a compromise that compositors would rather not make.
  328. Yet another option would be to have Xwayland request a lease with zero objects
  329. and scan connectors itself, but zero-object leases are not possible.
  330. The option I ended up going with is to have Xwayland open the DRM device itself
  331. and scan connectors there. This is less palatable because (1) we can't be sure
  332. which DRM device is correct, and (2) we can't be sure Xwayland will have
  333. permission to read it. We're still not sure how best to solve this in the long
  334. term. As it stands, this approach is sufficient to get it working in the common
  335. case. The code looks something like this:
  336. ```c
  337. static RRModePtr *
  338. xwl_get_rrmodes_from_connector_id(int32_t connector_id, int *nmode, int *npref)
  339. {
  340. drmDevicePtr devices[1];
  341. drmModeConnectorPtr conn;
  342. drmModeModeInfoPtr kmode;
  343. RRModePtr *rrmodes;
  344. int drm;
  345. int pref, i;
  346. *nmode = *npref = 0;
  347. /* TODO: replace with zero-object lease once kernel supports them */
  348. if (drmGetDevices2(DRM_NODE_PRIMARY, devices, 1) < 1
  349. || !*devices[0]->nodes[0]) {
  350. ErrorF("Failed to enumerate DRM devices");
  351. return NULL;
  352. }
  353. drm = open(devices[0]->nodes[0], O_RDONLY);
  354. drmFreeDevices(devices, 1);
  355. conn = drmModeGetConnector(drm, connector_id);
  356. if (!conn) {
  357. close(drm);
  358. ErrorF("drmModeGetConnector failed");
  359. return NULL;
  360. }
  361. rrmodes = xallocarray(conn->count_modes, sizeof(RRModePtr));
  362. if (!rrmodes) {
  363. close(drm);
  364. ErrorF("Failed to allocate connector modes");
  365. return NULL;
  366. }
  367. /* This spaghetti brought to you courtesey of xf86RandrR12.c
  368. * It adds preferred modes first, then non-preferred modes */
  369. for (pref = 1; pref >= 0; pref--) {
  370. for (i = 0; i < conn->count_modes; ++i) {
  371. kmode = &conn->modes[i];
  372. if ((pref != 0) == ((kmode->type & DRM_MODE_TYPE_PREFERRED) != 0)) {
  373. xRRModeInfo modeInfo;
  374. RRModePtr rrmode;
  375. modeInfo.nameLength = strlen(kmode->name);
  376. modeInfo.width = kmode->hdisplay;
  377. modeInfo.dotClock = kmode->clock * 1000;
  378. modeInfo.hSyncStart = kmode->hsync_start;
  379. modeInfo.hSyncEnd = kmode->hsync_end;
  380. modeInfo.hTotal = kmode->htotal;
  381. modeInfo.hSkew = kmode->hskew;
  382. modeInfo.height = kmode->vdisplay;
  383. modeInfo.vSyncStart = kmode->vsync_start;
  384. modeInfo.vSyncEnd = kmode->vsync_end;
  385. modeInfo.vTotal = kmode->vtotal;
  386. modeInfo.modeFlags = kmode->flags;
  387. rrmode = RRModeGet(&modeInfo, kmode->name);
  388. if (rrmode) {
  389. rrmodes[*nmode] = rrmode;
  390. *nmode = *nmode + 1;
  391. *npref = *npref + pref;
  392. }
  393. }
  394. }
  395. }
  396. close(drm);
  397. return rrmodes;
  398. }
  399. ```
  400. A simple update to the Wayland protocol was necessary to add the `CONNECTOR_ID`
  401. atom to the RandR output, which is used by Mesa's Xlib WSI code for acquiring
  402. the display, and was reused here to line up a connector offered by the Wayland
  403. compositor with a connector found in the kernel. The [rest of the
  404. changes][xwayland] were pretty simple, and the result is that SteamVR works,
  405. capping everything off nicely:
  406. <video src="https://yukari.sr.ht/steamvr.webm" controls>
  407. Your web browser does not support the webm video codec. Please consider using
  408. web browsers that support free and open standards.
  409. </video>