mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Merge PR #28: make a Vulkan driver that stops answering diagnosable
Rebased by the author onto 0.5, so the occlusion-query bound we shipped stays as it is and this only adds diagnostics on top of it: the fatal throw that ended the session is gone, and with it the Web of Shadows regression that kept both PRs out of 0.5. Also leaves the wait on shutdown, so a driver that never answers cannot wedge the exit. README.md is deliberately not taken from the PR -- it removed the whole status and differences-from-upstream section.
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
#include <algorithm>
|
||||
#include <android/api-level.h>
|
||||
#include <android/dlext.h>
|
||||
#include <android/log.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <elf.h>
|
||||
#include <jni.h>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -52,6 +56,7 @@ struct RPCSXApi {
|
||||
bool (*uninstallGame)(std::string_view path);
|
||||
std::string (*getVersion)();
|
||||
void *(*setCustomDriver)(void *driverHandle);
|
||||
void (*reportDriverProblem)(std::string message);
|
||||
bool (*saveState)();
|
||||
bool (*loadState)(unsigned int index);
|
||||
bool (*hasState)(unsigned int index);
|
||||
@@ -136,6 +141,7 @@ struct RPCSXLibrary : RPCSXApi {
|
||||
result.uninstallGame = reinterpret_cast<decltype(uninstallGame)>(dlsym(handle, "_rpcsx_uninstallGame"));
|
||||
result.getVersion = reinterpret_cast<decltype(getVersion)>(dlsym(handle, "_rpcsx_getVersion"));
|
||||
result.setCustomDriver = reinterpret_cast<decltype(setCustomDriver)>(dlsym(handle, "_rpcsx_setCustomDriver"));
|
||||
result.reportDriverProblem = reinterpret_cast<decltype(reportDriverProblem)>(dlsym(handle, "_rpcsx_reportDriverProblem"));
|
||||
result.saveState = reinterpret_cast<decltype(saveState)>(dlsym(handle, "_rpcsx_saveState"));
|
||||
result.loadState = reinterpret_cast<decltype(loadState)>(dlsym(handle, "_rpcsx_loadState"));
|
||||
result.hasState = reinterpret_cast<decltype(hasState)>(dlsym(handle, "_rpcsx_hasState"));
|
||||
@@ -558,6 +564,145 @@ Java_net_rpcsx_RPCSX_getVersion(JNIEnv *env, jobject) {
|
||||
return wrap(env, rpcsxLib.getVersion());
|
||||
}
|
||||
|
||||
#if defined(__aarch64__)
|
||||
// Why a driver will not load, when the answer is knowable before trying.
|
||||
//
|
||||
// A community driver built against a newer NDK imports symbols versioned against a libc
|
||||
// this device does not have, and the linker then refuses it. adrenotools reports that to
|
||||
// logcat and quietly substitutes the system driver, so from the app's side the load
|
||||
// "succeeded" and the user runs a driver they did not choose.
|
||||
//
|
||||
// The requirement is stated in the file: DT_VERNEED / .gnu.version_r lists the libc
|
||||
// versions it needs, e.g. LIBC_36 for API 36. Comparing that against the running API
|
||||
// turns "failed to load" into the actual reason, which is the difference between a
|
||||
// usable bug report and a shrug. Mr Purple T29 needs LIBC_36 (Android 16) and its own
|
||||
// meta.json claims minApi 30, so the metadata cannot be trusted for this -- only the
|
||||
// binary can.
|
||||
//
|
||||
// Returns an empty string when nothing conclusive was found. Advisory only: the load is
|
||||
// still attempted, so a wrong answer here costs a log line and never a working driver.
|
||||
static std::string driver_libc_requirement_blocker(const std::string &soPath) {
|
||||
std::FILE *f = std::fopen(soPath.c_str(), "rb");
|
||||
if (f == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<char> data;
|
||||
std::fseek(f, 0, SEEK_END);
|
||||
const long size = std::ftell(f);
|
||||
|
||||
// Header tables live near the start and end; the symbol names they point at can be
|
||||
// anywhere, so read the whole file. These are ~15-20 MB and read once per driver
|
||||
// switch, not per boot.
|
||||
if (size <= 0 || size > (256 << 20)) {
|
||||
std::fclose(f);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::fseek(f, 0, SEEK_SET);
|
||||
data.resize(static_cast<size_t>(size));
|
||||
const size_t got = std::fread(data.data(), 1, data.size(), f);
|
||||
std::fclose(f);
|
||||
|
||||
if (got != data.size() || data.size() < sizeof(Elf64_Ehdr)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto *ehdr = reinterpret_cast<const Elf64_Ehdr *>(data.data());
|
||||
|
||||
if (std::memcmp(ehdr->e_ident, ELFMAG, SELFMAG) != 0 ||
|
||||
ehdr->e_ident[EI_CLASS] != ELFCLASS64 || ehdr->e_shoff == 0 ||
|
||||
ehdr->e_shentsize != sizeof(Elf64_Shdr)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Section headers rather than PT_DYNAMIC: they carry file offsets directly, so no
|
||||
// vaddr-to-offset mapping is needed. Shared objects keep them; if they are gone, this
|
||||
// check simply declines to answer.
|
||||
const auto section_at = [&](size_t i) -> const Elf64_Shdr * {
|
||||
const size_t off = ehdr->e_shoff + i * sizeof(Elf64_Shdr);
|
||||
if (off + sizeof(Elf64_Shdr) > data.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<const Elf64_Shdr *>(data.data() + off);
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < ehdr->e_shnum; i++) {
|
||||
const Elf64_Shdr *sh = section_at(i);
|
||||
|
||||
if (sh == nullptr || sh->sh_type != SHT_GNU_verneed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Elf64_Shdr *strtab = section_at(sh->sh_link);
|
||||
|
||||
if (strtab == nullptr || strtab->sh_offset >= data.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const char *strings = data.data() + strtab->sh_offset;
|
||||
const size_t strings_max = data.size() - strtab->sh_offset;
|
||||
|
||||
size_t offset = sh->sh_offset;
|
||||
int highest_libc = 0;
|
||||
|
||||
for (size_t entry = 0; entry < sh->sh_info; entry++) {
|
||||
if (offset + sizeof(Elf64_Verneed) > data.size()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto *vn = reinterpret_cast<const Elf64_Verneed *>(data.data() + offset);
|
||||
size_t aux_offset = offset + vn->vn_aux;
|
||||
|
||||
for (size_t aux = 0; aux < vn->vn_cnt; aux++) {
|
||||
if (aux_offset + sizeof(Elf64_Vernaux) > data.size()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto *vna = reinterpret_cast<const Elf64_Vernaux *>(data.data() + aux_offset);
|
||||
|
||||
if (vna->vna_name < strings_max) {
|
||||
const char *name = strings + vna->vna_name;
|
||||
int level = 0;
|
||||
|
||||
// Only LIBC_<n> is a device-capability statement. Anything else (LIBC,
|
||||
// LIBC_PRIVATE, other sonames) says nothing about the API level.
|
||||
if (std::sscanf(name, "LIBC_%d", &level) == 1 && level > highest_libc) {
|
||||
highest_libc = level;
|
||||
}
|
||||
}
|
||||
|
||||
if (vna->vna_next == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
aux_offset += vna->vna_next;
|
||||
}
|
||||
|
||||
if (vn->vn_next == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
offset += vn->vn_next;
|
||||
}
|
||||
|
||||
const int device_api = android_get_device_api_level();
|
||||
|
||||
if (highest_libc > 0 && device_api > 0 && highest_libc > device_api) {
|
||||
char buf[256];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"it requires LIBC_%d (Android API %d) but this device provides API %d",
|
||||
highest_libc, highest_libc, device_api);
|
||||
return buf;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
#endif // __aarch64__
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_net_rpcsx_RPCSX_setCustomDriver(JNIEnv *env, jobject, jstring jpath,
|
||||
jstring jlibraryName, jstring jhookDir) {
|
||||
@@ -575,6 +720,26 @@ Java_net_rpcsx_RPCSX_setCustomDriver(JNIEnv *env, jobject, jstring jpath,
|
||||
__android_log_print(ANDROID_LOG_INFO, "RPCSX-UI", "Loading custom driver %s",
|
||||
path.c_str());
|
||||
|
||||
// Said before the attempt, because adrenotools swallows the failure: it logs to
|
||||
// logcat and hands back the system driver, so the caller cannot tell a real load
|
||||
// from a substitution, and the reason never reaches the emulator log at all.
|
||||
if (auto blocker = driver_libc_requirement_blocker(path + "/" + libraryName);
|
||||
!blocker.empty()) {
|
||||
const std::string report =
|
||||
"Custom driver '" + libraryName + "' cannot load on this device: " + blocker +
|
||||
". It was built against a newer NDK than this Android version supports; the "
|
||||
"driver's own metadata does not carry this. The system driver will be used "
|
||||
"instead.";
|
||||
|
||||
__android_log_print(ANDROID_LOG_ERROR, "RPCSX-UI", "%s", report.c_str());
|
||||
|
||||
// Also to the emulator log, which is the file that gets attached to issues.
|
||||
// logcat alone means the reason exists and no report ever contains it.
|
||||
if (rpcsxLib.reportDriverProblem != nullptr) {
|
||||
rpcsxLib.reportDriverProblem(report);
|
||||
}
|
||||
}
|
||||
|
||||
::dlerror();
|
||||
loader = adrenotools_open_libvulkan(
|
||||
RTLD_NOW, ADRENOTOOLS_DRIVER_CUSTOM, nullptr, (hookDir + "/").c_str(),
|
||||
|
||||
@@ -4279,6 +4279,15 @@ extern "C" std::string _rpcsx_getVersion() {
|
||||
//
|
||||
// Returns the PREVIOUS handle so the caller can dlclose it. Passing nullptr
|
||||
// reverts to the system driver.
|
||||
// Lets the UI glue put a driver problem where bug reports will carry it.
|
||||
//
|
||||
// The glue can only reach logcat, which nobody attaches to an issue, and the reason a
|
||||
// custom driver was refused is exactly what a report needs. Routed through the emulator
|
||||
// log channel so it lands in RPCSX.log beside the driver identity it explains.
|
||||
extern "C" void _rpcsx_reportDriverProblem(std::string message) {
|
||||
rpcsx_android.error("%s", message);
|
||||
}
|
||||
|
||||
extern "C" void *_rpcsx_setCustomDriver(void *driverHandle) {
|
||||
void *previous = vk::android::set_driver_handle(driverHandle);
|
||||
|
||||
|
||||
@@ -196,6 +196,14 @@ namespace vk
|
||||
|
||||
for (u32 spins = 0; !query_info.ready; spins++)
|
||||
{
|
||||
// Emulation is going away; a driver that never answers must not also wedge
|
||||
// the exit path. The value is irrelevant once we are aborting.
|
||||
if (thread_ctrl::state() == thread_state::aborting)
|
||||
{
|
||||
rsx_log.warning("Occlusion query %u abandoned: emulation is shutting down.", index);
|
||||
break;
|
||||
}
|
||||
|
||||
if ((spins & 0xffff) == 0xffff)
|
||||
{
|
||||
const auto waited = std::chrono::steady_clock::now() - wait_started;
|
||||
@@ -208,7 +216,18 @@ namespace vk
|
||||
|
||||
if (waited > std::chrono::seconds(3))
|
||||
{
|
||||
rsx_log.error("Occlusion query %u never completed; abandoning the wait and using result=%u.", index, query_info.data);
|
||||
// Ask once more directly before giving up, to record WHICH way the
|
||||
// driver is stalling: VK_NOT_READY, or VK_SUCCESS with the
|
||||
// availability word still clear. The two are indistinguishable
|
||||
// through poke_query and need different conversations with whoever
|
||||
// maintains the driver.
|
||||
u32 probe[2] = { 0, 0 };
|
||||
const VkResult status = vkGetQueryPoolResults(*owner, *query_info.pool, index, 1, 8, probe, 8,
|
||||
result_flags | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
|
||||
|
||||
rsx_log.error("Occlusion query %u never completed (last VkResult %d, result %u, availability %u); "
|
||||
"abandoning the wait and using result=%u.",
|
||||
index, static_cast<int>(status), probe[0], probe[1], query_info.data);
|
||||
query_info.ready = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -629,7 +629,12 @@ namespace vk::android
|
||||
}
|
||||
|
||||
g_handle = handle;
|
||||
vk_loader.success("Vulkan driver bound (%s)", g_custom ? "custom" : "system");
|
||||
|
||||
// "custom" describes the handle we were given, not necessarily the driver that
|
||||
// answers through it: adrenotools falls back to the system driver inside that
|
||||
// handle when its own dlopen fails, and says so only to logcat. The identity
|
||||
// logged by physical_device::create is what actually answered.
|
||||
vk_loader.success("Vulkan dispatch bound to the %s driver handle", g_custom ? "custom" : "system");
|
||||
return previous;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include "util/logs.hpp"
|
||||
#include "Emu/system_config.h"
|
||||
#include <vulkan/vulkan_core.h>
|
||||
#ifdef __ANDROID__
|
||||
#include "Emu/RSX/VK/vk_android_loader.h"
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
#include <vulkan/vulkan_beta.h>
|
||||
#endif
|
||||
@@ -246,6 +249,42 @@ namespace vk
|
||||
|
||||
rsx_log.always()("Found Vulkan-compatible GPU: '%s' running on driver %s", get_name(), get_driver_version());
|
||||
|
||||
// ARMSX3: say WHICH driver this is, not just its version.
|
||||
//
|
||||
// The version alone does not identify a driver, and on Android it is actively
|
||||
// misleading: adrenotools' hook falls back to the system driver when its dlopen
|
||||
// fails, so a session that silently ran the system driver looks here exactly like
|
||||
// one that ran the custom driver it was asked for. Every report naming a custom
|
||||
// driver is untrustworthy without this.
|
||||
if (driver_properties.driverID)
|
||||
{
|
||||
rsx_log.always()("Vulkan driver identity: '%s' (driverID %u), info '%s', conformance %u.%u.%u.%u",
|
||||
driver_properties.driverName,
|
||||
static_cast<u32>(driver_properties.driverID),
|
||||
driver_properties.driverInfo,
|
||||
static_cast<u32>(driver_properties.conformanceVersion.major),
|
||||
static_cast<u32>(driver_properties.conformanceVersion.minor),
|
||||
static_cast<u32>(driver_properties.conformanceVersion.subminor),
|
||||
static_cast<u32>(driver_properties.conformanceVersion.patch));
|
||||
}
|
||||
else
|
||||
{
|
||||
rsx_log.always()("Vulkan driver identity: VK_KHR_driver_properties unavailable, inferred from the GPU name only");
|
||||
}
|
||||
|
||||
#ifdef __ANDROID__
|
||||
// A custom driver was asked for, and the driver that answered is Qualcomm's own.
|
||||
// adrenotools installs Mesa/Turnip builds, so this combination means the load
|
||||
// failed and the fallback took over. It reports that here because the only other
|
||||
// trace is a logcat line from hook_impl, which never reaches a bug report.
|
||||
if (vk::android::using_custom_driver() && get_driver_vendor() == driver_vendor::ADRENO)
|
||||
{
|
||||
rsx_log.error("A custom Vulkan driver was requested, but the driver in use is Qualcomm's own. "
|
||||
"It most likely failed to load and fell back silently; `adb logcat | grep hook_impl` has the reason. "
|
||||
"Treat this session as running the SYSTEM driver.");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (get_driver_vendor() == driver_vendor::RADV && get_name().find("LLVM 8.0.0") != umax)
|
||||
{
|
||||
// Serious driver bug causing black screens
|
||||
|
||||
Reference in New Issue
Block a user