diff --git a/bin/system-info.c b/bin/system-info.c
index b426b28318c49a2b5091f75b61a6a1c9c556c96e..bf2debeea83965acc59123b9dfb34d08f320c965 100644
--- a/bin/system-info.c
+++ b/bin/system-info.c
@@ -451,7 +451,9 @@ main (int argc,
   gchar *inst_path = NULL;
   gchar *rt_path = NULL;
   int opt;
-  static const char * const multiarch_tuples[] = { SRT_ABI_I386, SRT_ABI_X86_64 };
+  static const char * const multiarch_tuples[] = { SRT_ABI_I386, SRT_ABI_X86_64, NULL };
+  GList *icds;
+  const GList *icd_iter;
 
   while ((opt = getopt_long (argc, argv, "", long_options, NULL)) != -1)
     {
@@ -528,7 +530,9 @@ main (int argc,
   json_builder_set_member_name (builder, "architectures");
   json_builder_begin_object (builder);
 
-  for (gsize i = 0; i < G_N_ELEMENTS (multiarch_tuples); i++)
+  g_assert (multiarch_tuples[G_N_ELEMENTS (multiarch_tuples) - 1] == NULL);
+
+  for (gsize i = 0; i < G_N_ELEMENTS (multiarch_tuples) - 1; i++)
     {
       GList *libraries = NULL;
 
@@ -615,6 +619,111 @@ main (int argc,
 
   json_builder_end_object (builder);
 
+  json_builder_set_member_name (builder, "egl");
+  json_builder_begin_object (builder);
+  json_builder_set_member_name (builder, "icds");
+  json_builder_begin_array (builder);
+  icds = srt_system_info_list_egl_icds (info, multiarch_tuples);
+
+  for (icd_iter = icds; icd_iter != NULL; icd_iter = icd_iter->next)
+    {
+      json_builder_begin_object (builder);
+      json_builder_set_member_name (builder, "json_path");
+      json_builder_add_string_value (builder,
+                                     srt_egl_icd_get_json_path (icd_iter->data));
+
+      if (srt_egl_icd_check_error (icd_iter->data, &error))
+        {
+          const gchar *library;
+          gchar *tmp;
+
+          library = srt_egl_icd_get_library_path (icd_iter->data);
+          json_builder_set_member_name (builder, "library_path");
+          json_builder_add_string_value (builder, library);
+
+          tmp = srt_egl_icd_resolve_library_path (icd_iter->data);
+
+          if (g_strcmp0 (library, tmp) != 0)
+            {
+              json_builder_set_member_name (builder, "dlopen");
+              json_builder_add_string_value (builder, tmp);
+            }
+
+          g_free (tmp);
+        }
+      else
+        {
+          json_builder_set_member_name (builder, "error-domain");
+          json_builder_add_string_value (builder,
+                                         g_quark_to_string (error->domain));
+          json_builder_set_member_name (builder, "error-code");
+          json_builder_add_int_value (builder, error->code);
+          json_builder_set_member_name (builder, "error");
+          json_builder_add_string_value (builder, error->message);
+          g_clear_error (&error);
+        }
+
+      json_builder_end_object (builder);
+    }
+
+  g_list_free_full (icds, g_object_unref);
+  json_builder_end_array (builder);   // egl.icds
+  json_builder_end_object (builder);  // egl
+
+  json_builder_set_member_name (builder, "vulkan");
+  json_builder_begin_object (builder);
+  json_builder_set_member_name (builder, "icds");
+  json_builder_begin_array (builder);
+  icds = srt_system_info_list_vulkan_icds (info, multiarch_tuples);
+
+  for (icd_iter = icds; icd_iter != NULL; icd_iter = icd_iter->next)
+    {
+      json_builder_begin_object (builder);
+      json_builder_set_member_name (builder, "json_path");
+      json_builder_add_string_value (builder,
+                                     srt_vulkan_icd_get_json_path (icd_iter->data));
+
+      if (srt_vulkan_icd_check_error (icd_iter->data, &error))
+        {
+          const gchar *library;
+          gchar *tmp;
+
+          library = srt_vulkan_icd_get_library_path (icd_iter->data);
+          json_builder_set_member_name (builder, "library_path");
+          json_builder_add_string_value (builder, library);
+          json_builder_set_member_name (builder, "api_version");
+          json_builder_add_string_value (builder,
+                                         srt_vulkan_icd_get_api_version (icd_iter->data));
+
+          tmp = srt_vulkan_icd_resolve_library_path (icd_iter->data);
+
+          if (g_strcmp0 (library, tmp) != 0)
+            {
+              json_builder_set_member_name (builder, "dlopen");
+              json_builder_add_string_value (builder, tmp);
+            }
+
+          g_free (tmp);
+        }
+      else
+        {
+          json_builder_set_member_name (builder, "error-domain");
+          json_builder_add_string_value (builder,
+                                         g_quark_to_string (error->domain));
+          json_builder_set_member_name (builder, "error-code");
+          json_builder_add_int_value (builder, error->code);
+          json_builder_set_member_name (builder, "error");
+          json_builder_add_string_value (builder, error->message);
+          g_clear_error (&error);
+        }
+
+      json_builder_end_object (builder);
+    }
+
+  g_list_free_full (icds, g_object_unref);
+  json_builder_end_array (builder);   // vulkan.icds
+  json_builder_end_object (builder);  // vulkan
+
   json_builder_end_object (builder); // End global object
 
   JsonNode *root = json_builder_get_root (builder);
diff --git a/bin/system-info.md b/bin/system-info.md
index 887c29a680b8c73404f90e71489fe51c21c5bb82..dd3d4f22a095050aa58b7cb2fd31cf514cfa96b9 100644
--- a/bin/system-info.md
+++ b/bin/system-info.md
@@ -294,6 +294,53 @@ keys:
     **error**
     :   A human-readable error message.
 
+**egl**
+:   An object describing EGL support. Currently the only key is
+    **icds**. Its value is an array of objects describing ICDs,
+    with the following keys and values if the metadata was loaded
+    successfully:
+
+    **json_path**
+    :   Absolute path to the JSON file describing the ICD
+
+    **library_path**
+    :   The library as described in the JSON file: either an absolute
+        path, a path relative to the JSON file containing at least one **/**,
+        or a bare library name to be loaded from the system default library
+        search path.
+
+    **dlopen**
+    :   The name to pass to **dlopen**(3), if it differs from
+        **library_path**. This key/value pair is omitted if it would
+        be the same as **library_path**. Currently, it is only shown
+        if the **library_path** is relative, in which case **dlopen**
+        is an absolute path formed by prefixing the directory part
+        of **json_path** to **library_path**.
+
+    or the following keys and values if the metadata failed to load:
+
+    **json_path**
+    :   Absolute path to the JSON file describing the ICD
+
+    **error-domain**
+    :   A machine-readable string indicating a category of errors.
+
+    **error-code**
+    :   A small integer indicating a specific error. Its meaning depends
+        on the **error-domain**.
+
+    **error**
+    :   A human-readable error message.
+
+**vulkan**
+:   An object describing Vulkan support. Currently the only key is
+    **icds**. Its value is an array of objects describing ICDs,
+    with the same keys and values as for EGL ICDs, plus one extra key:
+
+    **api_version**
+    :   Vulkan API version implemented by this ICD as a dotted-decimal
+        string, for example **1.1.90**
+
 # EXIT STATUS
 
 0
diff --git a/steam-runtime-tools/graphics-internal.h b/steam-runtime-tools/graphics-internal.h
index 28eb39d4572f4d59d2efcb8bc687c713d7bc9665..0bcaf05023652fe45c3b6d341096912d1ebb17f0 100644
--- a/steam-runtime-tools/graphics-internal.h
+++ b/steam-runtime-tools/graphics-internal.h
@@ -128,3 +128,7 @@ _srt_graphics_rendering_interface_string (SrtRenderingInterface rendering_interf
 
 #endif
 
+GList *_srt_load_egl_icds (gchar **envp,
+                           const char * const *multiarch_tuples);
+GList *_srt_load_vulkan_icds (gchar **envp,
+                              const char * const *multiarch_tuples);
diff --git a/steam-runtime-tools/graphics.c b/steam-runtime-tools/graphics.c
index 90f1a7d8165147f3ab82294f21671ba6d101f6a2..3aeaa10054ec98dc53f05f363a78c63a854dd685 100644
--- a/steam-runtime-tools/graphics.c
+++ b/steam-runtime-tools/graphics.c
@@ -27,6 +27,7 @@
 
 #include "steam-runtime-tools/architecture.h"
 #include "steam-runtime-tools/enums.h"
+#include "steam-runtime-tools/glib-compat.h"
 #include "steam-runtime-tools/graphics-internal.h"
 #include "steam-runtime-tools/utils.h"
 #include "steam-runtime-tools/utils-internal.h"
@@ -50,6 +51,16 @@
  * #SrtGraphics is an opaque object representing a graphics capabilities.
  * This is a reference-counted object: use g_object_ref() and
  * g_object_unref() to manage its lifecycle.
+ *
+ * #SrtEglIcd is an opaque object representing the metadata describing
+ * an EGL ICD.
+ * This is a reference-counted object: use g_object_ref() and
+ * g_object_unref() to manage its lifecycle.
+ *
+ * #SrtVulkanIcd is an opaque object representing the metadata describing
+ * a Vulkan ICD.
+ * This is a reference-counted object: use g_object_ref() and
+ * g_object_unref() to manage its lifecycle.
  */
 
 struct _SrtGraphics
@@ -743,3 +754,1653 @@ srt_graphics_get_messages (SrtGraphics *self)
   g_return_val_if_fail (SRT_IS_GRAPHICS (self), NULL);
   return self->messages;
 }
+
+/* EGL and Vulkan ICDs are actually basically the same, but we don't
+ * hard-code that in the API. */
+typedef struct
+{
+  GError *error;
+  gchar *api_version;   /* Always NULL when found in a SrtEglIcd */
+  gchar *json_path;
+  gchar *library_path;
+} SrtIcd;
+
+static void
+srt_icd_clear (SrtIcd *self)
+{
+  g_clear_error (&self->error);
+  g_clear_pointer (&self->api_version, g_free);
+  g_clear_pointer (&self->json_path, g_free);
+  g_clear_pointer (&self->library_path, g_free);
+}
+
+/*
+ * See srt_egl_icd_resolve_library_path(),
+ * srt_vulkan_icd_resolve_library_path()
+ */
+static gchar *
+srt_icd_resolve_library_path (const SrtIcd *self)
+{
+  gchar *dir;
+  gchar *ret;
+
+  /*
+   * In Vulkan, this function behaves according to the specification:
+   *
+   * The "library_path" specifies either a filename, a relative pathname,
+   * or a full pathname to an ICD shared library file. If "library_path"
+   * specifies a relative pathname, it is relative to the path of the
+   * JSON manifest file. If "library_path" specifies a filename, the
+   * library must live in the system's shared object search path.
+   * — https://github.com/KhronosGroup/Vulkan-Loader/blob/master/loader/LoaderAndLayerInterface.md#icd-manifest-file-format
+   *
+   * In GLVND, EGL ICDs with relative pathnames are currently passed
+   * directly to dlopen(), which will interpret them as relative to
+   * the current working directory - but upstream acknowledge in
+   * https://github.com/NVIDIA/libglvnd/issues/187 that this is not
+   * actually very useful, and have indicated that they would consider
+   * a patch to give it the same behaviour as Vulkan instead.
+   */
+
+  if (self->library_path == NULL)
+    return NULL;
+
+  if (self->library_path[0] == '/')
+    return g_strdup (self->library_path);
+
+  if (strchr (self->library_path, '/') == NULL)
+    return g_strdup (self->library_path);
+
+  dir = g_path_get_dirname (self->json_path);
+  ret = g_build_filename (dir, self->library_path, NULL);
+  g_free (dir);
+  g_return_val_if_fail (g_path_is_absolute (ret), ret);
+  return ret;
+}
+
+/* See srt_egl_icd_check_error(), srt_vulkan_icd_check_error() */
+static gboolean
+srt_icd_check_error (const SrtIcd *self,
+                     GError **error)
+{
+  if (self->error != NULL && error != NULL)
+    *error = g_error_copy (self->error);
+
+  return (self->error == NULL);
+}
+
+/* See srt_egl_icd_write_to_file(), srt_vulkan_icd_write_to_file() */
+static gboolean
+srt_icd_write_to_file (const SrtIcd *self,
+                       const char *path,
+                       GError **error)
+{
+  JsonBuilder *builder;
+  JsonGenerator *generator;
+  JsonNode *root;
+  gchar *json_output;
+  gboolean ret = FALSE;
+
+  if (!srt_icd_check_error (self, error))
+    {
+      g_prefix_error (error,
+                      "Cannot save ICD metadata to file because it is invalid: ");
+      return FALSE;
+    }
+
+  builder = json_builder_new ();
+  json_builder_begin_object (builder);
+  {
+    json_builder_set_member_name (builder, "file_format_version");
+    /* We parse and store all the information defined in file format
+     * version 1.0.0, but nothing beyond that, so we use this version
+     * in our output instead of quoting whatever was in the input.
+     *
+     * We don't currently need to distinguish between EGL and Vulkan here
+     * because the file format version we understand happens to be the
+     * same for both. */
+    json_builder_add_string_value (builder, "1.0.0");
+
+    json_builder_set_member_name (builder, "ICD");
+    json_builder_begin_object (builder);
+    {
+      json_builder_set_member_name (builder, "library_path");
+      json_builder_add_string_value (builder, self->library_path);
+
+      /* In the EGL case this will be NULL. In the Vulkan case it will
+       * be non-NULL, because if the API version was missing, we would
+       * have set the error indicator, so we wouldn't get here. */
+      if (self->api_version != NULL)
+        {
+          json_builder_set_member_name (builder, "api_version");
+          json_builder_add_string_value (builder, self->api_version);
+        }
+    }
+    json_builder_end_object (builder);
+  }
+  json_builder_end_object (builder);
+
+  root = json_builder_get_root (builder);
+  generator = json_generator_new ();
+  json_generator_set_root (generator, root);
+  json_generator_set_pretty (generator, TRUE);
+  json_output = json_generator_to_data (generator, NULL);
+
+  ret = g_file_set_contents (path, json_output, -1, error);
+
+  if (!ret)
+    g_prefix_error (error,
+                    "Cannot save ICD metadata to file :");
+
+  g_free (json_output);
+  g_object_unref (generator);
+  json_node_free (root);
+  g_object_unref (builder);
+  return ret;
+}
+
+/*
+ * indirect_strcmp0:
+ * @left: A non-%NULL pointer to a (possibly %NULL) `const char *`
+ * @right: A non-%NULL pointer to a (possibly %NULL) `const char *`
+ *
+ * A #GCompareFunc to sort pointers to strings in lexicographic
+ * (g_strcmp0()) order.
+ *
+ * Returns: An integer < 0 if left < right, > 0 if left > right,
+ *  or 0 if left == right or if they are not comparable
+ */
+static int
+indirect_strcmp0 (gconstpointer left,
+                  gconstpointer right)
+{
+  const gchar * const *l = left;
+  const gchar * const *r = right;
+
+  g_return_val_if_fail (l != NULL, 0);
+  g_return_val_if_fail (r != NULL, 0);
+  return g_strcmp0 (*l, *r);
+}
+
+/*
+ * A #GCompareFunc that does not sort the members of the directory.
+ */
+#define READDIR_ORDER ((GCompareFunc) NULL)
+
+/*
+ * load_json_dir:
+ * @sysroot: (nullable): Interpret directory names as being inside this
+ *  sysroot, mainly for unit testing
+ * @dir: A directory to search
+ * @suffix: (nullable): A path to append to @dir, such as `"vulkan/icd.d"`
+ * @sort: (nullable): If not %NULL, load ICDs sorted by filename in this order
+ * @load_json_cb: Called for each potential ICD found
+ * @user_data: Passed to @load_json_cb
+ */
+static void
+load_json_dir (const char *sysroot,
+               const char *dir,
+               const char *suffix,
+               GCompareFunc sort,
+               void (*load_json_cb) (const char *, const char *, void *),
+               void *user_data)
+{
+  GError *error = NULL;
+  GDir *dir_iter = NULL;
+  gchar *sysrooted_dir = NULL;
+  gchar *suffixed_dir = NULL;
+  const char *iter_dir;
+  const char *member;
+  GPtrArray *members;
+  gsize i;
+
+  g_return_if_fail (load_json_cb != NULL);
+
+  if (dir == NULL)
+    return;
+
+  if (suffix != NULL)
+    {
+      suffixed_dir = g_build_filename (dir, suffix, NULL);
+      dir = suffixed_dir;
+    }
+
+  iter_dir = dir;
+
+  if (sysroot != NULL)
+    {
+      sysrooted_dir = g_build_filename (sysroot, dir, NULL);
+      iter_dir = sysrooted_dir;
+    }
+
+  g_debug ("Looking for ICDs in %s...", dir);
+
+  dir_iter = g_dir_open (iter_dir, 0, &error);
+
+  if (dir_iter == NULL)
+    {
+      g_debug ("Failed to open \"%s\": %s", iter_dir, error->message);
+      goto out;
+    }
+
+  members = g_ptr_array_new_with_free_func (g_free);
+
+  while ((member = g_dir_read_name (dir_iter)) != NULL)
+    {
+      if (!g_str_has_suffix (member, ".json"))
+        continue;
+
+      g_ptr_array_add (members, g_strdup (member));
+    }
+
+  if (sort != READDIR_ORDER)
+    g_ptr_array_sort (members, sort);
+
+  for (i = 0; i < members->len; i++)
+    {
+      gchar *path;
+
+      member = g_ptr_array_index (members, i);
+      path = g_build_filename (dir, member, NULL);
+      load_json_cb (sysroot, path, user_data);
+      g_free (path);
+    }
+
+out:
+  if (dir_iter != NULL)
+    g_dir_close (dir_iter);
+
+  g_free (suffixed_dir);
+  g_free (sysrooted_dir);
+  g_clear_error (&error);
+}
+
+/*
+ * load_json_dir:
+ * @sysroot: (nullable): Interpret directory names as being inside this
+ *  sysroot, mainly for unit testing
+ * @search_paths: Directories to search
+ * @suffix: (nullable): A path to append to @dir, such as `"vulkan/icd.d"`
+ * @sort: (nullable): If not %NULL, load ICDs sorted by filename in this order
+ * @load_json_cb: Called for each potential ICD found
+ * @user_data: Passed to @load_json_cb
+ */
+static void
+load_json_dirs (const char *sysroot,
+                GStrv search_paths,
+                const char *suffix,
+                GCompareFunc sort,
+                void (*load_json_cb) (const char *, const char *, void *),
+                void *user_data)
+{
+  gchar **iter;
+
+  g_return_if_fail (load_json_cb != NULL);
+
+  for (iter = search_paths;
+       iter != NULL && *iter != NULL;
+       iter++)
+    load_json_dir (sysroot, *iter, suffix, sort, load_json_cb, user_data);
+}
+
+/*
+ * load_json:
+ * @type: %SRT_TYPE_EGL_ICD or %SRT_TYPE_VULKAN_ICD
+ * @path: (type filename) (transfer none): Path of JSON file
+ * @api_version_out: (out) (type utf8) (transfer full): Used to return
+ *  API version for %SRT_TYPE_VULKAN_ICD
+ * @library_path_out: (out) (type utf8) (transfer full): Used to return
+ *  shared library path
+ * @error: Used to raise an error on failure
+ *
+ * Try to load an EGL or Vulkan ICD from a JSON file.
+ *
+ * Returns: %TRUE if the JSON file was loaded successfully
+ */
+static gboolean
+load_json (GType type,
+           const char *path,
+           gchar **api_version_out,
+           gchar **library_path_out,
+           GError **error)
+{
+  JsonParser *parser = NULL;
+  gboolean ret = FALSE;
+  /* These are all borrowed from the parser */
+  JsonNode *node;
+  JsonObject *object;
+  JsonNode *subnode;
+  JsonObject *icd_object;
+  const char *file_format_version;
+  const char *api_version = NULL;
+  const char *library_path;
+
+  g_return_val_if_fail (type == SRT_TYPE_VULKAN_ICD
+                        || type == SRT_TYPE_EGL_ICD,
+                        FALSE);
+  g_return_val_if_fail (path != NULL, FALSE);
+  g_return_val_if_fail (api_version_out == NULL || *api_version_out == NULL,
+                        FALSE);
+  g_return_val_if_fail (library_path_out == NULL || *library_path_out == NULL,
+                        FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  g_debug ("Attempting to load %s from %s", g_type_name (type), path);
+
+  parser = json_parser_new ();
+
+  if (!json_parser_load_from_file (parser, path, error))
+    goto out;
+
+  node = json_parser_get_root (parser);
+
+  if (node == NULL || !JSON_NODE_HOLDS_OBJECT (node))
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Expected to find a JSON object in \"%s\"", path);
+      goto out;
+    }
+
+  object = json_node_get_object (node);
+
+  subnode = json_object_get_member (object, "file_format_version");
+
+  if (subnode == NULL
+      || !JSON_NODE_HOLDS_VALUE (subnode))
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "file_format_version in \"%s\" missing or not a value",
+                   path);
+      goto out;
+    }
+
+  file_format_version = json_node_get_string (subnode);
+
+  if (file_format_version == NULL)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "file_format_version in \"%s\" not a string", path);
+      goto out;
+    }
+
+  if (type == SRT_TYPE_VULKAN_ICD)
+    {
+      /*
+       * The compatibility rules for Vulkan ICDs are not clear.
+       * See https://github.com/KhronosGroup/Vulkan-Loader/issues/248
+       *
+       * The reference loader currently logs a warning, but carries on
+       * anyway, if the file format version is not 1.0.0 or 1.0.1.
+       * However, on #248 there's a suggestion that all the format versions
+       * that are valid for layer JSON (1.0.x up to 1.0.1 and 1.1.x up
+       * to 1.1.2) should also be considered valid for ICD JSON. For now
+       * we assume that the rule is the same as for EGL, below.
+       */
+      if (!g_str_has_prefix (file_format_version, "1.0."))
+        {
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "Vulkan file_format_version in \"%s\" is not 1.0.x",
+                       path);
+          goto out;
+        }
+    }
+  else
+    {
+      g_assert (type == SRT_TYPE_EGL_ICD);
+      /*
+       * For EGL, all 1.0.x versions are officially backwards compatible
+       * with 1.0.0.
+       * https://github.com/NVIDIA/libglvnd/blob/master/src/EGL/icd_enumeration.md
+       */
+      if (!g_str_has_prefix (file_format_version, "1.0."))
+        {
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "EGL file_format_version in \"%s\" is not 1.0.x",
+                       path);
+          goto out;
+        }
+    }
+
+  subnode = json_object_get_member (object, "ICD");
+
+  if (subnode == NULL
+      || !JSON_NODE_HOLDS_OBJECT (subnode))
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "No \"ICD\" object in \"%s\"", path);
+      goto out;
+    }
+
+  icd_object = json_node_get_object (subnode);
+
+  if (type == SRT_TYPE_VULKAN_ICD)
+    {
+      subnode = json_object_get_member (icd_object, "api_version");
+
+      if (subnode == NULL
+          || !JSON_NODE_HOLDS_VALUE (subnode))
+        {
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "ICD.api_version in \"%s\" missing or not a value",
+                       path);
+          goto out;
+        }
+
+      api_version = json_node_get_string (subnode);
+
+      if (api_version == NULL)
+        {
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "ICD.api_version in \"%s\" not a string", path);
+          goto out;
+        }
+    }
+
+  subnode = json_object_get_member (icd_object, "library_path");
+
+  if (subnode == NULL
+      || !JSON_NODE_HOLDS_VALUE (subnode))
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "ICD.library_path in \"%s\" missing or not a value",
+                   path);
+      goto out;
+    }
+
+  library_path = json_node_get_string (subnode);
+
+  if (library_path == NULL)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "ICD.library_path in \"%s\" not a string", path);
+      goto out;
+    }
+
+  if (api_version_out != NULL)
+    *api_version_out = g_strdup (api_version);
+
+  if (library_path_out != NULL)
+    *library_path_out = g_strdup (library_path);
+
+  ret = TRUE;
+
+out:
+  g_clear_object (&parser);
+  return ret;
+}
+
+/**
+ * SrtEglIcd:
+ *
+ * Opaque object representing an EGL ICD.
+ */
+
+struct _SrtEglIcd
+{
+  /*< private >*/
+  GObject parent;
+  SrtIcd icd;
+};
+
+struct _SrtEglIcdClass
+{
+  /*< private >*/
+  GObjectClass parent_class;
+};
+
+enum
+{
+  EGL_ICD_PROP_0,
+  EGL_ICD_PROP_ERROR,
+  EGL_ICD_PROP_JSON_PATH,
+  EGL_ICD_PROP_LIBRARY_PATH,
+  EGL_ICD_PROP_RESOLVED_LIBRARY_PATH,
+  N_EGL_ICD_PROPERTIES
+};
+
+G_DEFINE_TYPE (SrtEglIcd, srt_egl_icd, G_TYPE_OBJECT)
+
+static void
+srt_egl_icd_init (SrtEglIcd *self)
+{
+}
+
+static void
+srt_egl_icd_get_property (GObject *object,
+                          guint prop_id,
+                          GValue *value,
+                          GParamSpec *pspec)
+{
+  SrtEglIcd *self = SRT_EGL_ICD (object);
+
+  switch (prop_id)
+    {
+      case EGL_ICD_PROP_ERROR:
+        g_value_set_boxed (value, self->icd.error);
+        break;
+
+      case EGL_ICD_PROP_JSON_PATH:
+        g_value_set_string (value, self->icd.json_path);
+        break;
+
+      case EGL_ICD_PROP_LIBRARY_PATH:
+        g_value_set_string (value, self->icd.library_path);
+        break;
+
+      case EGL_ICD_PROP_RESOLVED_LIBRARY_PATH:
+        g_value_take_string (value, srt_egl_icd_resolve_library_path (self));
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_egl_icd_set_property (GObject *object,
+                          guint prop_id,
+                          const GValue *value,
+                          GParamSpec *pspec)
+{
+  SrtEglIcd *self = SRT_EGL_ICD (object);
+  const char *tmp;
+
+  switch (prop_id)
+    {
+      case EGL_ICD_PROP_ERROR:
+        g_return_if_fail (self->icd.error == NULL);
+        self->icd.error = g_value_dup_boxed (value);
+        break;
+
+      case EGL_ICD_PROP_JSON_PATH:
+        g_return_if_fail (self->icd.json_path == NULL);
+        tmp = g_value_get_string (value);
+
+        if (g_path_is_absolute (tmp))
+          {
+            self->icd.json_path = g_strdup (tmp);
+          }
+        else
+          {
+            gchar *cwd = g_get_current_dir ();
+
+            self->icd.json_path = g_build_filename (cwd, tmp, NULL);
+            g_free (cwd);
+          }
+        break;
+
+      case EGL_ICD_PROP_LIBRARY_PATH:
+        g_return_if_fail (self->icd.library_path == NULL);
+        self->icd.library_path = g_value_dup_string (value);
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_egl_icd_constructed (GObject *object)
+{
+  SrtEglIcd *self = SRT_EGL_ICD (object);
+
+  g_return_if_fail (self->icd.json_path != NULL);
+  g_return_if_fail (g_path_is_absolute (self->icd.json_path));
+  g_return_if_fail (self->icd.api_version == NULL);
+
+  if (self->icd.error != NULL)
+    g_return_if_fail (self->icd.library_path == NULL);
+  else
+    g_return_if_fail (self->icd.library_path != NULL);
+}
+
+static void
+srt_egl_icd_finalize (GObject *object)
+{
+  SrtEglIcd *self = SRT_EGL_ICD (object);
+
+  srt_icd_clear (&self->icd);
+
+  G_OBJECT_CLASS (srt_egl_icd_parent_class)->finalize (object);
+}
+
+static GParamSpec *egl_icd_properties[N_EGL_ICD_PROPERTIES] = { NULL };
+
+static void
+srt_egl_icd_class_init (SrtEglIcdClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->get_property = srt_egl_icd_get_property;
+  object_class->set_property = srt_egl_icd_set_property;
+  object_class->constructed = srt_egl_icd_constructed;
+  object_class->finalize = srt_egl_icd_finalize;
+
+  egl_icd_properties[EGL_ICD_PROP_ERROR] =
+    g_param_spec_boxed ("error", "Error",
+                        "GError describing how this ICD failed to load, or NULL",
+                        G_TYPE_ERROR,
+                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                        G_PARAM_STATIC_STRINGS);
+
+  egl_icd_properties[EGL_ICD_PROP_JSON_PATH] =
+    g_param_spec_string ("json-path", "JSON path",
+                         "Absolute path to JSON file describing this ICD. "
+                         "When constructing the object, a relative path can "
+                         "be given: it will be converted to an absolute path.",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                         G_PARAM_STATIC_STRINGS);
+
+  egl_icd_properties[EGL_ICD_PROP_LIBRARY_PATH] =
+    g_param_spec_string ("library-path", "Library path",
+                         "Library implementing this ICD, expressed as a "
+                         "basename to be searched for in the default "
+                         "library search path (e.g. libEGL_mesa.so.0), "
+                         "a relative path containing '/' to be resolved "
+                         "relative to #SrtEglIcd:json-path (e.g. "
+                         "./libEGL_myvendor.so), or an absolute path "
+                         "(e.g. /opt/EGL/libEGL_myvendor.so)",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                         G_PARAM_STATIC_STRINGS);
+
+  egl_icd_properties[EGL_ICD_PROP_RESOLVED_LIBRARY_PATH] =
+    g_param_spec_string ("resolved-library-path", "Resolved library path",
+                         "Library implementing this ICD, expressed as a "
+                         "basename to be searched for in the default "
+                         "library search path (e.g. libEGL_mesa.so.0) "
+                         "or an absolute path (e.g. "
+                         "/opt/EGL/libEGL_myvendor.so)",
+                         NULL,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_EGL_ICD_PROPERTIES,
+                                     egl_icd_properties);
+}
+
+/**
+ * srt_egl_icd_new:
+ * @json_path: (transfer none): the absolute path to the JSON file
+ * @library_path: (transfer none): the path to the library
+ *
+ * Returns: (transfer full): a new ICD
+ */
+static SrtEglIcd *
+srt_egl_icd_new (const gchar *json_path,
+                 const gchar *library_path)
+{
+  g_return_val_if_fail (json_path != NULL, NULL);
+  g_return_val_if_fail (library_path != NULL, NULL);
+
+  return g_object_new (SRT_TYPE_EGL_ICD,
+                       "json-path", json_path,
+                       "library-path", library_path,
+                       NULL);
+}
+
+/**
+ * srt_egl_icd_new_error:
+ * @error: (transfer none): Error that occurred when loading the ICD
+ *
+ * Returns: (transfer full): a new ICD
+ */
+static SrtEglIcd *
+srt_egl_icd_new_error (const gchar *json_path,
+                       const GError *error)
+{
+  g_return_val_if_fail (json_path != NULL, NULL);
+  g_return_val_if_fail (error != NULL, NULL);
+
+  return g_object_new (SRT_TYPE_EGL_ICD,
+                       "error", error,
+                       "json-path", json_path,
+                       NULL);
+}
+
+/**
+ * srt_egl_icd_check_error:
+ * @self: The ICD
+ * @error: Used to return #SrtEglIcd:error if the ICD description could
+ *  not be loaded
+ *
+ * Check whether we failed to load the JSON describing this EGL ICD.
+ * Note that this does not actually `dlopen()` the ICD itself.
+ *
+ * Returns: %TRUE if the JSON was loaded successfully
+ */
+gboolean
+srt_egl_icd_check_error (SrtEglIcd *self,
+                         GError **error)
+{
+  g_return_val_if_fail (SRT_IS_EGL_ICD (self), FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+  return srt_icd_check_error (&self->icd, error);
+}
+
+/**
+ * srt_egl_icd_get_json_path:
+ * @self: The ICD
+ *
+ * Return the absolute path to the JSON file representing this ICD.
+ *
+ * Returns: (type filename) (transfer none): #SrtEglIcd:json-path
+ */
+const gchar *
+srt_egl_icd_get_json_path (SrtEglIcd *self)
+{
+  g_return_val_if_fail (SRT_IS_EGL_ICD (self), NULL);
+  return self->icd.json_path;
+}
+
+/**
+ * srt_egl_icd_get_library_path:
+ * @self: The ICD
+ *
+ * Return the library path for this ICD. It is either an absolute path,
+ * a path relative to srt_egl_icd_get_json_path() containing at least one
+ * directory separator (slash), or a basename to be loaded from the
+ * shared library search path.
+ *
+ * If the JSON description for this ICD could not be loaded, return %NULL
+ * instead.
+ *
+ * Returns: (type filename) (transfer none) (nullable): #SrtEglIcd:library-path
+ */
+const gchar *
+srt_egl_icd_get_library_path (SrtEglIcd *self)
+{
+  g_return_val_if_fail (SRT_IS_EGL_ICD (self), NULL);
+  return self->icd.library_path;
+}
+
+/*
+ * egl_icd_load_json:
+ * @sysroot: Interpret the filename as being inside this sysroot,
+ *  mainly for unit testing
+ * @filename: The filename of the metadata
+ * @list: (element-type SrtEglIcd) (inout): Prepend the
+ *  resulting #SrtEglIcd to this list
+ *
+ * Load a single ICD metadata file.
+ */
+static void
+egl_icd_load_json (const char *sysroot,
+                   const char *filename,
+                   GList **list)
+{
+  GError *error = NULL;
+  gchar *in_sysroot = NULL;
+  gchar *library_path = NULL;
+
+  g_return_if_fail (list != NULL);
+
+  if (sysroot != NULL)
+    in_sysroot = g_build_filename (sysroot, filename, NULL);
+
+  if (load_json (SRT_TYPE_EGL_ICD,
+                 in_sysroot == NULL ? filename : in_sysroot,
+                 NULL, &library_path, &error))
+    {
+      g_assert (library_path != NULL);
+      g_assert (error == NULL);
+      *list = g_list_prepend (*list,
+                              srt_egl_icd_new (filename, library_path));
+    }
+  else
+    {
+      g_assert (library_path == NULL);
+      g_assert (error != NULL);
+      *list = g_list_prepend (*list,
+                              srt_egl_icd_new_error (filename, error));
+    }
+
+  g_free (in_sysroot);
+  g_free (library_path);
+  g_clear_error (&error);
+}
+
+/**
+ * srt_egl_icd_resolve_library_path:
+ * @self: An ICD
+ *
+ * Return the path that can be passed to `dlopen()` for this ICD.
+ *
+ * If srt_egl_icd_get_library_path() is a relative path, return the
+ * absolute path that is the result of interpreting it relative to
+ * an appropriate location (the exact interpretation is subject to change,
+ * depending on upstream decisions).
+ *
+ * Otherwise return a copy of srt_egl_icd_get_library_path().
+ *
+ * The result is either the basename of a shared library (to be found
+ * relative to some directory listed in `$LD_LIBRARY_PATH`, `/etc/ld.so.conf`,
+ * `/etc/ld.so.conf.d` or the hard-coded library search path), or an
+ * absolute path.
+ *
+ * Returns: (transfer full) (type filename) (nullable): A copy
+ *  of #SrtEglIcd:resolved-library-path. Free with g_free().
+ */
+gchar *
+srt_egl_icd_resolve_library_path (SrtEglIcd *self)
+{
+  g_return_val_if_fail (SRT_IS_EGL_ICD (self), NULL);
+  return srt_icd_resolve_library_path (&self->icd);
+}
+
+/**
+ * srt_egl_icd_new_replace_library_path:
+ * @self: An ICD
+ * @path: (type filename) (transfer none): A path
+ *
+ * Return a copy of @self with the srt_egl_icd_get_library_path()
+ * changed to @path. For example, this is useful when setting up a
+ * container where the underlying shared object will be made available
+ * at a different absolute path.
+ *
+ * If @self is in an error state, this returns a new reference to @self.
+ *
+ * Returns: (transfer full): A new reference to a #SrtEglIcd. Free with
+ *  g_object_unref().
+ */
+SrtEglIcd *
+srt_egl_icd_new_replace_library_path (SrtEglIcd *self,
+                                      const char *path)
+{
+  g_return_val_if_fail (SRT_IS_EGL_ICD (self), NULL);
+
+  if (self->icd.error != NULL)
+    return g_object_ref (self);
+
+  return srt_egl_icd_new (self->icd.json_path, path);
+}
+
+/**
+ * srt_egl_icd_write_to_file:
+ * @self: An ICD
+ * @path: (type filename): A filename
+ * @error: Used to describe the error on failure
+ *
+ * Serialize @self to the given JSON file.
+ *
+ * Returns: %TRUE on success
+ */
+gboolean
+srt_egl_icd_write_to_file (SrtEglIcd *self,
+                           const char *path,
+                           GError **error)
+{
+  g_return_val_if_fail (SRT_IS_EGL_ICD (self), FALSE);
+  g_return_val_if_fail (path != NULL, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+  return srt_icd_write_to_file (&self->icd, path, error);
+}
+
+static void
+egl_icd_load_json_cb (const char *sysroot,
+                      const char *filename,
+                      void *user_data)
+{
+  egl_icd_load_json (sysroot, filename, user_data);
+}
+
+#define EGL_VENDOR_SUFFIX "glvnd/egl_vendor.d"
+
+/*
+ * Return the ${sysconfdir} that we assume GLVND has.
+ *
+ * steam-runtime-tools is typically installed in the Steam Runtime,
+ * which is not part of the operating system, so we cannot assume
+ * that our own prefix is the same as GLVND. Assume a conventional
+ * OS-wide installation of GLVND.
+ */
+static const char *
+get_glvnd_sysconfdir (void)
+{
+  return "/etc";
+}
+
+/*
+ * Return the ${datadir} that we assume GLVND has. See above.
+ */
+static const char *
+get_glvnd_datadir (void)
+{
+  return "/usr/share";
+}
+
+/*
+ * _srt_load_egl_icds:
+ * @envp: (array zero-terminated=1): Behave as though `environ` was this
+ *  array
+ * @multiarch_tuples: (nullable): If not %NULL, and a Flatpak environment
+ *  is detected, assume a freedesktop-sdk-based runtime and look for
+ *  GL extensions for these multiarch tuples
+ *
+ * Implementation of srt_system_info_list_egl_icds().
+ *
+ * Returns: (transfer full) (element-type SrtEglIcd): A list of ICDs,
+ *  most-important first
+ */
+GList *
+_srt_load_egl_icds (gchar **envp,
+                    const char * const *multiarch_tuples)
+{
+  gchar **environ_copy = NULL;
+  const gchar *sysroot;
+  const gchar *value;
+  gsize i;
+  /* To avoid O(n**2) performance, we build this list in reverse order,
+   * then reverse it at the end. */
+  GList *ret = NULL;
+
+  g_return_val_if_fail (_srt_check_not_setuid (), NULL);
+
+  if (envp == NULL)
+    {
+      environ_copy = g_get_environ ();
+      envp = environ_copy;
+    }
+
+  /* See
+   * https://github.com/NVIDIA/libglvnd/blob/master/src/EGL/icd_enumeration.md
+   * for details of the search order. */
+
+  sysroot = g_environ_getenv (envp, "SRT_TEST_ICD_SYSROOT");
+  value = g_environ_getenv (envp, "__EGL_VENDOR_LIBRARY_FILENAMES");
+
+  if (value != NULL)
+    {
+      gchar **filenames = g_strsplit (value, G_SEARCHPATH_SEPARATOR_S, -1);
+
+      for (i = 0; filenames[i] != NULL; i++)
+        egl_icd_load_json (sysroot, filenames[i], &ret);
+
+      g_strfreev (filenames);
+    }
+  else
+    {
+      gchar **dirs;
+      gchar *flatpak_info = NULL;
+
+      value = g_environ_getenv (envp, "__EGL_VENDOR_LIBRARY_DIRS");
+
+      flatpak_info = g_build_filename (sysroot != NULL ? sysroot : "/",
+                                       ".flatpak-info", NULL);
+
+      if (value != NULL)
+        {
+          dirs = g_strsplit (value, G_SEARCHPATH_SEPARATOR_S, -1);
+          load_json_dirs (sysroot, dirs, NULL, indirect_strcmp0,
+                          egl_icd_load_json_cb, &ret);
+          g_strfreev (dirs);
+        }
+      else if (g_file_test (flatpak_info, G_FILE_TEST_EXISTS)
+               && multiarch_tuples != NULL)
+        {
+          g_debug ("Flatpak detected: assuming freedesktop-based runtime");
+
+          for (i = 0; multiarch_tuples[i] != NULL; i++)
+            {
+              gchar *tmp;
+
+              /* freedesktop-sdk reconfigures the EGL loader to look here. */
+              tmp = g_strdup_printf ("/usr/lib/%s/GL/" EGL_VENDOR_SUFFIX,
+                                     multiarch_tuples[i]);
+              load_json_dir (sysroot, tmp, NULL, indirect_strcmp0,
+                             egl_icd_load_json_cb, &ret);
+              g_free (tmp);
+            }
+        }
+      else
+        {
+          load_json_dir (sysroot, get_glvnd_sysconfdir (), EGL_VENDOR_SUFFIX,
+                         indirect_strcmp0, egl_icd_load_json_cb, &ret);
+          load_json_dir (sysroot, get_glvnd_datadir (), EGL_VENDOR_SUFFIX,
+                         indirect_strcmp0, egl_icd_load_json_cb, &ret);
+        }
+
+      g_free (flatpak_info);
+    }
+
+  g_strfreev (environ_copy);
+  return g_list_reverse (ret);
+}
+
+/**
+ * SrtVulkanIcd:
+ *
+ * Opaque object representing a Vulkan ICD.
+ */
+
+struct _SrtVulkanIcd
+{
+  /*< private >*/
+  GObject parent;
+  SrtIcd icd;
+};
+
+struct _SrtVulkanIcdClass
+{
+  /*< private >*/
+  GObjectClass parent_class;
+};
+
+enum
+{
+  VULKAN_ICD_PROP_0,
+  VULKAN_ICD_PROP_API_VERSION,
+  VULKAN_ICD_PROP_ERROR,
+  VULKAN_ICD_PROP_JSON_PATH,
+  VULKAN_ICD_PROP_LIBRARY_PATH,
+  VULKAN_ICD_PROP_RESOLVED_LIBRARY_PATH,
+  N_VULKAN_ICD_PROPERTIES
+};
+
+G_DEFINE_TYPE (SrtVulkanIcd, srt_vulkan_icd, G_TYPE_OBJECT)
+
+static void
+srt_vulkan_icd_init (SrtVulkanIcd *self)
+{
+}
+
+static void
+srt_vulkan_icd_get_property (GObject *object,
+                             guint prop_id,
+                             GValue *value,
+                             GParamSpec *pspec)
+{
+  SrtVulkanIcd *self = SRT_VULKAN_ICD (object);
+
+  switch (prop_id)
+    {
+      case VULKAN_ICD_PROP_API_VERSION:
+        g_value_set_string (value, self->icd.api_version);
+        break;
+
+      case VULKAN_ICD_PROP_ERROR:
+        g_value_set_boxed (value, self->icd.error);
+        break;
+
+      case VULKAN_ICD_PROP_JSON_PATH:
+        g_value_set_string (value, self->icd.json_path);
+        break;
+
+      case VULKAN_ICD_PROP_LIBRARY_PATH:
+        g_value_set_string (value, self->icd.library_path);
+        break;
+
+      case VULKAN_ICD_PROP_RESOLVED_LIBRARY_PATH:
+        g_value_take_string (value, srt_vulkan_icd_resolve_library_path (self));
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_vulkan_icd_set_property (GObject *object,
+                             guint prop_id,
+                             const GValue *value,
+                             GParamSpec *pspec)
+{
+  SrtVulkanIcd *self = SRT_VULKAN_ICD (object);
+  const char *tmp;
+
+  switch (prop_id)
+    {
+      case VULKAN_ICD_PROP_API_VERSION:
+        g_return_if_fail (self->icd.api_version == NULL);
+        self->icd.api_version = g_value_dup_string (value);
+        break;
+
+      case VULKAN_ICD_PROP_ERROR:
+        g_return_if_fail (self->icd.error == NULL);
+        self->icd.error = g_value_dup_boxed (value);
+        break;
+
+      case VULKAN_ICD_PROP_JSON_PATH:
+        g_return_if_fail (self->icd.json_path == NULL);
+        tmp = g_value_get_string (value);
+
+        if (g_path_is_absolute (tmp))
+          {
+            self->icd.json_path = g_strdup (tmp);
+          }
+        else
+          {
+            gchar *cwd = g_get_current_dir ();
+
+            self->icd.json_path = g_build_filename (cwd, tmp, NULL);
+            g_free (cwd);
+          }
+        break;
+
+      case VULKAN_ICD_PROP_LIBRARY_PATH:
+        g_return_if_fail (self->icd.library_path == NULL);
+        self->icd.library_path = g_value_dup_string (value);
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_vulkan_icd_constructed (GObject *object)
+{
+  SrtVulkanIcd *self = SRT_VULKAN_ICD (object);
+
+  g_return_if_fail (self->icd.json_path != NULL);
+  g_return_if_fail (g_path_is_absolute (self->icd.json_path));
+
+  if (self->icd.error != NULL)
+    {
+      g_return_if_fail (self->icd.api_version == NULL);
+      g_return_if_fail (self->icd.library_path == NULL);
+    }
+  else
+    {
+      g_return_if_fail (self->icd.api_version != NULL);
+      g_return_if_fail (self->icd.library_path != NULL);
+    }
+}
+
+static void
+srt_vulkan_icd_finalize (GObject *object)
+{
+  SrtVulkanIcd *self = SRT_VULKAN_ICD (object);
+
+  srt_icd_clear (&self->icd);
+
+  G_OBJECT_CLASS (srt_vulkan_icd_parent_class)->finalize (object);
+}
+
+static GParamSpec *vulkan_icd_properties[N_VULKAN_ICD_PROPERTIES] = { NULL };
+
+static void
+srt_vulkan_icd_class_init (SrtVulkanIcdClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->get_property = srt_vulkan_icd_get_property;
+  object_class->set_property = srt_vulkan_icd_set_property;
+  object_class->constructed = srt_vulkan_icd_constructed;
+  object_class->finalize = srt_vulkan_icd_finalize;
+
+  vulkan_icd_properties[VULKAN_ICD_PROP_API_VERSION] =
+    g_param_spec_string ("api-version", "API version",
+                         "Vulkan API version implemented by this ICD",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                         G_PARAM_STATIC_STRINGS);
+
+  vulkan_icd_properties[VULKAN_ICD_PROP_ERROR] =
+    g_param_spec_boxed ("error", "Error",
+                        "GError describing how this ICD failed to load, or NULL",
+                        G_TYPE_ERROR,
+                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                        G_PARAM_STATIC_STRINGS);
+
+  vulkan_icd_properties[VULKAN_ICD_PROP_JSON_PATH] =
+    g_param_spec_string ("json-path", "JSON path",
+                         "Absolute path to JSON file describing this ICD. "
+                         "When constructing the object, a relative path can "
+                         "be given: it will be converted to an absolute path.",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                         G_PARAM_STATIC_STRINGS);
+
+  vulkan_icd_properties[VULKAN_ICD_PROP_LIBRARY_PATH] =
+    g_param_spec_string ("library-path", "Library path",
+                         "Library implementing this ICD, expressed as a "
+                         "basename to be searched for in the default "
+                         "library search path (e.g. libvulkan_myvendor.so), "
+                         "a relative path containing '/' to be resolved "
+                         "relative to #SrtVulkanIcd:json-path (e.g. "
+                         "./libvulkan_myvendor.so), or an absolute path "
+                         "(e.g. /opt/vulkan/libvulkan_myvendor.so)",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                         G_PARAM_STATIC_STRINGS);
+
+  vulkan_icd_properties[VULKAN_ICD_PROP_RESOLVED_LIBRARY_PATH] =
+    g_param_spec_string ("resolved-library-path", "Resolved library path",
+                         "Library implementing this ICD, expressed as a "
+                         "basename to be searched for in the default "
+                         "library search path (e.g. libvulkan_myvendor.so) "
+                         "or an absolute path "
+                         "(e.g. /opt/vulkan/libvulkan_myvendor.so)",
+                         NULL,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_VULKAN_ICD_PROPERTIES,
+                                     vulkan_icd_properties);
+}
+
+/**
+ * srt_vulkan_icd_new:
+ * @json_path: (transfer none): the absolute path to the JSON file
+ * @api_version: (transfer none): the API version
+ * @library_path: (transfer none): the path to the library
+ *
+ * Returns: (transfer full): a new ICD
+ */
+static SrtVulkanIcd *
+srt_vulkan_icd_new (const gchar *json_path,
+                    const gchar *api_version,
+                    const gchar *library_path)
+{
+  g_return_val_if_fail (json_path != NULL, NULL);
+  g_return_val_if_fail (api_version != NULL, NULL);
+  g_return_val_if_fail (library_path != NULL, NULL);
+
+  return g_object_new (SRT_TYPE_VULKAN_ICD,
+                       "api-version", api_version,
+                       "json-path", json_path,
+                       "library-path", library_path,
+                       NULL);
+}
+
+/**
+ * srt_vulkan_icd_new_error:
+ * @error: (transfer none): Error that occurred when loading the ICD
+ *
+ * Returns: (transfer full): a new ICD
+ */
+static SrtVulkanIcd *
+srt_vulkan_icd_new_error (const gchar *json_path,
+                          const GError *error)
+{
+  g_return_val_if_fail (json_path != NULL, NULL);
+  g_return_val_if_fail (error != NULL, NULL);
+
+  return g_object_new (SRT_TYPE_VULKAN_ICD,
+                       "error", error,
+                       "json-path", json_path,
+                       NULL);
+}
+
+/**
+ * srt_vulkan_icd_check_error:
+ * @self: The ICD
+ * @error: Used to return details if the ICD description could not be loaded
+ *
+ * Check whether we failed to load the JSON describing this Vulkan ICD.
+ * Note that this does not actually `dlopen()` the ICD itself.
+ *
+ * Returns: %TRUE if the JSON was loaded successfully
+ */
+gboolean
+srt_vulkan_icd_check_error (SrtVulkanIcd *self,
+                            GError **error)
+{
+  g_return_val_if_fail (SRT_IS_VULKAN_ICD (self), FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+  return srt_icd_check_error (&self->icd, error);
+}
+
+/**
+ * srt_vulkan_icd_get_api_version:
+ * @self: The ICD
+ *
+ * Return the Vulkan API version of this ICD.
+ *
+ * If the JSON description for this ICD could not be loaded, return %NULL
+ * instead.
+ *
+ * Returns: (type utf8) (transfer none) (nullable): The API version as a string
+ */
+const gchar *
+srt_vulkan_icd_get_api_version (SrtVulkanIcd *self)
+{
+  g_return_val_if_fail (SRT_IS_VULKAN_ICD (self), NULL);
+  return self->icd.api_version;
+}
+
+/**
+ * srt_vulkan_icd_get_json_path:
+ * @self: The ICD
+ *
+ * Return the absolute path to the JSON file representing this ICD.
+ *
+ * Returns: (type filename) (transfer none): #SrtVulkanIcd:json-path
+ */
+const gchar *
+srt_vulkan_icd_get_json_path (SrtVulkanIcd *self)
+{
+  g_return_val_if_fail (SRT_IS_VULKAN_ICD (self), NULL);
+  return self->icd.json_path;
+}
+
+/**
+ * srt_vulkan_icd_get_library_path:
+ * @self: The ICD
+ *
+ * Return the library path for this ICD. It is either an absolute path,
+ * a path relative to srt_vulkan_icd_get_json_path() containing at least one
+ * directory separator (slash), or a basename to be loaded from the
+ * shared library search path.
+ *
+ * If the JSON description for this ICD could not be loaded, return %NULL
+ * instead.
+ *
+ * Returns: (type filename) (transfer none) (nullable): #SrtVulkanIcd:library-path
+ */
+const gchar *
+srt_vulkan_icd_get_library_path (SrtVulkanIcd *self)
+{
+  g_return_val_if_fail (SRT_IS_VULKAN_ICD (self), NULL);
+  return self->icd.library_path;
+}
+
+/**
+ * srt_vulkan_icd_resolve_library_path:
+ * @self: An ICD
+ *
+ * Return the path that can be passed to `dlopen()` for this ICD.
+ *
+ * If srt_vulkan_icd_get_library_path() is a relative path, return the
+ * absolute path that is the result of interpreting it relative to
+ * srt_vulkan_icd_get_json_path(). Otherwise return a copy of
+ * srt_vulkan_icd_get_library_path().
+ *
+ * The result is either the basename of a shared library (to be found
+ * relative to some directory listed in `$LD_LIBRARY_PATH`, `/etc/ld.so.conf`,
+ * `/etc/ld.so.conf.d` or the hard-coded library search path), or an
+ * absolute path.
+ *
+ * Returns: (transfer full) (type filename) (nullable): A copy
+ *  of #SrtVulkanIcd:resolved-library-path. Free with g_free().
+ */
+gchar *
+srt_vulkan_icd_resolve_library_path (SrtVulkanIcd *self)
+{
+  g_return_val_if_fail (SRT_IS_VULKAN_ICD (self), NULL);
+  return srt_icd_resolve_library_path (&self->icd);
+}
+
+/**
+ * srt_vulkan_icd_write_to_file:
+ * @self: An ICD
+ * @path: (type filename): A filename
+ * @error: Used to describe the error on failure
+ *
+ * Serialize @self to the given JSON file.
+ *
+ * Returns: %TRUE on success
+ */
+gboolean
+srt_vulkan_icd_write_to_file (SrtVulkanIcd *self,
+                              const char *path,
+                              GError **error)
+{
+  g_return_val_if_fail (SRT_IS_VULKAN_ICD (self), FALSE);
+  g_return_val_if_fail (path != NULL, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+  return srt_icd_write_to_file (&self->icd, path, error);
+}
+
+/**
+ * srt_vulkan_icd_new_replace_library_path:
+ * @self: An ICD
+ * @path: (type filename) (transfer none): A path
+ *
+ * Return a copy of @self with the srt_vulkan_icd_get_library_path()
+ * changed to @path. For example, this is useful when setting up a
+ * container where the underlying shared object will be made available
+ * at a different absolute path.
+ *
+ * If @self is in an error state, this returns a new reference to @self.
+ *
+ * Returns: (transfer full): A new reference to a #SrtVulkanIcd. Free with
+ *  g_object_unref().
+ */
+SrtVulkanIcd *
+srt_vulkan_icd_new_replace_library_path (SrtVulkanIcd *self,
+                                         const char *path)
+{
+  g_return_val_if_fail (SRT_IS_VULKAN_ICD (self), NULL);
+
+  if (self->icd.error != NULL)
+    return g_object_ref (self);
+
+  return srt_vulkan_icd_new (self->icd.json_path,
+                             self->icd.api_version,
+                             path);
+}
+
+/*
+ * vulkan_icd_load_json:
+ * @sysroot: Interpret the filename as being inside this sysroot,
+ *  mainly for unit testing
+ * @filename: The filename of the metadata
+ * @list: (element-type SrtVulkanIcd) (inout): Prepend the
+ *  resulting #SrtVulkanIcd to this list
+ *
+ * Load a single ICD metadata file.
+ */
+static void
+vulkan_icd_load_json (const char *sysroot,
+                      const char *filename,
+                      GList **list)
+{
+  GError *error = NULL;
+  gchar *in_sysroot = NULL;
+  gchar *api_version = NULL;
+  gchar *library_path = NULL;
+
+  g_return_if_fail (list != NULL);
+
+  if (sysroot != NULL)
+    in_sysroot = g_build_filename (sysroot, filename, NULL);
+
+  if (load_json (SRT_TYPE_VULKAN_ICD,
+                 in_sysroot == NULL ? filename : in_sysroot,
+                 &api_version, &library_path, &error))
+    {
+      g_assert (api_version != NULL);
+      g_assert (library_path != NULL);
+      g_assert (error == NULL);
+      *list = g_list_prepend (*list,
+                              srt_vulkan_icd_new (filename,
+                                                  api_version,
+                                                  library_path));
+    }
+  else
+    {
+      g_assert (api_version == NULL);
+      g_assert (library_path == NULL);
+      g_assert (error != NULL);
+      *list = g_list_prepend (*list,
+                              srt_vulkan_icd_new_error (filename, error));
+    }
+
+  g_free (in_sysroot);
+  g_free (api_version);
+  g_free (library_path);
+  g_clear_error (&error);
+}
+
+static void
+vulkan_icd_load_json_cb (const char *sysroot,
+                         const char *filename,
+                         void *user_data)
+{
+  vulkan_icd_load_json (sysroot, filename, user_data);
+}
+
+#define VULKAN_ICD_SUFFIX "vulkan/icd.d"
+
+/*
+ * Return the ${sysconfdir} that we assume the Vulkan loader has.
+ * See get_glvnd_sysconfdir().
+ */
+static const char *
+get_vulkan_sysconfdir (void)
+{
+  return "/etc";
+}
+
+/*
+ * _srt_load_vulkan_icds:
+ * @envp: (array zero-terminated=1): Behave as though `environ` was this
+ *  array
+ * @multiarch_tuples: (nullable): If not %NULL, and a Flatpak environment
+ *  is detected, assume a freedesktop-sdk-based runtime and look for
+ *  GL extensions for these multiarch tuples
+ *
+ * Implementation of srt_system_info_list_vulkan_icds().
+ *
+ * Returns: (transfer full) (element-type SrtVulkanIcd): A list of ICDs,
+ *  most-important first
+ */
+GList *
+_srt_load_vulkan_icds (gchar **envp,
+                       const char * const *multiarch_tuples)
+{
+  gchar **environ_copy = NULL;
+  const gchar *sysroot;
+  const gchar *value;
+  gsize i;
+  /* To avoid O(n**2) performance, we build this list in reverse order,
+   * then reverse it at the end. */
+  GList *ret = NULL;
+
+  g_return_val_if_fail (_srt_check_not_setuid (), NULL);
+
+  if (envp == NULL)
+    {
+      environ_copy = g_get_environ ();
+      envp = environ_copy;
+    }
+
+  /* See
+   * https://github.com/KhronosGroup/Vulkan-Loader/blob/master/loader/LoaderAndLayerInterface.md#icd-manifest-file-format
+   * for more details of the search order - but beware that the
+   * documentation is not completely up to date (as of September 2019)
+   * so you should also look at the reference implementation. */
+
+  sysroot = g_environ_getenv (envp, "SRT_TEST_ICD_SYSROOT");
+  value = g_environ_getenv (envp, "VK_ICD_FILENAMES");
+
+  if (value != NULL)
+    {
+      gchar **filenames = g_strsplit (value, G_SEARCHPATH_SEPARATOR_S, -1);
+
+      for (i = 0; filenames[i] != NULL; i++)
+        vulkan_icd_load_json (sysroot, filenames[i], &ret);
+
+      g_strfreev (filenames);
+    }
+  else
+    {
+      gchar **dirs;
+      gchar *tmp = NULL;
+      gchar *flatpak_info = NULL;
+
+      /* The reference Vulkan loader doesn't entirely follow
+       * https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html:
+       * it skips XDG_CONFIG_HOME and goes directly to XDG_CONFIG_DIRS.
+       * https://github.com/KhronosGroup/Vulkan-Loader/issues/246 */
+
+      value = g_environ_getenv (envp, "XDG_CONFIG_DIRS");
+
+      /* Constant and non-configurable fallback, as per
+       * https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html */
+      if (value == NULL)
+        value = "/etc/xdg";
+
+      dirs = g_strsplit (value, G_SEARCHPATH_SEPARATOR_S, -1);
+      load_json_dirs (sysroot, dirs, VULKAN_ICD_SUFFIX, READDIR_ORDER,
+                      vulkan_icd_load_json_cb, &ret);
+      g_strfreev (dirs);
+
+      value = get_vulkan_sysconfdir ();
+      load_json_dir (sysroot, value, VULKAN_ICD_SUFFIX,
+                     READDIR_ORDER, vulkan_icd_load_json_cb, &ret);
+
+      /* This is hard-coded in the reference loader: if its own sysconfdir
+       * is not /etc, it searches /etc afterwards. (In practice this
+       * won't trigger at the moment, because we assume the Vulkan
+       * loader's sysconfdir *is* /etc.) */
+      if (g_strcmp0 (value, "/etc") != 0)
+        load_json_dir (sysroot, "/etc", VULKAN_ICD_SUFFIX,
+                       READDIR_ORDER, vulkan_icd_load_json_cb, &ret);
+
+      flatpak_info = g_build_filename (sysroot != NULL ? sysroot : "/",
+                                       ".flatpak-info", NULL);
+
+      /* freedesktop-sdk patches the Vulkan loader to look here. */
+      if (g_file_test (flatpak_info, G_FILE_TEST_EXISTS)
+          && multiarch_tuples != NULL)
+        {
+          g_debug ("Flatpak detected: assuming freedesktop-based runtime");
+
+          for (i = 0; multiarch_tuples[i] != NULL; i++)
+            {
+              /* GL extensions */
+              tmp = g_build_filename ("/usr/lib",
+                                      multiarch_tuples[i],
+                                      "GL",
+                                      VULKAN_ICD_SUFFIX,
+                                      NULL);
+              load_json_dir (sysroot, tmp, NULL, READDIR_ORDER,
+                             vulkan_icd_load_json_cb, &ret);
+              g_free (tmp);
+
+              /* Built-in Mesa stack */
+              tmp = g_build_filename ("/usr/lib",
+                                      multiarch_tuples[i],
+                                      VULKAN_ICD_SUFFIX,
+                                      NULL);
+              load_json_dir (sysroot, tmp, NULL, READDIR_ORDER,
+                             vulkan_icd_load_json_cb, &ret);
+              g_free (tmp);
+            }
+        }
+
+      g_free (flatpak_info);
+
+      /* The reference Vulkan loader doesn't entirely follow
+       * https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html:
+       * it searches XDG_DATA_HOME *after* XDG_DATA_DIRS, and it still
+       * searches ~/.local/share even if XDG_DATA_HOME is set.
+       * https://github.com/KhronosGroup/Vulkan-Loader/issues/245 */
+
+      value = g_environ_getenv (envp, "XDG_DATA_DIRS");
+
+      /* Constant and non-configurable fallback, as per
+       * https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html */
+      if (value == NULL)
+        value = "/usr/local/share" G_SEARCHPATH_SEPARATOR_S "/usr/share";
+
+      dirs = g_strsplit (value, G_SEARCHPATH_SEPARATOR_S, -1);
+      load_json_dirs (sysroot, dirs, VULKAN_ICD_SUFFIX, READDIR_ORDER,
+                      vulkan_icd_load_json_cb, &ret);
+      g_strfreev (dirs);
+
+      /* I don't know why this is searched *after* XDG_DATA_DIRS in the
+       * reference loader, but we match that behaviour. */
+      value = g_environ_getenv (envp, "XDG_DATA_HOME");
+      load_json_dir (sysroot, value, VULKAN_ICD_SUFFIX, READDIR_ORDER,
+                     vulkan_icd_load_json_cb, &ret);
+
+      /* libvulkan searches this unconditionally, even if XDG_DATA_HOME
+       * is set. */
+      value = g_environ_getenv (envp, "HOME");
+
+      if (value == NULL)
+        value = g_get_home_dir ();
+
+      tmp = g_build_filename (value, ".local", "share",
+                              VULKAN_ICD_SUFFIX, NULL);
+      load_json_dir (sysroot, tmp, NULL, READDIR_ORDER,
+                     vulkan_icd_load_json_cb, &ret);
+      g_free (tmp);
+    }
+
+  g_strfreev (environ_copy);
+  return g_list_reverse (ret);
+}
diff --git a/steam-runtime-tools/graphics.h b/steam-runtime-tools/graphics.h
index 2071f59358c20e07707968109560d9e624c201e8..799d35e221cd8d9ba02c36f145130e6c88b1d1a6 100644
--- a/steam-runtime-tools/graphics.h
+++ b/steam-runtime-tools/graphics.h
@@ -109,6 +109,53 @@ const char *srt_graphics_get_renderer_string (SrtGraphics *self);
 const char *srt_graphics_get_messages (SrtGraphics *self);
 gchar *srt_graphics_dup_parameters_string (SrtGraphics *self);
 
+typedef struct _SrtEglIcd SrtEglIcd;
+typedef struct _SrtEglIcdClass SrtEglIcdClass;
+
+#define SRT_TYPE_EGL_ICD (srt_egl_icd_get_type ())
+#define SRT_EGL_ICD(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SRT_TYPE_EGL_ICD, SrtEglIcd))
+#define SRT_EGL_ICD_CLASS(cls) (G_TYPE_CHECK_CLASS_CAST ((cls), SRT_TYPE_EGL_ICD, SrtEglIcdClass))
+#define SRT_IS_EGL_ICD(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SRT_TYPE_EGL_ICD))
+#define SRT_IS_EGL_ICD_CLASS(cls) (G_TYPE_CHECK_CLASS_TYPE ((cls), SRT_TYPE_EGL_ICD))
+#define SRT_EGL_ICD_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SRT_TYPE_EGL_ICD, SrtEglIcdClass)
+GType srt_egl_icd_get_type (void);
+
+gboolean srt_egl_icd_check_error (SrtEglIcd *self,
+                                  GError **error);
+const gchar *srt_egl_icd_get_json_path (SrtEglIcd *self);
+const gchar *srt_egl_icd_get_library_path (SrtEglIcd *self);
+gchar *srt_egl_icd_resolve_library_path (SrtEglIcd *self);
+SrtEglIcd *srt_egl_icd_new_replace_library_path (SrtEglIcd *self,
+                                                 const char *path);
+gboolean srt_egl_icd_write_to_file (SrtEglIcd *self,
+                                    const char *path,
+                                    GError **error);
+
+typedef struct _SrtVulkanIcd SrtVulkanIcd;
+typedef struct _SrtVulkanIcdClass SrtVulkanIcdClass;
+
+#define SRT_TYPE_VULKAN_ICD (srt_vulkan_icd_get_type ())
+#define SRT_VULKAN_ICD(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SRT_TYPE_VULKAN_ICD, SrtVulkanIcd))
+#define SRT_VULKAN_ICD_CLASS(cls) (G_TYPE_CHECK_CLASS_CAST ((cls), SRT_TYPE_VULKAN_ICD, SrtVulkanIcdClass))
+#define SRT_IS_VULKAN_ICD(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SRT_TYPE_VULKAN_ICD))
+#define SRT_IS_VULKAN_ICD_CLASS(cls) (G_TYPE_CHECK_CLASS_TYPE ((cls), SRT_TYPE_VULKAN_ICD))
+#define SRT_VULKAN_ICD_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SRT_TYPE_VULKAN_ICD, SrtVulkanIcdClass)
+GType srt_vulkan_icd_get_type (void);
+
+gboolean srt_vulkan_icd_check_error (SrtVulkanIcd *self,
+                                     GError **error);
+const gchar *srt_vulkan_icd_get_api_version (SrtVulkanIcd *self);
+const gchar *srt_vulkan_icd_get_json_path (SrtVulkanIcd *self);
+const gchar *srt_vulkan_icd_get_library_path (SrtVulkanIcd *self);
+gchar *srt_vulkan_icd_resolve_library_path (SrtVulkanIcd *self);
+SrtVulkanIcd *srt_vulkan_icd_new_replace_library_path (SrtVulkanIcd *self,
+                                                       const char *path);
+gboolean srt_vulkan_icd_write_to_file (SrtVulkanIcd *self,
+                                       const char *path,
+                                       GError **error);
+
 #ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
 G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtGraphics, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtEglIcd, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtVulkanIcd, g_object_unref)
 #endif
diff --git a/steam-runtime-tools/system-info.c b/steam-runtime-tools/system-info.c
index 930b3b16734b0d0c7ae371238c4825753e322070..90018cadd7530d3cd3a117d953efd9b581c3ae9a 100644
--- a/steam-runtime-tools/system-info.c
+++ b/steam-runtime-tools/system-info.c
@@ -115,6 +115,13 @@ struct _SrtSystemInfo
     gchar *version;
     SrtRuntimeIssues issues;
   } runtime;
+  struct
+  {
+    GList *egl;
+    GList *vulkan;
+    gboolean have_egl;
+    gboolean have_vulkan;
+  } icds;
   SrtTestFlags test_flags;
   Tristate can_write_uinput;
   /* (element-type Abi) */
@@ -317,11 +324,26 @@ forget_steam (SrtSystemInfo *self)
   g_clear_pointer (&self->steam.bin32, g_free);
 }
 
+/*
+ * Forget any cached information about ICDs.
+ */
+static void
+forget_icds (SrtSystemInfo *self)
+{
+  self->icds.have_egl = FALSE;
+  g_list_free_full (self->icds.egl, g_object_unref);
+  self->icds.egl = NULL;
+  self->icds.have_vulkan = FALSE;
+  g_list_free_full (self->icds.vulkan, g_object_unref);
+  self->icds.vulkan = NULL;
+}
+
 static void
 srt_system_info_finalize (GObject *object)
 {
   SrtSystemInfo *self = SRT_SYSTEM_INFO (object);
 
+  forget_icds (self);
   forget_locales (self);
   forget_runtime (self);
   forget_steam (self);
@@ -1430,3 +1452,101 @@ srt_system_info_set_test_flags (SrtSystemInfo *self,
   g_return_if_fail (SRT_IS_SYSTEM_INFO (self));
   self->test_flags = flags;
 }
+
+/**
+ * srt_system_info_list_egl_icds:
+ *
+ * List the available EGL ICDs, using the same search paths as GLVND.
+ *
+ * This function is not architecture-specific and may return a mixture
+ * of ICDs for more than one architecture or ABI, because the way the
+ * GLVND EGL loader works is to read a single search path for metadata
+ * describing ICDs, then filter out the ones that are for the wrong
+ * architecture at load time.
+ *
+ * Some of the entries in the result might describe a bare SONAME in the
+ * standard library search path, which might exist for any or all
+ * architectures simultaneously (this is the most common approach for EGL).
+ * Other entries might describe the relative or absolute path to a
+ * specific library, which will only be usable for the architecture for
+ * which it was compiled.
+ *
+ * @multiarch_tuples is used if running in a Flatpak environment, to
+ * match the search paths used by the freedesktop.org runtime's patched
+ * GLVND.
+ *
+ * Returns: (transfer full) (element-type SrtEglIcd): A list of
+ *  opaque #SrtEglIcd objects. Free with
+ *  `g_list_free_full(icds, srt_egl_icd_unref)`.
+ */
+GList *
+srt_system_info_list_egl_icds (SrtSystemInfo *self,
+                               const char * const *multiarch_tuples)
+{
+  GList *ret = NULL;
+  const GList *iter;
+
+  g_return_val_if_fail (SRT_IS_SYSTEM_INFO (self), NULL);
+
+  if (!self->icds.have_egl)
+    {
+      g_assert (self->icds.egl == NULL);
+      self->icds.egl = _srt_load_egl_icds (self->env, multiarch_tuples);
+      self->icds.have_egl = TRUE;
+    }
+
+  for (iter = self->icds.egl; iter != NULL; iter = iter->next)
+    ret = g_list_prepend (ret, g_object_ref (iter->data));
+
+  return g_list_reverse (ret);
+}
+
+/**
+ * srt_system_info_list_vulkan_icds:
+ *
+ * List the available Vulkan ICDs, using the same search paths as the
+ * reference vulkan-loader.
+ *
+ * This function is not architecture-specific and may return a mixture
+ * of ICDs for more than one architecture or ABI, because the way the
+ * reference vulkan-loader works is to read a single search path for
+ * metadata describing ICDs, then filter out the ones that are for the
+ * wrong architecture at load time.
+ *
+ * Some of the entries in the result might describe a bare SONAME in the
+ * standard library search path, which might exist for any or all
+ * architectures simultaneously (for example, this approach is used for
+ * the NVIDIA binary driver on Debian systems). Other entries might
+ * describe the relative or absolute path to a specific library, which
+ * will only be usable for the architecture for which it was compiled
+ * (for example, this approach is used in Mesa).
+ *
+ * @multiarch_tuples is used if running in a Flatpak environment, to
+ * match the search paths used by the freedesktop.org runtime's patched
+ * vulkan-loader.
+ *
+ * Returns: (transfer full) (element-type SrtVulkanIcd): A list of
+ *  opaque #SrtVulkanIcd objects. Free with
+ *  `g_list_free_full(icds, srt_vulkan_icd_unref)`.
+ */
+GList *
+srt_system_info_list_vulkan_icds (SrtSystemInfo *self,
+                                  const char * const *multiarch_tuples)
+{
+  GList *ret = NULL;
+  const GList *iter;
+
+  g_return_val_if_fail (SRT_IS_SYSTEM_INFO (self), NULL);
+
+  if (!self->icds.have_vulkan)
+    {
+      g_assert (self->icds.vulkan == NULL);
+      self->icds.vulkan = _srt_load_vulkan_icds (self->env, multiarch_tuples);
+      self->icds.have_vulkan = TRUE;
+    }
+
+  for (iter = self->icds.vulkan; iter != NULL; iter = iter->next)
+    ret = g_list_prepend (ret, g_object_ref (iter->data));
+
+  return g_list_reverse (ret);
+}
diff --git a/steam-runtime-tools/system-info.h b/steam-runtime-tools/system-info.h
index 7caf98f9fe1ee814840a21946c373d7230670b5d..97c5c6c26f70b65e923e2a06e10f22353bfc05bf 100644
--- a/steam-runtime-tools/system-info.h
+++ b/steam-runtime-tools/system-info.h
@@ -87,6 +87,11 @@ SrtGraphicsIssues srt_system_info_check_graphics (SrtSystemInfo *self,
 GList * srt_system_info_check_all_graphics (SrtSystemInfo *self,
                                          const char *multiarch_tuple);
 
+GList *srt_system_info_list_egl_icds (SrtSystemInfo *self,
+                                      const char * const *multiarch_tuples);
+GList *srt_system_info_list_vulkan_icds (SrtSystemInfo *self,
+                                         const char * const *multiarch_tuples);
+
 void srt_system_info_set_environ (SrtSystemInfo *self,
                                   gchar * const *env);
 void srt_system_info_set_helpers_path (SrtSystemInfo *self,
diff --git a/tests/fake-icds/.flatpak-info b/tests/fake-icds/.flatpak-info
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/fake-icds/confdir/vulkan/icd.d/invalid.json b/tests/fake-icds/confdir/vulkan/icd.d/invalid.json
new file mode 100644
index 0000000000000000000000000000000000000000..2886dc10a7faf42e4fd14b99d6f797498c9398d6
--- /dev/null
+++ b/tests/fake-icds/confdir/vulkan/icd.d/invalid.json
@@ -0,0 +1,7 @@
+{
+    "ICD": {
+        "api_version": "1.1.102",
+        "library_path": "/usr/lib/x86_64-linux-gnu/libvulkan_intel.so"
+    },
+    "file_format_version": "1.1.0"
+}
diff --git a/tests/fake-icds/datadir/vulkan/icd.d/invalid.json b/tests/fake-icds/datadir/vulkan/icd.d/invalid.json
new file mode 100644
index 0000000000000000000000000000000000000000..f0d761d97c4bb68b020b981910409c3cc843a480
--- /dev/null
+++ b/tests/fake-icds/datadir/vulkan/icd.d/invalid.json
@@ -0,0 +1,7 @@
+{
+    "ICD": {
+        "api_version": "1.1.102",
+        "library_path": "/usr/lib/x86_64-linux-gnu/libvulkan_intel.so"
+    },
+    "file_format_version": "2.0.0"
+}
diff --git a/tests/fake-icds/datahome/vulkan/icd.d/invalid.json b/tests/fake-icds/datahome/vulkan/icd.d/invalid.json
new file mode 100644
index 0000000000000000000000000000000000000000..687b6556cc69d8fa8f33cdf778eddde0567f82a8
--- /dev/null
+++ b/tests/fake-icds/datahome/vulkan/icd.d/invalid.json
@@ -0,0 +1,6 @@
+{
+    "ICD": {
+        "library_path": "/usr/lib/x86_64-linux-gnu/libvulkan_intel.so"
+    },
+    "file_format_version": "1.0.0"
+}
diff --git a/tests/fake-icds/egl1/AAA.json b/tests/fake-icds/egl1/AAA.json
new file mode 100644
index 0000000000000000000000000000000000000000..270c611ee72c567bc1b2abec4cbc345bab9f15ba
--- /dev/null
+++ b/tests/fake-icds/egl1/AAA.json
@@ -0,0 +1 @@
+hello, world!
diff --git a/tests/fake-icds/egl1/BBB.json b/tests/fake-icds/egl1/BBB.json
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/fake-icds/egl1/a.json b/tests/fake-icds/egl1/a.json
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/fake-icds/egl1/b.json b/tests/fake-icds/egl1/b.json
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/fake-icds/egl1/z.json b/tests/fake-icds/egl1/z.json
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/fake-icds/egl2/absolute.json b/tests/fake-icds/egl2/absolute.json
new file mode 100644
index 0000000000000000000000000000000000000000..76b8903cde796147d41394d09c9416c545905291
--- /dev/null
+++ b/tests/fake-icds/egl2/absolute.json
@@ -0,0 +1,6 @@
+{
+    "file_format_version" : "1.0.0",
+    "ICD" : {
+        "library_path" : "/opt/libEGL_myvendor.so"
+    }
+}
diff --git a/tests/fake-icds/etc/glvnd/egl_vendor.d/invalid.json b/tests/fake-icds/etc/glvnd/egl_vendor.d/invalid.json
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/fake-icds/etc/vulkan/icd.d/basename.json b/tests/fake-icds/etc/vulkan/icd.d/basename.json
new file mode 100644
index 0000000000000000000000000000000000000000..cfee90f4c38b69f4990dbed61357062a2a5d66a8
--- /dev/null
+++ b/tests/fake-icds/etc/vulkan/icd.d/basename.json
@@ -0,0 +1,7 @@
+{
+    "ICD": {
+        "api_version": "1.2.3",
+        "library_path": "libvulkan_basename.so"
+    },
+    "file_format_version": "1.0.0"
+}
diff --git a/tests/fake-icds/etc/xdg/vulkan/icd.d/invalid.json b/tests/fake-icds/etc/xdg/vulkan/icd.d/invalid.json
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/fake-icds/etc/xdg/vulkan/icd.d/invalid.txt b/tests/fake-icds/etc/xdg/vulkan/icd.d/invalid.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/fake-icds/false.json b/tests/fake-icds/false.json
new file mode 100644
index 0000000000000000000000000000000000000000..c508d5366f70bba37fcc09d128b6537c4adb2c79
--- /dev/null
+++ b/tests/fake-icds/false.json
@@ -0,0 +1 @@
+false
diff --git a/tests/fake-icds/home/.local/share/vulkan/icd.d/invalid.json b/tests/fake-icds/home/.local/share/vulkan/icd.d/invalid.json
new file mode 100644
index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93
--- /dev/null
+++ b/tests/fake-icds/home/.local/share/vulkan/icd.d/invalid.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/fake-icds/no-api-version.json b/tests/fake-icds/no-api-version.json
new file mode 100644
index 0000000000000000000000000000000000000000..1736f34f3da239b1dc8d72d1887c7e3eb819f2c5
--- /dev/null
+++ b/tests/fake-icds/no-api-version.json
@@ -0,0 +1,6 @@
+{
+    "ICD": {
+        "library_path": "libfoo.so"
+    },
+    "file_format_version": "1.0.0"
+}
diff --git a/tests/fake-icds/no-library.json b/tests/fake-icds/no-library.json
new file mode 100644
index 0000000000000000000000000000000000000000..7cd3af7be50dfdaf93ae3283d19ed575960900eb
--- /dev/null
+++ b/tests/fake-icds/no-library.json
@@ -0,0 +1,6 @@
+{
+    "ICD": {
+        "api_version": "1.1.102"
+    },
+    "file_format_version": "1.0.0"
+}
diff --git a/tests/fake-icds/null.json b/tests/fake-icds/null.json
new file mode 100644
index 0000000000000000000000000000000000000000..19765bd501b636fce433540d9e6735f51d66151d
--- /dev/null
+++ b/tests/fake-icds/null.json
@@ -0,0 +1 @@
+null
diff --git a/tests/fake-icds/str.json b/tests/fake-icds/str.json
new file mode 100644
index 0000000000000000000000000000000000000000..749d69f41ec22d02ab902ae8a3dc995a8361694a
--- /dev/null
+++ b/tests/fake-icds/str.json
@@ -0,0 +1 @@
+"hello, world!"
diff --git a/tests/fake-icds/usr/lib/mock-abi/GL/glvnd/egl_vendor.d/relative.json b/tests/fake-icds/usr/lib/mock-abi/GL/glvnd/egl_vendor.d/relative.json
new file mode 100644
index 0000000000000000000000000000000000000000..13ff8653edefcb7b47331c2a5628878b2f9dc863
--- /dev/null
+++ b/tests/fake-icds/usr/lib/mock-abi/GL/glvnd/egl_vendor.d/relative.json
@@ -0,0 +1,6 @@
+{
+    "ICD": {
+        "library_path": "../libEGL_relative.so"
+    },
+    "file_format_version": "1.0.0"
+}
diff --git a/tests/fake-icds/usr/lib/mock-abi/GL/vulkan/icd.d/invalid.json b/tests/fake-icds/usr/lib/mock-abi/GL/vulkan/icd.d/invalid.json
new file mode 100644
index 0000000000000000000000000000000000000000..fe51488c7066f6687ef680d6bfaa4f7768ef205c
--- /dev/null
+++ b/tests/fake-icds/usr/lib/mock-abi/GL/vulkan/icd.d/invalid.json
@@ -0,0 +1 @@
+[]
diff --git a/tests/fake-icds/usr/lib/mock-abi/vulkan/icd.d/relative.json b/tests/fake-icds/usr/lib/mock-abi/vulkan/icd.d/relative.json
new file mode 100644
index 0000000000000000000000000000000000000000..da0863d13cbc7cb2cad75f8c5eb947a45f90814d
--- /dev/null
+++ b/tests/fake-icds/usr/lib/mock-abi/vulkan/icd.d/relative.json
@@ -0,0 +1,7 @@
+{
+    "ICD": {
+        "api_version": "1.1.1",
+        "library_path": "../libvulkan_relative.so"
+    },
+    "file_format_version": "1.0.0"
+}
diff --git a/tests/fake-icds/usr/local/share/vulkan/icd.d/intel_icd.i686.json b/tests/fake-icds/usr/local/share/vulkan/icd.d/intel_icd.i686.json
new file mode 100644
index 0000000000000000000000000000000000000000..0558e2756003e826b2b35c015a1e33c683b3f525
--- /dev/null
+++ b/tests/fake-icds/usr/local/share/vulkan/icd.d/intel_icd.i686.json
@@ -0,0 +1,7 @@
+{
+    "ICD": {
+        "api_version": "1.1.102",
+        "library_path": "/usr/lib/i386-linux-gnu/libvulkan_intel.so"
+    },
+    "file_format_version": "1.0.0"
+}
\ No newline at end of file
diff --git a/tests/fake-icds/usr/share/glvnd/egl_vendor.d/50_mesa.json b/tests/fake-icds/usr/share/glvnd/egl_vendor.d/50_mesa.json
new file mode 100644
index 0000000000000000000000000000000000000000..8aaaa100ffa5134672c27af08cbf33e77984602c
--- /dev/null
+++ b/tests/fake-icds/usr/share/glvnd/egl_vendor.d/50_mesa.json
@@ -0,0 +1,6 @@
+{
+    "file_format_version" : "1.0.0",
+    "ICD" : {
+        "library_path" : "libEGL_mesa.so.0"
+    }
+}
diff --git a/tests/fake-icds/usr/share/vulkan/icd.d/intel_icd.x86_64.json b/tests/fake-icds/usr/share/vulkan/icd.d/intel_icd.x86_64.json
new file mode 100644
index 0000000000000000000000000000000000000000..641f41b5ed46ce29190f48fffd4df729ddcd6e34
--- /dev/null
+++ b/tests/fake-icds/usr/share/vulkan/icd.d/intel_icd.x86_64.json
@@ -0,0 +1,7 @@
+{
+    "ICD": {
+        "api_version": "1.1.102",
+        "library_path": "/usr/lib/x86_64-linux-gnu/libvulkan_intel.so"
+    },
+    "file_format_version": "1.0.0"
+}
\ No newline at end of file
diff --git a/tests/graphics.c b/tests/graphics.c
index c02f512c9a9c2b401e7f35a7761ec64aca24759e..7199c5369049cb00ef4b8bd86a2a1a7c59c60c2d 100644
--- a/tests/graphics.c
+++ b/tests/graphics.c
@@ -25,6 +25,7 @@
 
 #include <steam-runtime-tools/steam-runtime-tools.h>
 
+#include <errno.h>
 #include <string.h>
 #include <unistd.h>
 
@@ -40,24 +41,137 @@ static const char *argv0;
 
 typedef struct
 {
-  int unused;
+  gchar *srcdir;
   gchar *builddir;
+  gchar **fake_icds_envp;
 } Fixture;
 
 typedef struct
 {
-  int unused;
+  enum
+  {
+    ICD_MODE_NORMAL,
+    ICD_MODE_XDG_DIRS,
+    ICD_MODE_FLATPAK,
+    ICD_MODE_EXPLICIT_DIRS,
+    ICD_MODE_EXPLICIT_FILENAMES,
+    ICD_MODE_RELATIVE_FILENAMES,
+  } icd_mode;
 } Config;
 
 static void
 setup (Fixture *f,
        gconstpointer context)
 {
-  G_GNUC_UNUSED const Config *config = context;
+  const Config *config = context;
+  gchar *tmp;
+
+  f->srcdir = g_strdup (g_getenv ("G_TEST_SRCDIR"));
   f->builddir = g_strdup (g_getenv ("G_TEST_BUILDDIR"));
 
+  if (f->srcdir == NULL)
+    f->srcdir = g_path_get_dirname (argv0);
+
   if (f->builddir == NULL)
     f->builddir = g_path_get_dirname (argv0);
+
+  if (g_chdir (f->srcdir) != 0)
+    g_error ("chdir %s: %s", f->srcdir, g_strerror (errno));
+
+  f->fake_icds_envp = g_get_environ ();
+
+  if (config == NULL || config->icd_mode != ICD_MODE_RELATIVE_FILENAMES)
+    {
+      tmp = g_build_filename (f->srcdir, "fake-icds", NULL);
+      f->fake_icds_envp = g_environ_setenv (f->fake_icds_envp,
+                                            "SRT_TEST_ICD_SYSROOT", tmp, TRUE);
+      g_free (tmp);
+    }
+
+  f->fake_icds_envp = g_environ_setenv (f->fake_icds_envp,
+                                        "HOME", "/home", TRUE);
+
+  if (config != NULL && config->icd_mode == ICD_MODE_XDG_DIRS)
+    {
+      f->fake_icds_envp = g_environ_setenv (f->fake_icds_envp,
+                                            "XDG_CONFIG_DIRS", "/confdir", TRUE);
+      f->fake_icds_envp = g_environ_setenv (f->fake_icds_envp,
+                                            "XDG_DATA_HOME", "/datahome", TRUE);
+      f->fake_icds_envp = g_environ_setenv (f->fake_icds_envp,
+                                            "XDG_DATA_DIRS", "/datadir", TRUE);
+    }
+  else
+    {
+      f->fake_icds_envp = g_environ_unsetenv (f->fake_icds_envp,
+                                              "XDG_CONFIG_DIRS");
+      f->fake_icds_envp = g_environ_unsetenv (f->fake_icds_envp,
+                                              "XDG_DATA_HOME");
+      f->fake_icds_envp = g_environ_unsetenv (f->fake_icds_envp,
+                                              "XDG_DATA_DIRS");
+    }
+
+  if (config != NULL && config->icd_mode == ICD_MODE_EXPLICIT_FILENAMES)
+    {
+      f->fake_icds_envp = g_environ_setenv (f->fake_icds_envp,
+                                            "__EGL_VENDOR_LIBRARY_FILENAMES",
+                                            "/not-a-file:"
+                                            "/null.json:"
+                                            "/false.json:"
+                                            "/str.json:"
+                                            "/no-library.json",
+                                            TRUE);
+      f->fake_icds_envp = g_environ_setenv (f->fake_icds_envp,
+                                            "VK_ICD_FILENAMES",
+                                            "/not-a-file:"
+                                            "/null.json:"
+                                            "/false.json:"
+                                            "/str.json:"
+                                            "/no-library.json:"
+                                            "/no-api-version.json",
+                                            TRUE);
+    }
+  else if (config != NULL && config->icd_mode == ICD_MODE_RELATIVE_FILENAMES)
+    {
+      f->fake_icds_envp = g_environ_setenv (f->fake_icds_envp,
+                                            "__EGL_VENDOR_LIBRARY_FILENAMES",
+                                            "fake-icds/not-a-file:"
+                                            "fake-icds/usr/share/glvnd/egl_vendor.d/50_mesa.json:"
+                                            "fake-icds/null.json:"
+                                            "fake-icds/false.json:"
+                                            "fake-icds/str.json:"
+                                            "fake-icds/no-library.json",
+                                            TRUE);
+      f->fake_icds_envp = g_environ_setenv (f->fake_icds_envp,
+                                            "VK_ICD_FILENAMES",
+                                            "fake-icds/not-a-file:"
+                                            "fake-icds/usr/share/vulkan/icd.d/intel_icd.x86_64.json:"
+                                            "fake-icds/null.json:"
+                                            "fake-icds/false.json:"
+                                            "fake-icds/str.json:"
+                                            "fake-icds/no-library.json:"
+                                            "fake-icds/no-api-version.json",
+                                            TRUE);
+    }
+  else
+    {
+      f->fake_icds_envp = g_environ_unsetenv (f->fake_icds_envp,
+                                              "__EGL_VENDOR_LIBRARY_FILENAMES");
+      f->fake_icds_envp = g_environ_unsetenv (f->fake_icds_envp,
+                                              "VK_ICD_FILENAMES");
+    }
+
+  if (config != NULL && config->icd_mode == ICD_MODE_EXPLICIT_DIRS)
+    {
+      f->fake_icds_envp = g_environ_setenv (f->fake_icds_envp,
+                                            "__EGL_VENDOR_LIBRARY_DIRS",
+                                            "/egl1:/egl2",
+                                            TRUE);
+    }
+  else
+    {
+      f->fake_icds_envp = g_environ_unsetenv (f->fake_icds_envp,
+                                              "__EGL_VENDOR_LIBRARY_DIRS");
+    }
 }
 
 static void
@@ -66,7 +180,9 @@ teardown (Fixture *f,
 {
   G_GNUC_UNUSED const Config *config = context;
 
+  g_free (f->srcdir);
   g_free (f->builddir);
+  g_strfreev (f->fake_icds_envp);
 }
 
 /*
@@ -393,6 +509,825 @@ test_bad_vulkan (Fixture *f,
   g_object_unref (info);
 }
 
+/*
+ * Assert that @icd is internally consistent.
+ */
+static void
+assert_egl_icd (SrtEglIcd *icd)
+{
+  GError *error = NULL;
+  GError *error_property = NULL;
+  gchar *json_path = NULL;
+  gchar *library_path = NULL;
+  gchar *resolved = NULL;
+  gchar *resolved_property = NULL;
+
+  g_assert_true (SRT_IS_EGL_ICD (icd));
+
+  g_object_get (icd,
+                "error", &error_property,
+                "json-path", &json_path,
+                "library-path", &library_path,
+                "resolved-library-path", &resolved_property,
+                NULL);
+  resolved = srt_egl_icd_resolve_library_path (icd);
+
+  g_assert_cmpstr (json_path, !=, NULL);
+  g_assert_cmpstr (json_path, ==, srt_egl_icd_get_json_path (icd));
+  g_assert_true (g_path_is_absolute (json_path));
+
+  /* These are invariants, even if they're NULL */
+  g_assert_cmpstr (library_path, ==, srt_egl_icd_get_library_path (icd));
+  g_assert_cmpstr (resolved_property, ==, resolved);
+
+  if (error_property == NULL)
+    {
+      g_assert_true (srt_egl_icd_check_error (icd, NULL));
+      g_assert_true (srt_egl_icd_check_error (icd, &error));
+      g_assert_no_error (error);
+      g_assert_no_error (error_property);
+      g_assert_cmpstr (library_path, !=, NULL);
+      g_assert_cmpstr (resolved, !=, NULL);
+      g_assert_cmpstr (resolved_property, !=, NULL);
+
+      if (strchr (resolved, '/') == NULL)
+        {
+          g_assert_cmpstr (resolved, ==, library_path);
+        }
+      else
+        {
+          g_assert_true (g_path_is_absolute (resolved));
+        }
+    }
+  else
+    {
+      g_assert_false (srt_egl_icd_check_error (icd, NULL));
+      g_assert_false (srt_egl_icd_check_error (icd, &error));
+      g_assert_nonnull (error);
+      g_assert_error (error, error_property->domain, error_property->code);
+      g_assert_cmpstr (error->message, ==, error_property->message);
+      g_assert_cmpstr (library_path, ==, NULL);
+      g_assert_cmpstr (resolved, ==, NULL);
+      g_assert_cmpstr (resolved_property, ==, NULL);
+    }
+
+  g_clear_error (&error);
+  g_clear_error (&error_property);
+  g_free (json_path);
+  g_free (library_path);
+  g_free (resolved);
+  g_free (resolved_property);
+}
+
+/*
+ * Assert that @icd is internally consistent and in a failed state.
+ */
+static void
+assert_egl_icd_has_error (SrtEglIcd *icd)
+{
+  g_assert_false (srt_egl_icd_check_error (icd, NULL));
+  assert_egl_icd (icd);
+}
+
+/*
+ * Assert that @icd is internally consistent and in a successful state.
+ */
+static void
+assert_egl_icd_no_error (SrtEglIcd *icd)
+{
+  GError *error = NULL;
+
+  srt_egl_icd_check_error (icd, &error);
+  g_assert_no_error (error);
+  assert_egl_icd (icd);
+}
+
+static gboolean
+same_stat (GStatBuf *left,
+           GStatBuf *right)
+{
+  return left->st_dev == right->st_dev && left->st_ino == right->st_ino;
+}
+
+/*
+ * We don't assert that filenames are literally the same, because they
+ * might canonicalize differently in the presence of symlinks: we just
+ * assert that they are the same file.
+ */
+static void
+assert_same_file (const char *expected,
+                  const char *actual)
+{
+  GStatBuf expected_stat, actual_stat;
+
+  if (g_stat (expected, &expected_stat) != 0)
+    g_error ("stat %s: %s", expected, g_strerror (errno));
+
+  if (g_stat (actual, &actual_stat) != 0)
+    g_error ("stat %s: %s", actual, g_strerror (errno));
+
+  if (!same_stat (&expected_stat, &actual_stat))
+    g_error ("%s is not the same file as %s", expected, actual);
+}
+
+static void
+test_icd_egl (Fixture *f,
+              gconstpointer context)
+{
+  const Config *config = context;
+  SrtSystemInfo *info = srt_system_info_new (NULL);
+  GList *icds;
+  const GList *iter;
+  const char * const multiarchs[] = { "mock-abi", NULL };
+
+  srt_system_info_set_environ (info, f->fake_icds_envp);
+
+  if (config != NULL && config->icd_mode == ICD_MODE_FLATPAK)
+    icds = srt_system_info_list_egl_icds (info, multiarchs);
+  else
+    icds = srt_system_info_list_egl_icds (info, NULL);
+
+  for (iter = icds; iter != NULL; iter = iter->next)
+    {
+      GError *error = NULL;
+
+      g_test_message ("ICD: %s", srt_egl_icd_get_json_path (iter->data));
+
+      if (srt_egl_icd_check_error (iter->data, &error))
+        {
+          g_test_message ("\tlibrary: %s",
+                          srt_egl_icd_get_library_path (iter->data));
+        }
+      else
+        {
+          g_test_message ("\terror: %s", error->message);
+          g_clear_error (&error);
+        }
+    }
+
+  if (config != NULL && config->icd_mode == ICD_MODE_EXPLICIT_DIRS)
+    {
+      SrtEglIcd *other;
+      gchar *resolved;
+
+      iter = icds;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==,
+                       "/egl1/AAA.json");
+      assert_egl_icd_has_error (iter->data);
+
+      other = srt_egl_icd_new_replace_library_path (iter->data,
+                                                    "/run/host/libEGL_icd.so");
+      /* Copying an invalid ICD yields another invalid ICD. */
+      assert_egl_icd_has_error (iter->data);
+      g_object_unref (other);
+
+      iter = iter->next;
+      /* We sort lexicographically with strcmp(), so BBB comes before a. */
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==,
+                       "/egl1/BBB.json");
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==,
+                       "/egl1/a.json");
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==,
+                       "/egl1/b.json");
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==,
+                       "/egl1/z.json");
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==,
+                       "/egl2/absolute.json");
+      assert_egl_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_egl_icd_get_library_path (iter->data), ==,
+                       "/opt/libEGL_myvendor.so");
+      resolved = srt_egl_icd_resolve_library_path (iter->data);
+      g_assert_cmpstr (resolved, ==, "/opt/libEGL_myvendor.so");
+      g_free (resolved);
+
+      iter = iter->next;
+      g_assert_null (iter);
+    }
+  else if (config != NULL && config->icd_mode == ICD_MODE_EXPLICIT_FILENAMES)
+    {
+      iter = icds;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==, "/not-a-file");
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==, "/null.json");
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==, "/false.json");
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==, "/str.json");
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==, "/no-library.json");
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_null (iter);
+    }
+  else if (config != NULL && config->icd_mode == ICD_MODE_RELATIVE_FILENAMES)
+    {
+      const char *path;
+      gchar *resolved;
+
+      iter = icds;
+      g_assert_nonnull (iter);
+      path = srt_egl_icd_get_json_path (iter->data);
+      g_assert_true (g_str_has_suffix (path, "/fake-icds/not-a-file"));
+      g_assert_true (g_path_is_absolute (path));
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      path = srt_egl_icd_get_json_path (iter->data);
+      g_assert_true (g_str_has_suffix (path, "/fake-icds/usr/share/glvnd/egl_vendor.d/50_mesa.json"));
+      g_assert_true (g_path_is_absolute (path));
+      assert_same_file ("fake-icds/usr/share/glvnd/egl_vendor.d/50_mesa.json", path);
+      assert_egl_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_egl_icd_get_library_path (iter->data), ==,
+                       "libEGL_mesa.so.0");
+      resolved = srt_egl_icd_resolve_library_path (iter->data);
+      g_assert_cmpstr (resolved, ==, "libEGL_mesa.so.0");
+      g_free (resolved);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      path = srt_egl_icd_get_json_path (iter->data);
+      g_assert_true (g_str_has_suffix (path, "/fake-icds/null.json"));
+      g_assert_true (g_path_is_absolute (path));
+      assert_same_file ("fake-icds/null.json", path);
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      path = srt_egl_icd_get_json_path (iter->data);
+      g_assert_true (g_str_has_suffix (path, "/fake-icds/false.json"));
+      g_assert_true (g_path_is_absolute (path));
+      assert_same_file ("fake-icds/false.json", path);
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      path = srt_egl_icd_get_json_path (iter->data);
+      g_assert_true (g_str_has_suffix (path, "/fake-icds/str.json"));
+      g_assert_true (g_path_is_absolute (path));
+      assert_same_file ("fake-icds/str.json", path);
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      path = srt_egl_icd_get_json_path (iter->data);
+      g_assert_true (g_str_has_suffix (path, "/fake-icds/no-library.json"));
+      g_assert_true (g_path_is_absolute (path));
+      assert_same_file ("fake-icds/no-library.json", path);
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_null (iter);
+    }
+  else if (config != NULL && config->icd_mode == ICD_MODE_FLATPAK)
+    {
+      SrtEglIcd *other;
+      gchar *resolved;
+
+      iter = icds;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==,
+                       "/usr/lib/mock-abi/GL/glvnd/egl_vendor.d/relative.json");
+      assert_egl_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_egl_icd_get_library_path (iter->data), ==,
+                       "../libEGL_relative.so");
+      resolved = srt_egl_icd_resolve_library_path (iter->data);
+      g_assert_cmpstr (resolved, ==,
+                       "/usr/lib/mock-abi/GL/glvnd/egl_vendor.d/../libEGL_relative.so");
+      g_free (resolved);
+
+      other = srt_egl_icd_new_replace_library_path (iter->data,
+                                                    "/run/host/libEGL.so");
+      assert_egl_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (other), ==,
+                       srt_egl_icd_get_json_path (iter->data));
+      /* The pointers are not equal */
+      g_assert_true (srt_egl_icd_get_json_path (other) !=
+                     srt_egl_icd_get_json_path (iter->data));
+      g_assert_cmpstr (srt_egl_icd_get_library_path (other), ==,
+                       "/run/host/libEGL.so");
+      g_object_unref (other);
+
+      iter = iter->next;
+      g_assert_null (iter);
+    }
+  else
+    {
+      gchar *resolved;
+
+      /* EGL ICDs don't respect the XDG variables, so XDG_DIRS is the same
+       * as NORMAL. */
+      iter = icds;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==,
+                       "/etc/glvnd/egl_vendor.d/invalid.json");
+      /* This one is invalid. */
+      assert_egl_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_egl_icd_get_json_path (iter->data), ==,
+                       "/usr/share/glvnd/egl_vendor.d/50_mesa.json");
+      assert_egl_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_egl_icd_get_library_path (iter->data), ==,
+                       "libEGL_mesa.so.0");
+      resolved = srt_egl_icd_resolve_library_path (iter->data);
+      g_assert_cmpstr (resolved, ==, "libEGL_mesa.so.0");
+      g_free (resolved);
+
+      iter = iter->next;
+      g_assert_null (iter);
+    }
+
+  g_list_free_full (icds, g_object_unref);
+}
+
+/*
+ * Assert that @icd is internally consistent.
+ */
+static void
+assert_vulkan_icd (SrtVulkanIcd *icd)
+{
+  GError *error = NULL;
+  GError *error_property = NULL;
+  gchar *api_version = NULL;
+  gchar *json_path = NULL;
+  gchar *library_path = NULL;
+  gchar *resolved = NULL;
+  gchar *resolved_property = NULL;
+
+  g_assert_true (SRT_IS_VULKAN_ICD (icd));
+
+  g_object_get (icd,
+                "api-version", &api_version,
+                "error", &error_property,
+                "json-path", &json_path,
+                "library-path", &library_path,
+                "resolved-library-path", &resolved_property,
+                NULL);
+  resolved = srt_vulkan_icd_resolve_library_path (icd);
+
+  g_assert_cmpstr (json_path, !=, NULL);
+  g_assert_cmpstr (json_path, ==, srt_vulkan_icd_get_json_path (icd));
+  g_assert_true (g_path_is_absolute (json_path));
+
+  /* These are invariants, even if they're NULL */
+  g_assert_cmpstr (api_version, ==, srt_vulkan_icd_get_api_version (icd));
+  g_assert_cmpstr (library_path, ==, srt_vulkan_icd_get_library_path (icd));
+  g_assert_cmpstr (resolved_property, ==, resolved);
+
+  if (error_property == NULL)
+    {
+      g_assert_true (srt_vulkan_icd_check_error (icd, NULL));
+      g_assert_true (srt_vulkan_icd_check_error (icd, &error));
+      g_assert_no_error (error);
+      g_assert_no_error (error_property);
+      g_assert_cmpstr (library_path, !=, NULL);
+      g_assert_cmpstr (api_version, !=, NULL);
+      g_assert_cmpstr (resolved, !=, NULL);
+      g_assert_cmpstr (resolved_property, !=, NULL);
+
+      if (strchr (resolved, '/') == NULL)
+        {
+          g_assert_cmpstr (resolved, ==, library_path);
+        }
+      else
+        {
+          g_assert_true (g_path_is_absolute (resolved));
+        }
+    }
+  else
+    {
+      g_assert_false (srt_vulkan_icd_check_error (icd, NULL));
+      g_assert_false (srt_vulkan_icd_check_error (icd, &error));
+      g_assert_nonnull (error);
+      g_assert_error (error, error_property->domain, error_property->code);
+      g_assert_cmpstr (error->message, ==, error_property->message);
+      g_assert_cmpstr (library_path, ==, NULL);
+      g_assert_cmpstr (api_version, ==, NULL);
+      g_assert_cmpstr (resolved, ==, NULL);
+      g_assert_cmpstr (resolved_property, ==, NULL);
+    }
+
+  g_clear_error (&error);
+  g_clear_error (&error_property);
+  g_free (api_version);
+  g_free (json_path);
+  g_free (library_path);
+  g_free (resolved);
+  g_free (resolved_property);
+}
+
+/*
+ * Assert that @icd is internally consistent and in a failed state.
+ */
+static void
+assert_vulkan_icd_has_error (SrtVulkanIcd *icd)
+{
+  g_assert_false (srt_vulkan_icd_check_error (icd, NULL));
+  assert_vulkan_icd (icd);
+}
+
+/*
+ * Assert that @icd is internally consistent and in a successful state.
+ */
+static void
+assert_vulkan_icd_no_error (SrtVulkanIcd *icd)
+{
+  GError *error = NULL;
+
+  srt_vulkan_icd_check_error (icd, &error);
+  g_assert_no_error (error);
+  assert_vulkan_icd (icd);
+}
+
+static void
+test_icd_vulkan (Fixture *f,
+                 gconstpointer context)
+{
+  const Config *config = context;
+  SrtSystemInfo *info = srt_system_info_new (NULL);
+  GList *icds;
+  const GList *iter;
+  const char * const multiarchs[] = { "mock-abi", NULL };
+
+  srt_system_info_set_environ (info, f->fake_icds_envp);
+
+  if (config != NULL && config->icd_mode == ICD_MODE_FLATPAK)
+    icds = srt_system_info_list_vulkan_icds (info, multiarchs);
+  else
+    icds = srt_system_info_list_vulkan_icds (info, NULL);
+
+  for (iter = icds; iter != NULL; iter = iter->next)
+    {
+      GError *error = NULL;
+
+      g_test_message ("ICD: %s", srt_vulkan_icd_get_json_path (iter->data));
+
+      if (srt_vulkan_icd_check_error (iter->data, &error))
+        {
+          g_test_message ("\tlibrary: %s",
+                          srt_vulkan_icd_get_library_path (iter->data));
+        }
+      else
+        {
+          g_test_message ("\terror: %s", error->message);
+          g_clear_error (&error);
+        }
+    }
+
+  if (config != NULL && config->icd_mode == ICD_MODE_EXPLICIT_FILENAMES)
+    {
+      SrtVulkanIcd *other;
+
+      iter = icds;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==, "/not-a-file");
+      assert_vulkan_icd_has_error (iter->data);
+
+      other = srt_vulkan_icd_new_replace_library_path (iter->data,
+                                                       "/run/host/vulkan_icd.so");
+      /* Copying an invalid ICD yields another invalid ICD. */
+      assert_vulkan_icd_has_error (iter->data);
+      g_object_unref (other);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==, "/null.json");
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==, "/false.json");
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==, "/str.json");
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==, "/no-library.json");
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==, "/no-api-version.json");
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_null (iter);
+    }
+  else if (config != NULL && config->icd_mode == ICD_MODE_RELATIVE_FILENAMES)
+    {
+      const char *path;
+
+      iter = icds;
+      g_assert_nonnull (iter);
+      path = srt_vulkan_icd_get_json_path (iter->data);
+      g_assert_true (g_str_has_suffix (path, "/fake-icds/not-a-file"));
+      g_assert_true (g_path_is_absolute (path));
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      path = srt_vulkan_icd_get_json_path (iter->data);
+      g_assert_true (g_str_has_suffix (path, "/fake-icds/usr/share/vulkan/icd.d/intel_icd.x86_64.json"));
+      g_assert_true (g_path_is_absolute (path));
+      assert_same_file ("fake-icds/usr/share/vulkan/icd.d/intel_icd.x86_64.json", path);
+      assert_vulkan_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_vulkan_icd_get_library_path (iter->data), ==,
+                       "/usr/lib/x86_64-linux-gnu/libvulkan_intel.so");
+      g_assert_cmpstr (srt_vulkan_icd_get_api_version (iter->data), ==, "1.1.102");
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      path = srt_vulkan_icd_get_json_path (iter->data);
+      g_assert_true (g_str_has_suffix (path, "/fake-icds/null.json"));
+      g_assert_true (g_path_is_absolute (path));
+      assert_same_file ("fake-icds/null.json", path);
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      path = srt_vulkan_icd_get_json_path (iter->data);
+      g_assert_true (g_str_has_suffix (path, "/fake-icds/false.json"));
+      g_assert_true (g_path_is_absolute (path));
+      assert_same_file ("fake-icds/false.json", path);
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      path = srt_vulkan_icd_get_json_path (iter->data);
+      g_assert_true (g_str_has_suffix (path, "/fake-icds/str.json"));
+      g_assert_true (g_path_is_absolute (path));
+      assert_same_file ("fake-icds/str.json", path);
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      path = srt_vulkan_icd_get_json_path (iter->data);
+      g_assert_true (g_str_has_suffix (path, "/fake-icds/no-library.json"));
+      g_assert_true (g_path_is_absolute (path));
+      assert_same_file ("fake-icds/no-library.json", path);
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      path = srt_vulkan_icd_get_json_path (iter->data);
+      g_assert_true (g_str_has_suffix (path, "/fake-icds/no-api-version.json"));
+      g_assert_true (g_path_is_absolute (path));
+      assert_same_file ("fake-icds/no-api-version.json", path);
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_null (iter);
+    }
+  else if (config != NULL && config->icd_mode == ICD_MODE_FLATPAK)
+    {
+      gchar *resolved;
+      SrtVulkanIcd *other;
+
+      iter = icds;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/etc/xdg/vulkan/icd.d/invalid.json");
+      /* This is not valid JSON (it's an empty file) so loading it fails */
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/etc/vulkan/icd.d/basename.json");
+      assert_vulkan_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_vulkan_icd_get_library_path (iter->data), ==,
+                       "libvulkan_basename.so");
+      g_assert_cmpstr (srt_vulkan_icd_get_api_version (iter->data), ==, "1.2.3");
+      resolved = srt_vulkan_icd_resolve_library_path (iter->data);
+      g_assert_cmpstr (resolved, ==, "libvulkan_basename.so");
+      g_free (resolved);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/usr/lib/mock-abi/GL/vulkan/icd.d/invalid.json");
+      /* This has a JSON array, not an object, so loading it fails */
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/usr/lib/mock-abi/vulkan/icd.d/relative.json");
+      assert_vulkan_icd_no_error (iter->data);
+      resolved = srt_vulkan_icd_resolve_library_path (iter->data);
+      g_assert_cmpstr (resolved, ==,
+                       "/usr/lib/mock-abi/vulkan/icd.d/../libvulkan_relative.so");
+      g_free (resolved);
+
+      other = srt_vulkan_icd_new_replace_library_path (iter->data,
+                                                       "/run/host/vulkan_icd.so");
+      assert_vulkan_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (other), ==,
+                       srt_vulkan_icd_get_json_path (iter->data));
+      /* The pointers are not equal */
+      g_assert_true (srt_vulkan_icd_get_json_path (other) !=
+                     srt_vulkan_icd_get_json_path (iter->data));
+      g_assert_cmpstr (srt_vulkan_icd_get_api_version (other), ==,
+                       srt_vulkan_icd_get_api_version (iter->data));
+      /* The pointers are not equal */
+      g_assert_true (srt_vulkan_icd_get_api_version (other) !=
+                     srt_vulkan_icd_get_api_version (iter->data));
+      g_assert_cmpstr (srt_vulkan_icd_get_library_path (other), ==,
+                       "/run/host/vulkan_icd.so");
+      g_object_unref (other);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/usr/local/share/vulkan/icd.d/intel_icd.i686.json");
+      assert_vulkan_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_vulkan_icd_get_library_path (iter->data), ==,
+                       "/usr/lib/i386-linux-gnu/libvulkan_intel.so");
+      g_assert_cmpstr (srt_vulkan_icd_get_api_version (iter->data), ==, "1.1.102");
+      resolved = srt_vulkan_icd_resolve_library_path (iter->data);
+      g_assert_cmpstr (resolved, ==, "/usr/lib/i386-linux-gnu/libvulkan_intel.so");
+      g_free (resolved);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/usr/share/vulkan/icd.d/intel_icd.x86_64.json");
+      assert_vulkan_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_vulkan_icd_get_library_path (iter->data), ==,
+                       "/usr/lib/x86_64-linux-gnu/libvulkan_intel.so");
+      g_assert_cmpstr (srt_vulkan_icd_get_api_version (iter->data), ==, "1.1.102");
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/home/.local/share/vulkan/icd.d/invalid.json");
+      /* This one lacks the required format version */
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_null (iter);
+    }
+  else if (config != NULL && config->icd_mode == ICD_MODE_XDG_DIRS)
+    {
+      gchar *resolved;
+
+      iter = icds;
+      g_assert_nonnull (iter);
+      /* We load $XDG_CONFIG_DIRS instead of /etc/xdg */
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/confdir/vulkan/icd.d/invalid.json");
+      /* Not format 1.0.x, so we can't be confident that we're reading
+       * it correctly */
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      /* /etc is unaffected by XDG variables */
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/etc/vulkan/icd.d/basename.json");
+      assert_vulkan_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_vulkan_icd_get_library_path (iter->data), ==,
+                       "libvulkan_basename.so");
+      g_assert_cmpstr (srt_vulkan_icd_get_api_version (iter->data), ==, "1.2.3");
+      resolved = srt_vulkan_icd_resolve_library_path (iter->data);
+      g_assert_cmpstr (resolved, ==, "libvulkan_basename.so");
+      g_free (resolved);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      /* We load $XDG_DATA_DIRS instead of /usr/local/share:/usr/share.
+       * In this case it only has one item. */
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/datadir/vulkan/icd.d/invalid.json");
+      /* Not format 1.0.x, so we can't be confident that we're reading
+       * it correctly */
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      /* We load $XDG_DATA_DIRS *before* $XDG_DATA_HOME for
+       * some reason. This is weird, but it matches the reference
+       * Vulkan loader. */
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/datahome/vulkan/icd.d/invalid.json");
+      /* Missing API version */
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      /* We load $XDG_DATA_HOME *as well as* ~/.local/share for some
+       * reason. This is weird, but it matches the reference Vulkan
+       * loader. */
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/home/.local/share/vulkan/icd.d/invalid.json");
+      /* This one lacks the required format version */
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_null (iter);
+    }
+  else
+    {
+      gchar *resolved;
+
+      iter = icds;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/etc/xdg/vulkan/icd.d/invalid.json");
+      /* This is not valid JSON (it's an empty file) so loading it fails */
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/etc/vulkan/icd.d/basename.json");
+      assert_vulkan_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_vulkan_icd_get_library_path (iter->data), ==,
+                       "libvulkan_basename.so");
+      g_assert_cmpstr (srt_vulkan_icd_get_api_version (iter->data), ==, "1.2.3");
+      resolved = srt_vulkan_icd_resolve_library_path (iter->data);
+      g_assert_cmpstr (resolved, ==, "libvulkan_basename.so");
+      g_free (resolved);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/usr/local/share/vulkan/icd.d/intel_icd.i686.json");
+      assert_vulkan_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_vulkan_icd_get_library_path (iter->data), ==,
+                       "/usr/lib/i386-linux-gnu/libvulkan_intel.so");
+      g_assert_cmpstr (srt_vulkan_icd_get_api_version (iter->data), ==, "1.1.102");
+      resolved = srt_vulkan_icd_resolve_library_path (iter->data);
+      g_assert_cmpstr (resolved, ==, "/usr/lib/i386-linux-gnu/libvulkan_intel.so");
+      g_free (resolved);
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/usr/share/vulkan/icd.d/intel_icd.x86_64.json");
+      assert_vulkan_icd_no_error (iter->data);
+      g_assert_cmpstr (srt_vulkan_icd_get_library_path (iter->data), ==,
+                       "/usr/lib/x86_64-linux-gnu/libvulkan_intel.so");
+      g_assert_cmpstr (srt_vulkan_icd_get_api_version (iter->data), ==, "1.1.102");
+
+      iter = iter->next;
+      g_assert_nonnull (iter);
+      g_assert_cmpstr (srt_vulkan_icd_get_json_path (iter->data), ==,
+                       "/home/.local/share/vulkan/icd.d/invalid.json");
+      /* This one lacks the required format version */
+      assert_vulkan_icd_has_error (iter->data);
+
+      iter = iter->next;
+      g_assert_null (iter);
+    }
+
+  g_list_free_full (icds, g_object_unref);
+}
+
+static const Config dir_config = { ICD_MODE_EXPLICIT_DIRS };
+static const Config filename_config = { ICD_MODE_EXPLICIT_FILENAMES };
+static const Config flatpak_config = { ICD_MODE_FLATPAK };
+static const Config relative_config = { ICD_MODE_RELATIVE_FILENAMES };
+static const Config xdg_config = { ICD_MODE_XDG_DIRS };
+
 int
 main (int argc,
       char **argv)
@@ -416,5 +1351,28 @@ main (int argc,
   g_test_add ("/vulkan-bad", Fixture, NULL,
               setup, test_bad_vulkan, teardown);
 
+  g_test_add ("/icd/egl/basic", Fixture, NULL,
+              setup, test_icd_egl, teardown);
+  g_test_add ("/icd/egl/dirs", Fixture, &dir_config,
+              setup, test_icd_egl, teardown);
+  g_test_add ("/icd/egl/filenames", Fixture, &filename_config,
+              setup, test_icd_egl, teardown);
+  g_test_add ("/icd/egl/flatpak", Fixture, &flatpak_config,
+              setup, test_icd_egl, teardown);
+  g_test_add ("/icd/egl/relative", Fixture, &relative_config,
+              setup, test_icd_egl, teardown);
+  g_test_add ("/icd/egl/xdg", Fixture, &xdg_config,
+              setup, test_icd_egl, teardown);
+  g_test_add ("/icd/vulkan/basic", Fixture, NULL,
+              setup, test_icd_vulkan, teardown);
+  g_test_add ("/icd/vulkan/filenames", Fixture, &filename_config,
+              setup, test_icd_vulkan, teardown);
+  g_test_add ("/icd/vulkan/flatpak", Fixture, &flatpak_config,
+              setup, test_icd_vulkan, teardown);
+  g_test_add ("/icd/vulkan/relative", Fixture, &relative_config,
+              setup, test_icd_vulkan, teardown);
+  g_test_add ("/icd/vulkan/xdg", Fixture, &xdg_config,
+              setup, test_icd_vulkan, teardown);
+
   return g_test_run ();
 }
diff --git a/tests/meson.build b/tests/meson.build
index b734d7ea039953b6beb788bed5e5ea8de0bd0163..f52bd91f45d3c6609b34a24951e5f820c1b0a67f 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -55,6 +55,10 @@ tests_metadir = join_paths(
 
 install_subdir('expectations', install_dir : tests_dir)
 install_subdir('expectations_with_missings', install_dir : tests_dir)
+# To avoid being dependent on readdir() order, don't include more
+# than one *.json file in any directory that we are going to scan for
+# Vulkan *.json files. (The order of EGL *.json files is well-defined.)
+install_subdir('fake-icds', install_dir : tests_dir)
 install_subdir('fake-steam-runtime', install_dir : tests_dir)
 
 executable(