diff --git a/.github/workflows/Linux-samples-gfx.yml b/.github/workflows/Linux-samples-gfx.yml index 8384453adb86..cb397090cba9 100644 --- a/.github/workflows/Linux-samples-gfx.yml +++ b/.github/workflows/Linux-samples-gfx.yml @@ -292,6 +292,54 @@ jobs: make -C samples/gfx/vulkan_driver_pacing CC=x86_64-w64-mingw32-gcc RUNNER=wine check make -C samples/gfx/vulkan_driver_pacing clean + vulkan-custom-driver: + name: Android custom Vulkan driver packages and crash state + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + APT_PKGS: gcc-mingw-w64-x86-64 wine64 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Apt cache key + id: apt + run: sh .github/workflows/scripts/apt-cache-key.sh "$APT_PKGS" + + - name: Restore apt cache + uses: actions/cache/restore@v4 + with: + path: ~/.apt-cache + key: ${{ steps.apt.outputs.key }} + restore-keys: ${{ steps.apt.outputs.prefix }} + + - name: Install dependencies + timeout-minutes: 15 + run: sh .github/workflows/scripts/apt-install.sh $APT_PKGS + + - name: Save apt cache + if: env.APT_CACHE_SAVE == '1' + uses: actions/cache/save@v4 + with: + path: ~/.apt-cache + key: ${{ steps.apt.outputs.key }} + + - name: Regression test (Linux, ASan + UBSan) + run: | + # Package checks, the extraction key, the sticky crash state, + # the session's driver choices and the soname patch, fed from + # buffers; the loader itself is Android-only. + UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \ + make -C samples/gfx/vulkan_custom_driver SANITIZER=address,undefined check + make -C samples/gfx/vulkan_custom_driver clean + + - name: Regression test (mingw-w64, Wine) + run: | + . "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh" + make -C samples/gfx/vulkan_custom_driver CC=x86_64-w64-mingw32-gcc RUNNER=wine check + make -C samples/gfx/vulkan_custom_driver clean + vulkan-upscaler: name: Vulkan temporal upscaler glue (lavapipe, stand-in NGX, FSR and XeSS) runs-on: ubuntu-latest diff --git a/configuration.c b/configuration.c index 86043dc9f536..0b94b0d2ecac 100644 --- a/configuration.c +++ b/configuration.c @@ -2208,6 +2208,9 @@ static struct config_path_setting *populate_settings_path( #endif SETTING_PATH("video_filter", settings->paths.path_softfilter_plugin, false, NULL, true); SETTING_PATH("video_font_path", settings->paths.path_font, false, NULL, true); +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + SETTING_PATH("video_vulkan_driver_path", settings->paths.path_vulkan_driver, false, NULL, true); +#endif SETTING_PATH("video_record_config", settings->paths.path_record_config, false, NULL, true); SETTING_PATH("video_stream_config", settings->paths.path_stream_config, false, NULL, true); SETTING_PATH("video_stream_url", settings->paths.path_stream_url, false, NULL, true); @@ -2506,6 +2509,7 @@ static const struct config_bool_row config_bool_rows[] = { #include "settings/settings_def_input_bind_timeouts.h" #include "settings/settings_def_video_hdr_toggles.h" #include "settings/settings_def_video_upscaler.h" +#include "settings/settings_def_vulkan_driver_path.h" #include "settings/settings_def_video_stereo.h" #include "settings/settings_def_video_headset.h" #include "settings/settings_def_rewind.h" @@ -3344,6 +3348,7 @@ static const struct config_float_row config_float_rows[] = { #include "settings/settings_def_input_bind_timeouts.h" #include "settings/settings_def_video_hdr_toggles.h" #include "settings/settings_def_video_upscaler.h" +#include "settings/settings_def_vulkan_driver_path.h" #include "settings/settings_def_video_stereo.h" #include "settings/settings_def_video_headset.h" #include "settings/settings_def_rewind.h" @@ -3988,6 +3993,7 @@ static const struct config_uint_row config_uint_rows[] = { #include "settings/settings_def_input_bind_timeouts.h" #include "settings/settings_def_video_hdr_toggles.h" #include "settings/settings_def_video_upscaler.h" +#include "settings/settings_def_vulkan_driver_path.h" #include "settings/settings_def_video_stereo.h" #include "settings/settings_def_video_headset.h" #include "settings/settings_def_rewind.h" @@ -4787,6 +4793,7 @@ static const struct config_int_row config_int_rows[] = { #include "settings/settings_def_input_bind_timeouts.h" #include "settings/settings_def_video_hdr_toggles.h" #include "settings/settings_def_video_upscaler.h" +#include "settings/settings_def_vulkan_driver_path.h" #include "settings/settings_def_video_stereo.h" #include "settings/settings_def_video_headset.h" #include "settings/settings_def_rewind.h" @@ -5430,6 +5437,7 @@ static struct config_int_setting *populate_settings_int( #include "settings/settings_def_input_bind_timeouts.h" #include "settings/settings_def_video_hdr_toggles.h" #include "settings/settings_def_video_upscaler.h" +#include "settings/settings_def_vulkan_driver_path.h" #include "settings/settings_def_video_stereo.h" #include "settings/settings_def_video_headset.h" #include "settings/settings_def_rewind.h" @@ -6452,6 +6460,9 @@ void config_set_defaults(settings_t *target) *settings->paths.path_softfilter_plugin = '\0'; *settings->paths.path_audio_dsp_plugin = '\0'; +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + *settings->paths.path_vulkan_driver = '\0'; +#endif *settings->paths.log_dir = '\0'; diff --git a/configuration.h b/configuration.h index de4000a31c6e..a9a99bce5e79 100644 --- a/configuration.h +++ b/configuration.h @@ -1531,6 +1531,9 @@ typedef struct settings char path_cheat_settings[PATH_MAX_LENGTH]; char path_font[PATH_MAX_LENGTH]; char path_rgui_theme_preset[PATH_MAX_LENGTH]; +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + char path_vulkan_driver[PATH_MAX_LENGTH]; +#endif char app_icon[PATH_MAX_LENGTH]; char browse_url[4096]; /* TODO/FIXME - check size */ diff --git a/frontend/drivers/platform_unix.c b/frontend/drivers/platform_unix.c index 0923c71055b7..bd8c87473de1 100644 --- a/frontend/drivers/platform_unix.c +++ b/frontend/drivers/platform_unix.c @@ -105,6 +105,10 @@ #include #endif +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +#include "../../gfx/common/vulkan_custom_driver.h" +#endif + #ifdef ANDROID static void frontend_unix_set_sustained_performance_mode(bool on); @@ -3038,6 +3042,26 @@ static void frontend_unix_get_env(int *argc, args->config_path = android_config_path; } +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + /* Safe mode, from the "Safe start" launcher shortcut. */ + CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra, + (*env)->NewStringUTF(env, "VULKAN_DRIVER")); + + { + bool safe_mode = false; + if (android_app->getStringExtra && jstr) + { + const char *argv = (*env)->GetStringUTFChars(env, jstr, 0); + + safe_mode = string_is_equal(argv, "system"); + __android_log_print(ANDROID_LOG_INFO, + "RetroArch", "[ENV] Vulkan driver: \"%s\".\n", argv); + (*env)->ReleaseStringUTFChars(env, jstr, argv); + } + vulkan_custom_driver_set_safe_mode(safe_mode); + } +#endif + /* Current IME. */ CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra, (*env)->NewStringUTF(env, "IME")); @@ -3976,6 +4000,10 @@ static void frontend_unix_init(void *data) "hideKeyboard", "()V"); GET_METHOD_ID(env, android_app->setSecondDisplay, class, "setSecondDisplay", "(II)V"); +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + GET_METHOD_ID(env, android_app->addSafeStartShortcut, class, + "addSafeStartShortcut", "()V"); +#endif CALL_BOOLEAN_METHOD(env, android_app->is_play_store_build, android_app->activity->clazz, android_app->isPlayStoreBuild); diff --git a/frontend/drivers/platform_unix.h b/frontend/drivers/platform_unix.h index 609cc4da0d21..2cc1a0d537f2 100644 --- a/frontend/drivers/platform_unix.h +++ b/frontend/drivers/platform_unix.h @@ -353,6 +353,9 @@ struct android_app jmethodID showKeyboard; jmethodID hideKeyboard; jmethodID setSecondDisplay; +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + jmethodID addSafeStartShortcut; +#endif /* Written by the Android UI thread in onContentRectChanged(), read by * the video thread in the context drivers, with no lock on either diff --git a/gfx/common/vulkan_common.c b/gfx/common/vulkan_common.c index f8e4ede8288a..e467f90c8abb 100644 --- a/gfx/common/vulkan_common.c +++ b/gfx/common/vulkan_common.c @@ -35,6 +35,9 @@ #include "vulkan_android_device.h" #include "vulkan_android_cache.h" #endif +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +#include "vulkan_custom_driver.h" +#endif #ifdef HAVE_TEMPORAL_UPSCALER #include "vulkan_upscaler.h" #include "../video_upscaler.h" @@ -99,6 +102,10 @@ * Vulkan contexts (multi-window, etc.). The cached_device_vk * mechanism assumes single-context ownership. */ static dylib_t vulkan_library; +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +/* From vulkan_custom_driver_open(), which keeps it: never closed here. */ +static bool vulkan_library_custom; +#endif static VkInstance cached_instance_vk; #if defined(_WIN32) && !defined(__WINRT__) #include "win_vulkan_dxgi.h" @@ -5276,6 +5283,76 @@ static void vulkan_context_openxr_init(gfx_ctx_vulkan_data_t *vk, } #endif +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +/* A custom driver has to see a GPU. A Mesa one must also not report the + * system driver's ID: the hook loads that driver when the custom library + * fails to. Other vendors' builds may report that ID themselves. */ +static bool vulkan_context_custom_driver_ok(gfx_ctx_vulkan_data_t *vk, + uint32_t api_version) +{ + VkPhysicalDeviceDriverProperties driver; + VkPhysicalDeviceProperties2 props; + PFN_vkGetPhysicalDeviceProperties2 get_props2 = NULL; + VkPhysicalDevice gpu = VK_NULL_HANDLE; + uint32_t count = 1; + VkResult res = vkEnumeratePhysicalDevices( + vk->context.instance, &count, &gpu); + + if ((res != VK_SUCCESS && res != VK_INCOMPLETE) || !count || !gpu) + { + RARCH_WARN("[Vulkan] Custom driver found no GPU.\n"); + return false; + } + if (!vulkan_custom_driver_mesa()) + { + RARCH_WARN("[Vulkan] Custom driver is not Mesa: its load cannot be confirmed.\n"); + return true; + } + if (api_version >= VK_API_VERSION_1_1) + get_props2 = (PFN_vkGetPhysicalDeviceProperties2) + vulkan_symbol_wrapper_instance_proc_addr()(vk->context.instance, + "vkGetPhysicalDeviceProperties2"); + if (!get_props2) + return true; + memset(&driver, 0, sizeof(driver)); + memset(&props, 0, sizeof(props)); + driver.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES; + props.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + props.pNext = &driver; + get_props2(gpu, &props); + if (driver.driverID == VK_DRIVER_ID_QUALCOMM_PROPRIETARY) + { + RARCH_WARN("[Vulkan] Custom driver did not load; the system one did.\n"); + return false; + } + return true; +} + +/* The custom driver failed before its surface: init once more. Its path + * is now off for the session, so this one gets the system driver. */ +static bool vulkan_context_init_system(gfx_ctx_vulkan_data_t *vk, + enum vulkan_wsi_type type) +{ + if (vk->context.instance) + { + PFN_vkDestroyInstance destroy = (PFN_vkDestroyInstance) + vulkan_symbol_wrapper_instance_proc_addr()(vk->context.instance, + "vkDestroyInstance"); +#ifdef VULKAN_DEBUG + if (vk->context.debug_callback) + vkDestroyDebugUtilsMessengerEXT(vk->context.instance, + vk->context.debug_callback, NULL); + vk->context.debug_callback = VK_NULL_HANDLE; +#endif + if (destroy) + destroy(vk->context.instance, NULL); + vk->context.instance = VK_NULL_HANDLE; + } + vulkan_custom_driver_failed(); + return vulkan_context_init(vk, type); +} +#endif + bool vulkan_context_init(gfx_ctx_vulkan_data_t *vk, enum vulkan_wsi_type type) { @@ -5285,6 +5362,9 @@ bool vulkan_context_init(gfx_ctx_vulkan_data_t *vk, video_driver_state_t *video_st = video_state_get_ptr(); struct retro_hw_render_context_negotiation_interface_vulkan *iface = (struct retro_hw_render_context_negotiation_interface_vulkan*)video_st->hw_render_context_negotiation; +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + bool custom_driver = false; +#endif if (iface && iface->interface_type != RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN) { @@ -5301,6 +5381,25 @@ bool vulkan_context_init(gfx_ctx_vulkan_data_t *vk, vk->wsi_type = type; vulkan_swapchain_settings_init(&vk->context.swapchain_settings); +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + /* A kept instance keeps the loader it was made with. */ + if (!cached_instance_vk) + { + dylib_t custom = vulkan_custom_driver_open(); + if ( vulkan_library != custom + && (custom || vulkan_library_custom)) + { + if (vulkan_library && !vulkan_library_custom) + dylib_close(vulkan_library); + vulkan_library = custom; + } + vulkan_library_custom = (custom != NULL); + custom_driver = vulkan_library_custom; + } + else + vulkan_custom_driver_kept(); +#endif + if (!vulkan_library) { #ifdef _WIN32 @@ -5345,6 +5444,10 @@ bool vulkan_context_init(gfx_ctx_vulkan_data_t *vk, if (!GetInstanceProcAddr) { RARCH_ERR("[Vulkan] Failed to load vkGetInstanceProcAddr symbol, broken loader?\n"); +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + if (custom_driver) + return vulkan_context_init_system(vk, type); +#endif return false; } @@ -5353,6 +5456,10 @@ bool vulkan_context_init(gfx_ctx_vulkan_data_t *vk, if (!vulkan_symbol_wrapper_load_global_symbols()) { RARCH_ERR("[Vulkan] Failed to load global Vulkan symbols, broken loader?\n"); +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + if (custom_driver) + return vulkan_context_init_system(vk, type); +#endif return false; } @@ -5458,6 +5565,10 @@ bool vulkan_context_init(gfx_ctx_vulkan_data_t *vk, if (vk->context.instance == VK_NULL_HANDLE) { RARCH_ERR("[Vulkan] Failed to create Vulkan instance.\n"); +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + if (custom_driver) + return vulkan_context_init_system(vk, type); +#endif return false; } } @@ -5492,9 +5603,18 @@ bool vulkan_context_init(gfx_ctx_vulkan_data_t *vk, if (!vulkan_load_instance_symbols(vk)) { RARCH_ERR("[Vulkan] Failed to load instance symbols.\n"); +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + if (custom_driver) + return vulkan_context_init_system(vk, type); +#endif return false; } +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + if (custom_driver && !vulkan_context_custom_driver_ok(vk, app.apiVersion)) + return vulkan_context_init_system(vk, type); +#endif + return true; } @@ -5581,6 +5701,8 @@ void vulkan_context_destroy(gfx_ctx_vulkan_data_t *vk, if (record_android_capture_device_pending(vk->context.device)) video_st_flags &= ~VIDEO_FLAG_CACHE_CONTEXT; #endif + if (vk->flags & VK_DATA_FLAG_DROP_CONTEXT) + video_st_flags &= ~VIDEO_FLAG_CACHE_CONTEXT; if (video_st_flags & VIDEO_FLAG_CACHE_CONTEXT) { cached_device_vk = vk->context.device; @@ -5653,7 +5775,14 @@ void vulkan_context_destroy(gfx_ctx_vulkan_data_t *vk, if (vulkan_library) { +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + /* A linker namespace cannot be unloaded. */ + if (!vulkan_library_custom) + dylib_close(vulkan_library); + vulkan_library_custom = false; +#else dylib_close(vulkan_library); +#endif vulkan_library = NULL; } } diff --git a/gfx/common/vulkan_common.h b/gfx/common/vulkan_common.h index cf723ff107a8..6cb2115b3770 100644 --- a/gfx/common/vulkan_common.h +++ b/gfx/common/vulkan_common.h @@ -227,7 +227,9 @@ enum gfx_ctx_vulkan_data_flags VK_DATA_FLAG_EMULATING_MAILBOX = (1 << 4), /* Used to check if we need to use mailbox emulation or not. * Only relevant on Windows for now. */ - VK_DATA_FLAG_FULLSCREEN = (1 << 5) + VK_DATA_FLAG_FULLSCREEN = (1 << 5), + /* Destroy drops the context even under VIDEO_FLAG_CACHE_CONTEXT. */ + VK_DATA_FLAG_DROP_CONTEXT = (1 << 6) }; enum vk_texture_flags diff --git a/gfx/common/vulkan_custom_driver.c b/gfx/common/vulkan_custom_driver.c new file mode 100644 index 000000000000..b5b991f45361 --- /dev/null +++ b/gfx/common/vulkan_custom_driver.c @@ -0,0 +1,605 @@ +/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: + * GPL-3.0-or-later */ +#include + +#include +#include +#include +#include + +#include "vulkan_custom_driver.h" + +enum meta_key +{ + META_KEY_NONE = 0, + META_KEY_SCHEMA, + META_KEY_VENDOR, + META_KEY_LIBRARY, + META_KEY_MIN_API, + META_KEY_NAME, + META_KEY_VERSION +}; + +struct meta_ctx +{ + struct vulkan_custom_driver_meta *meta; + const char *err; + int api_level; + unsigned depth; + enum meta_key key; + bool schema; +}; + +static bool meta_is(const char *s, size_t len, const char *name) +{ + return len == strlen(name) && !memcmp(s, name, len); +} + +/* Whole numbers only: meta.json has no other kind. */ +static bool meta_int(const char *s, size_t len, int *out) +{ + size_t i; + int v = 0; + if (len < 1 || len > 6) + return false; + for (i = 0; i < len; i++) + { + if (s[i] < '0' || s[i] > '9') + return false; + v = v * 10 + (s[i] - '0'); + } + *out = v; + return true; +} + +static const char *meta_library(const char *s, size_t len, char *out) +{ + if (len < 1 || len >= VULKAN_CUSTOM_DRIVER_LIBRARY_MAX) + return "libraryName is not 1 to 63 characters"; + if ( s[0] == '.' + || memchr(s, '/', len) + || memchr(s, '\\', len) + || memchr(s, '\0', len)) + return "libraryName is not a plain file name"; + if (len < 4 || memcmp(s + len - 3, ".so", 3)) + return "libraryName does not end in .so"; + memcpy(out, s, len); + out[len] = '\0'; + return NULL; +} + +/* Cosmetic text: cut to fit, without splitting a UTF-8 character. */ +static void meta_text(const char *s, size_t len, char *out) +{ + if (len >= VULKAN_CUSTOM_DRIVER_TEXT_MAX) + { + len = VULKAN_CUSTOM_DRIVER_TEXT_MAX - 1; + while (len && ((unsigned char)s[len] & 0xC0) == 0x80) + len--; + } + memcpy(out, s, len); + out[len] = '\0'; +} + +static bool meta_member(void *data, const char *s, size_t len) +{ + struct meta_ctx *ctx = (struct meta_ctx*)data; + if (ctx->depth != 1) + return true; + if (meta_is(s, len, "schemaVersion")) + ctx->key = META_KEY_SCHEMA; + else if (meta_is(s, len, "vendor")) + ctx->key = META_KEY_VENDOR; + else if (meta_is(s, len, "libraryName")) + ctx->key = META_KEY_LIBRARY; + else if (meta_is(s, len, "minApi")) + ctx->key = META_KEY_MIN_API; + else if (meta_is(s, len, "name")) + ctx->key = META_KEY_NAME; + else if (meta_is(s, len, "driverVersion")) + ctx->key = META_KEY_VERSION; + else + ctx->key = META_KEY_NONE; + return true; +} + +/* A value the current key cannot take, or a root that is not an object. + * name and driverVersion are cosmetic: a wrong type leaves them empty. */ +static bool meta_other(struct meta_ctx *ctx) +{ + if (ctx->depth == 0) + ctx->err = "meta.json is not an object"; + else if ( ctx->depth == 1 + && ctx->key != META_KEY_NONE + && ctx->key != META_KEY_NAME + && ctx->key != META_KEY_VERSION) + ctx->err = "meta.json has a value of the wrong type"; + ctx->key = META_KEY_NONE; + return !ctx->err; +} + +static bool meta_string(void *data, const char *s, size_t len) +{ + struct meta_ctx *ctx = (struct meta_ctx*)data; + if (ctx->depth != 1) + return ctx->depth ? true : meta_other(ctx); + switch (ctx->key) + { + case META_KEY_VENDOR: + if (len < 1 || len >= VULKAN_CUSTOM_DRIVER_TEXT_MAX) + ctx->err = "vendor is not 1 to 127 characters"; + else + meta_text(s, len, ctx->meta->vendor); + break; + case META_KEY_LIBRARY: + ctx->err = meta_library(s, len, ctx->meta->library_name); + break; + case META_KEY_NAME: + meta_text(s, len, ctx->meta->name); + break; + case META_KEY_VERSION: + meta_text(s, len, ctx->meta->version); + break; + case META_KEY_NONE: + break; + default: + return meta_other(ctx); + } + ctx->key = META_KEY_NONE; + return !ctx->err; +} + +static bool meta_number(void *data, const char *s, size_t len) +{ + int v; + struct meta_ctx *ctx = (struct meta_ctx*)data; + if (ctx->depth != 1) + return ctx->depth ? true : meta_other(ctx); + switch (ctx->key) + { + case META_KEY_SCHEMA: + ctx->schema = meta_int(s, len, &v) && v == 1; + if (!ctx->schema) + ctx->err = "schemaVersion is not 1"; + break; + case META_KEY_MIN_API: + if (!meta_int(s, len, &v)) + ctx->err = "minApi is not a whole number"; + else if (v > ctx->api_level) + ctx->err = "minApi is above the device's API level"; + break; + case META_KEY_NONE: + break; + default: + return meta_other(ctx); + } + ctx->key = META_KEY_NONE; + return !ctx->err; +} + +static bool meta_start_object(void *data) +{ + struct meta_ctx *ctx = (struct meta_ctx*)data; + if (ctx->depth && !meta_other(ctx)) + return false; + ctx->depth++; + return true; +} + +static bool meta_start_array(void *data) +{ + struct meta_ctx *ctx = (struct meta_ctx*)data; + if (!meta_other(ctx)) + return false; + ctx->depth++; + return true; +} + +static bool meta_end(void *data) +{ + ((struct meta_ctx*)data)->depth--; + return true; +} + +static bool meta_bool(void *data, bool value) +{ + (void)value; + return meta_other((struct meta_ctx*)data); +} + +static bool meta_null(void *data) +{ + return meta_other((struct meta_ctx*)data); +} + +const char *vulkan_custom_driver_parse_meta(const char *buf, size_t len, + int api_level, struct vulkan_custom_driver_meta *meta) +{ + struct meta_ctx ctx; + memset(meta, 0, sizeof(*meta)); + if (len > VULKAN_CUSTOM_DRIVER_META_MAX) + return "meta.json is over 4 KiB"; + ctx.meta = meta; + ctx.err = NULL; + ctx.api_level = api_level; + ctx.depth = 0; + ctx.key = META_KEY_NONE; + ctx.schema = false; + if (!rjson_parse_quick(buf, len, &ctx, RJSON_OPTION_ALLOW_UTF8BOM, + meta_member, meta_string, meta_number, + meta_start_object, meta_end, meta_start_array, meta_end, + meta_bool, meta_null, NULL)) + return ctx.err ? ctx.err : "meta.json is not valid JSON"; + if (!ctx.schema) + return "schemaVersion is not 1"; + if (!*meta->vendor) + return "vendor is missing"; + if (!*meta->library_name) + return "libraryName is missing"; + meta->mesa = string_is_equal_case_insensitive(meta->vendor, + VULKAN_CUSTOM_DRIVER_VENDOR); + return NULL; +} + +const char *vulkan_custom_driver_check_members( + const struct string_list *members, const char *library_name) +{ + size_t i; + unsigned so = 0; + bool has_meta = false; + bool has_lib = false; + for (i = 0; i < members->size; i++) + { + const char *e = members->elems[i].data; + if (string_is_equal(e, "meta.json")) + has_meta = true; + else if (string_ends_with(e, ".so")) + { + so++; + if (string_is_equal(e, library_name)) + has_lib = true; + } + } + if (!has_meta) + return "no meta.json at the zip root"; + if (so != 1) + return "the zip does not hold exactly one .so"; + if (!has_lib) + return "the .so is not libraryName at the zip root"; + return NULL; +} + +const char *vulkan_custom_driver_check_elf(const unsigned char *head, + size_t len, int64_t size) +{ + if (size > VULKAN_CUSTOM_DRIVER_SO_MAX) + return "the driver is over 64 MiB"; + if (len < VULKAN_CUSTOM_DRIVER_ELF_HEAD || memcmp(head, "\177ELF", 4)) + return "the driver is not an ELF file"; + /* ELFCLASS64, ELFDATA2LSB */ + if (head[4] != 2 || head[5] != 1) + return "the driver is not 64-bit little-endian"; + if ((head[16] | (head[17] << 8)) != 3) /* ET_DYN */ + return "the driver is not a shared object"; + if ((head[18] | (head[19] << 8)) != 183) /* EM_AARCH64 */ + return "the driver is not arm64"; + return NULL; +} + +uint32_t vulkan_custom_driver_key(const char *path, + int64_t size, int64_t mtime) +{ + unsigned i; + uint8_t b[16]; + uint32_t crc = encoding_crc32(0, (const uint8_t*)path, strlen(path)); + for (i = 0; i < 8; i++) + { + b[i] = (uint8_t)((uint64_t)size >> (8 * i)); + b[8 + i] = (uint8_t)((uint64_t)mtime >> (8 * i)); + } + return encoding_crc32(crc, b, sizeof(b)); +} + +/* Length of the line at s with its newline; 0 for a torn last line. */ +static size_t crash_line(const char *s, size_t len) +{ + const char *nl = (const char*)memchr(s, '\n', len); + return nl ? (size_t)(nl - s) + 1 : 0; +} + +/* A NULL core matches every core. */ +static bool crash_match(const char *s, size_t line, + const char *path, size_t path_len, + const char *core, size_t core_len) +{ + const char *tab = (const char*)memchr(s, '\t', line); + size_t len = tab ? (size_t)(tab - s) : 0; + if (!tab || line - len - 2 != path_len || memcmp(tab + 1, path, path_len)) + return false; + return !core || (len == core_len && !memcmp(s, core, core_len)); +} + +static bool crash_has(const char *state, size_t len, + const char *path, size_t path_len, + const char *core, size_t core_len) +{ + size_t i = 0; + size_t line; + while (i < len && (line = crash_line(state + i, len - i))) + { + if (crash_match(state + i, line, path, path_len, core, core_len)) + return true; + i += line; + } + return false; +} + +static size_t crash_put(char *state, size_t len, size_t cap, + const char *path, size_t path_len, + const char *core, size_t core_len) +{ + size_t need = core_len + path_len + 2; + while (len && state[len - 1] != '\n') + len--; + if ( !path_len + || need > cap + || memchr(path, '\n', path_len) + || memchr(core, '\n', core_len) + || memchr(core, '\t', core_len) + || crash_has(state, len, path, path_len, core, core_len)) + return len; + while (len + need > cap) + { + size_t line = crash_line(state, len); + memmove(state, state + line, len - line); + len -= line; + } + memcpy(state + len, core, core_len); + state[len + core_len] = '\t'; + memcpy(state + len + core_len + 1, path, path_len); + state[len + need - 1] = '\n'; + return len + need; +} + +bool vulkan_custom_driver_crash_find(const char *state, size_t len, + const char *path, const char *core) +{ + if (!core) + core = ""; + return crash_has(state, len, path, strlen(path), core, strlen(core)); +} + +size_t vulkan_custom_driver_crash_add(char *state, size_t len, size_t cap, + const char *path, const char *core) +{ + if (!core) + core = ""; + return crash_put(state, len, cap, path, strlen(path), core, strlen(core)); +} + +size_t vulkan_custom_driver_crash_clear(char *state, size_t len, + const char *path) +{ + size_t i = 0; + size_t path_len = strlen(path); + while (i < len) + { + size_t line = crash_line(state + i, len - i); + if (!line) + return i; + if (crash_match(state + i, line, path, path_len, NULL, 0)) + { + memmove(state + i, state + i + line, len - i - line); + len -= line; + } + else + i += line; + } + return len; +} + +bool vulkan_custom_driver_session_init(vulkan_custom_driver_session_t *s, + vulkan_custom_driver_load_t load, void *load_data, + const char *state, size_t state_len, + const char *marker, size_t marker_len) +{ + const char *tab; + size_t line, path_len; + memset(s, 0, sizeof(*s)); + s->load = load; + s->load_data = load_data; + if (state_len > sizeof(s->state)) + state_len = sizeof(s->state); + if (state_len) + memcpy(s->state, state, state_len); + while (state_len && s->state[state_len - 1] != '\n') + state_len--; + s->state_len = state_len; + if ( !marker_len + || !(line = crash_line(marker, marker_len)) + || !(tab = (const char*)memchr(marker, '\t', line)) + || !(path_len = line - (size_t)(tab - marker) - 2)) + return false; + s->state_len = crash_put(s->state, s->state_len, sizeof(s->state), + tab + 1, path_len, marker, (size_t)(tab - marker)); + return true; +} + +static struct vulkan_custom_driver_path *session_find( + vulkan_custom_driver_session_t *s, const char *path) +{ + unsigned i; + for (i = 0; i < s->path_count; i++) + if (string_is_equal(s->paths[i].path, path)) + return &s->paths[i]; + return NULL; +} + +/* NULL for a new path once the table is full. */ +static struct vulkan_custom_driver_path *session_path( + vulkan_custom_driver_session_t *s, const char *path) +{ + struct vulkan_custom_driver_path *p = session_find(s, path); + if (p || s->path_count == VULKAN_CUSTOM_DRIVER_PATHS) + return p; + p = &s->paths[s->path_count++]; + strlcpy(p->path, path, sizeof(p->path)); + return p; +} + +static enum vulkan_custom_driver_msg session_once( + struct vulkan_custom_driver_path *p, enum vulkan_custom_driver_msg msg) +{ + if (p->shown & (1u << msg)) + return VULKAN_CUSTOM_DRIVER_MSG_NONE; + p->shown |= 1u << msg; + return msg; +} + +/* True when req names a custom driver this session may use. */ +static bool session_wants(vulkan_custom_driver_session_t *s, + const struct vulkan_custom_driver_request *req, + struct vulkan_custom_driver_path *p, + enum vulkan_custom_driver_msg *msg) +{ + if (!req->supported) + { + *msg = session_once(p, VULKAN_CUSTOM_DRIVER_MSG_UNSUPPORTED); + return false; + } + if (vulkan_custom_driver_crash_find(s->state, s->state_len, + req->path, req->core)) + { + *msg = session_once(p, VULKAN_CUSTOM_DRIVER_MSG_CRASHED); + return false; + } + if (p->failed || p->invalid) + return false; + if (req->size < 0) + { + p->invalid = true; + *msg = session_once(p, VULKAN_CUSTOM_DRIVER_MSG_INVALID); + return false; + } + return true; +} + +void *vulkan_custom_driver_session_open(vulkan_custom_driver_session_t *s, + const struct vulkan_custom_driver_request *req, + const struct vulkan_custom_driver_meta **meta, + enum vulkan_custom_driver_msg *msg) +{ + unsigned i; + uint32_t key; + struct vulkan_custom_driver_path *p; + struct vulkan_custom_driver_slot *slot = NULL; + enum vulkan_custom_driver_msg err = VULKAN_CUSTOM_DRIVER_MSG_NONE; + + *meta = NULL; + *msg = VULKAN_CUSTOM_DRIVER_MSG_NONE; + s->current = NULL; + s->full = false; + if (string_is_empty(req->path) || req->safe_mode) + return NULL; + /* Past a full table, as past full slots: the system driver. */ + if (!(p = session_path(s, req->path))) + { + if (!s->full_shown) + *msg = VULKAN_CUSTOM_DRIVER_MSG_RESTART; + s->full = true; + s->full_shown = true; + return NULL; + } + if (!session_wants(s, req, p, msg)) + return NULL; + + key = vulkan_custom_driver_key(req->path, req->size, req->mtime); + for (i = 0; i < VULKAN_CUSTOM_DRIVER_SLOTS; i++) + { + if (s->slots[i].handle && s->slots[i].key == key) + { + slot = &s->slots[i]; + break; + } + if (!s->slots[i].handle && !slot) + slot = &s->slots[i]; + } + if (!slot) + { + *msg = session_once(p, VULKAN_CUSTOM_DRIVER_MSG_RESTART); + s->full = true; + return NULL; + } + if (!slot->handle) + { + if (!(slot->handle = s->load(s->load_data, req->path, key, + &slot->meta, &err))) + { + if (err == VULKAN_CUSTOM_DRIVER_MSG_INVALID) + p->invalid = true; + else + { + err = VULKAN_CUSTOM_DRIVER_MSG_FAILED; + p->failed = true; + } + *msg = session_once(p, err); + return NULL; + } + slot->key = key; + } + s->current = p; + s->pending = true; + *meta = &slot->meta; + return slot->handle; +} + +enum vulkan_custom_driver_msg vulkan_custom_driver_session_failed( + vulkan_custom_driver_session_t *s) +{ + struct vulkan_custom_driver_path *p = s->current; + s->current = NULL; + s->pending = false; + if (!p) + return VULKAN_CUSTOM_DRIVER_MSG_NONE; + p->failed = true; + return session_once(p, VULKAN_CUSTOM_DRIVER_MSG_FAILED); +} + +bool vulkan_custom_driver_session_started(vulkan_custom_driver_session_t *s) +{ + bool pending = s->pending; + s->pending = false; + return pending; +} + +/* pending stays: the marker still goes once the kept instance is up. */ +void vulkan_custom_driver_session_kept(vulkan_custom_driver_session_t *s) +{ + s->current = NULL; +} + +bool vulkan_custom_driver_session_current( + const vulkan_custom_driver_session_t *s) +{ + return s->current && s->pending; +} + +enum vulkan_custom_driver_msg vulkan_custom_driver_session_picked( + vulkan_custom_driver_session_t *s, const char *path) +{ + struct vulkan_custom_driver_path *p = session_find(s, path); + s->state_len = vulkan_custom_driver_crash_clear(s->state, s->state_len, + path); + if (!p) + { + if (s->path_count < VULKAN_CUSTOM_DRIVER_PATHS) + return VULKAN_CUSTOM_DRIVER_MSG_NONE; + s->full_shown = true; + return VULKAN_CUSTOM_DRIVER_MSG_RESTART; + } + if (p->failed) + return VULKAN_CUSTOM_DRIVER_MSG_RESTART; + p->invalid = false; + p->shown &= ~(1u << VULKAN_CUSTOM_DRIVER_MSG_INVALID); + return VULKAN_CUSTOM_DRIVER_MSG_NONE; +} diff --git a/gfx/common/vulkan_custom_driver.h b/gfx/common/vulkan_custom_driver.h new file mode 100644 index 000000000000..7169cd3af938 --- /dev/null +++ b/gfx/common/vulkan_custom_driver.h @@ -0,0 +1,187 @@ +/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: + * GPL-3.0-or-later */ +#ifndef VULKAN_CUSTOM_DRIVER_H +#define VULKAN_CUSTOM_DRIVER_H + +#include +#include + +#include +#include +#include +#include +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +#include +#endif + +RETRO_BEGIN_DECLS + +#define VULKAN_CUSTOM_DRIVER_VENDOR "Mesa" +#define VULKAN_CUSTOM_DRIVER_META_MAX 4096 +#define VULKAN_CUSTOM_DRIVER_SO_MAX (64L * 1024 * 1024) +#define VULKAN_CUSTOM_DRIVER_ELF_HEAD 20 +#define VULKAN_CUSTOM_DRIVER_LIBRARY_MAX 64 +#define VULKAN_CUSTOM_DRIVER_TEXT_MAX 128 + +struct vulkan_custom_driver_meta +{ + char library_name[VULKAN_CUSTOM_DRIVER_LIBRARY_MAX]; + char vendor[VULKAN_CUSTOM_DRIVER_TEXT_MAX]; + char name[VULKAN_CUSTOM_DRIVER_TEXT_MAX]; /* optional */ + char version[VULKAN_CUSTOM_DRIVER_TEXT_MAX]; /* driverVersion, optional */ + bool mesa; /* vendor is Mesa, any case */ +}; + +/* Package checks. Each returns NULL when the package passes, else the + * reason, for the log. */ +const char *vulkan_custom_driver_parse_meta(const char *buf, size_t len, + int api_level, struct vulkan_custom_driver_meta *meta); +const char *vulkan_custom_driver_check_members( + const struct string_list *members, const char *library_name); +const char *vulkan_custom_driver_check_elf(const unsigned char *head, + size_t len, int64_t size); + +/* Names the extraction dir, as "%08x". A new zip version gets a new key. */ +uint32_t vulkan_custom_driver_key(const char *path, + int64_t size, int64_t mtime); + +#define VULKAN_CUSTOM_DRIVER_SLOTS 4 +#define VULKAN_CUSTOM_DRIVER_PATHS 8 +#define VULKAN_CUSTOM_DRIVER_STATE_MAX 8192 + +enum vulkan_custom_driver_msg +{ + VULKAN_CUSTOM_DRIVER_MSG_NONE = 0, + VULKAN_CUSTOM_DRIVER_MSG_FAILED, + VULKAN_CUSTOM_DRIVER_MSG_CRASHED, + VULKAN_CUSTOM_DRIVER_MSG_INVALID, + VULKAN_CUSTOM_DRIVER_MSG_UNSUPPORTED, + VULKAN_CUSTOM_DRIVER_MSG_RESTART +}; + +/* Crash state: one "\t\n" line per driver that took the + * process down in init with that core; the menu's core is "". The crash + * marker is one such line. A NULL core is the menu's. */ +bool vulkan_custom_driver_crash_find(const char *state, size_t len, + const char *path, const char *core); +/* Returns the new length. The oldest lines go when cap is reached. */ +size_t vulkan_custom_driver_crash_add(char *state, size_t len, size_t cap, + const char *path, const char *core); +/* Drops path's lines for every core. Returns the new length. */ +size_t vulkan_custom_driver_crash_clear(char *state, size_t len, + const char *path); + +/* Loads the driver at path into the extraction dir named by key. Returns + * its loader with *meta filled in, or NULL with *msg set to + * VULKAN_CUSTOM_DRIVER_MSG_INVALID (bad package) or _FAILED. */ +typedef void *(*vulkan_custom_driver_load_t)(void *data, const char *path, + uint32_t key, struct vulkan_custom_driver_meta *meta, + enum vulkan_custom_driver_msg *msg); + +struct vulkan_custom_driver_request +{ + int64_t size; /* of the zip; < 0 when it is missing */ + int64_t mtime; + const char *path; /* the setting; empty: the system driver */ + const char *core; /* the running core; "" or NULL: the menu */ + bool supported; /* Adreno and API >= 28 */ + bool safe_mode; /* started with the system driver forced */ +}; + +struct vulkan_custom_driver_slot +{ + void *handle; /* NULL: free */ + uint32_t key; + struct vulkan_custom_driver_meta meta; +}; + +struct vulkan_custom_driver_path +{ + unsigned shown; /* 1 << msg for each message shown */ + bool failed; /* failed to start: the system driver all session */ + bool invalid; /* bad package: the system driver until picked again */ + char path[PATH_MAX_LENGTH]; +}; + +typedef struct vulkan_custom_driver_session +{ + vulkan_custom_driver_load_t load; + void *load_data; + struct vulkan_custom_driver_path *current; + size_t state_len; + struct vulkan_custom_driver_slot slots[VULKAN_CUSTOM_DRIVER_SLOTS]; + unsigned path_count; + bool pending; /* a custom driver opened and not started yet */ + bool full; /* the path opened last found the table or slots full */ + bool full_shown; /* RESTART shown for a path past a full table */ + struct vulkan_custom_driver_path paths[VULKAN_CUSTOM_DRIVER_PATHS]; + char state[VULKAN_CUSTOM_DRIVER_STATE_MAX]; +} vulkan_custom_driver_session_t; + +/* Starts a session from the crash state file and the crash marker the + * last process left (either may be empty). Returns true when the marker + * named a driver: it is now off for its core and the state must be + * saved. */ +bool vulkan_custom_driver_session_init(vulkan_custom_driver_session_t *s, + vulkan_custom_driver_load_t load, void *load_data, + const char *state, size_t state_len, + const char *marker, size_t marker_len); +/* Picks the loader for one Vulkan context init: a custom one, with *meta + * set, or NULL for the system one. *msg is the message to show, once per + * path per session. */ +void *vulkan_custom_driver_session_open(vulkan_custom_driver_session_t *s, + const struct vulkan_custom_driver_request *req, + const struct vulkan_custom_driver_meta **meta, + enum vulkan_custom_driver_msg *msg); +/* The custom driver opened last failed to start: the system one is used + * for its path all session. Returns the message to show. */ +enum vulkan_custom_driver_msg vulkan_custom_driver_session_failed( + vulkan_custom_driver_session_t *s); +/* The driver opened last is up, or no longer starting. Returns true when + * the crash marker is to be deleted. */ +bool vulkan_custom_driver_session_started(vulkan_custom_driver_session_t *s); +/* An init took a kept instance and skipped open: the driver opened last + * is not this init's to drop. */ +void vulkan_custom_driver_session_kept(vulkan_custom_driver_session_t *s); +/* True while the custom driver this init opened has not started. */ +bool vulkan_custom_driver_session_current( + const vulkan_custom_driver_session_t *s); +/* The user picked path again: its crash state goes for every core, and a + * bad package is tried again. Returns RESTART when path failed to start + * this session, as it may still be mapped, or has no room in the table. */ +enum vulkan_custom_driver_msg vulkan_custom_driver_session_picked( + vulkan_custom_driver_session_t *s, const char *path); + +/* Overwrites the start of an ELF64 file's DT_SONAME with patch, up to the + * shorter of the two. False, with elf untouched, when elf is not a + * little-endian ELF64 with a DT_SONAME inside len. */ +bool vulkan_custom_driver_patch_soname(uint8_t *elf, size_t len, + const char *patch); + +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +/* Android glue, vulkan_custom_driver_android.c. Context init and surface + * creation call open, kept, failed, started and current; the menu and the + * frontend call the rest. */ +dylib_t vulkan_custom_driver_open(void); /* NULL: use the system loader */ +void vulkan_custom_driver_failed(void); /* current custom driver failed */ +void vulkan_custom_driver_started(void); /* first surface up */ +void vulkan_custom_driver_kept(void); /* a kept instance is used, open skipped */ +bool vulkan_custom_driver_current(void); /* this init opened a custom driver, not started yet */ +bool vulkan_custom_driver_mesa(void); /* the driver open() returned is Mesa */ +bool vulkan_custom_driver_supported(void); /* Adreno and API >= 28 */ +bool vulkan_custom_driver_crashed(const char *path); /* off for the running core after a crash */ +void vulkan_custom_driver_picked(const char *path); /* clears path's crash state and INVALID mark */ +void vulkan_custom_driver_set_safe_mode(bool safe_mode); /* true: system driver until the process ends */ +bool vulkan_custom_driver_safe_mode(void); /* Safe start: system driver for this process */ +/* vulkan_custom_driver_linker.c: a private copy of the system Vulkan + * loader whose HAL is driver_dir/driver_name. hook_dir holds + * libvulkan_driver_hook.so; below API 29 the copy is written to tmp_dir. + * Directories end in a slash. NULL, with *reason set, on failure. */ +void *vulkan_custom_driver_linker_open(const char *hook_dir, + const char *driver_dir, const char *driver_name, const char *tmp_dir, + int flags, const char **reason); +#endif + +RETRO_END_DECLS + +#endif diff --git a/gfx/common/vulkan_custom_driver_android.c b/gfx/common/vulkan_custom_driver_android.c new file mode 100644 index 000000000000..d4439ac531ca --- /dev/null +++ b/gfx/common/vulkan_custom_driver_android.c @@ -0,0 +1,571 @@ +/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: + * GPL-3.0-or-later */ +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "vulkan_custom_driver.h" + +#include "../../configuration.h" +#include "../../frontend/drivers/platform_unix.h" +#include "../../msg_hash.h" +#include "../../runloop.h" +#include "../../verbosity.h" + +/* One load's paths, on the heap: frames stay under 4 KiB. */ +struct custom_driver_work +{ + char dir[PATH_MAX_LENGTH]; /* */ + char tmp[PATH_MAX_LENGTH]; /* .tmp */ + char file[PATH_MAX_LENGTH]; + char other[PATH_MAX_LENGTH]; + char member[PATH_MAX_LENGTH]; /* # */ + char hook[PATH_MAX_LENGTH]; /* nativeLibraryDir/ */ +}; + +static struct +{ + vulkan_custom_driver_session_t session; + char base[PATH_MAX_LENGTH]; /* /vulkan_drivers/ */ + char marker[PATH_MAX_LENGTH]; + char state[PATH_MAX_LENGTH]; + const char *core; /* of the open() in progress */ + bool mesa; /* of the last driver open() returned */ + bool ready; + bool pruned; + bool safe_mode; + bool safe_mode_msg; + bool shortcut; +} custom_driver_st; + +/* dladdr target: a data address needs no function-to-object cast. */ +static const char custom_driver_anchor = 0; + +static void custom_driver_msg(enum vulkan_custom_driver_msg msg) +{ + const char *s; + enum msg_hash_enums id; + switch (msg) + { + case VULKAN_CUSTOM_DRIVER_MSG_FAILED: + id = MSG_VULKAN_CUSTOM_DRIVER_FAILED; + break; + case VULKAN_CUSTOM_DRIVER_MSG_CRASHED: + id = MSG_VULKAN_CUSTOM_DRIVER_CRASHED; + break; + case VULKAN_CUSTOM_DRIVER_MSG_INVALID: + id = MSG_VULKAN_CUSTOM_DRIVER_INVALID; + break; + case VULKAN_CUSTOM_DRIVER_MSG_UNSUPPORTED: + id = MSG_VULKAN_CUSTOM_DRIVER_UNSUPPORTED; + break; + case VULKAN_CUSTOM_DRIVER_MSG_RESTART: + id = MSG_VULKAN_CUSTOM_DRIVER_RESTART; + break; + default: + return; + } + s = msg_hash_to_str(id); + runloop_msg_queue_push(s, strlen(s), 2, 360, false, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); +} + +/* A whole small file, NUL-terminated; NULL with *len 0 when missing. */ +static char *custom_driver_read(const char *path, int64_t *len) +{ + void *buf = NULL; + if (!filestream_read_file(path, &buf, len)) + { + *len = 0; + return NULL; + } + return (char*)buf; +} + +/* The crash state's key for the running core; "" for the menu. */ +static const char *custom_driver_core(void) +{ + runloop_state_t *runloop_st = runloop_state_get_ptr(); + if ( runloop_st->current_core_type == CORE_TYPE_DUMMY + || string_is_empty(runloop_st->system.info.library_name)) + return ""; + return runloop_st->system.info.library_name; +} + +static bool custom_driver_save_state(void) +{ + return filestream_write_file_atomic(custom_driver_st.state, + custom_driver_st.session.state, + (int64_t)custom_driver_st.session.state_len); +} + +/* Written before driver code runs, deleted once it is up or failed. */ +static void custom_driver_mark(const char *path, const char *core) +{ + size_t cap = strlen(path) + strlen(core) + 2; + char *line = (char*)malloc(cap); + size_t len; + if (!line) + return; + len = vulkan_custom_driver_crash_add(line, 0, cap, path, core); + filestream_write_file(custom_driver_st.marker, line, (int64_t)len); + free(line); +} + +/* Deletes what an extraction leaves in dir, file by file, then dir. + * Reads no meta.json, so a package this build rejects still goes. */ +static void custom_driver_remove(struct custom_driver_work *w, + const char *dir) +{ + struct RDIR *rdir = retro_opendir(dir); + if (rdir) + { + while (retro_readdir(rdir)) + { + if (retro_dirent_is_dir(rdir, NULL)) + continue; + fill_pathname_join_special(w->file, dir, + retro_dirent_get_name(rdir), sizeof(w->file)); + filestream_delete(w->file); + } + retro_closedir(rdir); + } + path_rmdir(dir); +} + +/* True when the dir named key still matches its source zip. */ +static bool custom_driver_current_dir(struct custom_driver_work *w, + const char *key) +{ + int64_t len = 0; + int64_t mtime = 0; + int64_t size; + char *zip; + bool keep = false; + fill_pathname_join_special(w->file, w->dir, "source", sizeof(w->file)); + if (!(zip = custom_driver_read(w->file, &len))) + return false; + if ( (size = path_get_size(zip)) >= 0 + && path_get_mtime(zip, &mtime)) + { + snprintf(w->other, sizeof(w->other), "%08x", + (unsigned)vulkan_custom_driver_key(zip, size, mtime)); + keep = string_is_equal(w->other, key); + } + free(zip); + return keep; +} + +/* Once per process, before any driver is mapped: drops each extraction + * whose zip is gone or changed, and any half-done one. No recursion. */ +static void custom_driver_prune(struct custom_driver_work *w) +{ + struct RDIR *rdir = retro_opendir(custom_driver_st.base); + if (!rdir) + return; + while (retro_readdir(rdir)) + { + const char *name = retro_dirent_get_name(rdir); + size_t len = strlen(name); + if ( !retro_dirent_is_dir(rdir, NULL) + || !(len == 8 || (len == 12 && string_is_equal(name + 8, ".tmp"))) + || strspn(name, "0123456789abcdef") != 8) + continue; + fill_pathname_join_special(w->dir, custom_driver_st.base, name, + sizeof(w->dir)); + if (len == 8 && custom_driver_current_dir(w, name)) + continue; + custom_driver_remove(w, w->dir); + } + retro_closedir(rdir); +} + +/* True when w->dir holds a finished extraction; fills *meta. */ +static bool custom_driver_extracted(struct custom_driver_work *w, + struct vulkan_custom_driver_meta *meta) +{ + int64_t len = 0; + char *buf; + bool ok = false; + fill_pathname_join_special(w->file, w->dir, "meta.json", sizeof(w->file)); + if (!(buf = custom_driver_read(w->file, &len))) + return false; + if (!vulkan_custom_driver_parse_meta(buf, (size_t)len, + g_android->activity->sdkVersion, meta)) + { + fill_pathname_join_special(w->file, w->dir, meta->library_name, + sizeof(w->file)); + ok = path_is_valid(w->file); + } + free(buf); + return ok; +} + +/* Extraction matches the member name ignoring case, so only one member + * may match, or it could write one other than the one checked. */ +static bool custom_driver_one_member(const struct string_list *members, + const char *name) +{ + size_t i; + unsigned n = 0; + for (i = 0; i < members->size; i++) + if (string_is_equal_noncase(members->elems[i].data, name)) + n++; + return n == 1; +} + +/* Checks the package at path and extracts it into w->dir. Returns NULL, + * or the reason it is not usable. */ +static const char *custom_driver_extract(struct custom_driver_work *w, + const char *path, struct vulkan_custom_driver_meta *meta) +{ + unsigned char head[VULKAN_CUSTOM_DRIVER_ELF_HEAD]; + uint64_t size = 0; + int64_t len = 0; + int64_t got = 0; + void *buf = NULL; + RFILE *file = NULL; + const char *reason = NULL; + struct string_list *members; + + if (!(members = file_archive_get_file_list(path, NULL))) + return "not a readable zip"; + snprintf(w->member, sizeof(w->member), "%s#meta.json", path); + file_archive_get_file_crc32_and_size(w->member, &size); + if (size > VULKAN_CUSTOM_DRIVER_META_MAX) + reason = "meta.json over 4 KiB"; + else if (!file_archive_compressed_read(w->member, &buf, NULL, &len)) + reason = "no readable meta.json"; + else if (!(reason = vulkan_custom_driver_parse_meta((const char*)buf, + (size_t)len, g_android->activity->sdkVersion, meta)) + && !(reason = vulkan_custom_driver_check_members(members, + meta->library_name)) + && !custom_driver_one_member(members, meta->library_name)) + reason = "libraryName matches more than one member"; + string_list_free(members); + + if (!reason) + { + snprintf(w->member, sizeof(w->member), "%s#%s", path, + meta->library_name); + file_archive_get_file_crc32_and_size(w->member, &size); + if (size > (uint64_t)VULKAN_CUSTOM_DRIVER_SO_MAX) + reason = "driver library over 64 MiB"; + } + if (!reason) + { + custom_driver_remove(w, w->tmp); + fill_pathname_join_special(w->other, w->tmp, "meta.json", + sizeof(w->other)); + if ( !path_mkdir(w->tmp) + || !filestream_write_file(w->other, buf, len)) + reason = "cannot write the driver dir"; + } + free(buf); + if (reason) + return reason; + + fill_pathname_join_special(w->other, w->tmp, "source", sizeof(w->other)); + if (!filestream_write_file(w->other, path, (int64_t)strlen(path))) + reason = "cannot write the driver dir"; + else if (!file_archive_extract_file(w->member, "so", w->tmp, + w->other, sizeof(w->other))) + reason = "cannot extract the driver library"; + else + { + fill_pathname_join_special(w->other, w->tmp, meta->library_name, + sizeof(w->other)); + if ((file = filestream_open(w->other, RETRO_VFS_FILE_ACCESS_READ, + RETRO_VFS_FILE_ACCESS_HINT_NONE))) + { + got = filestream_read(file, head, sizeof(head)); + filestream_close(file); + } + if (!(reason = vulkan_custom_driver_check_elf(head, + got > 0 ? (size_t)got : 0, path_get_size(w->other)))) + { + custom_driver_remove(w, w->dir); + if (filestream_rename(w->tmp, w->dir) != 0) + reason = "cannot write the driver dir"; + } + } + if (reason) + custom_driver_remove(w, w->tmp); + return reason; +} + +/* nativeLibraryDir/, where libvulkan_driver_hook.so is */ +static bool custom_driver_hook_dir(struct custom_driver_work *w) +{ + Dl_info info; + if (!dladdr(&custom_driver_anchor, &info) || !info.dli_fname) + return false; + fill_pathname_basedir(w->hook, info.dli_fname, sizeof(w->hook)); + return true; +} + +/* Adds the "Safe start" launcher shortcut once per process, before the + * first custom driver is loaded, so a black screen has a way back. */ +static void custom_driver_shortcut(void) +{ + JNIEnv *env; + if (custom_driver_st.shortcut) + return; + custom_driver_st.shortcut = true; + if (g_android->addSafeStartShortcut && (env = jni_thread_getenv())) + CALL_VOID_METHOD(env, g_android->activity->clazz, + g_android->addSafeStartShortcut); +} + +/* vulkan_custom_driver_load_t: runs on a loader cache miss only. */ +static void *custom_driver_load(void *data, const char *path, uint32_t key, + struct vulkan_custom_driver_meta *meta, + enum vulkan_custom_driver_msg *msg) +{ + void *handle = NULL; + const char *reason = NULL; + struct custom_driver_work *w = (struct custom_driver_work*) + calloc(1, sizeof(*w)); + + if (!w) + { + RARCH_WARN("[Vulkan] Custom driver \"%s\" not used: out of memory.\n", + path); + *msg = VULKAN_CUSTOM_DRIVER_MSG_FAILED; + return NULL; + } + if (!custom_driver_st.pruned) + { + custom_driver_st.pruned = true; + custom_driver_prune(w); + } + snprintf(w->other, sizeof(w->other), "%08x", (unsigned)key); + fill_pathname_join_special(w->dir, custom_driver_st.base, w->other, + sizeof(w->dir)); + strlcpy(w->tmp, w->dir, sizeof(w->tmp)); + strlcat(w->tmp, ".tmp", sizeof(w->tmp)); + + if ( !custom_driver_extracted(w, meta) + && (reason = custom_driver_extract(w, path, meta))) + *msg = VULKAN_CUSTOM_DRIVER_MSG_INVALID; + else + { + custom_driver_shortcut(); + custom_driver_mark(path, custom_driver_st.core); + if (!custom_driver_hook_dir(w)) + reason = "the library directory is unknown"; + else + { + fill_pathname_join_special(w->file, custom_driver_st.base, "tmp", + sizeof(w->file)); + fill_pathname_slash(w->file, sizeof(w->file)); + path_mkdir(w->file); + strlcpy(w->other, w->dir, sizeof(w->other)); + fill_pathname_slash(w->other, sizeof(w->other)); + handle = vulkan_custom_driver_linker_open(w->hook, w->other, + meta->library_name, w->file, RTLD_NOW, &reason); + } + if (!handle) + { + filestream_delete(custom_driver_st.marker); + *msg = VULKAN_CUSTOM_DRIVER_MSG_FAILED; + } + } + if (reason) + RARCH_WARN("[Vulkan] Custom driver \"%s\" not used: %s.\n", + path, reason); + free(w); + return handle; +} + +/* Reads the crash state and the marker the last process left, once. */ +static bool custom_driver_init(void) +{ + int64_t state_len = 0; + int64_t marker_len = 0; + char *state; + char *marker; + + if (custom_driver_st.ready) + return true; + if ( !g_android + || !g_android->activity + || string_is_empty(g_android->activity->internalDataPath)) + return false; + custom_driver_st.ready = true; + fill_pathname_join_special(custom_driver_st.base, + g_android->activity->internalDataPath, "vulkan_drivers", + sizeof(custom_driver_st.base)); + path_mkdir(custom_driver_st.base); + fill_pathname_join_special(custom_driver_st.marker, custom_driver_st.base, + "pending", sizeof(custom_driver_st.marker)); + fill_pathname_join_special(custom_driver_st.state, custom_driver_st.base, + "state", sizeof(custom_driver_st.state)); + fill_pathname_slash(custom_driver_st.base, sizeof(custom_driver_st.base)); + + state = custom_driver_read(custom_driver_st.state, &state_len); + marker = custom_driver_read(custom_driver_st.marker, &marker_len); + /* A failed save keeps the marker for the next start, until a custom + * driver opens and writes its own. */ + if ( !vulkan_custom_driver_session_init(&custom_driver_st.session, + custom_driver_load, NULL, state, (size_t)state_len, + marker, (size_t)marker_len) + || custom_driver_save_state()) + filestream_delete(custom_driver_st.marker); + free(state); + free(marker); + return true; +} + +dylib_t vulkan_custom_driver_open(void) +{ + struct vulkan_custom_driver_request req; + const struct vulkan_custom_driver_meta *meta = NULL; + enum vulkan_custom_driver_msg msg = VULKAN_CUSTOM_DRIVER_MSG_NONE; + settings_t *settings = config_get_ptr(); + int64_t mtime = 0; + void *handle; + + if (!custom_driver_init()) + return NULL; + if (vulkan_custom_driver_session_started(&custom_driver_st.session)) + filestream_delete(custom_driver_st.marker); + if (custom_driver_st.safe_mode_msg) + { + const char *s = msg_hash_to_str(MSG_VULKAN_CUSTOM_DRIVER_SAFE_MODE); + custom_driver_st.safe_mode_msg = false; + runloop_msg_queue_push(s, strlen(s), 2, 360, false, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); + } + + req.path = settings->paths.path_vulkan_driver; + req.core = custom_driver_core(); + req.safe_mode = custom_driver_st.safe_mode; + req.supported = false; + req.size = -1; + req.mtime = 0; + if (!string_is_empty(req.path) && !req.safe_mode) + { + req.supported = vulkan_custom_driver_supported(); + req.size = path_get_size(req.path); + if (path_get_mtime(req.path, &mtime)) + req.mtime = mtime; + } + custom_driver_st.core = req.core; + handle = vulkan_custom_driver_session_open(&custom_driver_st.session, + &req, &meta, &msg); + custom_driver_msg(msg); + custom_driver_st.mesa = handle && meta->mesa; + /* The load callback warns for the INVALID it returns. */ + if (msg == VULKAN_CUSTOM_DRIVER_MSG_INVALID && req.size < 0) + RARCH_WARN("[Vulkan] Custom driver \"%s\" not used: zip missing or not readable.\n", + req.path); + if (custom_driver_st.session.full) + RARCH_LOG("[Vulkan] Custom driver \"%s\" not used: too many drivers tried this session.\n", + req.path); + if (handle) + { + custom_driver_mark(req.path, req.core); + RARCH_LOG("[Vulkan] Custom driver: \"%s\" (%s), version \"%s\", core \"%s\".\n", + meta->name, meta->library_name, meta->version, req.core); + if (!custom_driver_st.mesa) + RARCH_WARN("[Vulkan] Custom driver vendor \"%s\" is untested.\n", + meta->vendor); + } + return (dylib_t)handle; +} + +void vulkan_custom_driver_failed(void) +{ + const struct vulkan_custom_driver_path *p; + enum vulkan_custom_driver_msg msg; + if (!custom_driver_st.ready) + return; + p = custom_driver_st.session.current; + msg = vulkan_custom_driver_session_failed(&custom_driver_st.session); + if (msg == VULKAN_CUSTOM_DRIVER_MSG_FAILED) + RARCH_WARN("[Vulkan] Custom driver \"%s\" not used: failed to start.\n", + p->path); + custom_driver_msg(msg); + filestream_delete(custom_driver_st.marker); +} + +void vulkan_custom_driver_started(void) +{ + if ( custom_driver_st.ready + && vulkan_custom_driver_session_started(&custom_driver_st.session)) + filestream_delete(custom_driver_st.marker); +} + +void vulkan_custom_driver_kept(void) +{ + if (custom_driver_st.ready) + vulkan_custom_driver_session_kept(&custom_driver_st.session); +} + +bool vulkan_custom_driver_current(void) +{ + return custom_driver_st.ready + && vulkan_custom_driver_session_current(&custom_driver_st.session); +} + +bool vulkan_custom_driver_mesa(void) +{ + return custom_driver_st.mesa; +} + +bool vulkan_custom_driver_supported(void) +{ + return g_android + && g_android->activity + && g_android->activity->sdkVersion >= 28 + && path_is_valid("/dev/kgsl-3d0"); +} + +bool vulkan_custom_driver_crashed(const char *path) +{ + if (string_is_empty(path) || !custom_driver_init()) + return false; + return vulkan_custom_driver_crash_find(custom_driver_st.session.state, + custom_driver_st.session.state_len, path, custom_driver_core()); +} + +void vulkan_custom_driver_picked(const char *path) +{ + size_t len; + if (string_is_empty(path) || !custom_driver_init()) + return; + len = custom_driver_st.session.state_len; + custom_driver_msg(vulkan_custom_driver_session_picked( + &custom_driver_st.session, path)); + if (custom_driver_st.session.state_len != len) + custom_driver_save_state(); +} + +void vulkan_custom_driver_set_safe_mode(bool safe_mode) +{ + /* Latched: a recreate may re-read an intent without the extra. + * Re-armed per session: a recreate frees the old session's message. */ + if (!safe_mode && !custom_driver_st.safe_mode) + return; + custom_driver_st.safe_mode = true; + custom_driver_st.safe_mode_msg = true; +} + +bool vulkan_custom_driver_safe_mode(void) +{ + return custom_driver_st.safe_mode; +} diff --git a/gfx/common/vulkan_custom_driver_linker.c b/gfx/common/vulkan_custom_driver_linker.c new file mode 100644 index 000000000000..fdfa9240d7ea --- /dev/null +++ b/gfx/common/vulkan_custom_driver_linker.c @@ -0,0 +1,459 @@ +/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: + * GPL-3.0-or-later + * + * Parts of this file follow libadrenotools and liblinkernsbypass: + * + * BSD 2-Clause License + * + * Copyright (c) 2021, Billy Laws + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +/* Built twice: into libretroarch-activity.so, and with + * VULKAN_CUSTOM_DRIVER_HOOK as libvulkan_driver_hook.so, which a private + * copy of the system Vulkan loader calls to open its HAL driver. */ +#include +#include +#include + +#ifndef VULKAN_CUSTOM_DRIVER_HOOK +#include "vulkan_custom_driver.h" + +/* ELF64 fields, little-endian, read byte by byte: no alignment needed */ +static uint64_t vkcd_rd(const uint8_t *p, unsigned n) +{ + uint64_t v = 0; + while (n--) + v = (v << 8) | p[n]; + return v; +} + +bool vulkan_custom_driver_patch_soname(uint8_t *elf, size_t len, + const char *patch) +{ + uint64_t shoff; + unsigned shentsize, shnum, i; + + if ( len < 64 + || memcmp(elf, "\177ELF", 4) + || elf[4] != 2 /* ELFCLASS64 */ + || elf[5] != 1) /* ELFDATA2LSB */ + return false; + shoff = vkcd_rd(elf + 40, 8); + shentsize = (unsigned)vkcd_rd(elf + 58, 2); + shnum = (unsigned)vkcd_rd(elf + 60, 2); + if ( shentsize < 64 + || shoff > len + || (uint64_t)shnum * shentsize > len - shoff) + return false; + + for (i = 0; i < shnum; i++) + { + const uint8_t *sh = elf + shoff + (uint64_t)i * shentsize; + const uint8_t *str_sh; + uint64_t off, size, entsize, str_off, str_size, k; + unsigned link; + + if (vkcd_rd(sh + 4, 4) != 6) /* SHT_DYNAMIC */ + continue; + off = vkcd_rd(sh + 24, 8); + size = vkcd_rd(sh + 32, 8); + link = (unsigned)vkcd_rd(sh + 40, 4); + entsize = vkcd_rd(sh + 56, 8); + if ( link >= shnum + || entsize < 16 + || off > len + || size > len - off) + return false; + str_sh = elf + shoff + (uint64_t)link * shentsize; + str_off = vkcd_rd(str_sh + 24, 8); + str_size = vkcd_rd(str_sh + 32, 8); + if (str_off > len || str_size > len - str_off) + return false; + + for (k = 0; k + entsize <= size; k += entsize) + { + const uint8_t *dyn = elf + off + k; + uint64_t name, room, j; + char *soname; + + if (vkcd_rd(dyn, 8) != 14) /* DT_SONAME */ + continue; + name = vkcd_rd(dyn + 8, 8); + if (name >= str_size) + return false; + soname = (char*)elf + str_off + name; + room = str_size - name; + for (j = 0; j < room && soname[j] && patch[j]; j++) + soname[j] = patch[j]; + return true; + } + } + return false; +} +#endif + +#if defined(__ANDROID__) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef VULKAN_CUSTOM_DRIVER_HOOK +#include +#endif + +#define VKCD_NS_SHARED 2 /* ANDROID_NAMESPACE_TYPE_SHARED */ + +typedef void *(*vkcd_dlopen_ext_t)(const char *name, int flags, + const android_dlextinfo *info); +typedef struct android_namespace_t *(*vkcd_create_ns_t)(const char *name, + const char *ld_library_path, const char *default_library_path, + uint64_t type, const char *permitted_when_isolated_path, + struct android_namespace_t *parent, const void *caller); +typedef struct android_namespace_t *(*vkcd_exported_ns_t)(const char *name); +typedef bool (*vkcd_link_ns_t)(struct android_namespace_t *from, + struct android_namespace_t *to, const char *shared_libs); +typedef bool (*vkcd_link_all_t)(struct android_namespace_t *from, + struct android_namespace_t *to); + +/* Filled by libretroarch-activity.so; the hook keeps it for good */ +struct vkcd_hook_params +{ + vkcd_dlopen_ext_t dlopen_ext; /* libdl's, never the hook's own */ + vkcd_create_ns_t create_ns; + vkcd_exported_ns_t exported_ns; + vkcd_link_ns_t link_ns; + char hook_dir[PATH_MAX]; + char driver_dir[PATH_MAX]; + char driver_name[PATH_MAX]; +}; + +#ifndef VULKAN_CUSTOM_DRIVER_HOOK +typedef void *(*vkcd_loader_dlopen_t)(const char *name, int flags, + const void *caller); +typedef void (*vkcd_hook_init_t)(const struct vkcd_hook_params *params); + +static struct +{ + vkcd_create_ns_t create_ns; + vkcd_exported_ns_t exported_ns; + vkcd_link_ns_t link_ns; + vkcd_link_all_t link_all; + struct android_namespace_t *default_ns; + unsigned copies; + bool tried; + bool ready; +} vkcd_st; + +static void *vkcd_page(const void *p) +{ + return (void*)((uintptr_t)p & ~(uintptr_t)(sysconf(_SC_PAGESIZE) - 1)); +} + +/* Finds the linker's own namespace functions, which apps may not call: + * dlopen passes its return address to __loader_dlopen, and calling that + * with dlopen as the caller opens libraries as the linker itself. */ +static bool vkcd_resolve(void) +{ + const uint32_t *insn = (const uint32_t*)(const void*)&dlopen; + long page = sysconf(_SC_PAGESIZE); + vkcd_loader_dlopen_t loader_dlopen; + void *ld; + void *libdl; + long off; + unsigned i; + + if (vkcd_st.tried) + return vkcd_st.ready; + vkcd_st.tried = true; + if (android_get_device_api_level() < 28) + return false; + + /* Some devices map system code execute-only */ + mprotect(vkcd_page(insn), page, PROT_READ | PROT_EXEC); + for (i = 0; i < 64; i++) + if ((insn[i] >> 26) == 0x25) /* BL */ + break; + if (i == 64) + return false; + off = (long)(insn[i] & 0x3ffffff); + if (off & 0x2000000) + off -= 0x4000000; + loader_dlopen = (vkcd_loader_dlopen_t)(void*)(insn + i + off); + /* No BTI: __loader_dlopen is not an indirect branch target */ + if (mprotect(vkcd_page((const void*)loader_dlopen), page, + PROT_READ | PROT_EXEC)) + return false; + + if ( !(ld = loader_dlopen("ld-android.so", RTLD_LAZY, + (const void*)&dlopen)) + || !(libdl = loader_dlopen("libdl_android.so", RTLD_LAZY, + (const void*)&dlopen))) + return false; + vkcd_st.link_all = (vkcd_link_all_t)dlsym(ld, + "__loader_android_link_namespaces_all_libs"); + vkcd_st.link_ns = (vkcd_link_ns_t)dlsym(ld, + "__loader_android_link_namespaces"); + vkcd_st.create_ns = (vkcd_create_ns_t)dlsym(libdl, + "__loader_android_create_namespace"); + vkcd_st.exported_ns = (vkcd_exported_ns_t)dlsym(libdl, + "__loader_android_get_exported_namespace"); + if ( !vkcd_st.link_all || !vkcd_st.link_ns + || !vkcd_st.create_ns || !vkcd_st.exported_ns) + return false; + /* The default namespace has no handle: a shared one made with no + * parent and dlopen as the caller is a copy of it. */ + if (!(vkcd_st.default_ns = vkcd_st.create_ns("default_copy", NULL, NULL, + VKCD_NS_SHARED, NULL, NULL, (const void*)&dlopen))) + return false; + vkcd_st.ready = true; + return true; +} + +static bool vkcd_read_all(int fd, uint8_t *buf, size_t len) +{ + while (len) + { + ssize_t n = read(fd, buf, len); + if (n <= 0) + return false; + buf += n; + len -= (size_t)n; + } + return true; +} + +/* The system loader is loaded already; a copy whose soname differs is a + * library of its own, and its HAL lookups reach the hook. */ +static void *vkcd_load_copy(const char *tmp_dir, int flags, + struct android_namespace_t *ns) +{ + static const char lib[] = "/system/lib64/libvulkan.so"; + static char path[PATH_MAX]; /* frames stay under 4 KiB */ + char patch[4]; + android_dlextinfo info; + struct stat st; + uint8_t *map = (uint8_t*)MAP_FAILED; + void *handle = NULL; + unsigned id = vkcd_st.copies++; + int in = -1; + int fd; + + if (android_get_device_api_level() >= 29) + fd = (int)syscall(__NR_memfd_create, lib, 0); + else + { + snprintf(path, sizeof(path), "%s%u_patched.so", tmp_dir, id); + fd = open(path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); + } + if (fd < 0) + return NULL; + snprintf(patch, sizeof(patch), "%03u", id % 1000); + if ( (in = open(lib, O_RDONLY)) >= 0 + && !fstat(in, &st) + && !ftruncate(fd, st.st_size) + && (map = (uint8_t*)mmap(NULL, (size_t)st.st_size, + PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) != MAP_FAILED + && vkcd_read_all(in, map, (size_t)st.st_size) + && vulkan_custom_driver_patch_soname(map, (size_t)st.st_size, + patch)) + { + info.flags = ANDROID_DLEXT_USE_NAMESPACE + | ANDROID_DLEXT_USE_LIBRARY_FD; + info.reserved_addr = NULL; + info.reserved_size = 0; + info.relro_fd = 0; + info.library_fd = fd; + info.library_fd_offset = 0; + info.library_namespace = ns; + snprintf(path, sizeof(path), "/proc/self/fd/%d", fd); + handle = android_dlopen_ext(path, flags, &info); + } + if (map != (uint8_t*)MAP_FAILED) + munmap(map, (size_t)st.st_size); + if (in >= 0) + close(in); + /* A loaded copy keeps its fd: the library's name points at it */ + if (!handle) + close(fd); + return handle; +} + +void *vulkan_custom_driver_linker_open(const char *hook_dir, + const char *driver_dir, const char *driver_name, const char *tmp_dir, + int flags, const char **reason) +{ + struct vkcd_hook_params *params = NULL; + struct android_namespace_t *ns; + vkcd_hook_init_t init; + void *hook; + void *handle; + android_dlextinfo info; + + *reason = "linker namespaces unavailable"; + if (!vkcd_resolve()) + return NULL; + + *reason = "hook library did not load"; + if ( !(params = (struct vkcd_hook_params*)calloc(1, sizeof(*params))) + || strlen(hook_dir) >= sizeof(params->hook_dir) + || strlen(driver_dir) >= sizeof(params->driver_dir) + || strlen(driver_name) >= sizeof(params->driver_name)) + { + free(params); + return NULL; + } + strcpy(params->hook_dir, hook_dir); + strcpy(params->driver_dir, driver_dir); + strcpy(params->driver_name, driver_name); + params->dlopen_ext = android_dlopen_ext; + params->create_ns = vkcd_st.create_ns; + params->exported_ns = vkcd_st.exported_ns; + params->link_ns = vkcd_st.link_ns; + + if ( !(ns = vkcd_st.create_ns("vulkan_driver_hook", hook_dir, NULL, + VKCD_NS_SHARED, NULL, NULL, + (const void*)&vulkan_custom_driver_linker_open)) + || !vkcd_st.link_all(ns, vkcd_st.default_ns)) + { + free(params); + return NULL; + } + info.flags = ANDROID_DLEXT_USE_NAMESPACE; + info.reserved_addr = NULL; + info.reserved_size = 0; + info.relro_fd = 0; + info.library_fd = 0; + info.library_fd_offset = 0; + info.library_namespace = ns; + if ( !(hook = android_dlopen_ext("libvulkan_driver_hook.so", + RTLD_NOW | RTLD_GLOBAL, &info)) + || !(init = (vkcd_hook_init_t)dlsym(hook, + "vulkan_custom_driver_hook_init"))) + { + free(params); + return NULL; + } + init(params); + + *reason = "Vulkan loader copy did not load"; + if (!(handle = vkcd_load_copy(tmp_dir, flags, ns))) + return NULL; + *reason = NULL; + return handle; +} + +#else /* VULKAN_CUSTOM_DRIVER_HOOK */ + +#define VKCD_EXPORT __attribute__((visibility("default"))) + +static const struct vkcd_hook_params *vkcd_hook; + +VKCD_EXPORT void vulkan_custom_driver_hook_init( + const struct vkcd_hook_params *params) +{ + vkcd_hook = params; +} + +/* The loader asks for its HAL here: the custom driver goes in a namespace + * of its own, a child of the one the loader asked for. */ +static void *vkcd_hook_open(const char *name, int flags, + const android_dlextinfo *info) +{ + android_dlextinfo driver_info; + struct android_namespace_t *ns; + void *handle; + + if (!vkcd_hook) + return NULL; + if ( !name || !strstr(name, "vulkan.") + || !info || !(info->flags & ANDROID_DLEXT_USE_NAMESPACE) + || !info->library_namespace) + return vkcd_hook->dlopen_ext(name, flags, info); + + if (!(ns = vkcd_hook->create_ns(name, vkcd_hook->driver_dir, + vkcd_hook->hook_dir, VKCD_NS_SHARED, NULL, + info->library_namespace, + (const void*)&vulkan_custom_driver_hook_init))) + { + const char *err = dlerror(); + __android_log_print(ANDROID_LOG_ERROR, "RetroArch", + "[Vulkan] Driver hook: failed to load custom driver %s: " + "no namespace (%s)\n", vkcd_hook->driver_name, + err ? err : "unknown"); + return vkcd_hook->dlopen_ext(name, flags, info); + } + vkcd_hook->link_ns(ns, NULL, "libandroid.so"); + driver_info = *info; + driver_info.library_namespace = ns; + if ((handle = vkcd_hook->dlopen_ext(vkcd_hook->driver_name, flags, + &driver_info))) + return handle; + __android_log_print(ANDROID_LOG_ERROR, "RetroArch", + "[Vulkan] Driver hook: failed to load custom driver %s: %s\n", + vkcd_hook->driver_name, dlerror()); + return vkcd_hook->dlopen_ext(name, flags, info); +} + +VKCD_EXPORT void *android_dlopen_ext(const char *name, int flags, + const android_dlextinfo *info) +{ + return vkcd_hook_open(name, flags, info); +} + +VKCD_EXPORT void *android_load_sphal_library(const char *name, int flags) +{ + static const char *const names[] = { "sphal", "vendor", "default" }; + android_dlextinfo info; + unsigned i; + + if (!vkcd_hook) + return NULL; + for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) + { + struct android_namespace_t *ns = vkcd_hook->exported_ns(names[i]); + if (ns) + { + info.flags = ANDROID_DLEXT_USE_NAMESPACE; + info.reserved_addr = NULL; + info.reserved_size = 0; + info.relro_fd = 0; + info.library_fd = 0; + info.library_fd_offset = 0; + info.library_namespace = ns; + return vkcd_hook_open(name, flags, &info); + } + } + return NULL; +} +#endif +#endif diff --git a/gfx/drivers_context/android_vk_ctx.c b/gfx/drivers_context/android_vk_ctx.c index e8daa1132429..b5f894cd7043 100644 --- a/gfx/drivers_context/android_vk_ctx.c +++ b/gfx/drivers_context/android_vk_ctx.c @@ -28,6 +28,9 @@ #include "../common/vulkan_common.h" #include "../common/android_windows.h" +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +#include "../common/vulkan_custom_driver.h" +#endif #include "../../frontend/frontend_driver.h" #include "../../frontend/drivers/platform_unix.h" @@ -149,6 +152,40 @@ static bool android_gfx_ctx_vk_set_resize(void *data, unsigned dims) return true; } +/* Makes the surface, and the device on the context's first call. A custom + * driver that fails there gets the context made again on the system + * driver. */ +static bool android_gfx_ctx_vk_first_surface(android_ctx_data_vk_t *and, + ANativeWindow *window) +{ +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + /* A later set_video_mode has the renderer's objects on the device. */ + bool first = (and->vk.context.device == VK_NULL_HANDLE); +#endif + if (vulkan_surface_create(&and->vk, VULKAN_WSI_ANDROID, + NULL, window, and->dims, and->swap_interval)) + { +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + vulkan_custom_driver_started(); +#endif + return true; + } +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + if (first && vulkan_custom_driver_current()) + { + /* This init made it, before the core's context_reset: drop it. */ + and->vk.flags |= VK_DATA_FLAG_DROP_CONTEXT; + vulkan_context_destroy(&and->vk, true); + vulkan_custom_driver_failed(); + memset(&and->vk, 0, sizeof(and->vk)); + return vulkan_context_init(&and->vk, VULKAN_WSI_ANDROID) + && vulkan_surface_create(&and->vk, VULKAN_WSI_ANDROID, + NULL, window, and->dims, and->swap_interval); + } +#endif + return false; +} + static bool android_gfx_ctx_vk_set_video_mode(void *data, unsigned dims, bool fullscreen) @@ -158,9 +195,8 @@ static bool android_gfx_ctx_vk_set_video_mode(void *data, int32_t w = ANativeWindow_getWidth(android_app_window(android_app)); int32_t h = ANativeWindow_getHeight(android_app_window(android_app)); and->dims = VIDEO_SCALE_PACK(w, h); - if (!vulkan_surface_create(&and->vk, VULKAN_WSI_ANDROID, - NULL, android_app_window(android_app), - and->dims, and->swap_interval)) + if (!android_gfx_ctx_vk_first_surface(and, + android_app_window(android_app))) { RARCH_ERR("[Vulkan] Failed to create surface.\n"); return false; diff --git a/griffin/griffin.c b/griffin/griffin.c index 4804e929d81e..1a1f718ab925 100644 --- a/griffin/griffin.c +++ b/griffin/griffin.c @@ -360,6 +360,11 @@ VIDEO CONTEXT #endif #if defined(HAVE_VULKAN) #include "../gfx/drivers_context/android_vk_ctx.c" +#if defined(HAVE_VULKAN_CUSTOM_DRIVER) +#include "../gfx/common/vulkan_custom_driver.c" +#include "../gfx/common/vulkan_custom_driver_android.c" +#include "../gfx/common/vulkan_custom_driver_linker.c" +#endif #if defined(HAVE_OPENXR) #include "../gfx/drivers_context/android_vk_openxr_ctx.c" #endif diff --git a/intl/msg_hash_lbl_rows.h b/intl/msg_hash_lbl_rows.h index d28baa857954..8acd278b0a45 100644 --- a/intl/msg_hash_lbl_rows.h +++ b/intl/msg_hash_lbl_rows.h @@ -132,6 +132,7 @@ #include "../settings/settings_def_input_bind_timeouts.h" #include "../settings/settings_def_video_hdr_toggles.h" #include "../settings/settings_def_video_upscaler.h" +#include "../settings/settings_def_vulkan_driver_path.h" #include "../settings/settings_def_video_stereo.h" #include "../settings/settings_def_video_headset.h" #include "../settings/settings_def_video_screens.h" diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index 4a66492a66c5..fe93fc8d35b7 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -7661,6 +7661,22 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_DLSS_NR_SKIN_STRUCTURE_FOLLOW, "Follow Local Structure" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_VULKAN_DRIVER_PATH_SYSTEM, + "System" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_VULKAN_DRIVER_PATH_CRASHED, + "Crashed last run, using System" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_VULKAN_DRIVER_PATH_SAFE_MODE, + "System (Safe start)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_VULKAN_DRIVER_PATH_SAFE_MODE, + "%s is used again at the next normal start" + ) /* GENERATED REGION: HDR toggle group, stereo 3D & screens group and headset group (see settings_def_video_hdr_toggles.h, settings_def_video_stereo.h and settings_def_video_headset.h). */ #define SETTINGS_DEF_STRINGS_PASS #define S_BOOL(f, T, n, d, sd, df, c, us, sub) \ @@ -7740,6 +7756,7 @@ MSG_HASH(MENU_ENUM_SUBLABEL_##T, sub) MSG_HASH(MENU_ENUM_LABEL_VALUE_##T, us) #include "../settings/settings_def_video_hdr_toggles.h" #include "../settings/settings_def_video_upscaler.h" +#include "../settings/settings_def_vulkan_driver_path.h" #include "../settings/settings_def_video_stereo.h" #include "../settings/settings_def_video_headset.h" #include "../settings/settings_def_video_screens.h" @@ -34228,6 +34245,30 @@ MSG_HASH( MSG_VIDEO_DRIVER_PACING_BOOST, "On + Boost" ) +MSG_HASH( + MSG_VULKAN_CUSTOM_DRIVER_FAILED, + "Custom Vulkan driver failed to start. Using the system driver." + ) +MSG_HASH( + MSG_VULKAN_CUSTOM_DRIVER_CRASHED, + "Custom Vulkan driver crashed before and is off for this core. Pick it again to retry." + ) +MSG_HASH( + MSG_VULKAN_CUSTOM_DRIVER_INVALID, + "Custom Vulkan driver package is not usable. Using the system driver." + ) +MSG_HASH( + MSG_VULKAN_CUSTOM_DRIVER_UNSUPPORTED, + "Custom Vulkan drivers need an Adreno GPU and Android 9. Using the system driver." + ) +MSG_HASH( + MSG_VULKAN_CUSTOM_DRIVER_RESTART, + "Restart RetroArch to change the Vulkan driver." + ) +MSG_HASH( + MSG_VULKAN_CUSTOM_DRIVER_SAFE_MODE, + "Started with the system Vulkan driver." + ) MSG_HASH( MSG_RESAMPLER_QUALITY_LOWEST, "Lowest" diff --git a/menu/cbs/menu_cbs_deferred_push.c b/menu/cbs/menu_cbs_deferred_push.c index a3b3fb6d3b0f..2a674b1eb508 100644 --- a/menu/cbs/menu_cbs_deferred_push.c +++ b/menu/cbs/menu_cbs_deferred_push.c @@ -264,6 +264,9 @@ GENERIC_DEFERRED_PUSH(deferred_push_core_list_deferred, DISPLAYLIST_ GENERIC_DEFERRED_PUSH(deferred_push_core_collection_list_deferred, DISPLAYLIST_CORES_COLLECTION_SUPPORTED) GENERIC_DEFERRED_PUSH(deferred_push_menu_sounds_list, DISPLAYLIST_MENU_SOUNDS_LIST) GENERIC_DEFERRED_PUSH(deferred_push_rgui_theme_preset, DISPLAYLIST_RGUI_THEME_PRESETS) +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +GENERIC_DEFERRED_PUSH(deferred_push_vulkan_driver, DISPLAYLIST_VULKAN_DRIVERS) +#endif #ifdef HAVE_NETWORKING GENERIC_DEFERRED_PUSH(deferred_push_pl_thumbnails_updater_list, DISPLAYLIST_PL_THUMBNAILS_UPDATER) @@ -857,6 +860,9 @@ static int menu_cbs_init_bind_deferred_push_compare_label( {MENU_ENUM_LABEL_RECORD_CONFIG, deferred_push_record_configfile}, {MENU_ENUM_LABEL_STREAM_CONFIG, deferred_push_stream_configfile}, {MENU_ENUM_LABEL_RGUI_MENU_THEME_PRESET, deferred_push_rgui_theme_preset}, +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + {MENU_ENUM_LABEL_VIDEO_VULKAN_DRIVER_PATH, deferred_push_vulkan_driver}, +#endif {MENU_ENUM_LABEL_NETPLAY, deferred_push_netplay}, {MENU_ENUM_LABEL_CONTENT_SETTINGS, deferred_push_content_settings}, {MENU_ENUM_LABEL_ADD_CONTENT_LIST, deferred_push_add_content_list}, @@ -940,6 +946,9 @@ static int menu_cbs_init_bind_deferred_push_compare_label( { MENU_ENUM_LABEL_RECORD_CONFIG, deferred_push_record_configfile }, { MENU_ENUM_LABEL_STREAM_CONFIG, deferred_push_stream_configfile }, { MENU_ENUM_LABEL_RGUI_MENU_THEME_PRESET, deferred_push_rgui_theme_preset }, +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + { MENU_ENUM_LABEL_VIDEO_VULKAN_DRIVER_PATH, deferred_push_vulkan_driver }, +#endif { MENU_ENUM_LABEL_SHADER_OPTIONS, deferred_push_shader_options }, { MENU_ENUM_LABEL_ONLINE_UPDATER, deferred_push_options }, { MENU_ENUM_LABEL_NETPLAY, deferred_push_netplay }, diff --git a/menu/cbs/menu_cbs_ok.c b/menu/cbs/menu_cbs_ok.c index 53a916a5b9e4..e7abb5eeadea 100644 --- a/menu/cbs/menu_cbs_ok.c +++ b/menu/cbs/menu_cbs_ok.c @@ -92,6 +92,9 @@ #include "../../bluetooth/bluetooth_driver.h" #endif #include "../../gfx/video_display_server.h" +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +#include "../../gfx/common/vulkan_custom_driver.h" +#endif #include "../../manual_content_scan.h" #ifdef __MACH__ #include @@ -149,6 +152,7 @@ enum ACTION_OK_SHOW_WIMP, ACTION_OK_LOAD_CHEAT_FILE_APPEND, ACTION_OK_LOAD_RGUI_MENU_THEME_PRESET, + ACTION_OK_LOAD_VULKAN_DRIVER, ACTION_OK_SET_MANUAL_CONTENT_SCAN_DAT_FILE }; @@ -1529,6 +1533,33 @@ int generic_action_ok_displaylist_push( info_path = parent_dir; } break; +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + case ACTION_OK_DL_VULKAN_DRIVER: + { + char drivers_dir[DIR_MAX_LENGTH]; + + filebrowser_clear_type(); + info.type = type; + info.directory_ptr = idx; + info_label = label; + dl_type = DISPLAYLIST_FILE_BROWSER_SELECT_FILE; + + fill_pathname_join_special(drivers_dir, + settings->paths.directory_system, "vulkan_drivers", + sizeof(drivers_dir)); + if ( *settings->paths.directory_system + && !path_is_directory(drivers_dir)) + path_mkdir(drivers_dir); + + action_ok_get_file_browser_start_path( + settings->paths.path_vulkan_driver, + drivers_dir, + parent_dir, sizeof(parent_dir), true); + + info_path = parent_dir; + } + break; +#endif case ACTION_OK_DL_CORE_LIST: filebrowser_clear_type(); info.type = type; @@ -2660,6 +2691,17 @@ static int generic_action_ok(const char *path, settings->paths.path_rgui_theme_preset, action_path); } break; +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + case ACTION_OK_LOAD_VULKAN_DRIVER: + flush_char = MENU_ENUM_LABEL_DEFERRED_VIDEO_OUTPUT_SETTINGS_LIST_STR; + if (settings) + { + configuration_set_string(settings, + settings->paths.path_vulkan_driver, action_path); + vulkan_custom_driver_picked(settings->paths.path_vulkan_driver); + } + break; +#endif case ACTION_OK_SUBSYSTEM_ADD: /* Return to the subsystem list (which rebuilds to offer the * next required ROM, or the Load entry once all are set) @@ -2871,6 +2913,9 @@ DEFAULT_ACTION_OK_SET(action_ok_shader_preset_load, ACTION_OK_LOAD_PRESET, DEFAULT_ACTION_OK_SET(action_ok_shader_pass_load, ACTION_OK_LOAD_SHADER_PASS, MENU_ENUM_LABEL_SHADER_OPTIONS) #endif DEFAULT_ACTION_OK_SET(action_ok_rgui_menu_theme_preset_load, ACTION_OK_LOAD_RGUI_MENU_THEME_PRESET, MENU_ENUM_LABEL_MENU_SETTINGS) +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +DEFAULT_ACTION_OK_SET(action_ok_vulkan_driver_load, ACTION_OK_LOAD_VULKAN_DRIVER, MENU_ENUM_LABEL_DEFERRED_VIDEO_OUTPUT_SETTINGS_LIST) +#endif DEFAULT_ACTION_OK_SET(action_ok_set_manual_content_scan_dat_file, ACTION_OK_SET_MANUAL_CONTENT_SCAN_DAT_FILE, MENU_ENUM_LABEL_DEFERRED_MANUAL_CONTENT_SCAN_LIST) static int action_ok_file_load(const char *path, @@ -7299,6 +7344,9 @@ STATIC_DEFAULT_ACTION_OK_FUNC(action_ok_push_accounts_cheevos_list, ACTION_OK_DL STATIC_DEFAULT_ACTION_OK_FUNC(action_ok_push_eject_disc, ACTION_OK_DL_EJECT_DISC) #endif STATIC_DEFAULT_ACTION_OK_FUNC(action_ok_rgui_menu_theme_preset, ACTION_OK_DL_RGUI_MENU_THEME_PRESET) +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +STATIC_DEFAULT_ACTION_OK_FUNC(action_ok_vulkan_driver, ACTION_OK_DL_VULKAN_DRIVER) +#endif #ifdef HAVE_NETWORKING STATIC_DEFAULT_ACTION_OK_FUNC(action_ok_pl_thumbnails_updater_list, ACTION_OK_DL_PL_THUMBNAILS_UPDATER_LIST) #endif @@ -9973,6 +10021,9 @@ static int menu_cbs_init_bind_ok_compare_label(menu_file_list_cbs_t *cbs, {MENU_ENUM_LABEL_STREAM_CONFIG, action_ok_stream_configfile}, #ifdef HAVE_RGUI {MENU_ENUM_LABEL_RGUI_MENU_THEME_PRESET, action_ok_rgui_menu_theme_preset}, +#endif +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + {MENU_ENUM_LABEL_VIDEO_VULKAN_DRIVER_PATH, action_ok_vulkan_driver}, #endif {MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_DIR, action_ok_push_manual_content_scan_dir_select}, {MENU_ENUM_LABEL_SCAN_METHOD, action_ok_scan_method}, @@ -10168,6 +10219,9 @@ static int menu_cbs_init_bind_ok_compare_label(menu_file_list_cbs_t *cbs, {MENU_ENUM_LABEL_RECORD_CONFIG, action_ok_record_configfile}, {MENU_ENUM_LABEL_STREAM_CONFIG, action_ok_stream_configfile}, {MENU_ENUM_LABEL_RGUI_MENU_THEME_PRESET, action_ok_rgui_menu_theme_preset}, +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + {MENU_ENUM_LABEL_VIDEO_VULKAN_DRIVER_PATH, action_ok_vulkan_driver}, +#endif {MENU_ENUM_LABEL_ACCOUNTS_RETRO_ACHIEVEMENTS, action_ok_push_accounts_cheevos_list}, {MENU_ENUM_LABEL_FAVORITES, action_ok_push_content_list}, {MENU_ENUM_LABEL_DOWNLOADED_FILE_DETECT_CORE_LIST, action_ok_push_downloads_dir}, @@ -10447,6 +10501,11 @@ static int menu_cbs_init_bind_ok_compare_type(menu_file_list_cbs_t *cbs, case FILE_TYPE_RGUI_THEME_PRESET: BIND_ACTION_OK(cbs, action_ok_rgui_menu_theme_preset_load); break; +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + case FILE_TYPE_VULKAN_DRIVER: + BIND_ACTION_OK(cbs, action_ok_vulkan_driver_load); + break; +#endif case FILE_TYPE_REMAP: BIND_ACTION_OK(cbs, action_ok_remap_file_load); break; diff --git a/menu/cbs/menu_cbs_start.c b/menu/cbs/menu_cbs_start.c index 25605d66d997..228866b3241f 100644 --- a/menu/cbs/menu_cbs_start.c +++ b/menu/cbs/menu_cbs_start.c @@ -190,6 +190,30 @@ static int action_start_audio_dsp_plugin_file_load( return 0; } +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +static int action_start_vulkan_driver_path( + const char *path, const char *label, + unsigned type, size_t idx, size_t entry_idx) +{ + settings_t *settings = config_get_ptr(); + + if (!settings) + return -1; + + if (*settings->paths.path_vulkan_driver) + { + struct menu_state *menu_st = menu_state_get_ptr(); + configuration_set_string(settings, + settings->paths.path_vulkan_driver, ""); + /* Rebinds the sublabel too */ + menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH + | MENU_ST_FLAG_PREVENT_POPULATE; + } + + return 0; +} +#endif + static int generic_action_start_performance_counters(struct retro_perf_counter **counters, unsigned offset, unsigned type, const char *label) { @@ -905,6 +929,11 @@ static int menu_cbs_init_bind_start_compare_label(menu_file_list_cbs_t *cbs) case MENU_ENUM_LABEL_AUDIO_DSP_PLUGIN: BIND_ACTION_START(cbs, action_start_audio_dsp_plugin_file_load); break; +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + case MENU_ENUM_LABEL_VIDEO_VULKAN_DRIVER_PATH: + BIND_ACTION_START(cbs, action_start_vulkan_driver_path); + break; +#endif case MENU_ENUM_LABEL_VIDEO_SHADER_PASS: #if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL) BIND_ACTION_START(cbs, action_start_shader_pass); diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index 618865afb28e..916878cc7ffb 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -63,6 +63,9 @@ #include "../../msg_hash_lbl_str.h" #include "../../playlist.h" #include "../../runtime_file.h" +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +#include "../../gfx/common/vulkan_custom_driver.h" +#endif #ifndef BIND_ACTION_SUBLABEL #define BIND_ACTION_SUBLABEL(cbs, name) (cbs)->action_sublabel = (name) @@ -1509,6 +1512,47 @@ static int action_bind_sublabel_core_backup_entry( return 1; } +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_video_vulkan_driver_path, MENU_ENUM_SUBLABEL_VIDEO_VULKAN_DRIVER_PATH) + +static int action_bind_sublabel_video_vulkan_driver_path_crashed( + file_list_t *list, + unsigned type, unsigned i, + const char *label, const char *path, + char *s, size_t len) +{ + size_t _len = 0; + strlcpy_append(s, len, &_len, msg_hash_to_str( + MENU_ENUM_SUBLABEL_VIDEO_VULKAN_DRIVER_PATH_CRASHED)); + strlcpy_append(s, len, &_len, "\n"); + strlcpy_append(s, len, &_len, msg_hash_to_str( + MENU_ENUM_SUBLABEL_VIDEO_VULKAN_DRIVER_PATH)); + return 1; +} + +static int action_bind_sublabel_video_vulkan_driver_path_safe_mode( + file_list_t *list, + unsigned type, unsigned i, + const char *label, const char *path, + char *s, size_t len) +{ + size_t _len = 0; + const char *saved = config_get_ptr()->paths.path_vulkan_driver; + if (!string_is_empty(saved)) + { + char name[NAME_MAX_LENGTH]; + fill_pathname(name, path_basename(saved), "", sizeof(name)); + snprintf(s, len, msg_hash_to_str( + MENU_ENUM_SUBLABEL_VIDEO_VULKAN_DRIVER_PATH_SAFE_MODE), name); + _len = strlen(s); + strlcpy_append(s, len, &_len, "\n"); + } + strlcpy_append(s, len, &_len, msg_hash_to_str( + MENU_ENUM_SUBLABEL_VIDEO_VULKAN_DRIVER_PATH)); + return 1; +} +#endif + static int action_bind_sublabel_generic( file_list_t *list, unsigned type, unsigned i, @@ -2972,6 +3016,22 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_MICROPHONE_RESAMPLER_QUALITY: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_microphone_resampler_quality); break; +#endif +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + /* Chosen here, on the main thread: Ozone draws sublabels + * from its frame callback, the video thread when threaded */ + case MENU_ENUM_LABEL_VIDEO_VULKAN_DRIVER_PATH: + if (vulkan_custom_driver_crashed( + config_get_ptr()->paths.path_vulkan_driver)) + BIND_ACTION_SUBLABEL(cbs, + action_bind_sublabel_video_vulkan_driver_path_crashed); + else if (vulkan_custom_driver_safe_mode()) + BIND_ACTION_SUBLABEL(cbs, + action_bind_sublabel_video_vulkan_driver_path_safe_mode); + else + BIND_ACTION_SUBLABEL(cbs, + action_bind_sublabel_video_vulkan_driver_path); + break; #endif case MENU_ENUM_LABEL_MATERIALUI_ICONS_ENABLE: #ifdef HAVE_MATERIALUI diff --git a/menu/menu_cbs.h b/menu/menu_cbs.h index 76e9b89c4060..8489fc60be9c 100644 --- a/menu/menu_cbs.h +++ b/menu/menu_cbs.h @@ -254,6 +254,7 @@ enum ACTION_OK_DL_CONTENT_SETTINGS, ACTION_OK_DL_CDROM_INFO_DETAIL_LIST, ACTION_OK_DL_RGUI_MENU_THEME_PRESET, + ACTION_OK_DL_VULKAN_DRIVER, ACTION_OK_DL_MANUAL_CONTENT_SCAN_LIST, ACTION_OK_DL_MANUAL_CONTENT_SCAN_DAT_FILE, ACTION_OK_DL_CORE_RESTORE_BACKUP_LIST, diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index c5febc3d8809..88a25c837772 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -505,7 +505,8 @@ static int filebrowser_parse( if ( type_default == FILE_TYPE_SHADER_PRESET || type_default == FILE_TYPE_SHADER || type_default == FILE_TYPE_OVERLAY - || type_default == FILE_TYPE_OSK_OVERLAY) + || type_default == FILE_TYPE_OSK_OVERLAY + || type_default == FILE_TYPE_VULKAN_DRIVER) filter_ext = true; if (string_is_equal(label, MENU_ENUM_LABEL_SCAN_FILE_STR)) @@ -12160,6 +12161,9 @@ unsigned menu_displaylist_build_list( { MENU_ENUM_LABEL_VIDEO_THREADED, PARSE_ONLY_BOOL, false }, { MENU_ENUM_LABEL_VIDEO_GPU_INDEX, PARSE_ONLY_INT, false }, { MENU_ENUM_LABEL_VIDEO_MONITOR_INDEX, PARSE_ONLY_UINT, false }, +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + { MENU_ENUM_LABEL_VIDEO_VULKAN_DRIVER_PATH, PARSE_ONLY_PATH, false }, +#endif { MENU_ENUM_LABEL_VIDEO_SWAPCHAIN_BIT_DEPTH, PARSE_ONLY_UINT, false }, { MENU_ENUM_LABEL_VIDEO_HW_DECODE, PARSE_ONLY_UINT, false }, }; @@ -18337,6 +18341,7 @@ static bool menu_displaylist_ctl_internal( case DISPLAYLIST_CONFIG_FILES: case DISPLAYLIST_REMAP_FILES: case DISPLAYLIST_RGUI_THEME_PRESETS: + case DISPLAYLIST_VULKAN_DRIVERS: case DISPLAYLIST_STREAM_CONFIG_FILES: case DISPLAYLIST_RECORD_CONFIG_FILES: case DISPLAYLIST_OVERLAYS: @@ -18369,6 +18374,10 @@ static bool menu_displaylist_ctl_internal( info->type_default = FILE_TYPE_RGUI_THEME_PRESET; info->exts = strldup("cfg", sizeof("cfg")); break; + case DISPLAYLIST_VULKAN_DRIVERS: + info->type_default = FILE_TYPE_VULKAN_DRIVER; + info->exts = strldup("zip", sizeof("zip")); + break; case DISPLAYLIST_STREAM_CONFIG_FILES: info->type_default = FILE_TYPE_STREAM_CONFIG; info->exts = strldup("cfg", sizeof("cfg")); diff --git a/menu/menu_displaylist.h b/menu/menu_displaylist.h index 8bd8b6c1f591..ff308c50c78d 100644 --- a/menu/menu_displaylist.h +++ b/menu/menu_displaylist.h @@ -158,6 +158,7 @@ enum menu_displaylist_ctl_state DISPLAYLIST_RECORD_CONFIG_FILES, DISPLAYLIST_STREAM_CONFIG_FILES, DISPLAYLIST_RGUI_THEME_PRESETS, + DISPLAYLIST_VULKAN_DRIVERS, DISPLAYLIST_CONFIG_FILES, DISPLAYLIST_IMAGES, DISPLAYLIST_FONTS, diff --git a/menu/menu_setting.c b/menu/menu_setting.c index d6243a686062..feca3deb35d6 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -135,6 +135,9 @@ void android_app_set_window_settings(bool notch_write_over, #ifdef HAVE_TEMPORAL_UPSCALER #include "../gfx/video_upscaler.h" #endif +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +#include "../gfx/common/vulkan_custom_driver.h" +#endif #include "../gfx/video_views.h" #include "../gfx/video_windows.h" #ifdef HAVE_MODELINE @@ -3671,6 +3674,23 @@ static size_t setting_get_string_representation_video_font_path( "", len); } +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +static size_t setting_get_string_representation_vulkan_driver( + rarch_setting_t *setting, char *s, size_t len) +{ + if (!setting) + return 0; + if (vulkan_custom_driver_safe_mode()) + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_VULKAN_DRIVER_PATH_SAFE_MODE), len); + if (!setting->value.target.string || !*setting->value.target.string) + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_VULKAN_DRIVER_PATH_SYSTEM), len); + return fill_pathname(s, path_basename(setting->value.target.string), + "", len); +} +#endif + static size_t setting_get_string_representation_video_hdr_expand_gamut( rarch_setting_t *setting, char *s, size_t len) { @@ -13592,6 +13612,13 @@ static const setting_desc_t vid_desc_3[] = { }; #endif +#ifdef HAVE_VULKAN_CUSTOM_DRIVER +static const setting_desc_t vulkan_driver_desc[] = { +/* GENERATED: rows come from settings_def_vulkan_driver_path.h in order. */ +#include "../settings/settings_def_vulkan_driver_path.h" +}; +#endif + #ifdef HAVE_D3D10 static const setting_desc_t vid_desc_4[] = { /* GENERATED: rows come from settings_def_gpu_index_d3d10.h in order. */ @@ -16578,6 +16605,11 @@ static void settings_build_video( ADD_DESC(vid_desc_3); } #endif +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + /* Any video driver: HW cores force Vulkan under gl too */ + if (vulkan_custom_driver_supported()) + ADD_DESC(vulkan_driver_desc); +#endif #ifdef HAVE_D3D10 if (string_is_equal(video_driver_get_ident(), "d3d10")) @@ -20364,6 +20396,9 @@ static const settings_desc_table_t settings_desc_registry[] = { #ifdef HAVE_VULKAN { vid_desc_3, (uint16_t)ARRAY_SIZE(vid_desc_3) }, #endif +#ifdef HAVE_VULKAN_CUSTOM_DRIVER + { vulkan_driver_desc, (uint16_t)ARRAY_SIZE(vulkan_driver_desc) }, +#endif #ifdef HAVE_D3D10 { vid_desc_4, (uint16_t)ARRAY_SIZE(vid_desc_4) }, #endif diff --git a/msg_hash.h b/msg_hash.h index 2dfe9347030d..c43d5d206b47 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -188,6 +188,7 @@ enum msg_file_type FILE_TYPE_MD5, FILE_TYPE_CRC, FILE_TYPE_RGUI_THEME_PRESET, + FILE_TYPE_VULKAN_DRIVER, FILE_TYPE_VIDEO_LAYOUT, FILE_TYPE_WEBM, FILE_TYPE_F4F, @@ -341,6 +342,12 @@ enum msg_hash_enums MSG_MENU_FRAME_RATE_CONTENT, MSG_VIDEO_DRIVER_PACING_ON, MSG_VIDEO_DRIVER_PACING_BOOST, + MSG_VULKAN_CUSTOM_DRIVER_FAILED, + MSG_VULKAN_CUSTOM_DRIVER_CRASHED, + MSG_VULKAN_CUSTOM_DRIVER_INVALID, + MSG_VULKAN_CUSTOM_DRIVER_UNSUPPORTED, + MSG_VULKAN_CUSTOM_DRIVER_RESTART, + MSG_VULKAN_CUSTOM_DRIVER_SAFE_MODE, MSG_RESAMPLER_QUALITY_LOWEST, MSG_RESAMPLER_QUALITY_LOWER, MSG_RESAMPLER_QUALITY_NORMAL, @@ -9346,6 +9353,10 @@ enum msg_hash_enums MENU_ENUM_LABEL_VALUE_VIDEO_DLSS_NR_STYLE_NATURAL, MENU_ENUM_LABEL_VALUE_VIDEO_DLSS_NR_STYLE_CINEMATIC, MENU_ENUM_LABEL_VALUE_VIDEO_DLSS_NR_SKIN_STRUCTURE_FOLLOW, + MENU_ENUM_LABEL_VALUE_VIDEO_VULKAN_DRIVER_PATH_SYSTEM, + MENU_ENUM_SUBLABEL_VIDEO_VULKAN_DRIVER_PATH_CRASHED, + MENU_ENUM_LABEL_VALUE_VIDEO_VULKAN_DRIVER_PATH_SAFE_MODE, + MENU_ENUM_SUBLABEL_VIDEO_VULKAN_DRIVER_PATH_SAFE_MODE, /* GENERATED REGION: HDR toggle group, stereo 3D & screens group and headset group enum rows (see settings/settings_def_video_hdr_toggles.h, settings/settings_def_video_stereo.h and settings/settings_def_video_headset.h). */ #define SETTINGS_DEF_ENUM_PASS #define SETTINGS_DEF_STRINGS_PASS @@ -9411,6 +9422,7 @@ enum msg_hash_enums #define S_ACTION_EX_NS_H(T, n, sd, ok, rp, c, us) MENU_LBL_H(T), #include "settings/settings_def_video_hdr_toggles.h" #include "settings/settings_def_video_upscaler.h" +#include "settings/settings_def_vulkan_driver_path.h" #include "settings/settings_def_video_stereo.h" #include "settings/settings_def_video_headset.h" #include "settings/settings_def_video_screens.h" diff --git a/msg_hash_lbl_str.h b/msg_hash_lbl_str.h index 3d46daa8ba41..55ad33ebcc10 100644 --- a/msg_hash_lbl_str.h +++ b/msg_hash_lbl_str.h @@ -1390,6 +1390,7 @@ extern const struct msg_hash_us_lbl_blob_s msg_hash_us_lbl_blob; #define MENU_ENUM_LABEL_VIDEO_DLSS_DIRECTORY_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_DLSS_DIRECTORY) #define MENU_ENUM_LABEL_VIDEO_FSR_DIRECTORY_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_FSR_DIRECTORY) #define MENU_ENUM_LABEL_VIDEO_XESS_DIRECTORY_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_XESS_DIRECTORY) +#define MENU_ENUM_LABEL_VIDEO_VULKAN_DRIVER_PATH_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_VULKAN_DRIVER_PATH) #define MENU_ENUM_LABEL_VIDEO_STEREO_SETTINGS_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_STEREO_SETTINGS) #define MENU_ENUM_LABEL_VIDEO_STEREO_MODE_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_STEREO_MODE) #define MENU_ENUM_LABEL_VIDEO_STEREO_SWAP_EYES_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_STEREO_SWAP_EYES) @@ -3154,6 +3155,7 @@ extern const struct msg_hash_us_lbl_blob_s msg_hash_us_lbl_blob; #define MENU_ENUM_LABEL_VIDEO_DLSS_DIRECTORY_STR "video_dlss_directory" #define MENU_ENUM_LABEL_VIDEO_FSR_DIRECTORY_STR "video_fsr_directory" #define MENU_ENUM_LABEL_VIDEO_XESS_DIRECTORY_STR "video_xess_directory" +#define MENU_ENUM_LABEL_VIDEO_VULKAN_DRIVER_PATH_STR "video_vulkan_driver_path" #define MENU_ENUM_LABEL_VIDEO_STEREO_SETTINGS_STR "video_stereo_settings" #define MENU_ENUM_LABEL_VIDEO_STEREO_MODE_STR "video_stereo_mode" #define MENU_ENUM_LABEL_VIDEO_STEREO_SWAP_EYES_STR "video_stereo_swap_eyes" diff --git a/pkg/android/phoenix-common/jni/Android.mk b/pkg/android/phoenix-common/jni/Android.mk index fa6ca5f1bd0a..2298f5091ad1 100644 --- a/pkg/android/phoenix-common/jni/Android.mk +++ b/pkg/android/phoenix-common/jni/Android.mk @@ -241,6 +241,23 @@ ifeq ($(HAVE_OPENXR),1) DEFINES += -DHAVE_OPENXR -DXR_OS_ANDROID -DXR_USE_PLATFORM_ANDROID endif +# Custom Vulkan driver packages: sideloaded arm64 builds only. +# Play builds pass PLAY_STORE_BUILD=1, Quest passes HAVE_OPENXR=1. +HAVE_VULKAN_CUSTOM_DRIVER := 0 +ifeq ($(TARGET_ARCH_ABI),arm64-v8a) + ifeq ($(HAVE_VULKAN),1) + ifneq ($(PLAY_STORE_BUILD),1) + ifneq ($(HAVE_OPENXR),1) + HAVE_VULKAN_CUSTOM_DRIVER := 1 + endif + endif + endif +endif + +ifeq ($(HAVE_VULKAN_CUSTOM_DRIVER),1) +DEFINES += -DHAVE_VULKAN_CUSTOM_DRIVER +endif + LOCAL_CFLAGS += -Wall -std=gnu99 -pthread -Wno-unused-function -fno-stack-protector -funroll-loops $(DEFINES) LOCAL_CPPFLAGS := -fexceptions -fpermissive -std=gnu++11 -fno-rtti -Wno-reorder $(DEFINES) @@ -305,3 +322,16 @@ ifeq ($(TARGET_ARCH_ABI),x86_64) endif include $(BUILD_SHARED_LIBRARY) + +ifeq ($(HAVE_VULKAN_CUSTOM_DRIVER),1) +# Loaded into a private linker namespace in front of a copy of the system +# Vulkan loader, which opens its HAL through it. +include $(CLEAR_VARS) +LOCAL_MODULE := vulkan_driver_hook +LOCAL_CFLAGS := -DVULKAN_CUSTOM_DRIVER_HOOK -fvisibility=hidden -Wall +LOCAL_SRC_FILES := $(RARCH_DIR)/gfx/common/vulkan_custom_driver_linker.c +LOCAL_LDFLAGS := -Wl,-z,global \ + -Wl,-z,max-page-size=16384,-z,common-page-size=16384 +LOCAL_LDLIBS := -ldl -llog +include $(BUILD_SHARED_LIBRARY) +endif diff --git a/pkg/android/phoenix-jelly-bean/build.gradle b/pkg/android/phoenix-jelly-bean/build.gradle index 03168ab6054a..12ab425480ba 100644 --- a/pkg/android/phoenix-jelly-bean/build.gradle +++ b/pkg/android/phoenix-jelly-bean/build.gradle @@ -35,7 +35,7 @@ android { minSdkVersion 16 externalNativeBuild { ndkBuild { - arguments "-j${Runtime.runtime.availableProcessors()}" + arguments "-j${Runtime.runtime.availableProcessors()}", "HAVE_VULKAN_CUSTOM_DRIVER=0" } } targetSdkVersion 28 diff --git a/pkg/android/phoenix/src/com/retroarch/browser/retroactivity/RetroActivityFuture.java b/pkg/android/phoenix/src/com/retroarch/browser/retroactivity/RetroActivityFuture.java index a6cd1ec66323..c7ab76aa6a92 100644 --- a/pkg/android/phoenix/src/com/retroarch/browser/retroactivity/RetroActivityFuture.java +++ b/pkg/android/phoenix/src/com/retroarch/browser/retroactivity/RetroActivityFuture.java @@ -2,6 +2,7 @@ import android.content.res.Configuration; import android.graphics.Rect; +import android.graphics.drawable.Icon; import android.util.Log; import android.view.Display; import android.view.DisplayCutout; @@ -14,6 +15,8 @@ import android.view.WindowMetrics; import android.content.Intent; import android.content.Context; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; import android.hardware.input.InputManager; import android.os.Build; import android.os.Bundle; @@ -23,6 +26,7 @@ import com.retroarch.browser.preferences.util.UserPreferences; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Collections; public final class RetroActivityFuture extends RetroActivityCamera { @@ -65,6 +69,13 @@ public void handleMessage(Message msg) { @Override public void onCreate(Bundle savedInstanceState) { + // Safe start is one-shot. A restore or a Recents relaunch brings the + // old intent back; drop it before super.onCreate starts the native thread. + // A recreate in this process stays safe: native code latches it. + if (savedInstanceState != null + || (getIntent().getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) + getIntent().removeExtra("VULKAN_DRIVER"); + super.onCreate(savedInstanceState); isRunning = true; @@ -94,16 +105,20 @@ public void onNewIntent(Intent intent) { // Extract game parameters from new intent String newRom = intent.getStringExtra("ROM"); String newCore = intent.getStringExtra("LIBRETRO"); + String newVulkanDriver = intent.getStringExtra("VULKAN_DRIVER"); // Get current intent parameters for comparison Intent currentIntent = getIntent(); String currentRom = currentIntent != null ? currentIntent.getStringExtra("ROM") : null; String currentCore = currentIntent != null ? currentIntent.getStringExtra("LIBRETRO") : null; + String currentVulkanDriver = currentIntent != null ? currentIntent.getStringExtra("VULKAN_DRIVER") : null; // Check if we're trying to launch different content if ((newRom != null && !newRom.equals(currentRom)) || - (newCore != null && !newCore.equals(currentCore))) { + (newCore != null && !newCore.equals(currentCore)) || + // Safe start: the Vulkan driver is chosen when the process starts + (newVulkanDriver != null && !newVulkanDriver.equals(currentVulkanDriver))) { // Different game content - start fresh instance then exit Intent restartIntent = new Intent(intent); restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); @@ -283,6 +298,29 @@ public void inputGrabMouse(boolean state) { mHandlerSendUiMessage(HANDLER_WHAT_TOGGLE_POINTER_ICON, state); } + // Called from native code before the first custom Vulkan driver load, + // so a driver that leaves a black screen still has a way back. + public void addSafeStartShortcut() { + // Dynamic shortcuts need Android 7.1 (25) and up + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + try { + Intent intent = new Intent(this, RetroActivityFuture.class); + intent.setAction(Intent.ACTION_MAIN); + intent.putExtra("VULKAN_DRIVER", "system"); + ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "safe_start") + .setShortLabel("Safe start") + .setLongLabel("Start with system Vulkan") + .setIcon(Icon.createWithResource(this, getApplicationInfo().icon)) + .setIntent(intent) + .build(); + getSystemService(ShortcutManager.class) + .addDynamicShortcuts(Collections.singletonList(shortcut)); + } catch (Exception e) { + Log.w("RetroActivityFuture", "[addSafeStartShortcut] exception thrown: " + e.getMessage()); + } + } + } + private void attemptToggleImmersiveMode(boolean state) { // Attempt to toggle "Immersive Mode" for Android 4.4 (19) and up if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { diff --git a/samples/gfx/vulkan_custom_driver/Makefile b/samples/gfx/vulkan_custom_driver/Makefile new file mode 100644 index 000000000000..bc4650081bf1 --- /dev/null +++ b/samples/gfx/vulkan_custom_driver/Makefile @@ -0,0 +1,70 @@ +include ../../sample_objdir.mk + +TARGET := vulkan_custom_driver_test + +# The pure half of the Android custom Vulkan driver loader, fed from +# buffers: package checks, the extraction key, the crash state, the +# session's choices and the loader copy's soname patch. Builds on Linux +# and with mingw-w64. +REPO_ROOT := ../../.. +LIBRETRO_COMM_DIR := $(REPO_ROOT)/libretro-common + +SOURCES := vulkan_custom_driver_test.c \ + $(REPO_ROOT)/gfx/common/vulkan_custom_driver.c \ + $(REPO_ROOT)/gfx/common/vulkan_custom_driver_linker.c \ + $(LIBRETRO_COMM_DIR)/formats/json/rjson.c \ + $(LIBRETRO_COMM_DIR)/string/rstrtod.c \ + $(LIBRETRO_COMM_DIR)/encodings/encoding_crc32.c \ + $(LIBRETRO_COMM_DIR)/features/features_cpu.c \ + $(LIBRETRO_COMM_DIR)/lists/string_list.c \ + $(LIBRETRO_COMM_DIR)/compat/compat_strl.c \ + $(LIBRETRO_COMM_DIR)/time/rtime.c + +ifneq ($(findstring mingw,$(CC)),) + EXE := .exe +else + EXE := +endif + +CFLAGS += -Wall -g1 -O0 -I$(LIBRETRO_COMM_DIR)/include + +OBJS := $(call sample_objs,$(SOURCES:.c=.o)) + +# libretro-common is built as the tree builds it; vulkan_custom_driver*.c +# and the test as C89 +STRICT_OBJS := vulkan_custom_driver_test.o \ + $(call sample_objs,$(REPO_ROOT)/gfx/common/vulkan_custom_driver.o) \ + $(call sample_objs,$(REPO_ROOT)/gfx/common/vulkan_custom_driver_linker.o) +STRICT_FLAGS := -std=c89 -pedantic -Wdeclaration-after-statement \ + -Wframe-larger-than=4096 -Werror +ifneq ($(findstring clang,$(shell $(CC) --version 2>/dev/null)),) +# boolean.h uses the compiler _Bool spelling in C89 builds. +STRICT_FLAGS += -Wno-c99-extensions +endif +$(STRICT_OBJS): CFLAGS += $(STRICT_FLAGS) + +ifneq ($(SANITIZER),) + CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS) + LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS) +endif + +all: $(TARGET)$(EXE) + +%.o: %.c + $(CC) -c -o $@ $< $(CFLAGS) + +$(SAMPLE_OBJDIR)/%.o: $$(call sample_src,$$*).c + @mkdir -p $(@D) + $(CC) -c -o $@ $< $(CFLAGS) + +$(TARGET)$(EXE): $(OBJS) + $(CC) -o $@ $^ $(LDFLAGS) + +check: all + $(RUNNER) ./$(TARGET)$(EXE) + +clean: + rm -rf $(SAMPLE_OBJDIR) + rm -f $(TARGET) $(TARGET).exe $(OBJS) + +.PHONY: all check clean diff --git a/samples/gfx/vulkan_custom_driver/vulkan_custom_driver_test.c b/samples/gfx/vulkan_custom_driver/vulkan_custom_driver_test.c new file mode 100644 index 000000000000..a20eed1c6474 --- /dev/null +++ b/samples/gfx/vulkan_custom_driver/vulkan_custom_driver_test.c @@ -0,0 +1,931 @@ +/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: + * GPL-3.0-or-later */ +#include +#include + +#include + +#include "../../../gfx/common/vulkan_custom_driver.h" + +static unsigned checks; +static unsigned failures; + +#define CHECK(cond) do { checks++; if (!(cond)) { failures++; \ + printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); } } while (0) + +/* meta.json of two real driver packages, byte for byte */ +static const char t30_meta[] = + "{\n" + " \"schemaVersion\": 1,\n" + " \"name\": \"Turnip Adreno Driver T30 (@Mr_Purple_666)\",\n" + " \"description\": \"Open-source Vulkan driver build, MrPurple still alive. Big thanks to DISKDVD\",\n" + " \"author\": \"Mr_Purple_666\",\n" + " \"packageVersion\": \"T30\",\n" + " \"vendor\": \"Mesa\",\n" + " \"driverVersion\": \"26.3.0-T30-1.4.359\",\n" + " \"minApi\": 30,\n" + " \"libraryName\": \"vulkan.purple.so\"\n" + "}"; +static const char k11_meta[] = + "{\n" + " \"schemaVersion\": 1,\n" + " \"name\": \"Mesa Turnip driver v26.0.0 - R8\",\n" + " \"description\": \"Compiled from Source + unsupported gpu hacks\",\n" + " \"author\": \"KIMCHI\",\n" + " \"packageVersion\": \"1\",\n" + " \"vendor\": \"Mesa\",\n" + " \"driverVersion\": \"Vulkan 1.4.335\",\n" + " \"minApi\": 27,\n" + " \"libraryName\": \"vulkan.ad07xx.so\"\n" + "}"; + +/* The first 20 bytes of vulkan.purple.so */ +static const unsigned char t30_elf[VULKAN_CUSTOM_DRIVER_ELF_HEAD] = { + 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0xb7, 0x00 }; + +static char json[VULKAN_CUSTOM_DRIVER_META_MAX * 2]; + +/* A meta.json with these raw JSON values; NULL leaves the key out. */ +static const char *meta_of(const char *schema, const char *vendor, + const char *library, const char *min_api) +{ + strcpy(json, "{\"name\": \"test\""); + if (schema) + { + strcat(json, ", \"schemaVersion\": "); + strcat(json, schema); + } + if (vendor) + { + strcat(json, ", \"vendor\": "); + strcat(json, vendor); + } + if (library) + { + strcat(json, ", \"libraryName\": "); + strcat(json, library); + } + if (min_api) + { + strcat(json, ", \"minApi\": "); + strcat(json, min_api); + } + strcat(json, "}"); + return json; +} + +static const char *parse(const char *buf, int api_level) +{ + struct vulkan_custom_driver_meta meta; + return vulkan_custom_driver_parse_meta(buf, strlen(buf), api_level, + &meta); +} + +static void test_meta(void) +{ + struct vulkan_custom_driver_meta meta; + char name[VULKAN_CUSTOM_DRIVER_LIBRARY_MAX + 8]; + size_t pad; + + CHECK(!vulkan_custom_driver_parse_meta(t30_meta, sizeof(t30_meta) - 1, + 33, &meta)); + CHECK(!strcmp(meta.library_name, "vulkan.purple.so")); + CHECK(!strcmp(meta.vendor, "Mesa") && meta.mesa); + CHECK(!strcmp(meta.name, "Turnip Adreno Driver T30 (@Mr_Purple_666)")); + CHECK(!strcmp(meta.version, "26.3.0-T30-1.4.359")); + CHECK(!vulkan_custom_driver_parse_meta(k11_meta, sizeof(k11_meta) - 1, + 28, &meta)); + CHECK(!strcmp(meta.library_name, "vulkan.ad07xx.so")); + CHECK(!strcmp(meta.version, "Vulkan 1.4.335")); + /* minApi is checked against the device */ + CHECK(!parse(t30_meta, 30)); + CHECK(parse(t30_meta, 29) != NULL); + CHECK(!parse(meta_of("1", "\"Mesa\"", "\"v.so\"", NULL), 28)); + + CHECK(parse(meta_of("1", "\"Mesa\"", NULL, NULL), 33) != NULL); + CHECK(parse(meta_of("1", "\"Mesa\"", "\"\"", NULL), 33) != NULL); + CHECK(parse(meta_of("1", "\"Mesa\"", "\"a/b.so\"", NULL), 33) != NULL); + CHECK(parse(meta_of("1", "\"Mesa\"", "\"a\\\\b.so\"", NULL), 33) != NULL); + CHECK(parse(meta_of("1", "\"Mesa\"", "\"a\\u0000.so\"", NULL), 33) != NULL); + CHECK(parse(meta_of("1", "\"Mesa\"", "\"..so\"", NULL), 33) != NULL); + CHECK(parse(meta_of("1", "\"Mesa\"", "\".so\"", NULL), 33) != NULL); + CHECK(parse(meta_of("1", "\"Mesa\"", "\"x.txt\"", NULL), 33) != NULL); + CHECK(parse(meta_of("1", "\"Mesa\"", "7", NULL), 33) != NULL); + CHECK(parse(meta_of("1", NULL, "\"v.so\"", NULL), 33) != NULL); + CHECK(parse(meta_of("2", "\"Mesa\"", "\"v.so\"", NULL), 33) != NULL); + CHECK(parse(meta_of("\"1\"", "\"Mesa\"", "\"v.so\"", NULL), 33) != NULL); + CHECK(parse(meta_of(NULL, "\"Mesa\"", "\"v.so\"", NULL), 33) != NULL); + CHECK(parse(meta_of("1", "\"Mesa\"", "\"v.so\"", "34"), 33) != NULL); + CHECK(parse(meta_of("1", "\"Mesa\"", "\"v.so\"", "\"30\""), 33) != NULL); + CHECK(parse("[1]", 33) != NULL); + CHECK(parse("", 33) != NULL); + /* unknown keys of any type are ignored */ + CHECK(!parse("{\"schemaVersion\": 1, \"vendor\": \"Mesa\", \"x\": " + "{\"libraryName\": 5}, \"y\": [1, null, true], " + "\"libraryName\": \"v.so\"}", 33)); + + /* 63 characters pass, 64 do not */ + memset(name, 'a', sizeof(name)); + name[0] = '"'; + strcpy(name + 61, ".so\""); + CHECK(!parse(meta_of("1", "\"Mesa\"", name, NULL), 33)); + name[0] = 'a'; + memmove(name + 1, name, strlen(name) + 1); + name[0] = '"'; + CHECK(parse(meta_of("1", "\"Mesa\"", name, NULL), 33) != NULL); + + /* truncated */ + CHECK(vulkan_custom_driver_parse_meta(t30_meta, sizeof(t30_meta) - 3, + 33, &meta) != NULL); + + /* 4 KiB pass, one byte more does not */ + pad = VULKAN_CUSTOM_DRIVER_META_MAX - (sizeof(k11_meta) - 1); + json[0] = '{'; + memset(json + 1, ' ', pad); + strcpy(json + 1 + pad, k11_meta + 1); + CHECK(strlen(json) == VULKAN_CUSTOM_DRIVER_META_MAX); + CHECK(!parse(json, 33)); + memmove(json + 1, json, strlen(json) + 1); + json[1] = ' '; + CHECK(parse(json, 33) != NULL); +} + +/* {schemaVersion 1, vendor, libraryName "v.so", } */ +static const char *meta_rest(const char *vendor, const char *rest, + struct vulkan_custom_driver_meta *meta) +{ + strcpy(json, "{\"schemaVersion\": 1, \"vendor\": "); + strcat(json, vendor); + strcat(json, ", \"libraryName\": \"v.so\""); + if (rest) + { + strcat(json, ", "); + strcat(json, rest); + } + strcat(json, "}"); + return vulkan_custom_driver_parse_meta(json, strlen(json), 33, meta); +} + +/* "": "", or the string alone for a NULL key */ +static const char *text_of(const char *key, size_t n, char c, + const char *tail) +{ + static char text[VULKAN_CUSTOM_DRIVER_TEXT_MAX * 4]; + size_t len; + text[0] = '\0'; + if (key) + { + strcpy(text, "\""); + strcat(text, key); + strcat(text, "\": "); + } + strcat(text, "\""); + len = strlen(text); + memset(text + len, c, n); + strcpy(text + len + n, tail); + strcat(text, "\""); + return text; +} + +/* Any vendor string passes; name and driverVersion never reject. */ +static void test_meta_policy(void) +{ + struct vulkan_custom_driver_meta meta; + char bom[sizeof(t30_meta) + 3]; + + CHECK(!meta_rest("\"Qualcomm\"", NULL, &meta)); + CHECK(!strcmp(meta.vendor, "Qualcomm") && !meta.mesa); + CHECK(!*meta.name && !*meta.version); + /* Mesa in any case, and only Mesa */ + CHECK(!meta_rest("\"mesa\"", NULL, &meta) && meta.mesa); + CHECK(!meta_rest("\"MESA\"", NULL, &meta) && meta.mesa); + CHECK(!meta_rest("\"Mesa3D\"", NULL, &meta) && !meta.mesa); + /* vendor: 127 characters pass, 128 do not */ + CHECK(!meta_rest(text_of(NULL, 127, 'v', ""), NULL, &meta)); + CHECK(strlen(meta.vendor) == 127); + CHECK(meta_rest(text_of(NULL, 128, 'v', ""), NULL, &meta) != NULL); + CHECK(meta_rest("\"\"", NULL, &meta) != NULL); + CHECK(meta_rest("7", NULL, &meta) != NULL); + CHECK(meta_rest("null", NULL, &meta) != NULL); + CHECK(meta_rest("true", NULL, &meta) != NULL); + CHECK(meta_rest("[\"Mesa\"]", NULL, &meta) != NULL); + CHECK(meta_rest("{\"a\": \"Mesa\"}", NULL, &meta) != NULL); + /* the other keys keep their types */ + CHECK(meta_rest("\"Mesa\"", "\"schemaVersion\": [1]", &meta) != NULL); + CHECK(meta_rest("\"Mesa\"", "\"libraryName\": null", &meta) != NULL); + CHECK(meta_rest("\"Mesa\"", "\"minApi\": null", &meta) != NULL); + + /* long text is cut to 127 bytes, before a split UTF-8 character */ + CHECK(!meta_rest("\"Mesa\"", text_of("name", 200, 'a', ""), &meta)); + CHECK(strlen(meta.name) == 127); + CHECK(!meta_rest("\"Mesa\"", text_of("driverVersion", 300, 'b', ""), + &meta)); + CHECK(strlen(meta.version) == 127); + CHECK(!meta_rest("\"Mesa\"", text_of("name", 125, 'a', "\xC3\xA9"), + &meta)); + CHECK(strlen(meta.name) == 127); + CHECK(!meta_rest("\"Mesa\"", text_of("name", 126, 'a', "\xC3\xA9"), + &meta)); + CHECK(strlen(meta.name) == 126); + CHECK(!meta_rest("\"Mesa\"", text_of("name", 125, 'a', "\xE2\x82\xAC"), + &meta)); + CHECK(strlen(meta.name) == 125); + + /* a wrong type counts as absent */ + CHECK(!meta_rest("\"Mesa\"", "\"name\": 1, \"driverVersion\": null", + &meta)); + CHECK(!*meta.name && !*meta.version); + CHECK(!meta_rest("\"Mesa\"", "\"name\": {\"libraryName\": 5}, " + "\"driverVersion\": [1, true]", &meta)); + CHECK(!*meta.name && !*meta.version); + CHECK(!strcmp(meta.library_name, "v.so")); + CHECK(!meta_rest("\"Mesa\"", "\"name\": false, \"driverVersion\": 2", + &meta)); + + /* a leading UTF-8 BOM */ + memcpy(bom, "\xEF\xBB\xBF", 3); + memcpy(bom + 3, t30_meta, sizeof(t30_meta)); + CHECK(!vulkan_custom_driver_parse_meta(bom, sizeof(bom) - 1, 33, &meta)); + CHECK(!strcmp(meta.library_name, "vulkan.purple.so")); + CHECK(!strcmp(meta.version, "26.3.0-T30-1.4.359")); +} + +static struct string_list *list_of(const char *a, const char *b, + const char *c) +{ + union string_list_elem_attr attr; + struct string_list *list = string_list_new(); + attr.i = 0; + string_list_append(list, a, attr); + if (b) + string_list_append(list, b, attr); + if (c) + string_list_append(list, c, attr); + return list; +} + +static void check_members(const char *a, const char *b, const char *c, + bool pass) +{ + struct string_list *list = list_of(a, b, c); + const char *err = vulkan_custom_driver_check_members(list, + "vulkan.purple.so"); + CHECK(pass ? !err : err != NULL); + string_list_free(list); +} + +static void test_members(void) +{ + check_members("meta.json", "vulkan.purple.so", NULL, true); + check_members("vulkan.purple.so", "meta.json", "README.txt", true); + check_members("meta.json", "vulkan.purple.so", "libextra.so", false); + check_members("meta.json", "lib/vulkan.purple.so", NULL, false); + check_members("meta.json", "vulkan.other.so", NULL, false); + check_members("vulkan.purple.so", NULL, NULL, false); + check_members("x/meta.json", "vulkan.purple.so", NULL, false); +} + +static void test_elf(void) +{ + unsigned char head[VULKAN_CUSTOM_DRIVER_ELF_HEAD]; + CHECK(!vulkan_custom_driver_check_elf(t30_elf, sizeof(t30_elf), + 18869912)); + CHECK(!vulkan_custom_driver_check_elf(t30_elf, sizeof(t30_elf), + VULKAN_CUSTOM_DRIVER_SO_MAX)); + CHECK(vulkan_custom_driver_check_elf(t30_elf, sizeof(t30_elf), + VULKAN_CUSTOM_DRIVER_SO_MAX + 1) != NULL); + CHECK(vulkan_custom_driver_check_elf(t30_elf, sizeof(t30_elf) - 1, + 19) != NULL); + /* 32-bit ARM */ + memcpy(head, t30_elf, sizeof(head)); + head[4] = 1; + head[18] = 40; + CHECK(vulkan_custom_driver_check_elf(head, sizeof(head), 1000) != NULL); + /* x86_64 */ + memcpy(head, t30_elf, sizeof(head)); + head[18] = 62; + CHECK(vulkan_custom_driver_check_elf(head, sizeof(head), 1000) != NULL); + /* ET_EXEC */ + memcpy(head, t30_elf, sizeof(head)); + head[16] = 2; + CHECK(vulkan_custom_driver_check_elf(head, sizeof(head), 1000) != NULL); + /* big-endian */ + memcpy(head, t30_elf, sizeof(head)); + head[5] = 2; + CHECK(vulkan_custom_driver_check_elf(head, sizeof(head), 1000) != NULL); + /* not ELF */ + memcpy(head, "PK\003\004", 4); + CHECK(vulkan_custom_driver_check_elf(head, sizeof(head), 1000) != NULL); +} + +static void test_key(void) +{ + uint32_t key = vulkan_custom_driver_key("/sdcard/a.zip", 18870258, 1000); + CHECK(key == vulkan_custom_driver_key("/sdcard/a.zip", 18870258, 1000)); + CHECK(key != vulkan_custom_driver_key("/sdcard/b.zip", 18870258, 1000)); + CHECK(key != vulkan_custom_driver_key("/sdcard/a.zip", 18870259, 1000)); + CHECK(key != vulkan_custom_driver_key("/sdcard/a.zip", 18870258, 1001)); + /* names dirs on disk: the same on every host */ + CHECK(key == 0xd78f6bd1UL); +} + +static void test_crash_state(void) +{ + char state[64]; + size_t len = 0; + + len = vulkan_custom_driver_crash_add(state, len, sizeof(state), + "/d/A.zip", "Core"); + CHECK(len == 14 && !memcmp(state, "Core\t/d/A.zip\n", 14)); + CHECK(vulkan_custom_driver_crash_find(state, len, "/d/A.zip", "Core")); + CHECK(!vulkan_custom_driver_crash_find(state, len, "/d/A.zip", "Other")); + CHECK(!vulkan_custom_driver_crash_find(state, len, "/d/A.zip", NULL)); + CHECK(!vulkan_custom_driver_crash_find(state, len, "/d/B.zip", "Core")); + CHECK(vulkan_custom_driver_crash_add(state, len, sizeof(state), + "/d/A.zip", "Core") == len); + + /* the menu is its own key */ + len = vulkan_custom_driver_crash_add(state, len, sizeof(state), + "/d/A.zip", NULL); + CHECK(len == 24); + CHECK(vulkan_custom_driver_crash_find(state, len, "/d/A.zip", "")); + CHECK(vulkan_custom_driver_crash_find(state, len, "/d/A.zip", NULL)); + len = vulkan_custom_driver_crash_add(state, len, sizeof(state), + "/d/B.zip", "Core"); + CHECK(len == 38); + + /* picking A again clears it for every core */ + len = vulkan_custom_driver_crash_clear(state, len, "/d/A.zip"); + CHECK(len == 14 && !memcmp(state, "Core\t/d/B.zip\n", 14)); + + /* a torn last line does not count, and is dropped by the next add */ + memcpy(state + len, "Core\t/d/C.z", 11); + CHECK(!vulkan_custom_driver_crash_find(state, len + 11, "/d/C.z", "Core")); + len = vulkan_custom_driver_crash_add(state, len + 11, sizeof(state), + "/d/C.zip", "X"); + CHECK(len == 25 && !memcmp(state + 14, "X\t/d/C.zip\n", 11)); + + /* full: the oldest lines go */ + len = vulkan_custom_driver_crash_add(state, len, sizeof(state), + "/d/D.zip", "Core"); + len = vulkan_custom_driver_crash_add(state, len, sizeof(state), + "/d/E.zip", "Core"); + CHECK(len == 53); + len = vulkan_custom_driver_crash_add(state, len, sizeof(state), + "/d/F.zip", "Core"); + CHECK(len == 53 && !memcmp(state, "X\t/d/C.zip\n", 11)); + CHECK(!vulkan_custom_driver_crash_find(state, len, "/d/B.zip", "Core")); + CHECK(vulkan_custom_driver_crash_find(state, len, "/d/F.zip", "Core")); + + /* a line that cannot be read back is not written */ + CHECK(vulkan_custom_driver_crash_add(state, len, sizeof(state), + "/d/\n.zip", "Core") == len); + CHECK(vulkan_custom_driver_crash_add(state, len, sizeof(state), + "/d/F.zip", "a\tb") == len); + CHECK(vulkan_custom_driver_crash_add(state, len, sizeof(state), + "", "Core") == len); + /* a tab in the path is fine: the core has none */ + len = vulkan_custom_driver_crash_add(state, 0, sizeof(state), + "/d/a\tb.zip", "Core"); + CHECK(vulkan_custom_driver_crash_find(state, len, "/d/a\tb.zip", "Core")); +} + +static int handles[16]; + +struct fake_loader +{ + unsigned loads; + bool fixed; /* "bad" packages load */ +}; + +static void *fake_load(void *data, const char *path, uint32_t key, + struct vulkan_custom_driver_meta *meta, + enum vulkan_custom_driver_msg *msg) +{ + struct fake_loader *f = (struct fake_loader*)data; + (void)key; + f->loads++; + if (strstr(path, "bad") && !f->fixed) + { + *msg = VULKAN_CUSTOM_DRIVER_MSG_INVALID; + return NULL; + } + if (strstr(path, "broken")) + { + *msg = VULKAN_CUSTOM_DRIVER_MSG_FAILED; + return NULL; + } + strcpy(meta->library_name, path + strlen(path) - 5); + return &handles[f->loads]; +} + +static vulkan_custom_driver_session_t session; +static struct fake_loader loader; +static char saved[VULKAN_CUSTOM_DRIVER_STATE_MAX + 1]; + +static void begin(const char *state, const char *marker) +{ + loader.loads = 0; + loader.fixed = false; + vulkan_custom_driver_session_init(&session, fake_load, &loader, + state, state ? strlen(state) : 0, + marker, marker ? strlen(marker) : 0); +} + +static struct vulkan_custom_driver_request request(const char *path, + const char *core) +{ + struct vulkan_custom_driver_request req; + req.size = 18870258; + req.mtime = 1000; + req.path = path; + req.core = core; + req.supported = true; + req.safe_mode = false; + return req; +} + +static void *open_req(const struct vulkan_custom_driver_request *req, + enum vulkan_custom_driver_msg *msg) +{ + const struct vulkan_custom_driver_meta *meta; + void *h = vulkan_custom_driver_session_open(&session, req, &meta, msg); + CHECK(h ? meta != NULL : meta == NULL); + return h; +} + +static void *open_drv(const char *path, const char *core, + enum vulkan_custom_driver_msg *msg) +{ + struct vulkan_custom_driver_request req = request(path, core); + return open_req(&req, msg); +} + +static void test_session(void) +{ + enum vulkan_custom_driver_msg msg; + struct vulkan_custom_driver_request req; + const struct vulkan_custom_driver_meta *meta; + void *a; + + /* empty value: the system driver, nothing shown */ + begin(NULL, NULL); + CHECK(!open_drv("", "Core", &msg) && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(!open_drv(NULL, "Core", &msg) && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(loader.loads == 0); + + /* safe mode */ + req = request("/d/A.zip", "Core"); + req.safe_mode = true; + CHECK(!open_req(&req, &msg) && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(loader.loads == 0); + + /* not Adreno: UNSUPPORTED once per path */ + req = request("/d/A.zip", "Core"); + req.supported = false; + CHECK(!open_req(&req, &msg) && msg == VULKAN_CUSTOM_DRIVER_MSG_UNSUPPORTED); + CHECK(!open_req(&req, &msg) && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE); + req.path = "/d/B.zip"; + CHECK(!open_req(&req, &msg) && msg == VULKAN_CUSTOM_DRIVER_MSG_UNSUPPORTED); + CHECK(loader.loads == 0); + + /* loaded once, then kept; the marker is outstanding until started */ + begin(NULL, NULL); + req = request("/d/A.zip", "Core"); + a = vulkan_custom_driver_session_open(&session, &req, &meta, &msg); + CHECK(a && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE && loader.loads == 1); + CHECK(meta && !strcmp(meta->library_name, "A.zip")); + CHECK(vulkan_custom_driver_session_started(&session)); + CHECK(!vulkan_custom_driver_session_started(&session)); + CHECK(open_drv("/d/A.zip", "Core", &msg) == a && loader.loads == 1); + + /* A, system, B, A: A's slot is used again */ + CHECK(vulkan_custom_driver_session_started(&session)); + CHECK(!open_drv("", "Core", &msg)); + CHECK(!vulkan_custom_driver_session_started(&session)); + CHECK(open_drv("/d/B.zip", "Core", &msg) != a && loader.loads == 2); + CHECK(open_drv("/d/A.zip", "Other", &msg) == a && loader.loads == 2); + + /* a new version of A is a new driver */ + req = request("/d/A.zip", "Core"); + req.mtime = 2000; + CHECK(open_req(&req, &msg) != a && loader.loads == 3); + + /* a 5th distinct driver: RESTART once, and the system driver */ + CHECK(open_drv("/d/C.zip", "Core", &msg) && loader.loads == 4 + && !session.full); + CHECK(!open_drv("/d/E.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_RESTART && session.full); + CHECK(!open_drv("/d/E.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE && session.full); + CHECK(open_drv("/d/A.zip", "Core", &msg) == a && loader.loads == 4 + && !session.full); + + /* a bad package: INVALID once, not loaded again until picked again */ + begin(NULL, NULL); + CHECK(!open_drv("/d/bad.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_INVALID); + CHECK(!open_drv("/d/bad.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(loader.loads == 1); + CHECK(vulkan_custom_driver_session_picked(&session, "/d/bad.zip") + == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(!open_drv("/d/bad.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_INVALID); + CHECK(loader.loads == 2); + /* fixed and picked again: it loads, no restart */ + loader.fixed = true; + vulkan_custom_driver_session_picked(&session, "/d/bad.zip"); + CHECK(open_drv("/d/bad.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE && loader.loads == 3); + loader.fixed = false; + /* a missing zip is not loaded at all, and is tried again once picked */ + req = request("/d/gone.zip", "Core"); + req.size = -1; + CHECK(!open_req(&req, &msg) && msg == VULKAN_CUSTOM_DRIVER_MSG_INVALID); + CHECK(loader.loads == 3); + vulkan_custom_driver_session_picked(&session, "/d/gone.zip"); + CHECK(open_drv("/d/gone.zip", "Core", &msg) && loader.loads == 4); + /* the loader refused it: picking it again needs a restart */ + CHECK(!open_drv("/d/broken.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_FAILED); + CHECK(!open_drv("/d/broken.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(loader.loads == 5); + CHECK(vulkan_custom_driver_session_picked(&session, "/d/broken.zip") + == VULKAN_CUSTOM_DRIVER_MSG_RESTART); + CHECK(!open_drv("/d/broken.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE && loader.loads == 5); + /* a path this session never opened */ + CHECK(vulkan_custom_driver_session_picked(&session, "/d/new.zip") + == VULKAN_CUSTOM_DRIVER_MSG_NONE); + + /* failed in init: FAILED once, the system driver all session */ + begin(NULL, NULL); + CHECK(open_drv("/d/A.zip", "Core", &msg)); + CHECK(vulkan_custom_driver_session_failed(&session) + == VULKAN_CUSTOM_DRIVER_MSG_FAILED); + CHECK(!vulkan_custom_driver_session_started(&session)); + CHECK(!open_drv("/d/A.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(!open_drv("/d/A.zip", "Other", &msg)); + CHECK(vulkan_custom_driver_session_failed(&session) + == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(open_drv("/d/B.zip", "Core", &msg) && loader.loads == 2); + /* picked again: it may still be mapped, so only a restart retries */ + CHECK(vulkan_custom_driver_session_picked(&session, "/d/A.zip") + == VULKAN_CUSTOM_DRIVER_MSG_RESTART); + CHECK(vulkan_custom_driver_session_picked(&session, "/d/A.zip") + == VULKAN_CUSTOM_DRIVER_MSG_RESTART); + CHECK(!open_drv("/d/A.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE && loader.loads == 2); +} + +static bool opened(void) +{ + return vulkan_custom_driver_session_current(&session); +} + +/* Fills the table past its first n entries with bad packages, which + * take entries but no slots. */ +static void fill_table(unsigned n) +{ + enum vulkan_custom_driver_msg msg; + char path[32]; + unsigned i; + for (i = n; i < VULKAN_CUSTOM_DRIVER_PATHS; i++) + { + sprintf(path, "/d/bad%u.zip", i); + CHECK(!open_drv(path, "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_INVALID); + } + CHECK(session.path_count == VULKAN_CUSTOM_DRIVER_PATHS); +} + +static void test_session_table(void) +{ + enum vulkan_custom_driver_msg msg; + char state[64]; + size_t len; + void *a; + + /* a full table: a new path gets the system driver and RESTART once + * a session */ + begin(NULL, NULL); + a = open_drv("/d/A.zip", "Core", &msg); + CHECK(a && vulkan_custom_driver_session_started(&session)); + CHECK(!open_drv("/d/broken.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_FAILED); + fill_table(2); + CHECK(loader.loads == 8); + CHECK(!session.full); + CHECK(!open_drv("/d/C.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_RESTART && session.full); + CHECK(!open_drv("/d/C.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE && session.full); + CHECK(!open_drv("/d/D.zip", "Other", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE && session.full); + CHECK(loader.loads == 8 && session.path_count == 8); + /* the 8 entries keep their state */ + CHECK(open_drv("/d/A.zip", "Core", &msg) == a + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE && !session.full); + CHECK(!open_drv("/d/C.zip", "Core", &msg) && session.full); + CHECK(!open_drv("/d/broken.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE && !session.full); + CHECK(!open_drv("/d/C.zip", "Core", &msg) && session.full); + CHECK(!open_drv("", "Core", &msg) && !session.full); + CHECK(!open_drv("/d/bad2.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(loader.loads == 8); + CHECK(vulkan_custom_driver_session_picked(&session, "/d/broken.zip") + == VULKAN_CUSTOM_DRIVER_MSG_RESTART); + CHECK(vulkan_custom_driver_session_picked(&session, "/d/bad2.zip") + == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(!open_drv("/d/bad2.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_INVALID && loader.loads == 9); + /* picking a path with no room needs a restart, every time */ + CHECK(vulkan_custom_driver_session_picked(&session, "/d/C.zip") + == VULKAN_CUSTOM_DRIVER_MSG_RESTART); + CHECK(vulkan_custom_driver_session_picked(&session, "/d/E.zip") + == VULKAN_CUSTOM_DRIVER_MSG_RESTART); + CHECK(session.path_count == 8); + + /* picked first: that RESTART is the session's, and its crash state + * still goes */ + len = vulkan_custom_driver_crash_add(state, 0, sizeof(state), + "/d/C.zip", "Core"); + state[len] = '\0'; + begin(state, NULL); + fill_table(0); + CHECK(vulkan_custom_driver_session_picked(&session, "/d/C.zip") + == VULKAN_CUSTOM_DRIVER_MSG_RESTART); + CHECK(session.state_len == 0); + CHECK(!open_drv("/d/C.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(loader.loads == 8); + + /* level 2 only while the driver this init opened has not started */ + begin(NULL, NULL); + CHECK(!opened()); + CHECK(open_drv("/d/A.zip", "Core", &msg) && opened()); + CHECK(vulkan_custom_driver_session_started(&session)); + CHECK(!opened() && session.current); + CHECK(open_drv("/d/A.zip", "Core", &msg) && opened()); + CHECK(vulkan_custom_driver_session_failed(&session) + == VULKAN_CUSTOM_DRIVER_MSG_FAILED); + CHECK(!opened()); + CHECK(!open_drv("/d/A.zip", "Core", &msg) && !opened()); + CHECK(open_drv("/d/B.zip", "Core", &msg) && opened()); + CHECK(!open_drv("", "Core", &msg) && !opened()); + /* an init that took the instance B's init kept, before B started */ + CHECK(open_drv("/d/B.zip", "Core", &msg) && opened()); + vulkan_custom_driver_session_kept(&session); + CHECK(!opened()); + CHECK(vulkan_custom_driver_session_started(&session)); + CHECK(!opened()); +} + +static void test_crash_marker(void) +{ + enum vulkan_custom_driver_msg msg; + char marker[64]; + size_t len; + + /* the marker the glue writes when open() returns a custom driver */ + begin(NULL, NULL); + CHECK(open_drv("/d/A.zip", "Core", &msg)); + len = vulkan_custom_driver_crash_add(marker, 0, sizeof(marker), + "/d/A.zip", "Core"); + marker[len] = '\0'; + + /* the process died before started(): next start, A is off for Core, + * CRASHED once */ + begin(NULL, marker); + CHECK(session.state_len == len && !memcmp(session.state, marker, len)); + CHECK(!open_drv("/d/A.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_CRASHED); + CHECK(!open_drv("/d/A.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(loader.loads == 0); + CHECK(open_drv("/d/A.zip", "Other", &msg) && loader.loads == 1); + CHECK(open_drv("/d/A.zip", NULL, &msg) && loader.loads == 1); + CHECK(open_drv("/d/B.zip", "Core", &msg) && loader.loads == 2); + + /* sticky: every later start skips it too, with CRASHED once */ + memcpy(saved, session.state, session.state_len); + saved[session.state_len] = '\0'; + begin(saved, NULL); + CHECK(open_drv("/d/A.zip", NULL, &msg) && loader.loads == 1); + CHECK(!open_drv("/d/A.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_CRASHED); + CHECK(!open_drv("/d/A.zip", "Core", &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(loader.loads == 1); + + /* picked again: on for every core */ + CHECK(vulkan_custom_driver_session_picked(&session, "/d/A.zip") + == VULKAN_CUSTOM_DRIVER_MSG_NONE); + CHECK(session.state_len == 0); + CHECK(open_drv("/d/A.zip", "Core", &msg) && loader.loads == 1); + + /* a crash in the menu only turns it off for the menu */ + len = vulkan_custom_driver_crash_add(marker, 0, sizeof(marker), + "/d/A.zip", ""); + marker[len] = '\0'; + begin(NULL, marker); + CHECK(!open_drv("/d/A.zip", NULL, &msg) + && msg == VULKAN_CUSTOM_DRIVER_MSG_CRASHED); + CHECK(open_drv("/d/A.zip", "Core", &msg)); + + /* no marker, or one that names nothing */ + CHECK(!vulkan_custom_driver_session_init(&session, fake_load, &loader, + NULL, 0, NULL, 0)); + CHECK(!vulkan_custom_driver_session_init(&session, fake_load, &loader, + NULL, 0, "Core/d/A.zip\n", 13)); + CHECK(!vulkan_custom_driver_session_init(&session, fake_load, &loader, + NULL, 0, "Core\t/d/A.zip", 13)); + CHECK(!vulkan_custom_driver_session_init(&session, fake_load, &loader, + NULL, 0, "Core\t\n", 6)); + CHECK(vulkan_custom_driver_session_init(&session, fake_load, &loader, + NULL, 0, marker, strlen(marker))); +} + +#define ELF_LEN 336 + +static void put_le(unsigned char *p, uint64_t v, unsigned n) +{ + while (n--) + { + *p++ = (unsigned char)(v & 0xff); + v >>= 8; + } +} + +/* A little-endian ELF64: section headers at 64 (null, .dynstr, .dynamic), + * .dynstr at 256 holding "\0libvulkan.so\0", .dynamic at 288 holding + * DT_STRSZ, DT_SONAME 1 and DT_NULL. */ +static void make_elf(unsigned char *elf) +{ + unsigned char *sh; + memset(elf, 0, ELF_LEN); + memcpy(elf, "\177ELF", 4); + elf[4] = 2; + elf[5] = 1; + elf[6] = 1; + put_le(elf + 40, 64, 8); + put_le(elf + 58, 64, 2); + put_le(elf + 60, 3, 2); + sh = elf + 128; + put_le(sh + 4, 3, 4); + put_le(sh + 24, 256, 8); + put_le(sh + 32, 32, 8); + sh = elf + 192; + put_le(sh + 4, 6, 4); + put_le(sh + 24, 288, 8); + put_le(sh + 32, 48, 8); + put_le(sh + 40, 1, 4); + put_le(sh + 56, 16, 8); + memcpy(elf + 257, "libvulkan.so", 12); + put_le(elf + 288, 10, 8); + put_le(elf + 296, 32, 8); + put_le(elf + 304, 14, 8); + put_le(elf + 312, 1, 8); +} + +static void test_patch_soname(void) +{ + static unsigned char elf[ELF_LEN]; + static unsigned char want[ELF_LEN]; + + make_elf(elf); + make_elf(want); + memcpy(want + 257, "007", 3); + CHECK(vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + /* A longer patch stops at the soname's end */ + make_elf(elf); + make_elf(want); + memcpy(want + 257, "0123456789AB", 12); + CHECK(vulkan_custom_driver_patch_soname(elf, ELF_LEN, "0123456789ABCDEF")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + /* ... and at the end of .dynstr when the soname has no NUL there */ + make_elf(elf); + memcpy(elf + 276, "abcdefghijkl", 12); + put_le(elf + 312, 20, 8); + memcpy(want, elf, ELF_LEN); + memcpy(want + 276, "0123456789AB", 12); + CHECK(vulkan_custom_driver_patch_soname(elf, ELF_LEN, "0123456789ABCDEF")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + elf[0] = 0; + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + elf[4] = 1; + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + elf[5] = 2; + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, 63, "007")); + CHECK(!vulkan_custom_driver_patch_soname(elf, 255, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + put_le(elf + 58, 32, 2); + put_le(elf + 40, 128, 8); + put_le(elf + 192 + 40, 0, 4); + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + put_le(elf + 40, 4096, 8); + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + put_le(elf + 192 + 40, 3, 4); + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + put_le(elf + 192 + 56, 0, 8); + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + put_le(elf + 192 + 24, 4096, 8); + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + put_le(elf + 192 + 32, 4096, 8); + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + put_le(elf + 128 + 24, 4096, 8); + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + put_le(elf + 128 + 32, 4096, 8); + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + put_le(elf + 312, 32, 8); + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + put_le(elf + 304, 1, 8); + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); + + make_elf(elf); + put_le(elf + 192 + 4, 1, 4); + memcpy(want, elf, ELF_LEN); + CHECK(!vulkan_custom_driver_patch_soname(elf, ELF_LEN, "007")); + CHECK(!memcmp(elf, want, ELF_LEN)); +} + +int main(void) +{ + test_meta(); + test_meta_policy(); + test_members(); + test_elf(); + test_key(); + test_crash_state(); + test_session(); + test_session_table(); + test_crash_marker(); + test_patch_soname(); + printf("vulkan_custom_driver_test: %u checks, %u failures\n", + checks, failures); + return failures ? 1 : 0; +} diff --git a/settings/settings_def_vulkan_driver_path.h b/settings/settings_def_vulkan_driver_path.h new file mode 100644 index 000000000000..dc0e62651c04 --- /dev/null +++ b/settings/settings_def_vulkan_driver_path.h @@ -0,0 +1,19 @@ +/* Single-source definitions: custom Vulkan driver path setting. + * Grammar identical to settings_def_video_sync.h plus S_FLOAT and + * the _NS no-sublabel variants; the descriptor argument span + * matches SDESC__ROW; row order is menu display order; + * h2json.py parses these rows for the Crowdin source upload. */ + +/* Descriptor rows are #ifdef HAVE_VULKAN_CUSTOM_DRIVER; the string tables + * always carry this row via the strings pass. */ +#if defined(HAVE_VULKAN_CUSTOM_DRIVER) || defined(SETTINGS_DEF_STRINGS_PASS) +/* configuration.c registers "video_vulkan_driver_path" with a literal + * row under HAVE_VULKAN_CUSTOM_DRIVER. */ +#ifndef SETTINGS_DEF_CONFIG_PASS +S_PATH_DS(path_vulkan_driver, VIDEO_VULKAN_DRIVER_PATH, + "video_vulkan_driver_path", + directory_system, SD_FLAG_NONE, 0, "zip", setting_get_string_representation_vulkan_driver, 0, + "Custom Vulkan Driver", + "Use a Vulkan driver package (.zip) instead of the system driver. The driver runs inside RetroArch: only use packages from a source you trust. Takes effect the next time Vulkan starts. Start: system driver.") +#endif +#endif