VK: frame generation through Lossless Scaling, experimental

Interpolates frames between the ones the game draws, at x2/x3/x4. The shaders
come from the user's own Lossless.dll; nothing is bundled or downloaded.

framegen runs on its OWN VkDevice and statically links volk, which defines 655
globals named vkCreateImage, vkQueueSubmit and so on -- including all 124 our
loader declares. Linked into the core those either fail to link or, worse,
merge, and framegen's volkLoadDevice() then repoints the whole RSX renderer at
framegen's device. So it lives in libarmsx3_lsfg.so, reached only by dlopen
with RTLD_LOCAL, behind a C ABI and a version script that exports eleven
symbols and nothing else. Verify with llvm-nm --dynamic --defined-only: only
armsx3_lsfg_* may appear.

Two devices with no shared semaphore means images cross as AHardwareBuffer --
Adreno and Mali both refuse vkGetMemoryFdKHR(OPAQUE_FD) on AHB-imported memory,
so upstream's FD path does not work on this hardware. Capture costs 0.007
ms/frame CPU, measured; the cost is the synchronisation, not the copies.

Notes for anyone reading this later:

  * The shader loader's user pointer must outlive initialize(). framegen copies
    the callback into ShaderPool::source and resolves shaders lazily while
    BUILDING THE CONTEXT, so a stack local there is read back from a dead frame
    -- a segfault executing at a mapped, non-executable address.
  * The "device UUID" is not one. framegen matches (vendorID << 32) | deviceID.
    Zero matches nothing.
  * Imported shaders are cached to disk. They used to live only in the library's
    map, so every restart silently had none and generate() returned 0 before
    doing any work.
  * Capture takes the COMPOSITED swapchain image, after overlays. Capturing the
    game image put the perf overlay on real frames only, so it blinked at half
    the display rate.
  * generate() runs only on a frame the game actually drew, or the PPU/SPU
    compilation screen gets interpolated too.

The pipelined path that would take waitIdle off the critical path is present but
disabled behind k_framegen_pipelining_enabled: holding a frame back conflicts
with frame-context recycling, and at least one reclaim path has not been found.
The serialised path is what works. Frame generation costs some real framerate
and wants a steady one -- interpolating an unstable rate reads as judder -- so
it is labelled experimental in the UI.
This commit is contained in:
jpolo1224
2026-08-15 01:25:21 -04:00
parent dbbb6fbde0
commit 7d25a7086e
17 changed files with 2648 additions and 18 deletions
+9
View File
@@ -373,6 +373,15 @@ add_subdirectory(fusion EXCLUDE_FROM_ALL)
# FERAL INTERACTIVE
add_subdirectory(feralinteractive EXCLUDE_FROM_ALL)
# LSFG: Lossless Scaling frame generation. Android only, and deliberately NOT EXCLUDE_FROM_ALL --
# libarmsx3_lsfg.so has to be built and packaged even though nothing links it, because the core
# reaches it by dlopen rather than by linking. Marking it excluded produces a build that succeeds
# and an APK with no frame generation in it.
#
# The subdir returns immediately when the submodule is absent, so a checkout without it still
# builds; frame generation simply reports itself unavailable at runtime.
add_subdirectory(lsfg)
# add nice ALIAS targets for ease of use
if(USE_SYSTEM_LIBUSB)
add_library(3rdparty::libusb ALIAS usb-1.0-shared)
+147
View File
@@ -0,0 +1,147 @@
# libarmsx3_lsfg.so -- Lossless Scaling frame generation, sealed away from the emulator core.
#
# The entire reason this is a separate shared object is symbol collision. volk defines 655 globals
# named vkCreateImage, vkQueueSubmit, ... and all 124 that our Vulkan loader declares in
# rpcs3/Emu/RSX/VK/vk_android_loader.h are among them. Linked into libarmsx3-core.so this either
# fails at link or, worse, merges -- and framegen's volkLoadDevice(itsOwnDevice) then repoints the
# whole RSX renderer at framegen's VkDevice. See armsx3_lsfg_shim.h.
#
# Android only. framegen's non-Android path shares images by FD, which Adreno and Mali refuse for
# AHB-imported memory, so there is nothing here worth building for desktop.
if (NOT ANDROID)
return()
endif()
set(LSFG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/lsfg-vk-android")
if (NOT EXISTS "${LSFG_ROOT}/framegen/CMakeLists.txt")
message(STATUS "LSFG: 3rdparty/lsfg/lsfg-vk-android is missing, frame generation will not be built")
return()
endif()
if (NOT EXISTS "${LSFG_ROOT}/thirdparty/volk/volk.c")
# Called out explicitly because the failure is otherwise mystifying: framegen links volk
# PUBLIC, so without it the error names framegen rather than the submodule that is missing.
message(STATUS "LSFG: thirdparty/volk is missing (git submodule update --init), skipping")
return()
endif()
# volk, built for Android.
#
# VK_USE_PLATFORM_ANDROID_KHR has to be set on VOLK ITSELF, not only on framegen. Without it volk
# never defines vkGetAndroidHardwareBufferPropertiesANDROID, and the resulting undefined symbol
# points at framegen -- sending you to debug the wrong target entirely.
add_library(armsx3_lsfg_volk STATIC "${LSFG_ROOT}/thirdparty/volk/volk.c")
target_include_directories(armsx3_lsfg_volk PUBLIC "${LSFG_ROOT}/thirdparty/volk")
target_compile_definitions(armsx3_lsfg_volk PUBLIC VK_USE_PLATFORM_ANDROID_KHR VK_NO_PROTOTYPES)
set_target_properties(armsx3_lsfg_volk PROPERTIES
POSITION_INDEPENDENT_CODE ON
C_VISIBILITY_PRESET hidden)
# framegen.
#
# Its own CMakeLists expects a target called `volk`, so alias ours rather than patching upstream.
if (NOT TARGET volk)
add_library(volk ALIAS armsx3_lsfg_volk)
endif()
add_subdirectory("${LSFG_ROOT}/framegen" "${CMAKE_CURRENT_BINARY_DIR}/framegen" EXCLUDE_FROM_ALL)
# Undo the project-wide -fno-exceptions for framegen and the shim.
#
# The top-level build sets it with add_compile_options, which every later add_subdirectory
# inherits. framegen has dozens of throw sites and they do not warn -- they fail to compile. The
# shim needs exceptions for the opposite reason: it exists to CATCH them so none reach the dlopen
# boundary.
foreach (tgt lsfg-vk-framegen)
if (TARGET ${tgt})
target_compile_options(${tgt} PRIVATE -fexceptions)
set_target_properties(${tgt} PROPERTIES
POSITION_INDEPENDENT_CODE ON
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON)
# PRIVATE, not PUBLIC: leaking this onto consumers collides with the valueless #define
# our own Vulkan headers use, in hundreds of RSX translation units.
target_compile_definitions(${tgt} PRIVATE VK_USE_PLATFORM_ANDROID_KHR)
endif()
endforeach()
# Shader extraction: DXBC out of the user's own Lossless.dll, translated to SPIR-V.
#
# framegen asks for SPIR-V by name and does not read the DLL itself, so this chain is the caller's
# responsibility. Building upstream's own libraries rather than writing a DXBC translator: dxbc is
# DXVK's, and reimplementing it would be absurd.
#
# Optional. Without these the library still builds and frame generation still reports itself
# available -- it just cannot initialize until shaders exist, which is also what happens when the
# user has not supplied a DLL.
set(LSFG_HAS_EXTRACT OFF)
if (EXISTS "${LSFG_ROOT}/thirdparty/dxbc/CMakeLists.txt" AND
EXISTS "${LSFG_ROOT}/thirdparty/pe-parse/CMakeLists.txt")
# pe-parse defaults to a shared library and command-line tools, neither of which belongs in
# an APK. Forced here because its options are plain option(), so they take whatever is
# already in the cache unless overridden.
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_COMMAND_LINE_TOOLS OFF CACHE BOOL "" FORCE)
set(PEPARSE_ENABLE_EXAMPLES OFF CACHE BOOL "" FORCE)
set(PEPARSE_ENABLE_TESTING OFF CACHE BOOL "" FORCE)
add_subdirectory("${LSFG_ROOT}/thirdparty/dxbc" "${CMAKE_CURRENT_BINARY_DIR}/dxbc" EXCLUDE_FROM_ALL)
add_subdirectory("${LSFG_ROOT}/thirdparty/pe-parse" "${CMAKE_CURRENT_BINARY_DIR}/pe-parse" EXCLUDE_FROM_ALL)
foreach (tgt dxbc pe-parse)
if (TARGET ${tgt})
# Same -fno-exceptions problem as framegen: both throw, and inheriting the
# project-wide flag turns that into a compile error rather than a warning.
target_compile_options(${tgt} PRIVATE -fexceptions)
set_target_properties(${tgt} PROPERTIES
POSITION_INDEPENDENT_CODE ON
CXX_VISIBILITY_PRESET hidden)
set(LSFG_HAS_EXTRACT ON)
endif()
endforeach()
endif()
add_library(armsx3_lsfg SHARED armsx3_lsfg_shim.cpp)
target_include_directories(armsx3_lsfg PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${LSFG_ROOT}/framegen/public")
target_compile_options(armsx3_lsfg PRIVATE -fexceptions)
target_compile_definitions(armsx3_lsfg PRIVATE VK_USE_PLATFORM_ANDROID_KHR)
set_target_properties(armsx3_lsfg PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
OUTPUT_NAME "armsx3_lsfg")
target_link_libraries(armsx3_lsfg PRIVATE lsfg-vk-framegen armsx3_lsfg_volk android log)
if (LSFG_HAS_EXTRACT)
target_sources(armsx3_lsfg PRIVATE
"${LSFG_ROOT}/src/extract/trans.cpp"
"${LSFG_ROOT}/src/extract/extract.cpp")
target_include_directories(armsx3_lsfg PRIVATE "${LSFG_ROOT}/include")
target_link_libraries(armsx3_lsfg PRIVATE dxbc pe-parse)
target_compile_definitions(armsx3_lsfg PRIVATE ARMSX3_LSFG_HAVE_EXTRACT=1)
message(STATUS "LSFG: shader extraction enabled (dxbc + pe-parse)")
else()
message(STATUS "LSFG: shader extraction NOT available, frame generation cannot initialize")
endif()
# Keep the exported surface to the shim alone.
#
# The version script is what makes the isolation real rather than aspirational: without it,
# framegen's and volk's symbols are still dynamic and the loader can bind our renderer's vk* to
# them. Verify with:
# llvm-nm --defined-only --extern-only libarmsx3_lsfg.so
# Only armsx3_lsfg_* may appear. Any vk* or LSFG_3_1 symbol means this stopped working.
target_link_options(armsx3_lsfg PRIVATE
"-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/armsx3_lsfg.map"
"-Wl,--no-undefined")
+32
View File
@@ -0,0 +1,32 @@
/* Exported surface of libarmsx3_lsfg.so.
*
* This list IS the isolation. framegen and volk are statically linked into this library and
* between them define 655 globals named vkCreateImage, vkQueueSubmit, ... -- 124 of which are
* exactly the names libarmsx3-core.so's Vulkan loader declares. If any of those stay dynamic,
* the loader is free to bind the renderer's entry points to framegen's copies, and framegen's
* volkLoadDevice() has already pointed those at a different VkDevice.
*
* -fvisibility=hidden covers most of it; this covers the rest, including anything upstream marks
* __attribute__((visibility("default"))) -- which framegen's public API does.
*
* Check it, do not assume it:
* llvm-nm --defined-only --extern-only libarmsx3_lsfg.so
* Nothing but armsx3_lsfg_* should be listed.
*/
{
global:
armsx3_lsfg_abi_version;
armsx3_lsfg_initialize;
armsx3_lsfg_create_context_ahb;
armsx3_lsfg_present;
armsx3_lsfg_destroy_context;
armsx3_lsfg_wait_idle;
armsx3_lsfg_finalize;
armsx3_lsfg_last_error;
armsx3_lsfg_import_shaders;
armsx3_lsfg_shader_count;
armsx3_lsfg_get_shader;
local:
*;
};
+404
View File
@@ -0,0 +1,404 @@
// Implementation of the C ABI in armsx3_lsfg_shim.h.
//
// This translation unit is the ONLY thing in libarmsx3_lsfg.so that anyone outside it may touch.
// Everything else -- framegen, volk, and volk's 655 vk* globals -- stays hidden behind
// -fvisibility=hidden so the dynamic linker cannot bind our renderer's vkCmdDraw to framegen's
// copy. See the header for why that matters.
//
// Rules for every entry point here:
// * no C++ type crosses the boundary (separate libc++ per .so under c++_static),
// * no exception crosses the boundary (framegen throws; dlopen'd code must not),
// * a failure returns a code and leaves a message in armsx3_lsfg_last_error().
#include "armsx3_lsfg_shim.h"
#include <lsfg_3_1.hpp>
#include <lsfg_3_1p.hpp>
#ifdef ARMSX3_LSFG_HAVE_EXTRACT
#include <extract/extract.hpp>
#include <extract/trans.hpp>
#include <config/config.hpp>
#endif
#include <exception>
#include <map>
#include <string>
#include <vector>
namespace
{
// thread_local because the renderer and whatever calls initialize() are not the same thread,
// and a shared buffer would let one overwrite the other's message mid-report.
thread_local std::string g_last_error;
bool g_initialized = false;
// Which shader family initialize() chose. Fixed until finalize(): LSFG_3_1 and LSFG_3_1P keep
// entirely separate device state and context tables, so a context created by one cannot be
// presented or destroyed through the other -- every entry point below has to dispatch on this.
bool g_performance = false;
void clear_error()
{
g_last_error.clear();
}
void set_error(const char* what)
{
g_last_error = what ? what : "unknown error";
}
void set_error(const std::string& what)
{
g_last_error = what.empty() ? "unknown error" : what;
}
}
// Wrap a call so nothing escapes.
//
// catch (...) rather than catching LSFG's types: framegen throws several, they are not part of
// its public header, and an exception reaching the dlopen boundary is undefined behaviour -- so
// the exact type matters less than the guarantee that none of them get out.
#define ARMSX3_LSFG_GUARD(expr, failure_result) \
try \
{ \
clear_error(); \
expr; \
} \
catch (const std::exception& e) \
{ \
set_error(e.what()); \
return (failure_result); \
} \
catch (...) \
{ \
set_error("unknown exception from framegen"); \
return (failure_result); \
}
extern "C" uint32_t armsx3_lsfg_abi_version(void)
{
return ARMSX3_LSFG_ABI_VERSION;
}
extern "C" const char* armsx3_lsfg_last_error(void)
{
return g_last_error.c_str();
}
extern "C" int armsx3_lsfg_initialize(uint64_t device_uuid, int is_hdr, float flow_scale,
uint64_t generation_count, int performance, armsx3_lsfg_shader_loader loader, void* user)
{
if (!loader)
{
set_error("no shader loader supplied");
return ARMSX3_LSFG_ERR_BAD_ARGUMENT;
}
// The std::function is built HERE, on framegen's side of the boundary, from a plain C
// function pointer. That is the whole point of taking a function pointer in the header: an
// std::function constructed by the core would be a different type under a different libc++.
//
// Throwing out of this lambda is how a missing shader is reported to framegen, which is what
// it expects -- and the throw stays inside this .so, caught by the guard below.
const auto bridge = [loader, user](const std::string& name) -> std::vector<uint8_t>
{
const uint8_t* data = nullptr;
uint32_t size = 0;
if (loader(name.c_str(), &data, &size, user) != ARMSX3_LSFG_OK || !data || !size)
{
throw std::runtime_error("shader not available: " + name);
}
return std::vector<uint8_t>(data, data + size);
};
// Recorded BEFORE the call so the guard's failure path cannot leave the two disagreeing.
g_performance = performance != 0;
if (g_performance)
{
ARMSX3_LSFG_GUARD(
LSFG_3_1P::initialize(device_uuid, is_hdr != 0, flow_scale, generation_count, bridge),
ARMSX3_LSFG_ERR_SHADERS)
}
else
{
ARMSX3_LSFG_GUARD(
LSFG_3_1::initialize(device_uuid, is_hdr != 0, flow_scale, generation_count, bridge),
ARMSX3_LSFG_ERR_SHADERS)
}
g_initialized = true;
return ARMSX3_LSFG_OK;
}
extern "C" int32_t armsx3_lsfg_create_context_ahb(void* in0, void* in1, void* const* out_n,
uint32_t out_count, uint32_t width, uint32_t height, int32_t format)
{
if (!g_initialized)
{
set_error("not initialized");
return ARMSX3_LSFG_ERR_NOT_INITIALIZED;
}
if (!in0 || !in1 || !out_n || !out_count)
{
set_error("null image or empty output set");
return ARMSX3_LSFG_ERR_BAD_ARGUMENT;
}
int32_t id = ARMSX3_LSFG_ERR_UNKNOWN;
// AHardwareBuffer* arrives as void* so the header stays free of android/hardware_buffer.h,
// which the core has no reason to include.
std::vector<AHardwareBuffer*> outs;
outs.reserve(out_count);
for (uint32_t i = 0; i < out_count; ++i)
{
outs.push_back(static_cast<AHardwareBuffer*>(out_n[i]));
}
if (g_performance)
{
ARMSX3_LSFG_GUARD(
id = LSFG_3_1P::createContextFromAHB(
static_cast<AHardwareBuffer*>(in0), static_cast<AHardwareBuffer*>(in1), outs,
VkExtent2D{width, height}, static_cast<VkFormat>(format)),
ARMSX3_LSFG_ERR_VULKAN)
}
else
{
ARMSX3_LSFG_GUARD(
id = LSFG_3_1::createContextFromAHB(
static_cast<AHardwareBuffer*>(in0), static_cast<AHardwareBuffer*>(in1), outs,
VkExtent2D{width, height}, static_cast<VkFormat>(format)),
ARMSX3_LSFG_ERR_VULKAN)
}
return id;
}
extern "C" int armsx3_lsfg_present(int32_t ctx, int in_sem, const int* out_sems, uint32_t out_count)
{
if (!g_initialized)
{
set_error("not initialized");
return ARMSX3_LSFG_ERR_NOT_INITIALIZED;
}
std::vector<int> outs;
outs.reserve(out_count);
for (uint32_t i = 0; i < out_count; ++i)
{
outs.push_back(out_sems ? out_sems[i] : -1);
}
if (g_performance)
{
ARMSX3_LSFG_GUARD(LSFG_3_1P::presentContext(ctx, in_sem, outs), ARMSX3_LSFG_ERR_VULKAN)
}
else
{
ARMSX3_LSFG_GUARD(LSFG_3_1::presentContext(ctx, in_sem, outs), ARMSX3_LSFG_ERR_VULKAN)
}
return ARMSX3_LSFG_OK;
}
extern "C" int armsx3_lsfg_destroy_context(int32_t ctx)
{
if (!g_initialized)
{
return ARMSX3_LSFG_OK; // nothing to release
}
if (g_performance)
{
ARMSX3_LSFG_GUARD(LSFG_3_1P::deleteContext(ctx), ARMSX3_LSFG_ERR_UNKNOWN)
}
else
{
ARMSX3_LSFG_GUARD(LSFG_3_1::deleteContext(ctx), ARMSX3_LSFG_ERR_UNKNOWN)
}
return ARMSX3_LSFG_OK;
}
extern "C" void armsx3_lsfg_wait_idle(void)
{
if (!g_initialized)
{
return;
}
try
{
if (g_performance) LSFG_3_1P::waitIdle(); else LSFG_3_1::waitIdle();
}
catch (...)
{
// Deliberately swallowed and not recorded: this is called on the present path, and a
// failure to wait is reported by whatever uses the images next. Setting the error string
// here would overwrite a more useful message from the call that actually failed.
}
}
extern "C" void armsx3_lsfg_finalize(void)
{
if (!g_initialized)
{
return;
}
try
{
if (g_performance) LSFG_3_1P::finalize(); else LSFG_3_1::finalize();
}
catch (...)
{
}
g_initialized = false;
}
#ifdef ARMSX3_LSFG_HAVE_EXTRACT
// Satisfy the one symbol upstream's extract.cpp needs from its config layer.
//
// It reads exactly one field, Config::activeConf.dll, to find the file. Defining the object here
// rather than compiling their config module avoids dragging in toml11 and a config-file format
// that has no meaning inside an APK -- the path comes from the user's file picker instead.
namespace Config { Configuration activeConf; }
namespace
{
// name -> SPIR-V, translated once at import.
std::map<std::string, std::vector<uint8_t>> g_shaders;
}
extern "C" int armsx3_lsfg_import_shaders(const char* dll_path)
{
if (!dll_path || !*dll_path)
{
set_error("no file selected");
return ARMSX3_LSFG_ERR_BAD_ARGUMENT;
}
clear_error();
g_shaders.clear();
// Upstream's own shader names, both families.
//
// Taken verbatim from nameIdxTable in extract.cpp rather than guessed -- a made-up name fails
// as "Shader hash not found", which reads like a corrupt DLL and is not.
//
// Two sets: the plain names are LSFG 3.1 and the p_ prefixed ones are 3.1p. Which family gets
// used depends on which framegen entry point runs, so both are extracted and whatever the DLL
// actually contains is kept. Missing names are skipped rather than fatal, because a given
// Lossless Scaling version legitimately ships only one family.
static const char* const k_names[] = {
"mipmaps", "alpha[0]", "alpha[1]", "alpha[2]", "alpha[3]",
"beta[0]", "beta[1]", "beta[2]", "beta[3]", "beta[4]",
"gamma[0]", "gamma[1]", "gamma[2]", "gamma[3]", "gamma[4]",
"delta[0]", "delta[1]", "delta[2]", "delta[3]", "delta[4]",
"delta[5]", "delta[6]", "delta[7]", "delta[8]", "delta[9]",
"generate",
"p_mipmaps", "p_alpha[0]", "p_alpha[1]", "p_alpha[2]", "p_alpha[3]",
"p_beta[0]", "p_beta[1]", "p_beta[2]", "p_beta[3]", "p_beta[4]",
"p_gamma[0]", "p_gamma[1]", "p_gamma[2]", "p_gamma[3]", "p_gamma[4]",
"p_delta[0]", "p_delta[1]", "p_delta[2]", "p_delta[3]", "p_delta[4]",
"p_delta[5]", "p_delta[6]", "p_delta[7]", "p_delta[8]", "p_delta[9]",
"p_generate",
};
try
{
Config::activeConf.dll = dll_path;
Extract::extractShaders();
for (const char* name : k_names)
{
// getShader hands back DXBC; framegen wants SPIR-V. Translating at import rather than
// on demand keeps the cost off the present path entirely.
//
// Individually guarded: a DLL that ships only one shader family throws on every name
// in the other, and that is normal rather than a failure of the import.
try
{
auto spirv = Extract::translateShader(Extract::getShader(name));
if (!spirv.empty())
{
g_shaders[name] = std::move(spirv);
}
}
catch (const std::exception&)
{
// Not in this DLL. Keep going.
}
}
if (g_shaders.empty())
{
set_error("no usable shaders in that file -- is it Lossless.dll from Lossless Scaling?");
return ARMSX3_LSFG_ERR_SHADERS;
}
}
catch (const std::exception& e)
{
g_shaders.clear();
set_error(e.what());
return ARMSX3_LSFG_ERR_SHADERS;
}
catch (...)
{
g_shaders.clear();
set_error("unknown failure reading the file");
return ARMSX3_LSFG_ERR_SHADERS;
}
return static_cast<int>(g_shaders.size());
}
extern "C" int armsx3_lsfg_shader_count(void)
{
return static_cast<int>(g_shaders.size());
}
extern "C" int armsx3_lsfg_get_shader(const char* name, const uint8_t** out_data, uint32_t* out_size)
{
if (!name || !out_data || !out_size)
{
return ARMSX3_LSFG_ERR_BAD_ARGUMENT;
}
const auto it = g_shaders.find(name);
if (it == g_shaders.end() || it->second.empty())
{
return ARMSX3_LSFG_ERR_SHADERS;
}
*out_data = it->second.data();
*out_size = static_cast<uint32_t>(it->second.size());
return ARMSX3_LSFG_OK;
}
#else
extern "C" int armsx3_lsfg_import_shaders(const char*)
{
set_error("this build has no shader extraction support");
return ARMSX3_LSFG_ERR_SHADERS;
}
extern "C" int armsx3_lsfg_shader_count(void) { return 0; }
extern "C" int armsx3_lsfg_get_shader(const char*, const uint8_t**, uint32_t*)
{
return ARMSX3_LSFG_ERR_SHADERS;
}
#endif
+142
View File
@@ -0,0 +1,142 @@
// C ABI for Lossless Scaling frame generation.
//
// framegen CANNOT be linked into libarmsx3-core.so. It links volk, which defines 655 globals
// named vkCreateImage, vkQueueSubmit, ... and 124 of those are byte-for-byte the names our own
// Vulkan loader declares in rpcs3/Emu/RSX/VK/vk_android_loader.h -- every single symbol the RSX
// renderer uses. Two ways that goes wrong, and the second is the one that costs a week:
//
// 1. duplicate symbol at link time (clang defaults to -fno-common), or
// 2. the linker merges them, and framegen's volkLoadDevice(itsOwnDevice) then repoints every
// entry point the renderer uses at framegen's VkDevice. Every later vkCmdDraw goes to the
// wrong device, and it presents as a driver crash with nothing pointing at frame generation.
//
// So framegen and volk live in their own libarmsx3_lsfg.so, reached by dlopen + dlsym through
// this header. Nothing here is C++: the CMake project builds ANDROID_STL=c++_static, so each .so
// carries its own libc++ and an std::vector or std::function crossing the boundary would be two
// unrelated types that happen to share a name. The shim builds those on its own side.
//
// framegen also throws (LSFG::vulkan_error and friends). Exceptions must not cross a dlopen
// boundary either, so every entry point here catches everything and returns a code; the message
// is retrievable with armsx3_lsfg_last_error().
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// Bump when anything below changes shape. The loader refuses a library whose version it does not
// recognise, so a stale libarmsx3_lsfg.so on a user's device fails loudly at load instead of
// quietly passing mismatched structs.
#define ARMSX3_LSFG_ABI_VERSION 2u
// Mark the exported surface explicitly.
//
// The library is built -fvisibility=hidden so framegen's and volk's symbols stay in, and a
// version script narrows the dynamic table further. Neither of those can PROMOTE a symbol: a
// function hidden at compile time is local in the object, and `global:` in the linker script
// cannot bring it back. Without this attribute the .so builds and exports nothing at all, and
// the failure only shows up as dlsym returning null at runtime.
#if defined(__GNUC__) || defined(__clang__)
#define ARMSX3_LSFG_API __attribute__((visibility("default")))
#else
#define ARMSX3_LSFG_API
#endif
enum armsx3_lsfg_result
{
ARMSX3_LSFG_OK = 0,
ARMSX3_LSFG_ERR_UNKNOWN = -1,
ARMSX3_LSFG_ERR_NOT_INITIALIZED = -2,
ARMSX3_LSFG_ERR_BAD_ARGUMENT = -3,
ARMSX3_LSFG_ERR_SHADERS = -4,
ARMSX3_LSFG_ERR_VULKAN = -5,
};
// Hand back the SPIR-V for a named shader.
//
// framegen does NOT read Lossless.dll -- it asks for shaders by name and expects SPIR-V back.
// Extracting them from the user's own copy (PE resource -> DXBC -> SPIR-V) is the caller's job,
// which is deliberate: the shaders are THS's property and nothing here ships or downloads them.
//
// Return ARMSX3_LSFG_OK and set *out_data / *out_size on success. The buffer must stay valid
// until the initialize() call that triggered this returns. Any other return means "no such
// shader" and fails initialization.
typedef int (*armsx3_lsfg_shader_loader)(const char* name, const uint8_t** out_data,
uint32_t* out_size, void* user);
// Version of the loaded library. Call first; anything else on a mismatched library is undefined.
ARMSX3_LSFG_API uint32_t armsx3_lsfg_abi_version(void);
// Bring up framegen on the adapter identified by device_uuid (VkPhysicalDeviceIDProperties
// deviceUUID, 16 bytes, passed as the first 8 -- that is what framegen matches on).
//
// framegen creates its OWN VkDevice on that adapter. It does not share ours, which is why images
// have to be handed over as AHardwareBuffer below rather than as VkImage.
// performance selects framegen's 3.1p shader family instead of 3.1: a cheaper pipeline at lower
// quality, which is the difference between usable and not on a mobile GPU. It is fixed for the
// lifetime of the library state -- every context, present and teardown after this call goes to the
// family chosen here, because the two keep separate contexts and separate device state.
//
// flow_scale is the optical-flow resolution as a fraction of full: 1.0 is upstream's default and
// lower is cheaper. Note the sense is inverted from upstream's own config file, which stores a
// divisor and passes 1.0f/value here.
ARMSX3_LSFG_API int armsx3_lsfg_initialize(uint64_t device_uuid, int is_hdr, float flow_scale,
uint64_t generation_count, int performance, armsx3_lsfg_shader_loader loader, void* user);
// Create a context over a set of shared images.
//
// AHardwareBuffer rather than the FD path framegen also offers, because Adreno and Mali both
// refuse vkGetMemoryFdKHR(OPAQUE_FD) on AHB-imported memory -- the FD path simply does not work
// on the hardware this port runs on.
//
// The caller keeps ownership of every AHardwareBuffer and must keep them alive until the context
// is destroyed. Returns a context id >= 0, or a negative armsx3_lsfg_result.
ARMSX3_LSFG_API int32_t armsx3_lsfg_create_context_ahb(void* in0, void* in1, void* const* out_n,
uint32_t out_count, uint32_t width, uint32_t height, int32_t format);
// Generate frames for one presented pair.
//
// Semaphores are sync file descriptors, not VkSemaphore: framegen is on a different device and a
// VkSemaphore handle would be meaningless to it. in_sem is waited on before generation starts;
// each out_sems[i] is signalled when output image i is ready. Pass -1 for an unused slot.
ARMSX3_LSFG_API int armsx3_lsfg_present(int32_t ctx, int in_sem, const int* out_sems, uint32_t out_count);
ARMSX3_LSFG_API int armsx3_lsfg_destroy_context(int32_t ctx);
// Read the user's own Lossless.dll and keep the shaders it contains.
//
// Nothing is bundled or downloaded: the shaders are THS's property and the user must supply a
// legitimately purchased copy. Only the extracted SPIR-V is kept -- the DLL itself is not needed
// afterwards and the caller may delete its copy.
//
// The work is PE resource walk -> DXBC -> SPIR-V, and it is slow enough to be worth doing once
// and caching rather than at every boot. Returns the number of shaders extracted, or a negative
// armsx3_lsfg_result; armsx3_lsfg_last_error() explains a failure in terms a user can act on
// ("is Lossless Scaling up to date?" rather than a resource id).
ARMSX3_LSFG_API int armsx3_lsfg_import_shaders(const char* dll_path);
// How many shaders are currently held. Zero means frame generation cannot start.
ARMSX3_LSFG_API int armsx3_lsfg_shader_count(void);
// Serve a previously imported shader by name, for initialize()'s loader.
//
// Pass a null loader to armsx3_lsfg_initialize to use these instead of supplying your own.
ARMSX3_LSFG_API int armsx3_lsfg_get_shader(const char* name, const uint8_t** out_data, uint32_t* out_size);
// Block until framegen's device is idle.
//
// Needed on Android because framegen's device reads AHBs that OUR device writes, and there is no
// semaphore shared between the two. Without this the read races the write. It is also the reason
// frame generation cannot be free here: this is a device-level stall, not a queue wait.
ARMSX3_LSFG_API void armsx3_lsfg_wait_idle(void);
ARMSX3_LSFG_API void armsx3_lsfg_finalize(void);
// Message for the last failing call on this thread, or "" if none. Never null.
ARMSX3_LSFG_API const char* armsx3_lsfg_last_error(void);
#ifdef __cplusplus
}
#endif
+1
View File
@@ -655,6 +655,7 @@ if(TARGET 3rdparty_vulkan)
RSX/VK/VKCompute.cpp
RSX/VK/VKDataHeapManager.cpp
RSX/VK/VKDMA.cpp
RSX/VK/VKFrameGen.cpp
RSX/VK/VKDraw.cpp
RSX/VK/VKFormats.cpp
RSX/VK/VKFragmentProgram.cpp
@@ -1,4 +1,5 @@
#include "stdafx.h"
#include "Emu/RSX/VK/VKFrameGen.h"
#include "overlay_manager.h"
#include "overlay_perf_metrics.h"
#include "Emu/RSX/RSXThread.h"
@@ -624,6 +625,21 @@ namespace rsx
}
}
// Frame generation, when it is running.
//
// Added after the switch so it appears at every detail level without touching four
// format strings and their positional arguments. It has to be shown next to FPS to mean
// anything: FPS is the rate the GAME renders at, which frame generation deliberately does
// not change, and this is the rate reaching the display. The gap between them is the
// entire feature, and without this there is no way to tell it is working.
//
// Absent entirely when frame generation is off, so the overlay is unchanged for anyone
// not using it.
if (const std::string lsfg = vk::frame_gen::status_text(); !lsfg.empty())
{
fmt::append(perf_text, "%s%s", perf_text.empty() ? "" : "\n", lsfg);
}
m_body.set_text(perf_text);
if (perf_text.empty())
File diff suppressed because it is too large Load Diff
+194
View File
@@ -0,0 +1,194 @@
#pragma once
// Frame generation (Lossless Scaling) as seen by the renderer.
//
// The implementation lives in libarmsx3_lsfg.so and is reached by dlopen, never by linking. That
// is not a packaging preference: framegen statically links volk, which defines 655 globals named
// vkCreateImage, vkQueueSubmit, ... including all 124 that rpcs3/Emu/RSX/VK/vk_android_loader.h
// declares for our own renderer. Link them together and either the link fails, or the symbols
// merge and framegen's volkLoadDevice() silently repoints our entire renderer at framegen's
// VkDevice. See 3rdparty/lsfg/armsx3_lsfg_shim.h.
//
// Everything here is a no-op returning false off Android, or when the library is absent, or when
// the user has not supplied the shaders. Callers do not need to check the platform.
#include "util/types.hpp"
#include "VulkanAPI.h"
#include <string>
#include <vector>
// Forward-declared rather than including device.h: this header is pulled in by the present
// path, and device.h drags most of the Vulkan utility layer with it.
namespace vk
{
class render_device;
class command_buffer;
class image;
}
namespace vk::frame_gen
{
// Is the library present and its ABI one we understand?
//
// Cheap after the first call. A false here means frame generation is simply not on offer --
// no library in the APK, wrong ABI, or a load failure -- and the caller should carry on
// presenting normally.
bool available();
// Why available() said no, for the settings screen. Empty when it said yes.
std::string unavailable_reason();
// Bring framegen up on the adapter we are rendering with.
//
// device_uuid comes from VkPhysicalDeviceIDProperties. framegen creates its OWN VkDevice on
// that adapter rather than sharing ours, which is why images have to be handed over as
// AHardwareBuffer rather than VkImage.
//
// shader_for returns the SPIR-V for a named shader, or an empty vector if it has none.
// framegen does not read Lossless.dll -- extracting the shaders from the user's own copy is
// the caller's job, and without them this fails and frame generation stays off.
bool initialize(u64 device_uuid, bool is_hdr, f32 flow_scale, u32 generated_frames,
bool performance, std::vector<u8> (*shader_for)(const std::string& name, void* user), void* user);
// True once initialize() has succeeded and contexts can be created.
bool initialized();
void shutdown();
// Message from the last failed call, for logging. Never null.
const char* last_error();
// Read the shaders out of the user's own Lossless.dll.
//
// Frame generation cannot start without these and nothing ships them -- they are THS's
// property and the user must supply a legitimately purchased copy. Only the translated SPIR-V
// is kept; the file itself is not needed afterwards.
//
// Returns how many shaders were extracted, or a negative value on failure, in which case
// last_error() carries a message meant for the user rather than for a log.
int import_shaders(const std::string& dll_path);
// How many shaders are held. Zero means frame generation will not start.
int shader_count();
// An image both devices can see.
//
// framegen has its own VkDevice, so a VkImage of ours is meaningless to it and a VkImage of
// its own is meaningless to us. The only currency both understand is an AHardwareBuffer: we
// allocate one, import it as a VkImage on OUR device so the renderer can blit into or out of
// it, and hand the raw AHardwareBuffer* to framegen, which imports it on ITS device.
//
// This is also why frame generation cannot be free. The presented frame is not in one of
// these -- it is in a swapchain image -- so every frame has to be copied in, and every
// generated frame copied back out.
class shared_image
{
public:
shared_image() = default;
~shared_image();
shared_image(const shared_image&) = delete;
shared_image& operator=(const shared_image&) = delete;
// Allocate the buffer and import it. False leaves the object empty and logs why.
bool create(const vk::render_device& dev, u32 width, u32 height, VkFormat format);
void destroy();
bool valid() const { return m_image != VK_NULL_HANDLE; }
// For our own command buffers.
VkImage handle() const { return m_image; }
// For framegen, which takes it as an opaque void*.
void* hardware_buffer() const { return m_ahb; }
u32 width() const { return m_width; }
u32 height() const { return m_height; }
VkFormat format() const { return m_format; }
// Tracked here rather than in vk::image because these live outside the renderer's normal
// resource management -- nothing else knows they exist, so nothing else can track them.
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
private:
void* m_ahb = nullptr;
VkImage m_image = VK_NULL_HANDLE;
VkDeviceMemory m_memory = VK_NULL_HANDLE;
VkDevice m_device = VK_NULL_HANDLE;
u32 m_width = 0;
u32 m_height = 0;
VkFormat m_format = VK_FORMAT_UNDEFINED;
};
// Copy the frame about to be presented into a shared image.
//
// This is the price of frame generation before any frame is generated: framegen's device
// cannot see a swapchain image, so every presented frame has to be copied into storage both
// devices can reach, and every generated frame copied back out. Measuring it in isolation is
// the point -- if the copies alone do not fit in the GPU's idle time, nothing built on top of
// them will either, and that is worth knowing before the shader pipeline exists.
//
// src is the FINAL swapchain image -- game plus every overlay -- stated in whatever layout it
// currently holds, and left in that same layout.
//
// Capturing the composited image rather than the game image is not a detail. Generated frames
// are presented from framegen's output, so anything not in the captured image is missing from
// them: capturing pre-overlay put the performance overlay on real frames only, and presenting
// generated/real/generated/real made it blink at half the display rate.
//
// Returns false when frame generation is off, unsupported, or the shared images could not be
// created; callers present normally either way.
bool capture_presented_frame(const vk::command_buffer& cmd, const vk::render_device& dev,
VkImage src, VkImageLayout src_layout, u32 width, u32 height);
// Promote the capture recorded this frame to one framegen is allowed to read.
//
// Call immediately after the command buffer holding the capture has been submitted, and only
// then. capture_presented_frame() records a blit; it does not perform one, and framegen sits on
// a second VkDevice with no semaphore joining the two, so a capture that has only been recorded
// is a write that has not happened. Interpolating from it is what made the game image flicker
// between real frames and half-written ones.
void commit_capture();
// Release the shared images. Safe to call when none exist.
void release_shared_images();
// How many frames the current setting asks us to generate between each real pair.
// Zero when frame generation is off, unsupported, or has no shaders.
u32 generated_frame_count();
// Run framegen over the last two COMMITTED captures.
//
// Only meaningful after two captures. Returns the number of generated images now waiting in
// generated_image(), which is zero on any failure -- and a failure disables frame generation
// for the rest of the session rather than retrying every frame, because the causes (no
// shaders, no device support, a context that would not create) do not fix themselves.
//
// This reads and writes AHardwareBuffers on framegen's device and returns only once that device
// is idle again, so the caller must not have a submission in flight that touches any of them.
// In practice that means: call it BEFORE submitting the command buffer that carries this frame's
// capture, since that capture overwrites one of the two images being read. Doing it the other
// way round is the race the caller used to pay a full frame-completion wait to avoid.
u32 generate(const vk::render_device& dev);
// Handle of generated image i, for blitting into a swapchain image.
VkImage generated_image(u32 index);
// Frames per second actually reaching the display, or 0 when frame generation is not running.
//
// Exists because the FPS counter cannot answer "is this working": it reports the rate the GAME
// renders at, which frame generation deliberately leaves alone. The only visible difference is
// how many frames get presented, so that is what this counts.
f32 display_fps();
// One line for the performance overlay, or empty when the user has not turned frame generation
// on.
//
// Deliberately NOT just the FPS. Frame generation has several ways to be switched on and still
// do nothing -- no library, no imported shaders, a failure that disabled it for the session --
// and every one of them looked identical on screen to "off": no line. Reporting the reason is
// the difference between a feature the user can fix and one that appears broken.
std::string status_text();
}
+24 -4
View File
@@ -401,6 +401,10 @@ namespace vk
properties.state.set_multisample_shading_rate(1.f);
}
// Last, after the stencil block above has finished reading state.ds -- this erases the
// fields the draw path sets per draw and nothing may look at them afterwards.
vk::normalize_dynamic_pipeline_state(properties);
return properties;
}
}
@@ -624,7 +628,16 @@ VKGSRender::VKGSRender(utils::serial* ar) noexcept : GSRender(ar)
else
m_vertex_cache = std::make_unique<vk::weak_vertex_cache>();
m_shaders_cache = std::make_unique<vk::shader_cache>(*m_prog_buffer, "vulkan", "v1.95");
// The on-disk cache stores pipeline_props as a raw struct, so the meaning of the bytes is
// what the version directory has to protect -- not just their layout. Two things move here:
// v1.95 -> v1.96 because the fields normalize_dynamic_pipeline_state() erases changed what a
// given props means, and the "-eds" suffix because whether they were erased depends on the
// DEVICE, not on the build. A user swapping in a driver through adrenotools can lose the
// extension between two runs of the same game, and reading a normalized entry back without
// it would build pipelines with culling off and the depth test disabled -- silently, since
// nothing in the entry says which convention wrote it.
m_shaders_cache = std::make_unique<vk::shader_cache>(*m_prog_buffer, "vulkan",
m_device->get_extended_dynamic_state_support() ? "v1.96-eds" : "v1.96");
for (u32 i = 0; i < m_swapchain->get_swap_image_count(); ++i)
{
@@ -1895,6 +1908,12 @@ bool VKGSRender::load_program()
// TODO: EXT_dynamic_state should get rid of this sillyness soon (kd)
const auto vertex_state = vk::decode_vertex_input_assembly_state();
// What the pipeline object is keyed on, which is no longer the topology the draw uses once
// the topology is dynamic. Both are needed: this one to decide whether the current pipeline
// still fits, the real one below to issue with the draw.
const auto pipeline_topology = vk::get_pipeline_topology(vertex_state.primitive, vertex_state.restart_index_enabled);
m_current_primitive_topology = vertex_state.primitive;
if (m_graphics_state & rsx::pipeline_state::invalidate_pipeline_bits)
{
get_current_fragment_program(fs_sampler_state);
@@ -1906,7 +1925,7 @@ bool VKGSRender::load_program()
}
else if (!(m_graphics_state & rsx::pipeline_state::pipeline_config_dirty) &&
m_program &&
m_pipeline_properties.state.ia.topology == vertex_state.primitive &&
m_pipeline_properties.state.ia.topology == pipeline_topology &&
m_pipeline_properties.state.ia.primitiveRestartEnable == vertex_state.restart_index_enabled)
{
if (!m_shader_interpreter.is_interpreter(m_program)) [[ likely ]]
@@ -1957,8 +1976,9 @@ bool VKGSRender::load_program()
}
else
{
// Update primitive type and restart index. Note that this is not needed with EXT_dynamic_state
m_pipeline_properties.state.set_primitive_type(vertex_state.primitive);
// Update primitive type and restart index. With EXT_extended_dynamic_state only the
// topology class is left in here; restart is not covered by it and still keys pipelines.
m_pipeline_properties.state.set_primitive_type(pipeline_topology);
m_pipeline_properties.state.enable_primitive_restart(vertex_state.restart_index_enabled);
m_pipeline_properties.renderpass_key = m_current_renderpass_key;
}
+32
View File
@@ -122,6 +122,13 @@ private:
sizeu m_swapchain_dims{};
bool swapchain_unavailable = false;
u64 m_display_epoch = 0; // GSFrameBase::display_epoch the swapchain was built against
// Consecutive VK_SUBOPTIMAL_KHR presents, and the size we were at when we last acted on them.
// See the present path: Android needs this signal, but cannot be allowed to rebuild on it
// every frame.
u32 m_suboptimal_present_count = 0;
u32 m_suboptimal_handled_at = 0;
bool should_reinitialize_swapchain = false;
u64 m_last_heap_sync_time = 0;
@@ -179,6 +186,23 @@ private:
vk::frame_context_t* m_current_frame = nullptr;
std::deque<vk::frame_context_t*> m_queued_frames;
// Frame generation interpolates between the two most recent frames that have actually reached
// the GPU, so the generated image belongs BEFORE the newer of that pair -- which means that real
// frame has to be held back one present. Null when nothing is being held, which is every frame
// with frame generation off.
vk::frame_context_t* m_deferred_present_frame = nullptr;
// Whether that holding-back is engaged. It costs one more swapchain image in flight than normal
// presentation does, so a shallow swapchain keeps the old serialised path instead of starving
// the generated frames of an image to be presented from.
bool m_framegen_pipelined = false;
// The command buffers the previous frame's generated images were blitted with. framegen writes
// the same output images every generation, so those blits have to have retired before the next
// generation starts overwriting them.
vk::command_buffer_chunk* m_framegen_blit_cb[3] = {};
u32 m_framegen_blit_cb_count = 0;
VkViewport m_viewport {};
VkRect2D m_scissor {};
@@ -195,6 +219,12 @@ private:
rsx::invalidation_cause m_offloader_fault_cause;
vk::draw_call_t m_current_draw {};
// The topology the draw actually wants, which stops being readable from m_pipeline_properties
// once VK_EXT_extended_dynamic_state reduces that to a topology class. Written by
// load_program(), which always runs before the subdraws of the clause it decoded.
VkPrimitiveTopology m_current_primitive_topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
u64 m_current_renderpass_key = 0;
VkRenderPass m_cached_renderpass = VK_NULL_HANDLE;
std::vector<vk::image*> m_fbo_images;
@@ -232,6 +262,7 @@ private:
void frame_context_cleanup(vk::frame_context_t *ctx);
void advance_queued_frames();
void present(vk::frame_context_t *ctx);
vk::command_buffer_chunk* present_generated_frame(VkImage src);
bool reinitialize_swapchain();
vk::viewable_image* get_present_source(vk::present_surface_info* info, const rsx::avconf& avconfig);
@@ -242,6 +273,7 @@ private:
void invalidate_render_pass();
void update_draw_state();
void set_extended_dynamic_state();
void check_present_status();
vk::vertex_upload_info upload_vertex_data();
+358 -3
View File
@@ -1,5 +1,6 @@
#include "stdafx.h"
#include "VKGSRender.h"
#include "VKFrameGen.h"
#include "vkutils/buffer_object.h"
#include "vkutils/memory.h"
#include "Emu/RSX/Overlays/overlay_manager.h"
@@ -63,6 +64,11 @@ bool VKGSRender::reinitialize_swapchain()
m_swapchain_dims.width = m_frame->client_width();
m_swapchain_dims.height = m_frame->client_height();
m_display_epoch = m_frame->display_epoch();
// A genuine rebuild re-arms the SUBOPTIMAL handling above: the latch exists to stop a rebuild
// that changes nothing from repeating, not to disable the signal for the rest of the session.
m_suboptimal_present_count = 0;
// Reject requests to acquire new swapchain if the window is minimized
// The NVIDIA driver will spam VK_ERROR_OUT_OF_DATE_KHR if you try to acquire an image from the swapchain and the window is minimized
@@ -118,11 +124,27 @@ bool VKGSRender::reinitialize_swapchain()
m_current_queue_index = 0;
m_frame_context_storage.clear();
// The frame frame generation was holding back lives in the storage just cleared, and its
// swapchain image belongs to the swapchain about to be destroyed. There is nothing left to
// present it to, so drop it rather than carry a pointer into freed contexts.
m_deferred_present_frame = nullptr;
m_framegen_blit_cb_count = 0;
// Rebuild swapchain. Old swapchain destruction is handled by the init_swapchain call
if (!m_swapchain->init(m_swapchain_dims.width, m_swapchain_dims.height))
{
rsx_log.warning("Swapchain initialization failed. Request ignored [%dx%d]", m_swapchain_dims.width, m_swapchain_dims.height);
swapchain_unavailable = true;
// Distinguish "no usable window right now" from "the VkSurfaceKHR is dead". Retrying
// against a dead surface queries the same dead handle forever; the block at the top of
// this function is what rebuilds it, and it only runs when this flag is set.
if (auto* wsi = dynamic_cast<vk::swapchain_WSI*>(m_swapchain.get());
wsi && wsi->surface_is_lost())
{
m_surface_lost = true;
}
return false;
}
@@ -184,9 +206,84 @@ bool VKGSRender::reinitialize_swapchain()
return true;
}
// Put one generated image on screen.
//
// A self-contained acquire/blit/present that deliberately does NOT touch m_current_frame: that
// context belongs to the real frame still being assembled, and borrowing its image index or
// semaphores would present the same swapchain image twice.
//
// Best-effort throughout. A generated frame is an extra, so every failure path here simply
// returns and lets the real frame present normally -- dropping an interpolated frame is invisible,
// while stalling or presenting a torn one is not.
vk::command_buffer_chunk* VKGSRender::present_generated_frame(VkImage src)
{
if (src == VK_NULL_HANDLE || swapchain_unavailable || m_swapchain->is_headless())
{
return nullptr;
}
u32 image = umax;
// Zero timeout: if no swapchain image is free the display is already keeping up, and waiting
// for one would make frame generation cost latency instead of adding smoothness.
if (m_swapchain->acquire_next_swapchain_image(VK_NULL_HANDLE, 0ull, &image) != VK_SUCCESS ||
image == umax)
{
return nullptr;
}
auto* cmd = m_primary_cb_list.next();
cmd->reset();
cmd->begin();
VkImage target = m_swapchain->get_image(image);
const VkImageSubresourceRange range = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
vk::change_image_layout(*cmd, target, VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, range);
VkImageBlit region = {};
region.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 };
region.dstSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 };
region.srcOffsets[1] = { s32(m_swapchain_dims.width), s32(m_swapchain_dims.height), 1 };
region.dstOffsets[1] = { s32(m_swapchain_dims.width), s32(m_swapchain_dims.height), 1 };
// The generated image was written by framegen's device and waited on with its device idle, so
// it is complete by the time we get here; no cross-device semaphore exists to use instead. The
// opposite direction -- framegen overwriting this image while this blit is still reading it --
// is guarded by the caller, which waits for this command buffer before the next generation.
vkCmdBlitImage(*cmd, src, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region, VK_FILTER_LINEAR);
vk::change_image_layout(*cmd, target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, range);
cmd->end();
// No semaphores either side. The source was already made visible by framegen's device idle,
// and the present below has nothing to wait on because this command buffer is the only writer
// of this swapchain image.
vk::queue_submit_t submit_info{};
submit_info.queue = m_device->get_graphics_queue();
cmd->submit(submit_info);
m_swapchain->present(VK_NULL_HANDLE, image);
return cmd;
}
void VKGSRender::present(vk::frame_context_t *ctx)
{
ensure(ctx->present_image != umax);
// Both of these are torn down by frame_context_cleanup, and frame generation's deferred
// present is the one caller that can hold a context across the throttle that reclaims them.
// The reclaim itself is prevented in advance_queued_frames, but a present owed by a context
// that has already been recycled must not take the RSX thread down with it -- dropping the
// present costs one frame on screen, and says so.
if (ctx->present_image == umax || !ctx->swap_command_buffer)
{
rsx_log.error("Present requested for a frame context that was already retired; dropping it.");
return;
}
// Partial CS flush
ctx->swap_command_buffer->flush();
@@ -196,10 +293,38 @@ void VKGSRender::present(vk::frame_context_t *ctx)
switch (VkResult error = m_swapchain->present(ctx->present_wait_semaphore, ctx->present_image))
{
case VK_SUCCESS:
m_suboptimal_present_count = 0;
break;
case VK_SUBOPTIMAL_KHR:
#ifndef ANDROID
should_reinitialize_swapchain = true;
#else
// Android used to drop this signal entirely, which left no way back from a swapchain
// the surface has outgrown.
//
// SUBOPTIMAL is what the driver reports when the swapchain no longer suits the surface
// -- most often because its preTransform (forced to IDENTITY when the surface supports
// it, see swapchain.cpp) disagrees with a currentTransform that has since rotated.
// Opening a game the moment the app starts is how that happens: the swapchain is built
// while the surface is still portrait and the landscape surface arrives a second later.
// With this ignored, the only remaining recovery was VK_ERROR_OUT_OF_DATE_KHR, which is
// what a MANUAL orientation change produces -- hence a black picture that the user could
// only fix by rotating the device.
//
// It was ignored for a reason, though: some Android drivers report SUBOPTIMAL as a
// standing condition, and rebuilding the swapchain on every frame that reports it would
// be far worse than the problem. So act on it only once it has persisted, and only once
// per surface size -- a rebuild that does not clear the condition must not be retried
// forever.
if (++m_suboptimal_present_count > 8 && m_suboptimal_handled_at != m_swapchain_dims.width)
{
m_suboptimal_handled_at = m_swapchain_dims.width;
m_suboptimal_present_count = 0;
should_reinitialize_swapchain = true;
rsx_log.warning("Swapchain reported SUBOPTIMAL persistently at %ux%u; rebuilding.",
m_swapchain_dims.width, m_swapchain_dims.height);
}
#endif
break;
case VK_ERROR_OUT_OF_DATE_KHR:
@@ -277,6 +402,24 @@ void VKGSRender::advance_queued_frames()
{
auto frame = m_queued_frames.front();
// A frame frame generation is holding back has NOT been presented yet, and it still owns
// the swapchain image it acquired. Reclaiming its context here tears down the two things
// the outstanding present needs -- present_image and swap_command_buffer -- so the present
// that comes due next frame dereferences a command buffer that is gone, and the acquired
// image is never handed back to the swapchain either.
//
// That is the crash from turning frame generation on mid-game: the deferral begins, this
// throttle reclaims the deferred context on the very next frame (max_frames_in_flight is
// only 1 once the memory-pressure path drops depth_limit to 2), and the following
// queue_swap_request presents it. Flush the deferral instead -- presenting a frame slightly
// early costs one interpolated frame's ordering, which is invisible; the alternative is a
// dead RSX thread.
if (frame == m_deferred_present_frame)
{
present(m_deferred_present_frame);
m_deferred_present_frame = nullptr;
}
if (!frame->swap_command_buffer)
{
m_queued_frames.pop_front();
@@ -301,6 +444,102 @@ void VKGSRender::queue_swap_request()
ensure(!m_current_frame->swap_command_buffer);
m_current_frame->swap_command_buffer = m_current_command_buffer;
const u32 framegen_frames = vk::frame_gen::generated_frame_count();
if (!framegen_frames)
{
// Forget the recorded blits rather than leave them to be waited on if the setting comes back
// on: the command buffers come from a ring and will have been reused by then, so the wait
// would be for unrelated work.
m_framegen_blit_cb_count = 0;
}
// Does this swapchain have room to run frame generation a frame behind?
//
// The pipelined path holds the real frame back by one present so the generated image can go out
// ahead of it, which means one more image in flight than normal presentation needs: the one
// being composited into, the one held back, and one for the generated frame. Below that,
// present_generated_frame's zero-timeout acquire would fail every frame and frame generation
// would compute images nothing ever puts on screen -- worse than the serialised path it
// replaces, and silently so. So fall back rather than degrade.
// Pipelining is OFF, and this is the switch rather than a revert.
//
// Holding the real frame back by one present conflicts with how frame contexts are recycled.
// They are a fixed array, and the throttle in advance_queued_frames retires the deferred one
// before the present it still owes -- first as a null swap_command_buffer dereference on the
// RSX thread, then, once present() was taught to drop a retired context, as a lockup: the
// dropped present never hands its acquired swapchain image back and acquire eventually starves.
// Presenting the deferred frame before the throttle can reclaim it closed one of those paths,
// but "already retired" still fired twice on device, so at least one more reclaim path exists
// and has not been found.
//
// Everything else the pipelined work brought stays live and is worth keeping: the AHB teardown
// ordering (release_shared_images used to free the input buffers while framegen's context still
// held them imported -- a use-after-free on any resize with frame generation on), the leaked
// g_shared_out[2] when dropping x4 to x2, and the recorded-vs-committed capture split.
//
// Flip this to true to resume that work; the serialised path below is what shipped working.
constexpr bool k_framegen_pipelining_enabled = false;
const bool pipelined = k_framegen_pipelining_enabled && framegen_frames != 0 &&
!m_swapchain->is_headless() && m_swapchain->get_swap_image_count() >= 4;
if (pipelined != m_framegen_pipelined)
{
m_framegen_pipelined = pipelined;
if (framegen_frames)
{
rsx_log.notice("Frame generation: %s (swapchain has %u images)",
pipelined ? "pipelined a frame behind" : "serialised, swapchain too shallow to pipeline",
m_swapchain->get_swap_image_count());
}
}
// Every generation reuses the same output images, and the blits that read the previous set were
// submitted a frame ago. This is the cheapest place to make that reuse safe: the work is a frame
// old, so in the steady state the fence is already signalled and this costs a fence poll.
auto retire_generated_blits = [this]()
{
for (u32 i = 0; i < m_framegen_blit_cb_count; ++i)
{
m_framegen_blit_cb[i]->wait(FRAME_PRESENT_TIMEOUT);
}
m_framegen_blit_cb_count = 0;
};
u32 generated = 0;
if (m_framegen_pipelined)
{
// Generate BEFORE this frame is submitted, from the pair of captures that have already
// retired.
//
// This is the whole point of running a frame behind. framegen reads the shared images on its
// own VkDevice with no semaphore joining it to ours, so it can only be given captures whose
// submission has completed -- and the way that used to be arranged was to submit this frame
// and then block on it, which put a full frame of GPU work on the critical path before
// framegen had even started. Here the newest usable capture is the one from the previous
// frame, which the game has had a whole frame to finish, so the wait below is normally
// already satisfied. Waiting for that one covers the other half of the pair as well: it was
// submitted to the same queue a frame earlier, and this renderer already treats work on the
// graphics queue as retiring in submission order -- frame_context_cleanup walks the queued
// frames oldest-first on exactly that basis.
//
// It also has to happen before the submit rather than after: this frame's command buffer
// carries a capture that overwrites one of the two images framegen is about to read, and
// with only two of them there is no slot that is not being read. See VKFrameGen.cpp for why
// a third one cannot exist.
if (m_deferred_present_frame && m_deferred_present_frame->swap_command_buffer)
{
m_deferred_present_frame->swap_command_buffer->wait(FRAME_PRESENT_TIMEOUT);
}
retire_generated_blits();
generated = vk::frame_gen::generate(*m_device);
}
if (m_swapchain->is_headless())
{
m_swapchain->end_frame(*m_current_command_buffer, m_current_frame->present_image);
@@ -314,8 +553,87 @@ void VKGSRender::queue_swap_request()
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT);
}
// Set up a present request for this frame as well
present(m_current_frame);
// The capture recorded during flip() is only now a write that has happened.
//
// Nothing may interpolate from it before this point, and framegen has no way to find out on its
// own: it is a second VkDevice reading the same AHardwareBuffer, with no semaphore between them.
vk::frame_gen::commit_capture();
if (framegen_frames && !m_framegen_pipelined)
{
// Serialised fallback, for a swapchain with no room to hold a frame back.
//
// framegen reads the captured frame on ITS OWN device, so the only thing that can guarantee
// our capture blit has actually landed is waiting for the submission that contains it --
// this frame's, which was submitted moments ago, so this is a full frame of GPU work on the
// critical path. That is the cost the pipelined path above exists to avoid.
//
// Without the wait the read races the write. It went unnoticed while the capture sat early
// in the command buffer, with the whole output pipeline behind it as accidental slack;
// moving the capture to the end of the frame closed that gap and the game image started
// flickering between real and half-written interpolations. The overlay did not, because it
// is drawn last and was therefore the one part reliably present in the copy.
if (m_current_frame->swap_command_buffer)
{
m_current_frame->swap_command_buffer->wait(FRAME_PRESENT_TIMEOUT);
}
retire_generated_blits();
generated = vk::frame_gen::generate(*m_device);
}
// Present the generated frames before the real one.
//
// This is what makes frame generation visible rather than merely computed: each generated
// image gets its own acquire, blit and present, so the display sees more frames than the game
// drew. Before the real frame because the interpolated image sits BETWEEN the previous frame
// and this one -- presenting it afterwards would run time backwards.
//
// Which real frame that is differs by path, and it is the reason the pipelined one holds a frame
// back at all: it interpolates the pair ending at the PREVIOUS frame, so the frame the generated
// image leads into is the previous one, not this one. Presenting this frame here and the
// generated image before it would show the interpolation after the frame it was interpolated
// towards, which is time running backwards by half a frame, every frame.
//
// Everything is gated on generate() having succeeded, and any failure inside it disables the
// feature for the session rather than retrying per frame. If nothing was generated this is a
// single integer test and presentation is untouched.
for (u32 i = 0; i < generated; ++i)
{
auto* cb = present_generated_frame(vk::frame_gen::generated_image(i));
// Null when the acquire found no free swapchain image, which is a dropped generated frame
// and nothing more. Only a blit that actually happened has to be waited for.
if (cb && m_framegen_blit_cb_count < std::size(m_framegen_blit_cb))
{
m_framegen_blit_cb[m_framegen_blit_cb_count++] = cb;
}
}
if (m_framegen_pipelined)
{
// Hand this frame's present to the next call and put the held-back one on screen now.
vk::frame_context_t* const to_present = m_deferred_present_frame;
m_deferred_present_frame = m_current_frame;
if (to_present)
{
present(to_present);
}
}
else
{
// A frame held back by the pipelined path has to go out before this one, or it is stranded
// holding an acquired swapchain image that is never presented.
if (m_deferred_present_frame)
{
present(m_deferred_present_frame);
m_deferred_present_frame = nullptr;
}
// Set up a present request for this frame as well
present(m_current_frame);
}
// Grab next cb in line and make it usable
m_current_command_buffer = m_primary_cb_list.next();
@@ -620,6 +938,17 @@ void VKGSRender::flip(const rsx::display_flip_info_t& info)
}
}
// A replacement window is not a resize, and the size check above cannot see one.
//
// Opening a game the instant the app starts swaps the SurfaceView under a swapchain that has
// already been built, at identical dimensions -- so nothing above fired and the renderer went
// on presenting into a window that was no longer composited. That is the black screen that
// "fixed itself" on rotation: rotating changes the size, which is the trigger that did exist.
if (m_display_epoch != m_frame->display_epoch())
{
swapchain_unavailable = true;
}
if (m_vsync_mode != g_cfg.video.vsync)
{
swapchain_unavailable = true;
@@ -1181,6 +1510,32 @@ void VKGSRender::flip(const rsx::display_flip_info_t& info)
direct_fbo->release();
}
// Hand the finished frame to frame generation.
//
// HERE, after the overlays and before the image becomes PRESENT_SRC, so what framegen
// interpolates is exactly what the user sees. It used to capture image_to_flip -- the game
// image, upstream of the output pipeline and of every overlay -- and since generated frames are
// presented straight from framegen's output, the performance overlay ended up on real frames
// only. Presenting generated/real/generated/real then blinked it at half the display rate.
//
// This is also the one place that covers every configuration: presentation splits into a
// calibration pass and a raw blit depending on scaling mode and stereo, and both converge here.
//
// Still gated on image_to_flip, which is what the old site got for free by sitting inside that
// branch. Without the gate this also captures frames the GAME did not draw -- the PPU/SPU
// compilation screen is the obvious one, being overlay on a cleared background -- and
// interpolating a progress dialog produced exactly the flicker that moving the capture was
// meant to remove.
//
// A swapchain image is not something framegen's separate VkDevice can see, so this copies into
// AHardwareBuffer-backed storage both devices share. Returns false and costs nothing when the
// feature is off, which is the default.
if (image_to_flip)
{
vk::frame_gen::capture_presented_frame(*m_current_command_buffer, *m_device,
target_image, target_layout, m_swapchain_dims.width, m_swapchain_dims.height);
}
if (target_layout != present_layout)
{
vk::change_image_layout(*m_current_command_buffer, target_image, target_layout, present_layout, subresource_range);
+9 -10
View File
@@ -60,6 +60,7 @@ extern "C"
PFN_vkCmdSetViewport vkCmdSetViewport = nullptr;
PFN_vkCmdUpdateBuffer vkCmdUpdateBuffer = nullptr;
PFN_vkCmdWaitEvents vkCmdWaitEvents = nullptr;
PFN_vkCmdWriteTimestamp vkCmdWriteTimestamp = nullptr;
PFN_vkCreateAndroidSurfaceKHR vkCreateAndroidSurfaceKHR = nullptr;
PFN_vkCreateBuffer vkCreateBuffer = nullptr;
PFN_vkCreateBufferView vkCreateBufferView = nullptr;
@@ -111,6 +112,7 @@ extern "C"
PFN_vkFlushMappedMemoryRanges vkFlushMappedMemoryRanges = nullptr;
PFN_vkFreeCommandBuffers vkFreeCommandBuffers = nullptr;
PFN_vkFreeMemory vkFreeMemory = nullptr;
PFN_vkGetAndroidHardwareBufferPropertiesANDROID vkGetAndroidHardwareBufferPropertiesANDROID = nullptr;
PFN_vkGetBufferMemoryRequirements vkGetBufferMemoryRequirements = nullptr;
PFN_vkGetBufferMemoryRequirements2 vkGetBufferMemoryRequirements2 = nullptr;
PFN_vkGetDeviceProcAddr vkGetDeviceProcAddr = nullptr;
@@ -134,7 +136,6 @@ extern "C"
PFN_vkGetPhysicalDeviceSurfacePresentModesKHR vkGetPhysicalDeviceSurfacePresentModesKHR = nullptr;
PFN_vkGetPhysicalDeviceSurfaceSupportKHR vkGetPhysicalDeviceSurfaceSupportKHR = nullptr;
PFN_vkGetQueryPoolResults vkGetQueryPoolResults = nullptr;
PFN_vkCmdWriteTimestamp vkCmdWriteTimestamp = nullptr;
PFN_vkInvalidateMappedMemoryRanges vkInvalidateMappedMemoryRanges = nullptr;
PFN_vkMapMemory vkMapMemory = nullptr;
PFN_vkQueueSubmit vkQueueSubmit = nullptr;
@@ -267,6 +268,8 @@ namespace vk::android
if (!vkCmdUpdateBuffer) vkCmdUpdateBuffer = reinterpret_cast<PFN_vkCmdUpdateBuffer>(dlsym(handle, "vkCmdUpdateBuffer"));
vkCmdWaitEvents = reinterpret_cast<PFN_vkCmdWaitEvents>(vkGetInstanceProcAddr(nullptr, "vkCmdWaitEvents"));
if (!vkCmdWaitEvents) vkCmdWaitEvents = reinterpret_cast<PFN_vkCmdWaitEvents>(dlsym(handle, "vkCmdWaitEvents"));
vkCmdWriteTimestamp = reinterpret_cast<PFN_vkCmdWriteTimestamp>(vkGetInstanceProcAddr(nullptr, "vkCmdWriteTimestamp"));
if (!vkCmdWriteTimestamp) vkCmdWriteTimestamp = reinterpret_cast<PFN_vkCmdWriteTimestamp>(dlsym(handle, "vkCmdWriteTimestamp"));
vkCreateAndroidSurfaceKHR = reinterpret_cast<PFN_vkCreateAndroidSurfaceKHR>(vkGetInstanceProcAddr(nullptr, "vkCreateAndroidSurfaceKHR"));
if (!vkCreateAndroidSurfaceKHR) vkCreateAndroidSurfaceKHR = reinterpret_cast<PFN_vkCreateAndroidSurfaceKHR>(dlsym(handle, "vkCreateAndroidSurfaceKHR"));
vkCreateBuffer = reinterpret_cast<PFN_vkCreateBuffer>(vkGetInstanceProcAddr(nullptr, "vkCreateBuffer"));
@@ -369,6 +372,8 @@ namespace vk::android
if (!vkFreeCommandBuffers) vkFreeCommandBuffers = reinterpret_cast<PFN_vkFreeCommandBuffers>(dlsym(handle, "vkFreeCommandBuffers"));
vkFreeMemory = reinterpret_cast<PFN_vkFreeMemory>(vkGetInstanceProcAddr(nullptr, "vkFreeMemory"));
if (!vkFreeMemory) vkFreeMemory = reinterpret_cast<PFN_vkFreeMemory>(dlsym(handle, "vkFreeMemory"));
vkGetAndroidHardwareBufferPropertiesANDROID = reinterpret_cast<PFN_vkGetAndroidHardwareBufferPropertiesANDROID>(vkGetInstanceProcAddr(nullptr, "vkGetAndroidHardwareBufferPropertiesANDROID"));
if (!vkGetAndroidHardwareBufferPropertiesANDROID) vkGetAndroidHardwareBufferPropertiesANDROID = reinterpret_cast<PFN_vkGetAndroidHardwareBufferPropertiesANDROID>(dlsym(handle, "vkGetAndroidHardwareBufferPropertiesANDROID"));
vkGetBufferMemoryRequirements = reinterpret_cast<PFN_vkGetBufferMemoryRequirements>(vkGetInstanceProcAddr(nullptr, "vkGetBufferMemoryRequirements"));
if (!vkGetBufferMemoryRequirements) vkGetBufferMemoryRequirements = reinterpret_cast<PFN_vkGetBufferMemoryRequirements>(dlsym(handle, "vkGetBufferMemoryRequirements"));
vkGetBufferMemoryRequirements2 = reinterpret_cast<PFN_vkGetBufferMemoryRequirements2>(vkGetInstanceProcAddr(nullptr, "vkGetBufferMemoryRequirements2"));
@@ -413,8 +418,6 @@ namespace vk::android
if (!vkGetPhysicalDeviceSurfaceSupportKHR) vkGetPhysicalDeviceSurfaceSupportKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceSurfaceSupportKHR>(dlsym(handle, "vkGetPhysicalDeviceSurfaceSupportKHR"));
vkGetQueryPoolResults = reinterpret_cast<PFN_vkGetQueryPoolResults>(vkGetInstanceProcAddr(nullptr, "vkGetQueryPoolResults"));
if (!vkGetQueryPoolResults) vkGetQueryPoolResults = reinterpret_cast<PFN_vkGetQueryPoolResults>(dlsym(handle, "vkGetQueryPoolResults"));
vkCmdWriteTimestamp = reinterpret_cast<PFN_vkCmdWriteTimestamp>(vkGetInstanceProcAddr(nullptr, "vkCmdWriteTimestamp"));
if (!vkCmdWriteTimestamp) vkCmdWriteTimestamp = reinterpret_cast<PFN_vkCmdWriteTimestamp>(dlsym(handle, "vkCmdWriteTimestamp"));
vkInvalidateMappedMemoryRanges = reinterpret_cast<PFN_vkInvalidateMappedMemoryRanges>(vkGetInstanceProcAddr(nullptr, "vkInvalidateMappedMemoryRanges"));
if (!vkInvalidateMappedMemoryRanges) vkInvalidateMappedMemoryRanges = reinterpret_cast<PFN_vkInvalidateMappedMemoryRanges>(dlsym(handle, "vkInvalidateMappedMemoryRanges"));
vkMapMemory = reinterpret_cast<PFN_vkMapMemory>(vkGetInstanceProcAddr(nullptr, "vkMapMemory"));
@@ -497,6 +500,7 @@ namespace vk::android
if (auto p = vkGetInstanceProcAddr(instance, "vkCmdSetViewport")) vkCmdSetViewport = reinterpret_cast<PFN_vkCmdSetViewport>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkCmdUpdateBuffer")) vkCmdUpdateBuffer = reinterpret_cast<PFN_vkCmdUpdateBuffer>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkCmdWaitEvents")) vkCmdWaitEvents = reinterpret_cast<PFN_vkCmdWaitEvents>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkCmdWriteTimestamp")) vkCmdWriteTimestamp = reinterpret_cast<PFN_vkCmdWriteTimestamp>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkCreateAndroidSurfaceKHR")) vkCreateAndroidSurfaceKHR = reinterpret_cast<PFN_vkCreateAndroidSurfaceKHR>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkCreateBuffer")) vkCreateBuffer = reinterpret_cast<PFN_vkCreateBuffer>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkCreateBufferView")) vkCreateBufferView = reinterpret_cast<PFN_vkCreateBufferView>(p);
@@ -547,6 +551,7 @@ namespace vk::android
if (auto p = vkGetInstanceProcAddr(instance, "vkFlushMappedMemoryRanges")) vkFlushMappedMemoryRanges = reinterpret_cast<PFN_vkFlushMappedMemoryRanges>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkFreeCommandBuffers")) vkFreeCommandBuffers = reinterpret_cast<PFN_vkFreeCommandBuffers>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkFreeMemory")) vkFreeMemory = reinterpret_cast<PFN_vkFreeMemory>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkGetAndroidHardwareBufferPropertiesANDROID")) vkGetAndroidHardwareBufferPropertiesANDROID = reinterpret_cast<PFN_vkGetAndroidHardwareBufferPropertiesANDROID>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkGetBufferMemoryRequirements")) vkGetBufferMemoryRequirements = reinterpret_cast<PFN_vkGetBufferMemoryRequirements>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkGetBufferMemoryRequirements2")) vkGetBufferMemoryRequirements2 = reinterpret_cast<PFN_vkGetBufferMemoryRequirements2>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkGetDeviceProcAddr")) vkGetDeviceProcAddr = reinterpret_cast<PFN_vkGetDeviceProcAddr>(p);
@@ -569,7 +574,6 @@ namespace vk::android
if (auto p = vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceSurfacePresentModesKHR")) vkGetPhysicalDeviceSurfacePresentModesKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceSurfacePresentModesKHR>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceSurfaceSupportKHR")) vkGetPhysicalDeviceSurfaceSupportKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceSurfaceSupportKHR>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkGetQueryPoolResults")) vkGetQueryPoolResults = reinterpret_cast<PFN_vkGetQueryPoolResults>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkCmdWriteTimestamp")) vkCmdWriteTimestamp = reinterpret_cast<PFN_vkCmdWriteTimestamp>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkInvalidateMappedMemoryRanges")) vkInvalidateMappedMemoryRanges = reinterpret_cast<PFN_vkInvalidateMappedMemoryRanges>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkMapMemory")) vkMapMemory = reinterpret_cast<PFN_vkMapMemory>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkQueueSubmit")) vkQueueSubmit = reinterpret_cast<PFN_vkQueueSubmit>(p);
@@ -629,12 +633,7 @@ namespace vk::android
}
g_handle = handle;
// "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");
vk_loader.success("Vulkan driver bound (%s)", g_custom ? "custom" : "system");
return previous;
}
+2 -1
View File
@@ -81,6 +81,7 @@ extern "C"
extern PFN_vkCmdSetViewport vkCmdSetViewport;
extern PFN_vkCmdUpdateBuffer vkCmdUpdateBuffer;
extern PFN_vkCmdWaitEvents vkCmdWaitEvents;
extern PFN_vkCmdWriteTimestamp vkCmdWriteTimestamp;
extern PFN_vkCreateAndroidSurfaceKHR vkCreateAndroidSurfaceKHR;
extern PFN_vkCreateBuffer vkCreateBuffer;
extern PFN_vkCreateBufferView vkCreateBufferView;
@@ -132,6 +133,7 @@ extern "C"
extern PFN_vkFlushMappedMemoryRanges vkFlushMappedMemoryRanges;
extern PFN_vkFreeCommandBuffers vkFreeCommandBuffers;
extern PFN_vkFreeMemory vkFreeMemory;
extern PFN_vkGetAndroidHardwareBufferPropertiesANDROID vkGetAndroidHardwareBufferPropertiesANDROID;
extern PFN_vkGetBufferMemoryRequirements vkGetBufferMemoryRequirements;
extern PFN_vkGetBufferMemoryRequirements2 vkGetBufferMemoryRequirements2;
extern PFN_vkGetDeviceProcAddr vkGetDeviceProcAddr;
@@ -155,7 +157,6 @@ extern "C"
extern PFN_vkGetPhysicalDeviceSurfacePresentModesKHR vkGetPhysicalDeviceSurfacePresentModesKHR;
extern PFN_vkGetPhysicalDeviceSurfaceSupportKHR vkGetPhysicalDeviceSurfaceSupportKHR;
extern PFN_vkGetQueryPoolResults vkGetQueryPoolResults;
extern PFN_vkCmdWriteTimestamp vkCmdWriteTimestamp;
extern PFN_vkInvalidateMappedMemoryRanges vkInvalidateMappedMemoryRanges;
extern PFN_vkMapMemory vkMapMemory;
extern PFN_vkQueueSubmit vkQueueSubmit;
+21
View File
@@ -187,6 +187,27 @@ struct cfg_root : cfg::node
cfg::_bool disable_msl_fast_math{ this, "Disable MSL Fast Math", false };
cfg::_bool disable_async_host_memory_manager{ this, "Disable Asynchronous Memory Manager", false, true };
cfg::_enum<output_scaling_mode> output_scaling{ this, "Output Scaling Mode", output_scaling_mode::bilinear, true };
#ifdef __ANDROID__
// ARMSX3: Lossless Scaling frame generation.
//
// Default off and it stays off unless the user supplies shaders from their own copy of
// Lossless Scaling -- nothing here ships or downloads them. Dynamic (the trailing true)
// because it can be turned off mid-game; turning it ON mid-game still has to rebuild the
// shared images, which the present path handles.
cfg::_enum<frame_generation_mode> frame_generation{ this, "Frame Generation", frame_generation_mode::off, true };
// Defaults to ON. This selects framegen's 3.1p shader family, which is materially cheaper
// than 3.1, and on a mobile GPU the full-quality path costs more than the frames it buys.
// Both families are extracted from the user's DLL already, so this switches between shaders
// that are both sitting in the cache.
cfg::_bool frame_generation_performance{ this, "Frame Generation Performance Mode", true, true };
// Optical-flow resolution, as a percentage of full. Lower is cheaper and blurrier in
// motion. Stored as a percentage rather than upstream's divisor because a slider from 25
// to 100 reads the right way round -- bigger is better quality -- and the conversion to
// framegen's fraction happens at the one call site.
cfg::uint<25, 100> frame_generation_flow_scale{ this, "Frame Generation Flow Scale", 100, true };
#endif
#ifdef __ANDROID__
// ARMSX3: absolute path to a RetroArch .slangp preset, used when
// Output Scaling Mode is "Shader chain (librashader)". Empty = no chain,
+19
View File
@@ -691,6 +691,25 @@ void fmt_class_string<stereo_render_mode_options>::format(std::string& out, u64
});
}
template <>
void fmt_class_string<frame_generation_mode>::format(std::string& out, u64 arg)
{
format_enum(out, arg, [](frame_generation_mode value)
{
switch (value)
{
case frame_generation_mode::off: return "Off";
case frame_generation_mode::x2: return "x2";
case frame_generation_mode::x3: return "x3";
case frame_generation_mode::x4: return "x4";
}
// A missing case serialises as `unknown`, and cfg::_enum then refuses the value on load --
// the setting silently reverts to Off with nothing explaining why.
return unknown;
});
}
template <>
void fmt_class_string<output_scaling_mode>::format(std::string& out, u64 arg)
{
+15
View File
@@ -345,6 +345,21 @@ enum class gpu_preset_level
_auto
};
// How many frames Lossless Scaling generates between each pair of real ones.
//
// Multipliers, not counts: "x2" inserts one generated frame, "x3" two, "x4" three. Named the way
// the feature is described everywhere else so the setting reads the same as the docs.
//
// Android only -- frame generation shares images as AHardwareBuffer, which is the only route
// framegen's separate VkDevice can accept.
enum class frame_generation_mode
{
off = 0,
x2,
x3,
x4,
};
enum class output_scaling_mode
{
nearest,