/* Wayland compositor running on top of an X server.

Copyright (C) 2022 to various contributors.

This file is part of 12to11.

12to11 is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.

12to11 is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
for more details.

You should have received a copy of the GNU General Public License
along with 12to11.  If not, see <https://www.gnu.org/licenses/>.  */

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#include <sys/fcntl.h>

#include <xcb/randr.h>
#include <xcb/dri3.h>

#include <xf86drm.h>

#include "compositor.h"
#include "drm-lease-v1.h"

#if defined DEBUG
#define DebugPrint(format, args...)				\
  fprintf (stderr, "%s: " format "\n", __FUNCTION__, ## args)
#else
#define DebugPrint(fmt, ...) ((void) 0)
#endif

/* DRM Leasing.

   Modern applications can demand direct access to the kernel
   modesetting resources underlying an output.  Wayland exposes this
   functionality via the wp_drm_lease_device_v1 protocol.

   There is some mismatch between the X server terminology and the
   kernel speak used in Wayland and in the KMS/DRM APIs themselves.
   Under X, DRM nodes are named "providers", and DRM connectors are
   named "outputs".  This file uses a mix of both the the X
   terminology and the KMS/DRM terminology.  */

typedef struct _DrmLeaseDevice DrmLeaseDevice;
typedef struct _DrmLeaseDeviceRef DrmLeaseDeviceRef;
typedef struct _DrmLeaseConnector DrmLeaseConnector;
typedef struct _DrmLeaseConnectorRef DrmLeaseConnectorRef;
typedef struct _DrmLeaseConnectorList DrmLeaseConnectorList;
typedef struct _DrmLeaseRequest DrmLeaseRequest;
typedef struct _DrmLease DrmLease;

typedef struct _ProviderOutputTree ProviderOutputTree;

enum
  {
    /* These flags are only used by outputs.  */
    InvalidConnectorID = 1,
    IsDisconnected     = (1 << 2),

    /* These flags are shared by both providers and outputs.  */
    IsMarked           = (1 << 3),
    IsRemoved	       = (1 << 4),

    /* These flags apply to both outputs and output references.  */
    IsWithdrawn        = (1 << 5),
  };

struct _DrmLeaseConnectorRef
{
  /* The next and last references to this connector.  */
  DrmLeaseConnectorRef *next, *last;

  /* The next and last global references to this connector.  */
  DrmLeaseConnectorRef *gcnext, *gclast;

  /* The associated connector.  */
  DrmLeaseConnector *connector;

  /* The resource associated with this connector.  */
  struct wl_resource *resource;

  /* Flags.  */
  int flags;
};

struct _DrmLeaseConnector
{
  /* The output associated with this connector.  */
  RROutput output;

  /* The CRTC associated with this connector.  */
  RRCrtc crtc;

  /* The connector ID and some flags.  */
  int connector_id, flags;

  /* The next and last outputs associated with this device.  */
  DrmLeaseConnector *next, *last;

  /* References to this connector.  */
  DrmLeaseConnectorRef references;

  /* The associated device.  */
  DrmLeaseDevice *device;

  /* The human readable name of this output.  */
  char *name;
};

struct _DrmLeaseDeviceRef
{
  /* The next and last references to this provider.  */
  DrmLeaseDeviceRef *next, *last;

  /* The next and last global references to this provider.  */
  DrmLeaseDeviceRef *gcnext, *gclast;

  /* The referenced device.  */
  DrmLeaseDevice *device;

  /* The wl_resource associated with this reference.  */
  struct wl_resource *resource;
};

struct _DrmLeaseDevice
{
  /* The struct wl_global associated with this provider.  */
  struct wl_global *global;

  /* Any references to this provider.  */
  DrmLeaseDeviceRef references;

  /* The provider associated with this provider.  */
  RRProvider provider;

  /* The next and last devices in this list.  */
  DrmLeaseDevice *next, *last;

  /* The file descriptor of this provider, and some flags.  */
  int fd, flags;

  /* The outputs attached to this provider.  */
  DrmLeaseConnector outputs;
};

struct _DrmLeaseConnectorList
{
  /* The next and last connectors in this list.  */
  DrmLeaseConnectorList *next, *last;

  /* The output.  */
  DrmLeaseConnector *connector;
};

struct _DrmLeaseRequest
{
  /* List of requested outputs.  */
  DrmLeaseConnectorList outputs;

  /* The next and last lease requests.  */
  DrmLeaseRequest *gcnext, *gclast;

  /* The request device.  */
  DrmLeaseDevice *device;

  /* The struct wl_resource associated with this lease request.  */
  struct wl_resource *resource;

  /* The number of outputs requested.  */
  int noutputs;
};

struct _DrmLease
{
  /* The XID of the lease.  */
  xcb_randr_lease_t lease;

  /* The resource of the lease.  */
  struct wl_resource *resource;
};

struct _ProviderOutputTree
{
  /* List of provider IDs.  */
  xcb_randr_provider_t *providers;

  /* List of outputs associated with each of the providers.  */
  xcb_randr_output_t *outputs;

  /* List of output info associated with each of the providers.  */
  xcb_randr_get_output_info_reply_t **output_info;

  /* Number of outputs and crtcs associated with each provider.  */
  int *nconnectors;

  /* Number of providers.  */
  int nproviders;

  /* When the tree data was found.  */
  Time timestamp;
};

/* List of all providers.  */
static DrmLeaseDevice all_devices;

/* List of all provider references.  */
static DrmLeaseDeviceRef all_device_references;

/* List of all connector references.  */
static DrmLeaseConnectorRef all_connector_references;

/* List of all lease requests.  */
static DrmLeaseRequest all_lease_requests;

/* The last time the provider info was updated.  */
static Time last_change_time;

static void
DeleteConnector (DrmLeaseConnector *connector)
{
  /* There should be no more references at this point.  */
  XLAssert (connector->references.next == &connector->references);

  /* Free the name.  */
  XLFree (connector->name);

  DebugPrint ("destroying connector %p (crtc %lu output %lu)",
	      connector, connector->crtc, connector->output);

  /* Unlink the connector.  */
  connector->next->last = connector->last;
  connector->last->next = connector->next;

  /* Free the connector.  */
  XLFree (connector);
  return;
}

static void
DeleteDevice (DrmLeaseDevice *device)
{
  /* There should be no more connectors at this point.  */
  XLAssert (device->outputs.next == &device->outputs);

  /* device->global must be gone as well.  */
  XLAssert (device->global == NULL);

  DebugPrint ("destroying device %p (%lu) w/ fd %d",
	      device, device->provider, device->fd);

  /* Close the fd.  */
  close (device->fd);
}

/* Connector and device "garbage collection".

   Managing the complicated reference cycles between connectors
   resources, outputs, device resources and providers is a tricky
   business.  Every time a resource is destroyed, we mark each
   provider and output referenced from Wayland resources, and if there
   are no more references to a dead provider or output, destroy
   them.  */

static void
CollectDeadResources (void)
{
  DrmLeaseDeviceRef *device_ref;
  DrmLeaseConnectorRef *connector_ref;
  DrmLeaseRequest *request;
  DrmLeaseConnectorList *item;
  DrmLeaseDevice *device, *last_device;
  DrmLeaseConnector *connector, *last_connector;

  DebugPrint ("collecting dead resources");

  /* Mark all provider references.  */
  device_ref = all_device_references.gcnext;
  while (device_ref != &all_device_references)
    {
      /* Mark the device referenced.  */
      device_ref->device->flags |= IsMarked;

      /* Move to the next device.  */
      device_ref = device_ref->gcnext;
    }

  /* Mark all connector references.  */
  connector_ref = all_connector_references.gcnext;
  while (connector_ref != &all_connector_references)
    {
      DebugPrint ("marked via connector: connector %p, device %p (%lu)",
		  connector_ref->connector,
		  connector_ref->connector->device,
		  connector_ref->connector->device->provider);

      /* Mark the connector and device referenced.  */
      connector_ref->connector->flags |= IsMarked;
      connector_ref->connector->device->flags |= IsMarked;

      /* Move to the next connector reference.  */
      connector_ref = connector_ref->gcnext;
    }

  /* Mark all lease requests.  */
  request = all_lease_requests.gcnext;
  while (request != &all_lease_requests)
    {
      /* Mark each referenced connector.  */
      item = request->outputs.next;
      while (item != &request->outputs)
	{
	  DebugPrint ("marked via req: connector %p, device %p (%lu)",
		      item->connector, item->connector->device,
		      item->connector->device->provider);

	  item->connector->flags |= IsMarked;
	  item->connector->device->flags |= IsMarked;

	  item = item->next;
	}

      /* Move to the next lease request.  */
      request = request->gcnext;
    }

  /* Now, judge each device's connectors and then the device
     itself.  */
  device = all_devices.next;
  while (device != &all_devices)
    {
      DebugPrint ("judging device %p", device);

      /* Do the connectors first.  If the device is not marked, then
	 there should be no marked connectors at all, but if it is,
	 then any dead connectors must be removed.  */
      connector = device->outputs.next;
      while (connector != &device->outputs)
	{
	  DebugPrint ("judging connector %p of device %p", connector,
		      connector->device);

	  XLAssert (connector->device == device);

	  if (!(device->flags & IsMarked))
	    XLAssert (!(connector->flags & IsMarked));

	  last_connector = connector;
	  connector = connector->next;

	  /* If the connector is no longer marked and also dead,
	     remove it.  */
	  if (!(last_connector->flags & IsMarked))
	    {
	      DebugPrint ("connector %lu %lu is no longer marked",
			  last_connector->output, last_connector->crtc);

	      if (last_connector->flags & IsRemoved)
		/* Delete the connector.  */
		DeleteConnector (last_connector);
	      else
		/* The connector is still alive.  */
		DebugPrint ("not removing live connector");
	    }
	  else
	    /* Clear the marked flag.  */
	    last_connector->flags &= ~IsMarked;
	}

      last_device = device;
      device = device->next;

      /* Now, consider the device.  */
      if (!(device->flags & IsMarked))
	{
	  DebugPrint ("device %p (%lu) is no longer marked",
		      device, device->provider);

	  if (device->flags & IsRemoved)
	    DeleteDevice (device);
	  else
	    DebugPrint ("not removing live device");
	}
      else
	last_device->flags &= ~IsMarked;
    }
}



static void
Destroy (struct wl_client *client, struct wl_resource *resource)
{
  wl_resource_destroy (resource);
}

static struct wp_drm_lease_connector_v1_interface drm_lease_connector_impl =
  {
    .destroy = Destroy,
  };

static void
HandleConnectorResourceDestroy (struct wl_resource *resource)
{
  DrmLeaseConnectorRef *ref;

  ref = wl_resource_get_user_data (resource);
  ref->last->next = ref->next;
  ref->next->last = ref->last;
  ref->gcnext->gclast = ref->gclast;
  ref->gclast->gcnext = ref->gcnext;

  XLFree (ref);
  CollectDeadResources ();
}



static void
RequestConnector (struct wl_client *client, struct wl_resource *resource,
		  struct wl_resource *connector_resource)
{
  DrmLeaseRequest *request;
  DrmLeaseConnectorRef *ref;
  DrmLeaseConnector *connector;
  DrmLeaseConnectorList *list;

  request = wl_resource_get_user_data (resource);
  ref = wl_resource_get_user_data (connector_resource);
  connector = ref->connector;

  if (connector->device != request->device)
    {
      wl_resource_post_error (resource,
			      WP_DRM_LEASE_REQUEST_V1_ERROR_WRONG_DEVICE,
			      "the specified connector is on a different device");
      return;
    }

#define DuplicateConnector					\
  WP_DRM_LEASE_REQUEST_V1_ERROR_DUPLICATE_CONNECTOR

  /* See if the connector has already been added.  */
  list = request->outputs.next;
  while (list != &request->outputs)
    {
      if (connector == list->connector)
	{
	  wl_resource_post_error (resource, DuplicateConnector,
				  "the same connector got attached twice");
	  return;
	}

      list = list->next;
    }

#undef DuplicateConnector

  DebugPrint ("requesting connector %p", connector);

  /* Insert the connector into the list.  */
  list = XLCalloc (1, sizeof *list);
  list->next = request->outputs.next;
  list->last = &request->outputs;
  list->connector = connector;
  request->outputs.next->last = list;
  request->outputs.next = list;
  request->noutputs++;
}



static void
DestroyLease (struct wl_client *client, struct wl_resource *resource)
{
  wl_resource_destroy (resource);
}

static const struct wp_drm_lease_v1_interface drm_lease_impl =
  {
    .destroy = DestroyLease,
  };

static void
HandleLeaseResourceDestroy (struct wl_resource *resource)
{
  DrmLease *lease;
  xcb_void_cookie_t cookie;
  xcb_generic_error_t *error;

  lease = wl_resource_get_user_data (resource);

  /* Cancel the lease.  */
  if (lease->lease)
    {
      cookie = xcb_randr_free_lease_checked (compositor.conn,
					     lease->lease, 1);
      error = xcb_request_check (compositor.conn, cookie);

      if (error)
	{
	  DebugPrint ("rid: %"PRIu32", minor: %"PRIu16", major: %"PRIu8", "
		      "error: %"PRIu8, error->resource_id, error->minor_code,
		      error->major_code, error->error_code);
	  free (error);
	}
    }

  /* Free the rec.  */
  XLFree (lease);
  CollectDeadResources ();
}

static void
Submit (struct wl_client *client, struct wl_resource *resource, uint32_t id)
{
  DrmLeaseRequest *request;
  xcb_randr_crtc_t *crtcs;
  xcb_randr_output_t *outputs;
  xcb_randr_lease_t lease_id;
  DrmLease *lease;
  DrmLeaseConnectorList *item;
  int i, *fds;
  xcb_randr_create_lease_cookie_t cookie;
  xcb_randr_create_lease_reply_t *reply;
  xcb_generic_error_t *error;

  request = wl_resource_get_user_data (resource);

  /* If the lease request is empty, post that error.  */
  if (request->outputs.next == &request->outputs)
    {
      wl_resource_post_error (resource,
			      WP_DRM_LEASE_REQUEST_V1_ERROR_EMPTY_LEASE,
			      "trying to lease without specifying connectors");
      return;
    }

  lease = XLSafeMalloc (sizeof *lease);
  error = NULL;

  if (!lease)
    {
      wl_resource_post_no_memory (resource);
      return;
    }

  /* Create the lease resource.  */
  memset (lease, 0, sizeof *lease);
  lease->resource = wl_resource_create (client, &wp_drm_lease_v1_interface,
					wl_resource_get_version (resource),
					id);

  if (!lease->resource)
    {
      XLFree (lease);
      wl_resource_post_no_memory (resource);
      return;
    }

  /* Populate crtcs and outputs.  */
  crtcs = alloca (sizeof *crtcs * request->noutputs);
  outputs = alloca (sizeof *outputs * request->noutputs);
  item = request->outputs.next;
  i = 0;
  reply = NULL;

  while (item != &request->outputs)
    {
      if (item->connector->flags & IsRemoved)
	{
	  DebugPrint ("removed connector was used in lease request");

	  /* The connector was removed.  This means it can no longer
	     be leased.  */
	  wl_resource_set_implementation (lease->resource, &drm_lease_impl,
					  lease, HandleLeaseResourceDestroy);

	  /* Send failure and return.  */
	  wp_drm_lease_v1_send_finished (lease->resource);
	  return;
	}

      crtcs[i++] = item->connector->crtc;
      outputs[i - 1] = item->connector->output;

      DebugPrint ("adding output: %u crtc: %u",
		  outputs[i - 1], crtcs[i - 1]);

      item = item->next;
    }

  /* Do the lease.  Generate the resource ID for the lease.  */
  lease_id = xcb_generate_id (compositor.conn);

  /* Now, try to create the lease.  */
  cookie = xcb_randr_create_lease (compositor.conn,
				   DefaultRootWindow (compositor.display),
				   lease_id, request->noutputs,
				   request->noutputs, crtcs, outputs);
  reply = xcb_randr_create_lease_reply (compositor.conn, cookie, &error);

  /* Set the resource implementation now.  */
  wl_resource_set_implementation (lease->resource, &drm_lease_impl,
				  lease, HandleLeaseResourceDestroy);

  if (!reply)
    {
      DebugPrint ("lease failure");

      if (error)
	DebugPrint ("rid: %"PRIu32", minor: %"PRIu16", major: %"PRIu8", "
		    "error: %"PRIu8, error->resource_id, error->minor_code,
		    error->major_code, error->error_code);

      /* Send failure.  */
      wp_drm_lease_v1_send_finished (lease->resource);

      if (error)
	free (error);
    }
  else
    {
      fds = xcb_randr_create_lease_reply_fds (compositor.conn, reply);

      if (!fds)
	/* Obtaining the reply fds failed.  */
	wp_drm_lease_v1_send_finished (lease->resource);
      else
	{
	  /* Send the lease file descriptor.  */
	  wp_drm_lease_v1_send_lease_fd (lease->resource, fds[0]);
	  close (fds[0]);
	}

      /* Set the lease resource.  */
      lease->lease = lease_id;

      /* Free the reply.  */
      free (reply);
    }
}

static struct wp_drm_lease_request_v1_interface drm_lease_request_impl =
  {
    .request_connector = RequestConnector,
    .submit = Submit,
  };

static void
HandleRequestResourceDestroy (struct wl_resource *resource)
{
  DrmLeaseRequest *request;
  DrmLeaseConnectorList *item, *last;

  request = wl_resource_get_user_data (resource);

  /* Free each element of the connector list.  */
  item = request->outputs.next;
  while (item != &request->outputs)
    {
      last = item;
      item = item->next;

      XLFree (last);
    }

  /* Remove the request from the live request list.  */
  request->gclast->gcnext = request->gcnext;
  request->gcnext->gclast = request->gclast;

  /* Free the request itself.  */
  XLFree (request);
}



static void
CreateLeaseRequest (struct wl_client *client, struct wl_resource *resource,
		    uint32_t id)
{
  DrmLeaseRequest *request;
  DrmLeaseDeviceRef *ref;
  DrmLeaseDevice *device;

  request = XLSafeMalloc (sizeof *request);

  if (!request)
    {
      wl_resource_post_no_memory (resource);
      return;
    }

  memset (request, 0, sizeof *request);
  request->resource
    = wl_resource_create (client, &wp_drm_lease_request_v1_interface,
			  wl_resource_get_version (resource), id);

  if (!request->resource)
    {
      XLFree (request);
      return;
    }

  ref = wl_resource_get_user_data (resource);
  device = ref->device;

  /* Initialize the list of DRM connectors.  */
  request->outputs.next = &request->outputs;
  request->outputs.last = &request->outputs;

  /* Set the device.  */
  request->device = device;

  /* Link the device onto the list of references.  */
  request->gcnext = all_lease_requests.gcnext;
  request->gclast = &all_lease_requests;
  all_lease_requests.gcnext->gclast = request;
  all_lease_requests.gcnext = request;

  /* Set the implementation.  */
  wl_resource_set_implementation (request->resource, &drm_lease_request_impl,
				  request, HandleRequestResourceDestroy);
}

static void
Release (struct wl_client *client, struct wl_resource *resource)
{
  /* Release the resource, but not before sending a `released'
     event.  */
  wp_drm_lease_device_v1_send_released (resource);
  wl_resource_destroy (resource);
}

static const struct wp_drm_lease_device_v1_interface drm_lease_device_impl =
  {
    .release = Release,
    .create_lease_request = CreateLeaseRequest,
  };

static void
HandleResourceDestroy (struct wl_resource *resource)
{
  DrmLeaseDeviceRef *ref;

  ref = wl_resource_get_user_data (resource);
  ref->last->next = ref->next;
  ref->next->last = ref->last;
  ref->gcnext->gclast = ref->gclast;
  ref->gclast->gcnext = ref->gcnext;

  XLFree (ref);
  CollectDeadResources ();
}

static DrmLeaseConnectorRef *
AddConnectorRef (DrmLeaseConnector *connector,
		 DrmLeaseDeviceRef *ref)
{
  DrmLeaseConnectorRef *connector_ref;
  struct wl_client *client;

  client = wl_resource_get_client (ref->resource);
  connector_ref = XLCalloc (1, sizeof *connector_ref);
  connector_ref->resource
    = wl_resource_create (client,
			  &wp_drm_lease_connector_v1_interface,
			  wl_resource_get_version (ref->resource),
			  0);

  if (!connector_ref->resource)
    {
      fprintf (stderr, "failed to allocate output ref\n");
      abort ();
    }

  connector_ref->connector = connector;
  connector_ref->next = connector->references.next;
  connector_ref->last = &connector->references;
  connector_ref->gcnext = all_connector_references.gcnext;
  connector_ref->gclast = &all_connector_references;
  all_connector_references.gcnext->gclast = connector_ref;
  all_connector_references.gcnext = connector_ref;
  connector->references.next->last = connector_ref;
  connector->references.next = connector_ref;
  wl_resource_set_implementation (connector_ref->resource,
				  &drm_lease_connector_impl,
				  connector_ref,
				  HandleConnectorResourceDestroy);

  return connector_ref;
}

static void
SendOutputs (DrmLeaseDevice *device, DrmLeaseDeviceRef *ref)
{
  DrmLeaseConnector *connector;
  DrmLeaseConnectorRef *connector_ref;
  char buf[sizeof "xxxxxxxxxx" + 1];

  connector = device->outputs.next;
  while (connector != &device->outputs)
    {
      if (!(connector->flags & IsDisconnected)
	  && !(connector->flags & InvalidConnectorID)
	  && !(connector->flags & IsRemoved))
	{
	  connector_ref = AddConnectorRef (connector, ref);
	  wp_drm_lease_device_v1_send_connector (ref->resource,
						 connector_ref->resource);

	  DebugPrint ("sending connector %lu:%lu to %p",
		      connector->output, connector->crtc, ref);

	  /* Send the connector id.  */
	  wp_drm_lease_connector_v1_send_connector_id (connector_ref->resource,
						       connector->connector_id);

	  /* Send the unique connector name.  */
	  sprintf (buf, "%d", connector->connector_id);
	  wp_drm_lease_connector_v1_send_name (connector_ref->resource, buf);

	  /* Send the connector description.  */
	  wp_drm_lease_connector_v1_send_description (connector_ref->resource,
						      connector->name);

	  /* Send done.  */
	  wp_drm_lease_connector_v1_send_done (connector_ref->resource);
	}

      connector = connector->next;
    }
}

static void
HandleBind (struct wl_client *client, void *data, uint32_t version,
	    uint32_t id)
{
  DrmLeaseDeviceRef *ref;
  DrmLeaseDevice *device;

  ref = XLSafeMalloc (sizeof *ref);
  device = data;

  if (!ref)
    {
      wl_client_post_no_memory (client);
      return;
    }

  memset (ref, 0, sizeof *ref);
  ref->resource = wl_resource_create (client,
				      &wp_drm_lease_device_v1_interface,
				      version, id);

  if (!ref->resource)
    {
      XLFree (ref);
      wl_client_post_no_memory (client);
      return;
    }

  ref->next = device->references.next;
  ref->last = &device->references;
  ref->device = device;
  device->references.next->last = ref;
  device->references.next = ref;
  ref->gcnext = all_device_references.gcnext;
  ref->gclast = &all_device_references;
  all_device_references.gcnext->gclast = ref;
  all_device_references.gcnext = ref;

  wl_resource_set_implementation (ref->resource, &drm_lease_device_impl,
				  ref, HandleResourceDestroy);

  DebugPrint ("sending fd %d to %p", device->fd, ref);

  /* Send the drm_fd to the client.  */
  wp_drm_lease_device_v1_send_drm_fd (ref->resource, device->fd);

  /* Send each output.  */
  SendOutputs (device, ref);

  /* Send done.  */
  wp_drm_lease_device_v1_send_done (ref->resource);
}

static DrmLeaseDevice *
AddProvider (RRProvider provider)
{
  DrmLeaseDevice *device;
  xcb_dri3_open_cookie_t cookie;
  xcb_dri3_open_reply_t *reply;
  int *fds, fd, new;
  xcb_generic_error_t *error;
  char *name;

  error = NULL;

  device = XLCalloc (1, sizeof *device);
  device->references.next = &device->references;
  device->references.last = &device->references;

  /* Add the given provider.  Obtain the file descriptor it is
     associated with.  */
  cookie = xcb_dri3_open (compositor.conn,
			  DefaultRootWindow (compositor.display),
			  provider);
  reply = xcb_dri3_open_reply (compositor.conn, cookie, &error);

  if (!reply)
    goto error;

  fds = xcb_dri3_open_reply_fds (compositor.conn, reply);

  if (!fds)
    {
      free (reply);
      goto error;
    }

  fd = fds[0];

  /* Make the file descriptor FD_CLOEXEC.  */
  XLAddFdFlag (fd, FD_CLOEXEC, True);

  if (drmGetNodeTypeFromFd (fd) != DRM_NODE_RENDER)
    {
      name = drmGetDeviceNameFromFd2 (fd);

      if (name)
	{
	  DebugPrint ("device name is %s", name);
	  new = open (name, O_RDWR | O_CLOEXEC);

	  if (new >= 0)
	    {
	      if (drmIsMaster (fd))
		drmDropMaster (fd);

	      /* Close the old file descriptor.  */
	      close (fd);
	      fd = new;
	    }
	  else
	    DebugPrint ("failed to open device");

	  free (name);
	}
    }

  device->fd = fd;

  DebugPrint ("obtained provider %lu's fd %d", provider, fd);

  free (reply);

  /* Set the provider.  */
  device->provider = provider;

  /* Create the global.  */
  device->global = wl_global_create (compositor.wl_display,
				     &wp_drm_lease_device_v1_interface,
				     1, device, HandleBind);

  /* Chain the provider onto the list of all devices.  */
  device->next = all_devices.next;
  device->last = &all_devices;
  all_devices.next->last = device;
  all_devices.next = device;

  /* Initialize the device's connector list.  */
  device->outputs.next = &device->outputs;
  device->outputs.last = &device->outputs;

  return device;

 error:
  if (error)
    free (error);

  XLFree (device);
  return NULL;
}

static DrmLeaseConnector *
AddOutput (DrmLeaseDevice *device, RROutput output, RRCrtc crtc,
	   XRROutputInfo *info)
{
  DrmLeaseConnector *connector;
  int rc, actual_format;
  Atom actual_type;
  unsigned long nitems, bytes_after;
  unsigned char *data;

  connector = XLCalloc (1, sizeof *connector);
  connector->output = output;
  connector->crtc = crtc;
  connector->name = XLStrdup (info->name);

  /* Try to determine the connector ID.  */
  data = NULL;

  CatchXErrors ();
  rc = XRRGetOutputProperty (compositor.display, output,
			     CONNECTOR_ID, 0, 1, False,
			     False, XA_INTEGER, &actual_type,
			     &actual_format, &nitems, &bytes_after,
			     &data);
  UncatchXErrors (NULL);

  if (rc != Success || !data || actual_format != 32
      || nitems < 1 || actual_type != XA_INTEGER)
    {
      if (data)
	XFree (data);

      /* Mark this connector as invalid.  */
      connector->flags |= InvalidConnectorID;
      DebugPrint ("invalid connector id");
    }
  else
    {
      /* Set the connector ID.  */
      connector->connector_id = *(unsigned long *) data;
      DebugPrint ("connector ID is %d", connector->connector_id);
    }

  if (info->connection == RR_Disconnected)
    connector->flags |= IsDisconnected;

  connector->references.next = &connector->references;
  connector->references.last = &connector->references;

  /* Link the output onto the device's output list.  */
  connector->next = device->outputs.next;
  connector->last = &device->outputs;
  connector->device = device;
  device->outputs.next->last = connector;
  device->outputs.next = connector;

  if (data)
    XFree (data);

  return connector;
}

static void
InitializeProviderOutputs (void)
{
  XRRProviderInfo *info;
  XRRScreenResources *screen_resources;
  Window root;
  DrmLeaseDevice *device;
  int i;
  XRROutputInfo *output;

  root = DefaultRootWindow (compositor.display);
  screen_resources = XRRGetScreenResources (compositor.display,
					    root);

  device = all_devices.next;
  while (device != &all_devices)
    {
      CatchXErrors ();
      info = XRRGetProviderInfo (compositor.display,
				 screen_resources,
				 device->provider);
      UncatchXErrors (NULL);

      DebugPrint ("provider info: %p", info);

      if (!info)
	goto next;

      DebugPrint ("obtained provider info %lu; cap: %u"
		  " ncrtcs: %d noutputs %d",
		  device->provider, info->capabilities,
		  info->ncrtcs, info->noutputs);

      /* Now loop through each output.  */
      for (i = 0; i < info->noutputs; ++i)
	{
	  /* Try to obtain the output info.  */
	  CatchXErrors ();
	  output = XRRGetOutputInfo (compositor.display,
				     screen_resources,
				     info->outputs[i]);
	  UncatchXErrors (NULL);

	  DebugPrint ("obtained output %i %lu %p", i,
		      info->outputs[i], output);

	  if (!output)
	    continue;

	  DebugPrint ("output %s crtc is %lu", output->name,
		      output->crtc);
	  AddOutput (device, info->outputs[i], output->crtc,
		     output);
	  XRRFreeOutputInfo (output);
	}

      XRRFreeProviderInfo (info);
    next:
      device = device->next;
    }

  XRRFreeScreenResources (screen_resources);
}

static void
InitializeProviderList (void)
{
  Window root;
  XRRProviderResources *resources;
  int i;

  root = DefaultRootWindow (compositor.display);
  resources = XRRGetProviderResources (compositor.display, root);

  DebugPrint ("providers: %d", resources->nproviders);

  for (i = 0; i < resources->nproviders; ++i)
    AddProvider (resources->providers[i]);

  XRRFreeProviderResources (resources);

  DebugPrint ("initializing outputs");
  InitializeProviderOutputs ();
}

static ProviderOutputTree *
BuildProviderTree (void)
{
  ProviderOutputTree *tree;
  xcb_randr_get_providers_cookie_t cookie;
  xcb_randr_get_providers_reply_t *reply;
  xcb_randr_get_provider_info_cookie_t *cookies;
  xcb_randr_get_provider_info_reply_t **replies;
  xcb_randr_get_output_info_cookie_t *output_cookies;
  xcb_randr_get_output_info_reply_t **output_replies;
  int i, noutputs, j, num_outputs, k;
  xcb_timestamp_t reply_timestamp;
  Window root;
  xcb_randr_output_t *output_ptr, *outputs;
  xcb_randr_get_output_info_reply_t **output_info_ptr;
  xcb_generic_error_t *error;

  tree = XLCalloc (1, sizeof *tree);
  root = DefaultRootWindow (compositor.display);

  /* Now, query for all providers.  */
  cookie = xcb_randr_get_providers (compositor.conn, root);
  reply = xcb_randr_get_providers_reply (compositor.conn, cookie,
					 NULL);

  if (!reply)
    abort ();

  /* Obtain the providers.  */
  tree->nproviders = xcb_randr_get_providers_providers_length (reply);
  tree->providers = XLCalloc (tree->nproviders, sizeof *tree->providers);
  memcpy (tree->providers, xcb_randr_get_providers_providers (reply),
	  sizeof *tree->providers * tree->nproviders);

  /* Record the timestamp.  */
  reply_timestamp = reply->timestamp;
  tree->timestamp = reply_timestamp;

  /* Free the reply.  */
  free (reply);

  /* Now that we know how many providers there are, look at all the
     outputs for each provider.  */
  cookies = alloca (sizeof *cookies * tree->nproviders);
  replies = alloca (sizeof *replies * tree->nproviders);

  /* Satisfy -Wanalyzer-use-of-uninitialized-value 13 lines below.
     Where is the uninitialized value?  */
  memset (cookies, 0, sizeof *cookies * tree->nproviders);
  memset (replies, 0, sizeof *replies * tree->nproviders);

  for (i = 0; i < tree->nproviders; i++)
    cookies[i] = xcb_randr_get_provider_info (compositor.conn,
					      tree->providers[i],
					      reply_timestamp);
  noutputs = 0;

  for (i = 0; i < tree->nproviders; i++)
    {
      error = NULL;
      replies[i] = xcb_randr_get_provider_info_reply (compositor.conn,
						      cookies[i], &error);
      if (error)
	free (error);

      if (replies[i])
	/* Set the number of outputs.  */
	noutputs += xcb_randr_get_provider_info_outputs_length (replies[i]);
    }

  /* Retrieve the output info for each provider.  It is too hard to
     reason about doing this asychronously across providers, so we
     sync at the end of each processing outputs for each provider
     despite there being no hard data dependency there.  */
  tree->outputs = XLCalloc (noutputs, sizeof *tree->outputs);
  tree->output_info = XLCalloc (noutputs, sizeof *tree->output_info);
  tree->nconnectors = XLCalloc (tree->nproviders,
				sizeof *tree->nconnectors);
  output_ptr = tree->outputs;
  output_info_ptr = tree->output_info;

  for (i = 0, j = 0; i < tree->nproviders; ++i)
    {
      if (!replies[i])
	continue;

      num_outputs = xcb_randr_get_provider_info_outputs_length (replies[i]);

      DebugPrint ("num_outputs: %d", num_outputs);

      outputs = xcb_randr_get_provider_info_outputs (replies[i]);
      output_cookies = alloca (num_outputs * sizeof *output_cookies);
      output_replies = alloca (num_outputs * sizeof *output_replies);

      for (k = 0; k < num_outputs; ++k)
	output_cookies[k] = xcb_randr_get_output_info (compositor.conn,
						       outputs[k],
						       reply_timestamp);

      for (k = 0; k < num_outputs; ++k)
	{
	  error = NULL;
	  output_replies[k]
	    = xcb_randr_get_output_info_reply (compositor.conn,
					       output_cookies[k],
					       &error);

	  if (error)
	    free (error);

	  if (!output_replies[k])
	    continue;

	  tree->nconnectors[j] += 1;
	  DebugPrint ("nconnectors[%d] became: %d",
		      j, tree->nconnectors[j]);

	  /* Record the output and output info.  */
	  XLAssert (output_ptr < tree->outputs + noutputs);

	  *output_ptr++ = outputs[k];
	  *output_info_ptr++ = output_replies[k];
	}

      /* Free the provider info.  */
      free (replies[i]);

      j++;
    }

  /* Return the resulting tree.  */
  return tree;
}

static void
FreeProviderTree (ProviderOutputTree *tree)
{
  int i, j, k;

  XLFree (tree->providers);
  XLFree (tree->outputs);

  /* Free all output info.  */
  for (i = 0, j = 0; i < tree->nproviders; ++i)
    {
      for (k = 0; k < tree->nconnectors[i]; ++k)
	free (tree->output_info[j + k]);
      j += k;
    }

  XLFree (tree->output_info);
  XLFree (tree->nconnectors);
  XLFree (tree);
}

static DrmLeaseDevice *
FindProvider (RRProvider id)
{
  DrmLeaseDevice *device;

  device = all_devices.next;
  while (device != &all_devices)
    {
      if (!(device->flags & IsRemoved)
	  && device->provider == id)
	return device;
    }

  return NULL;
}

static void
RemoveDevice (DrmLeaseDevice *device)
{
  /* Mark the device as invalid and free its fd and global.  The
     device itself will be destroyed once no more references to it
     exist from clients.  */
  device->flags |= IsRemoved;
  close (device->fd);
  wl_global_destroy (device->global);
}

static void
RemoveConnector (DrmLeaseConnector *connector)
{
  DrmLeaseConnectorRef *ref;

  /* Mark the output as removed.  */
  connector->flags |= IsRemoved | IsWithdrawn;

  /* Withdraw each of the references.  */
  ref = connector->references.next;
  while (ref != &connector->references)
    {
      if (!(ref->flags & IsWithdrawn))
	wp_drm_lease_connector_v1_send_withdrawn (ref->resource);

      ref->flags |= IsWithdrawn;
      ref = ref->next;
    }
}

static void
WithdrawConnector (DrmLeaseConnector *connector)
{
  DrmLeaseConnectorRef *ref;

  if (connector->flags & IsWithdrawn)
    return;

  connector->flags |= IsWithdrawn;

  /* Withdraw each of the references.  */
  ref = connector->references.next;
  while (ref != &connector->references)
    {
      if (!(ref->flags & IsWithdrawn))
	wp_drm_lease_connector_v1_send_withdrawn (ref->resource);

      ref->flags |= IsWithdrawn;
      ref = ref->next;
    }
}

static void
SendConnectorToClients (DrmLeaseConnector *connector)
{
  DrmLeaseConnectorRef *ref;
  DrmLeaseDeviceRef *device_ref;
  char buf[sizeof "xxxxxxxxxx" + 1];

  XLAssert (!(connector->flags & IsRemoved));

  connector->flags &= ~IsWithdrawn;

  device_ref = connector->device->references.next;
  while (device_ref != &connector->device->references)
    {
      ref = AddConnectorRef (connector, device_ref);

      wp_drm_lease_device_v1_send_connector (device_ref->resource,
					     ref->resource);

      /* Send the connector id.  */
      wp_drm_lease_connector_v1_send_connector_id (ref->resource,
						   connector->connector_id);

      /* Send the unique connector name.  */
      sprintf (buf, "%d", connector->connector_id);
      wp_drm_lease_connector_v1_send_name (ref->resource, buf);

      /* Send the connector description.  */
      wp_drm_lease_connector_v1_send_description (ref->resource,
						  connector->name);

      /* Send done.  */
      wp_drm_lease_connector_v1_send_done (ref->resource);

      device_ref = device_ref->next;
    }
}

static DrmLeaseConnector *
FindOutput (DrmLeaseDevice *device, RROutput id)
{
  DrmLeaseConnector *connector;

  connector = device->outputs.next;
  while (connector != &device->outputs)
    {
      if (connector->output == id)
	return connector;

      connector = connector->next;
    }

  return NULL;
}

static void
HandleSingleProvider (ProviderOutputTree *tree, int index,
		      int connector_offset)
{
  RRProvider provider;
  xcb_randr_output_t *outputs;
  xcb_randr_get_output_info_reply_t **info;
  DrmLeaseDevice *device;
  int i, name_length;
  XRROutputInfo outputinfo;
  DrmLeaseConnector *connector;
  DrmLeaseDeviceRef *ref;

  provider = tree->providers[index];
  outputs = tree->outputs + connector_offset;
  info = tree->output_info + connector_offset;

  /* Try to find an existing provider.  */
  device = FindProvider (provider);

  /* If there is no existing provider, then add the new device.  */
  if (!device)
    {
      DebugPrint ("adding provider for provider %lu", provider);

      device = AddProvider (provider);

      /* Add all the outputs.  */
      for (i = 0; i < tree->nconnectors[index]; ++i)
	{
	  name_length = xcb_randr_get_output_info_name_length (*info);

	  outputinfo.connection = (*info)->connection;
	  outputinfo.name = XLMalloc (name_length + 1);
	  memcpy (outputinfo.name,
		  xcb_randr_get_output_info_name (*info),
		  name_length);
	  outputinfo.name[name_length] = '\0';

	  DebugPrint ("adding output named %s", outputinfo.name);

	  /* It seems a little wrong to use a fake output info
	     structure.  */
	  AddOutput (device, *outputs, (*info)->crtc, &outputinfo);

	  XLFree (outputinfo.name);
	  outputs++;
	  info++;
	}
    }
  else
    {
      DebugPrint ("provider %p found", device);

      /* Otherwise, compare the outputs of the provider with what is
	 currently present.  First, remove each output that is not
	 still present.  */

      connector = device->outputs.next;
      while (connector != &device->outputs)
	{
	  if (connector->flags & IsRemoved)
	    continue;

	  for (i = 0; i < tree->nconnectors[index]; ++i)
	    {
	      DebugPrint ("consideration: %p %"PRIu32" %lu", connector,
			  outputs[i], connector->output);

	      if (outputs[i] == connector->output)
		goto next;
	    }

	  DebugPrint ("removing connector %p", connector);

	  /* Remove the connector.  */
	  RemoveConnector (connector);

	next:
	  connector = connector->next;
	}

      /* Next, look through each output.  */
      for (i = 0; i < tree->nconnectors[index]; ++i)
	{
	  connector = FindOutput (device, outputs[i]);

	  if (!connector)
	    {
	      /* If the connector does not exist, add it.  */
	      name_length = xcb_randr_get_output_info_name_length (info[i]);

	      outputinfo.connection = info[i]->connection;
	      outputinfo.name = XLMalloc (name_length + 1);
	      memcpy (outputinfo.name,
		      xcb_randr_get_output_info_name (info[i]),
		      name_length);
	      outputinfo.name[name_length] = '\0';

	      /* It seems a little wrong to use a fake output info
		 structure.  */
	      connector = AddOutput (device, outputs[i], info[i]->crtc,
				     &outputinfo);
	      SendConnectorToClients (connector);
	      DebugPrint ("added output named %s", outputinfo.name);

	      XLFree (outputinfo.name);
	      continue;
	    }

	  DebugPrint ("updating existing connector %p", connector);

	  /* Otherwise, the connector already exists.  Compare it with
	     the new connector info and see what changed.  */
	  if (connector->flags & IsDisconnected
	      && info[i]->connection != XCB_RANDR_CONNECTION_DISCONNECTED)
	    {
	      /* The connector was previously disconnected, but not
		 anymore.  Send the connector to clients.  */
	      SendConnectorToClients (connector);

	      /* Update the flag.  */
	      connector->flags &= ~IsDisconnected;

	      DebugPrint ("output named %s was connected", connector->name);
	    }
	  else if (!(connector->flags & IsDisconnected)
		   && info[i]->connection == XCB_RANDR_CONNECTION_DISCONNECTED)
	    {
	      /* The connector was just disconnected.  Withdraw the
		 connector.  */
	      WithdrawConnector (connector);

	      /* Update the flag.  */
	      connector->flags |= IsDisconnected;

	      DebugPrint ("output named %s disconnected", connector->name);
	    }

	  /* Set the crtc.  */
	  connector->crtc = info[i]->crtc;
	}
    }

  /* Now send done to each device.  */
  ref = device->references.next;
  while (ref != &device->references)
    {
      wp_drm_lease_device_v1_send_done (ref->resource);
      ref = ref->next;
    }
}

static void
HandleOutputOrResourceChange (Time timestamp)
{
  ProviderOutputTree *tree;
  int i, connectors_read;
  DrmLeaseDevice *device;

  DebugPrint ("timestamp: %lu, last-change-time: %lu", timestamp,
	      last_change_time);

  if (timestamp != CurrentTime
      && (timestamp - last_change_time) <= 0
      /* If timestamp is 500 ms later, assume that the time
	 overflowed.  */
      && (timestamp - last_change_time) > -500)
    {
      DebugPrint ("rejecting outdated event");
      return;
    }

  /* Outputs or resources changed.  First, build a "provider-output
     tree" structure.  */
  tree = BuildProviderTree ();

  DebugPrint ("provider tree obtained with %d providers",
	      tree->nproviders);

  /* Afterwards, mark every provider that is no longer present as
     removed.  */
  device = all_devices.next;
  while (device != &all_devices)
    {
      if (device->flags & IsRemoved)
	goto next_device;

      for (i = 0; i < tree->nproviders; ++i)
	{
	  if (tree->providers[i] == device->provider)
	    goto next_device;
	}

      DebugPrint ("device %p was not found in tree",
		  device);

      /* Remove the device.  */
      RemoveDevice (device);

    next_device:
      device = device->next;
    }

  /* Next, compare each provider in the tree with the currently
     attached devices.  */

  connectors_read = 0;
  for (i = 0; i < tree->nproviders; ++i)
    {
      HandleSingleProvider (tree, i, connectors_read);
      connectors_read += tree->nconnectors[i];
    }

  /* Set the last change time.  */
  last_change_time = MAX (tree->timestamp, timestamp);

  /* Finally, free the provider tree.  */
  FreeProviderTree (tree);

  /* And collect dead resources.  */
  CollectDeadResources ();
}

void
XLInitDrmLease (void)
{
  xcb_randr_query_version_reply_t *reply;
  xcb_randr_query_version_cookie_t cookie;

  /* This shouldn't be freed.  */
  const xcb_query_extension_reply_t *ext;

  /* Initialize XRandR with XCB as well.  Version 1.6 of the extension
     must be available.  */
  ext = xcb_get_extension_data (compositor.conn, &xcb_randr_id);

  if (!ext || !ext->present)
    /* DRM leasing will not be supported.  */
    return;


  cookie = xcb_randr_query_version (compositor.conn, 1, 6);
  reply = xcb_randr_query_version_reply (compositor.conn,
					 cookie, NULL);

  if (!reply)
    return;

  if (reply->major_version < 1
      || (reply->major_version == 1
	  && reply->minor_version < 6))
    {
      free (reply);
      return;
    }

  /* Free the reply.  */
  free (reply);

  all_devices.next = &all_devices;
  all_devices.last = &all_devices;
  all_device_references.gclast = &all_device_references;
  all_device_references.gcnext = &all_device_references;
  all_connector_references.gcnext = &all_connector_references;
  all_connector_references.gclast = &all_connector_references;
  all_lease_requests.gcnext = &all_lease_requests;
  all_lease_requests.gclast = &all_lease_requests;

  /* Initialize the provider list.  */
  InitializeProviderList ();

  /* Add a hook that runs upon notification.  */
  XLOutputSetChangeFunction (HandleOutputOrResourceChange);
}
