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(