From 55f1eb7f90b2f2239f166022a60b232ed84dead3 Mon Sep 17 00:00:00 2001 From: davey Date: Fri, 30 Oct 2026 20:04:53 -0700 Subject: [PATCH 1/2] Android: load custom Vulkan driver packages on Adreno Sideloaded arm64 builds can load a Vulkan driver package (a zip with meta.json and one .so, such as Mesa's Turnip) on Adreno GPUs with Android 9 or later. As libadrenotools does, a private copy of the system Vulkan loader is opened in a linker namespace of its own, where a small hook library hands it the custom driver as its HAL. The new video_vulkan_driver_path setting names the zip; empty is the system driver, and core, content-directory and game overrides carry it. The package is checked and extracted to internal storage once per version. Packages from any vendor are accepted. Only Mesa ones have been tested, so any other vendor logs that it is untested and that its load cannot be confirmed. A Mesa package that leaves the stock Qualcomm driver running, as the hook does when the custom library fails to load, counts as failed. When the custom driver fails to create an instance, a device or the first swapchain, the context is redone on the system driver in the same process. A driver that takes the process down during its init is turned off for that core until it is picked again, and a "Safe start" launcher shortcut starts RetroArch once on the system driver. Play Store, Quest and API 16 builds do not carry any of it. Co-authored-by: lim2222 --- .github/workflows/Linux-samples-gfx.yml | 48 + configuration.c | 6 + configuration.h | 3 + frontend/drivers/platform_unix.c | 28 + frontend/drivers/platform_unix.h | 3 + gfx/common/vulkan_common.c | 129 +++ gfx/common/vulkan_common.h | 4 +- gfx/common/vulkan_custom_driver.c | 605 ++++++++++++ gfx/common/vulkan_custom_driver.h | 187 ++++ gfx/common/vulkan_custom_driver_android.c | 571 +++++++++++ gfx/common/vulkan_custom_driver_linker.c | 459 +++++++++ gfx/drivers_context/android_vk_ctx.c | 42 +- griffin/griffin.c | 5 + intl/msg_hash_us.h | 24 + msg_hash.h | 6 + pkg/android/phoenix-common/jni/Android.mk | 30 + pkg/android/phoenix-jelly-bean/build.gradle | 2 +- .../retroactivity/RetroActivityFuture.java | 40 +- samples/gfx/vulkan_custom_driver/Makefile | 70 ++ .../vulkan_custom_driver_test.c | 931 ++++++++++++++++++ 20 files changed, 3187 insertions(+), 6 deletions(-) create mode 100644 gfx/common/vulkan_custom_driver.c create mode 100644 gfx/common/vulkan_custom_driver.h create mode 100644 gfx/common/vulkan_custom_driver_android.c create mode 100644 gfx/common/vulkan_custom_driver_linker.c create mode 100644 samples/gfx/vulkan_custom_driver/Makefile create mode 100644 samples/gfx/vulkan_custom_driver/vulkan_custom_driver_test.c 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..6db21950abfc 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); @@ -6452,6 +6455,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_us.h b/intl/msg_hash_us.h index 4a66492a66c5..1f089b80f23f 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -34228,6 +34228,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/msg_hash.h b/msg_hash.h index 2dfe9347030d..161ecf97aa27 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -341,6 +341,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, 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; +} From 00ca0bfa847d4be23f347de4a8f580a5a652ce70 Mon Sep 17 00:00:00 2001 From: davey Date: Fri, 30 Oct 2026 20:05:02 -0700 Subject: [PATCH 2/2] Menu: Custom Vulkan Driver setting Settings > Video > Output gains a Custom Vulkan Driver row on builds and devices that can load one. OK opens a browser for driver zips in /vulkan_drivers, Start goes back to the system driver, and the sublabel says when the chosen driver crashed during its last start and is off for the running core. While RetroArch runs from the Safe start shortcut, the row reads "System (Safe start)" and the sublabel names the driver the next normal start uses. The choice applies at the next Vulkan start; picking a driver never restarts video. Co-authored-by: lim2222 --- configuration.c | 5 ++ intl/msg_hash_lbl_rows.h | 1 + intl/msg_hash_us.h | 17 ++++++ menu/cbs/menu_cbs_deferred_push.c | 9 ++++ menu/cbs/menu_cbs_ok.c | 59 +++++++++++++++++++++ menu/cbs/menu_cbs_start.c | 29 +++++++++++ menu/cbs/menu_cbs_sublabel.c | 60 ++++++++++++++++++++++ menu/menu_cbs.h | 1 + menu/menu_displaylist.c | 11 +++- menu/menu_displaylist.h | 1 + menu/menu_setting.c | 35 +++++++++++++ msg_hash.h | 6 +++ msg_hash_lbl_str.h | 2 + settings/settings_def_vulkan_driver_path.h | 19 +++++++ 14 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 settings/settings_def_vulkan_driver_path.h diff --git a/configuration.c b/configuration.c index 6db21950abfc..0b94b0d2ecac 100644 --- a/configuration.c +++ b/configuration.c @@ -2509,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" @@ -3347,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" @@ -3991,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" @@ -4790,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" @@ -5433,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" 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 1f089b80f23f..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" 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 161ecf97aa27..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, @@ -9352,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 @@ -9417,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/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