Files
ARMSX1/frontend/render_vk.cpp
T

1654 lines
69 KiB
C++

/*
ARMSX — Vulkan presentation backend.
============================ STATUS: EXPERIMENTAL ============================
This backend is COMPILE-VERIFIED ONLY. It has not been executed on a device by
the author of this file. It is never selected unless the user explicitly asks for
`gpu_backend = "vulkan"`, and every failure path returns NULL so the frontend's
fallback ladder (vulkan -> opengl -> sdl-accelerated -> software, see
ArmsxApp::createManagedRenderer) keeps the emulator bootable. Treat any claim of
"Vulkan works" as unproven until it has been run.
==============================================================================
DESIGN
------
Presentation only — no graphics pipeline, no render pass, no SPIR-V. The frame is moved
with transfer commands alone:
host staging VkBuffer --vkCmdCopyBufferToImage--> R8G8B8A8_UNORM VkImage
--vkCmdBlitImage-----------> swapchain image
vkCmdClearColorImage paints the letterbox bars first, and vkCmdBlitImage does the scale
with VK_FILTER_LINEAR / VK_FILTER_NEAREST, which is exactly the filtering choice the
settings expose. Avoiding a pipeline means no shader toolchain in the Makefile and no
hand-written SPIR-V, at the cost of one CPU-side format conversion.
WHY THE CPU CONVERSION
----------------------
The core's 16-bit format is A1B5G5R5 (mask in bit 15, then B, G, R). Core Vulkan has no
matching packed format (VK_FORMAT_A1B5G5R5_UNORM_PACK16 only arrived with
maintenance5/1.4), and VK_FORMAT_R8G8B8_UNORM is very rarely blit-source capable. So the
dirty rows are unpacked to RGBA8 on the CPU at native PlayStation resolution
(320x240-ish, ~0.3 MB/frame) rather than at output resolution. That is a small fraction
of the full-resolution surface blit this backend replaces. If a shader-based decode is
wanted later, it drops in as a render pass between the staging copy and the present.
CUSTOM DRIVER (adrenotools)
---------------------------
libvulkan is never linked. It is opened through a replaceable function pointer,
armsx_render_set_vulkan_loader() (frontend/render.h). Handing it a function that calls
adrenotools_open_libvulkan() is the whole integration — see the REPORT notes and
pcsx2/GS/Renderers/Vulkan/VKLoader.cpp in the PS2 tree for the reference implementation.
*/
#include "render_internal.h"
#include "gpu_profile.h"
#if defined(ARMSX_ENABLE_VULKAN)
#define VK_NO_PROTOTYPES
#if defined(__ANDROID__)
#define VK_USE_PLATFORM_ANDROID_KHR
#endif
#include <vulkan/vulkan.h>
#if defined(__ANDROID__)
#include <android/native_window.h>
#endif
#if !defined(_WIN32)
#include <dlfcn.h>
#endif
#include <SDL_vulkan.h>
/* AFTER <vulkan/vulkan.h>, deliberately: render_shaders.h includes it too, but without
VK_USE_PLATFORM_ANDROID_KHR. Letting it get there first would leave this file without
VkAndroidSurfaceCreateInfoKHR. */
#include "render_shaders.h"
namespace {
/* RAII over the shader module's VkQueue guard. Scoped so an early return inside a submit block
can never leave the queue locked against the builder thread. */
struct ShaderQueueGuard {
ShaderQueueGuard() { armsx_shader_vk_queue_lock(); }
~ShaderQueueGuard() { armsx_shader_vk_queue_unlock(); }
ShaderQueueGuard(const ShaderQueueGuard&) = delete;
ShaderQueueGuard& operator=(const ShaderQueueGuard&) = delete;
};
} // namespace
#include <algorithm>
#include <cstring>
#include <mutex>
#include <string>
#include <vector>
namespace {
std::mutex g_loader_lock;
armsx_vk_library_open_fn g_loader_fn = nullptr;
void* g_loader_user = nullptr;
/* Whether the handle we are running on came from the caller-supplied (custom driver) loader
or from the system Vulkan loader. A custom driver that was configured but failed to open
falls back silently at the dlopen level, so this is the only place the difference survives
— and it is what stops the driver manager from claiming a driver is active when it is not. */
bool g_custom_loader_active = false;
void* OpenVulkanLibrary() {
armsx_vk_library_open_fn custom = nullptr;
void* user = nullptr;
{
std::lock_guard<std::mutex> lock(g_loader_lock);
custom = g_loader_fn;
user = g_loader_user;
}
g_custom_loader_active = false;
if (custom) {
if (void* handle = custom(user)) {
armsx_render_log("renderer", "Vulkan: using the caller-supplied loader handle.");
g_custom_loader_active = true;
return handle;
}
armsx_render_log("renderer",
"Vulkan: the custom driver could NOT be opened; falling back to the "
"system loader. The reported renderer will not claim a custom driver.");
}
#if defined(_WIN32)
return nullptr; /* Not wired: this backend targets Android/Linux. */
#else
static const char* const kCandidates[] = {
"libvulkan.so",
"libvulkan.so.1",
"libvulkan.1.dylib",
"libMoltenVK.dylib",
};
for (const char* name : kCandidates) {
if (void* handle = dlopen(name, RTLD_NOW | RTLD_LOCAL)) {
return handle;
}
}
return nullptr;
#endif
}
void CloseVulkanLibrary(void* handle) {
#if !defined(_WIN32)
if (handle) {
dlclose(handle);
}
#else
(void)handle;
#endif
}
/* ---- entry point tables --------------------------------------------------------------- */
#define ARMSX_VK_INSTANCE_FUNCS(X) \
X(vkDestroyInstance) \
X(vkEnumeratePhysicalDevices) \
X(vkGetPhysicalDeviceProperties) \
X(vkGetPhysicalDeviceMemoryProperties) \
X(vkGetPhysicalDeviceQueueFamilyProperties) \
X(vkGetPhysicalDeviceFormatProperties) \
X(vkGetPhysicalDeviceSurfaceSupportKHR) \
X(vkGetPhysicalDeviceSurfaceCapabilitiesKHR) \
X(vkGetPhysicalDeviceSurfaceFormatsKHR) \
X(vkGetPhysicalDeviceSurfacePresentModesKHR) \
X(vkDestroySurfaceKHR) \
X(vkCreateDevice) \
X(vkGetDeviceProcAddr)
#define ARMSX_VK_DEVICE_FUNCS(X) \
X(vkDestroyDevice) \
X(vkGetDeviceQueue) \
X(vkDeviceWaitIdle) \
X(vkQueueWaitIdle) \
X(vkQueueSubmit) \
X(vkCreateSwapchainKHR) \
X(vkDestroySwapchainKHR) \
X(vkGetSwapchainImagesKHR) \
X(vkAcquireNextImageKHR) \
X(vkQueuePresentKHR) \
X(vkCreateCommandPool) \
X(vkDestroyCommandPool) \
X(vkAllocateCommandBuffers) \
X(vkBeginCommandBuffer) \
X(vkEndCommandBuffer) \
X(vkResetCommandBuffer) \
X(vkCreateSemaphore) \
X(vkDestroySemaphore) \
X(vkCreateFence) \
X(vkDestroyFence) \
X(vkWaitForFences) \
X(vkResetFences) \
X(vkCreateBuffer) \
X(vkDestroyBuffer) \
X(vkGetBufferMemoryRequirements) \
X(vkAllocateMemory) \
X(vkFreeMemory) \
X(vkBindBufferMemory) \
X(vkMapMemory) \
X(vkUnmapMemory) \
X(vkCreateImage) \
X(vkDestroyImage) \
X(vkGetImageMemoryRequirements) \
X(vkBindImageMemory) \
X(vkCmdPipelineBarrier) \
X(vkCmdCopyBufferToImage) \
X(vkCmdBlitImage) \
X(vkCmdClearColorImage)
struct VkApi {
PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = nullptr;
PFN_vkCreateInstance vkCreateInstance = nullptr;
PFN_vkEnumerateInstanceExtensionProperties vkEnumerateInstanceExtensionProperties = nullptr;
#define ARMSX_VK_DECL(name) PFN_##name name = nullptr;
ARMSX_VK_INSTANCE_FUNCS(ARMSX_VK_DECL)
ARMSX_VK_DEVICE_FUNCS(ARMSX_VK_DECL)
#undef ARMSX_VK_DECL
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
PFN_vkCreateAndroidSurfaceKHR vkCreateAndroidSurfaceKHR = nullptr;
#endif
/* OPTIONAL — deliberately outside ARMSX_VK_INSTANCE_FUNCS, whose loader treats a missing
entry point as fatal. vkGetPhysicalDeviceProperties2 is core in Vulkan 1.1 and an
extension before it, so on a 1.0 instance it is legitimately absent and must not stop
the renderer coming up. It is how we read VkPhysicalDeviceDriverProperties, i.e. the
driver ID — the only signal precise enough to gate a workaround on (see gpu_profile.h). */
PFN_vkGetPhysicalDeviceProperties2 vkGetPhysicalDeviceProperties2 = nullptr;
};
/* ---- backend state ---------------------------------------------------------------------- */
struct VkRenderer {
armsx_renderer_t base;
/* The apiVersion the instance was actually created with. Gates every core-1.1 entry point:
calling one on a 1.0 instance is undefined and Mesa/Turnip enforces it by dying. */
uint32_t instance_api_version = VK_API_VERSION_1_0;
void* library = nullptr;
VkApi api{};
SDL_Window* window = nullptr;
VkInstance instance = VK_NULL_HANDLE;
VkSurfaceKHR surface = VK_NULL_HANDLE;
/* Window-surface rebuild state — see armsx_render_native_window_generation. Vulkan needs
BOTH the VkSurfaceKHR and the swapchain rebuilt: the surface wraps the ANativeWindow that
Android destroyed on background, and OUT_OF_DATE handling alone cannot rescue a swapchain
whose underlying surface is gone. */
unsigned long window_generation = 0;
long surface_rebuilds = 0;
VkPhysicalDevice physical_device = VK_NULL_HANDLE;
VkPhysicalDeviceMemoryProperties memory_properties{};
VkDevice device = VK_NULL_HANDLE;
uint32_t queue_family = 0;
VkQueue queue = VK_NULL_HANDLE;
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
ANativeWindow* native_window = nullptr;
#endif
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
VkFormat swapchain_format = VK_FORMAT_UNDEFINED;
VkExtent2D swapchain_extent{0, 0};
std::vector<VkImage> swapchain_images;
bool swapchain_valid = false;
bool vsync = true;
/* Bring-up trace: the surface's reported rotation, and a handful of fully-logged
presents so the letterbox can be checked against real numbers from logcat. */
VkSurfaceTransformFlagBitsKHR surface_transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
int present_traces = 0;
/* [video] display_rotation: this backend cannot do it; warn once, never per frame. */
bool rotation_warned = false;
VkCommandPool command_pool = VK_NULL_HANDLE;
VkCommandBuffer command_buffer = VK_NULL_HANDLE;
VkSemaphore acquire_semaphore = VK_NULL_HANDLE;
VkSemaphore release_semaphore = VK_NULL_HANDLE;
VkFence frame_fence = VK_NULL_HANDLE;
/* Staging (host visible) + device-local source image, both RGBA8. */
VkBuffer staging_buffer = VK_NULL_HANDLE;
VkDeviceMemory staging_memory = VK_NULL_HANDLE;
void* staging_mapped = nullptr;
VkDeviceSize staging_size = 0;
VkImage frame_image = VK_NULL_HANDLE;
VkDeviceMemory frame_memory = VK_NULL_HANDLE;
int frame_width = 0;
int frame_height = 0;
bool frame_image_initialized = false;
Uint32 source_format = SDL_PIXELFORMAT_UNKNOWN;
int dirty_first = 0;
int dirty_last = -1;
bool has_frame = false;
std::string driver_name;
};
VkRenderer* Self(armsx_renderer_t* base) {
return static_cast<VkRenderer*>(base->impl);
}
bool Check(VkResult result, const char* what) {
if (result != VK_SUCCESS) {
armsx_render_log("renderer", "Vulkan: %s failed (VkResult %d)", what, (int)result);
return false;
}
return true;
}
uint32_t FindMemoryType(VkRenderer* self, uint32_t type_bits, VkMemoryPropertyFlags wanted) {
for (uint32_t index = 0; index < self->memory_properties.memoryTypeCount; ++index) {
if ((type_bits & (1u << index)) == 0) {
continue;
}
if ((self->memory_properties.memoryTypes[index].propertyFlags & wanted) == wanted) {
return index;
}
}
return UINT32_MAX;
}
/* ---- CPU format conversion --------------------------------------------------------------- */
/* A1B5G5R5 (bit 15 = mask, 14:10 = B, 9:5 = G, 4:0 = R) -> RGBA8. */
void Convert555Rows(const uint8_t* source, int pitch, int width, int first_row, int rows,
uint8_t* dst, size_t dst_pitch) {
for (int row = 0; row < rows; ++row) {
const auto* src = reinterpret_cast<const uint16_t*>(source + (static_cast<size_t>(first_row + row) *
static_cast<size_t>(pitch)));
uint8_t* out = dst + (static_cast<size_t>(row) * dst_pitch);
for (int x = 0; x < width; ++x) {
const uint16_t value = src[x];
const uint32_t r = value & 0x1Fu;
const uint32_t g = (value >> 5) & 0x1Fu;
const uint32_t b = (value >> 10) & 0x1Fu;
/* 5 -> 8 bits with the standard replicate-high-bits expansion. */
out[0] = static_cast<uint8_t>((r << 3) | (r >> 2));
out[1] = static_cast<uint8_t>((g << 3) | (g >> 2));
out[2] = static_cast<uint8_t>((b << 3) | (b >> 2));
out[3] = 0xFF;
out += 4;
}
}
}
void ConvertRgb24Rows(const uint8_t* source, int pitch, int width, int first_row, int rows,
uint8_t* dst, size_t dst_pitch) {
for (int row = 0; row < rows; ++row) {
const uint8_t* src = source + (static_cast<size_t>(first_row + row) * static_cast<size_t>(pitch));
uint8_t* out = dst + (static_cast<size_t>(row) * dst_pitch);
for (int x = 0; x < width; ++x) {
out[0] = src[0];
out[1] = src[1];
out[2] = src[2];
out[3] = 0xFF;
src += 3;
out += 4;
}
}
}
/* ---- setup ------------------------------------------------------------------------------- */
bool LoadLoaderFunctions(VkRenderer* self) {
#if defined(_WIN32)
return false;
#else
auto get = [&](const char* name) { return dlsym(self->library, name); };
self->api.vkGetInstanceProcAddr = reinterpret_cast<PFN_vkGetInstanceProcAddr>(get("vkGetInstanceProcAddr"));
if (!self->api.vkGetInstanceProcAddr) {
armsx_render_log("renderer", "Vulkan: vkGetInstanceProcAddr not found in the loader.");
return false;
}
self->api.vkCreateInstance = reinterpret_cast<PFN_vkCreateInstance>(
self->api.vkGetInstanceProcAddr(VK_NULL_HANDLE, "vkCreateInstance"));
self->api.vkEnumerateInstanceExtensionProperties = reinterpret_cast<PFN_vkEnumerateInstanceExtensionProperties>(
self->api.vkGetInstanceProcAddr(VK_NULL_HANDLE, "vkEnumerateInstanceExtensionProperties"));
return self->api.vkCreateInstance != nullptr;
#endif
}
bool CreateInstance(VkRenderer* self) {
std::vector<const char*> extensions;
extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME);
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
const bool use_android_surface = armsx_render_native_window() != nullptr;
if (use_android_surface) {
extensions.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
}
#else
const bool use_android_surface = false;
#endif
if (!use_android_surface) {
if (!self->window) {
armsx_render_log("renderer", "Vulkan: no window and no native surface to present to.");
return false;
}
unsigned int count = 0;
if (!SDL_Vulkan_GetInstanceExtensions(self->window, &count, nullptr)) {
armsx_render_log("renderer", "Vulkan: SDL_Vulkan_GetInstanceExtensions failed: %s", SDL_GetError());
return false;
}
std::vector<const char*> sdl_extensions(count);
if (count && !SDL_Vulkan_GetInstanceExtensions(self->window, &count, sdl_extensions.data())) {
armsx_render_log("renderer", "Vulkan: SDL_Vulkan_GetInstanceExtensions failed: %s", SDL_GetError());
return false;
}
extensions.assign(sdl_extensions.begin(), sdl_extensions.end());
}
VkApplicationInfo app{};
app.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
app.pApplicationName = "ARMSX";
app.applicationVersion = 1;
app.pEngineName = "ARMSX";
app.engineVersion = 1;
/*
1.1 when the loader supports it, 1.0 otherwise.
vkGetPhysicalDeviceProperties2 — which is how the driver ID is read — is CORE 1.1.
Calling it on a 1.0 instance is undefined, and the two drivers disagree about what
undefined means: the Qualcomm blob returns something and carries on, while Mesa/Turnip
takes the process down with it. That is the whole "custom drivers crash the app" bug —
the driver loaded correctly and then this call killed it.
vkEnumerateInstanceVersion is itself 1.1-only, so its absence IS the 1.0 answer.
*/
uint32_t loader_version = VK_API_VERSION_1_0;
if (auto enumerate_version = reinterpret_cast<PFN_vkEnumerateInstanceVersion>(
self->api.vkGetInstanceProcAddr(nullptr, "vkEnumerateInstanceVersion"))) {
if (enumerate_version(&loader_version) != VK_SUCCESS) {
loader_version = VK_API_VERSION_1_0;
}
}
self->instance_api_version =
(loader_version >= VK_API_VERSION_1_1) ? VK_API_VERSION_1_1 : VK_API_VERSION_1_0;
app.apiVersion = self->instance_api_version;
VkInstanceCreateInfo info{};
info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
info.pApplicationInfo = &app;
info.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
info.ppEnabledExtensionNames = extensions.data();
if (!Check(self->api.vkCreateInstance(&info, nullptr, &self->instance), "vkCreateInstance")) {
return false;
}
#define ARMSX_VK_LOAD_INSTANCE(name) \
self->api.name = reinterpret_cast<PFN_##name>(self->api.vkGetInstanceProcAddr(self->instance, #name)); \
if (!self->api.name) { \
armsx_render_log("renderer", "Vulkan: missing instance entry point %s", #name); \
return false; \
}
ARMSX_VK_INSTANCE_FUNCS(ARMSX_VK_LOAD_INSTANCE)
#undef ARMSX_VK_LOAD_INSTANCE
/* Core name first, then the pre-1.1 extension name. Absence is not an error: it only means
we identify the GPU by vendor ID and device name and leave the driver "unknown", which
every gate already treats as the conservative case. */
self->api.vkGetPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
self->api.vkGetInstanceProcAddr(self->instance, "vkGetPhysicalDeviceProperties2"));
if (!self->api.vkGetPhysicalDeviceProperties2) {
self->api.vkGetPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
self->api.vkGetInstanceProcAddr(self->instance, "vkGetPhysicalDeviceProperties2KHR"));
}
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
if (use_android_surface) {
self->api.vkCreateAndroidSurfaceKHR = reinterpret_cast<PFN_vkCreateAndroidSurfaceKHR>(
self->api.vkGetInstanceProcAddr(self->instance, "vkCreateAndroidSurfaceKHR"));
if (!self->api.vkCreateAndroidSurfaceKHR) {
armsx_render_log("renderer", "Vulkan: vkCreateAndroidSurfaceKHR unavailable.");
return false;
}
}
#endif
return true;
}
bool CreateSurface(VkRenderer* self) {
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
if (auto* native = static_cast<ANativeWindow*>(armsx_render_native_window())) {
VkAndroidSurfaceCreateInfoKHR info{};
info.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
info.window = native;
if (!Check(self->api.vkCreateAndroidSurfaceKHR(self->instance, &info, nullptr, &self->surface),
"vkCreateAndroidSurfaceKHR")) {
return false;
}
/* Hold our own reference for the lifetime of the VkSurfaceKHR. */
ANativeWindow_acquire(native);
self->native_window = native;
self->window_generation = armsx_render_native_window_generation();
armsx_render_set_native_window_claimed(true);
armsx_render_log("renderer", "Vulkan: Android surface on ANativeWindow %p (%dx%d)",
static_cast<void*>(native), ANativeWindow_getWidth(native),
ANativeWindow_getHeight(native));
return true;
}
#endif
if (!self->window) {
return false;
}
if (!SDL_Vulkan_CreateSurface(self->window, self->instance, &self->surface)) {
armsx_render_log("renderer", "Vulkan: SDL_Vulkan_CreateSurface failed: %s", SDL_GetError());
return false;
}
return true;
}
bool PickPhysicalDevice(VkRenderer* self) {
uint32_t count = 0;
if (!Check(self->api.vkEnumeratePhysicalDevices(self->instance, &count, nullptr), "vkEnumeratePhysicalDevices") ||
count == 0) {
return false;
}
std::vector<VkPhysicalDevice> devices(count);
if (!Check(self->api.vkEnumeratePhysicalDevices(self->instance, &count, devices.data()),
"vkEnumeratePhysicalDevices")) {
return false;
}
for (VkPhysicalDevice candidate : devices) {
uint32_t family_count = 0;
self->api.vkGetPhysicalDeviceQueueFamilyProperties(candidate, &family_count, nullptr);
std::vector<VkQueueFamilyProperties> families(family_count);
self->api.vkGetPhysicalDeviceQueueFamilyProperties(candidate, &family_count, families.data());
for (uint32_t family = 0; family < family_count; ++family) {
if ((families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) {
continue;
}
VkBool32 present_supported = VK_FALSE;
self->api.vkGetPhysicalDeviceSurfaceSupportKHR(candidate, family, self->surface, &present_supported);
if (present_supported != VK_TRUE) {
continue;
}
self->physical_device = candidate;
self->queue_family = family;
self->api.vkGetPhysicalDeviceMemoryProperties(candidate, &self->memory_properties);
VkPhysicalDeviceProperties properties{};
self->api.vkGetPhysicalDeviceProperties(candidate, &properties);
self->driver_name = properties.deviceName;
armsx_render_log("renderer",
"Vulkan: device=%s api=%u.%u.%u driver_version=0x%08x queue_family=%u",
properties.deviceName, VK_VERSION_MAJOR(properties.apiVersion),
VK_VERSION_MINOR(properties.apiVersion), VK_VERSION_PATCH(properties.apiVersion),
properties.driverVersion, family);
/* Identify the driver, not just the vendor. driverInfo is the human-readable version
("r44p1" on Mali), which is what a bug report can be matched against — the packed
driverVersion above is unreadable and vendor-specific in its encoding. */
VkPhysicalDeviceDriverProperties driver_properties{};
driver_properties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES;
if (self->api.vkGetPhysicalDeviceProperties2 &&
self->instance_api_version >= VK_API_VERSION_1_1) {
VkPhysicalDeviceProperties2 properties2{};
properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
properties2.pNext = &driver_properties;
self->api.vkGetPhysicalDeviceProperties2(candidate, &properties2);
}
armsx_gpu_profile_note_vk(properties.vendorID, driver_properties.driverID,
properties.deviceName, driver_properties.driverName,
driver_properties.driverInfo);
char profile_line[320];
armsx_gpu_profile_describe(profile_line, sizeof(profile_line));
armsx_render_log("renderer", "GPU profile: %s", profile_line);
return true;
}
}
armsx_render_log("renderer", "Vulkan: no physical device with a present-capable graphics queue.");
return false;
}
bool CreateDeviceAndQueue(VkRenderer* self) {
const float priority = 1.0f;
VkDeviceQueueCreateInfo queue_info{};
queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queue_info.queueFamilyIndex = self->queue_family;
queue_info.queueCount = 1;
queue_info.pQueuePriorities = &priority;
const char* device_extensions[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
VkDeviceCreateInfo info{};
info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
info.queueCreateInfoCount = 1;
info.pQueueCreateInfos = &queue_info;
info.enabledExtensionCount = 1;
info.ppEnabledExtensionNames = device_extensions;
if (!Check(self->api.vkCreateDevice(self->physical_device, &info, nullptr, &self->device), "vkCreateDevice")) {
return false;
}
#define ARMSX_VK_LOAD_DEVICE(name) \
self->api.name = reinterpret_cast<PFN_##name>(self->api.vkGetDeviceProcAddr(self->device, #name)); \
if (!self->api.name) { \
armsx_render_log("renderer", "Vulkan: missing device entry point %s", #name); \
return false; \
}
ARMSX_VK_DEVICE_FUNCS(ARMSX_VK_LOAD_DEVICE)
#undef ARMSX_VK_LOAD_DEVICE
self->api.vkGetDeviceQueue(self->device, self->queue_family, 0, &self->queue);
return self->queue != VK_NULL_HANDLE;
}
bool CreateFrameResources(VkRenderer* self) {
VkCommandPoolCreateInfo pool_info{};
pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
pool_info.queueFamilyIndex = self->queue_family;
if (!Check(self->api.vkCreateCommandPool(self->device, &pool_info, nullptr, &self->command_pool),
"vkCreateCommandPool")) {
return false;
}
VkCommandBufferAllocateInfo alloc{};
alloc.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
alloc.commandPool = self->command_pool;
alloc.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
alloc.commandBufferCount = 1;
if (!Check(self->api.vkAllocateCommandBuffers(self->device, &alloc, &self->command_buffer),
"vkAllocateCommandBuffers")) {
return false;
}
VkSemaphoreCreateInfo semaphore_info{};
semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
if (!Check(self->api.vkCreateSemaphore(self->device, &semaphore_info, nullptr, &self->acquire_semaphore),
"vkCreateSemaphore") ||
!Check(self->api.vkCreateSemaphore(self->device, &semaphore_info, nullptr, &self->release_semaphore),
"vkCreateSemaphore")) {
return false;
}
VkFenceCreateInfo fence_info{};
fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT;
return Check(self->api.vkCreateFence(self->device, &fence_info, nullptr, &self->frame_fence), "vkCreateFence");
}
void DestroySwapchain(VkRenderer* self) {
if (self->device && self->swapchain != VK_NULL_HANDLE) {
self->api.vkDestroySwapchainKHR(self->device, self->swapchain, nullptr);
}
self->swapchain = VK_NULL_HANDLE;
self->swapchain_images.clear();
self->swapchain_valid = false;
}
bool CreateSwapchain(VkRenderer* self) {
VkSurfaceCapabilitiesKHR capabilities{};
if (!Check(self->api.vkGetPhysicalDeviceSurfaceCapabilitiesKHR(self->physical_device, self->surface,
&capabilities),
"vkGetPhysicalDeviceSurfaceCapabilitiesKHR")) {
return false;
}
/* The blit target must accept transfer writes and clears. */
const VkImageUsageFlags wanted_usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT;
if ((capabilities.supportedUsageFlags & wanted_usage) != wanted_usage) {
armsx_render_log("renderer",
"Vulkan: the surface does not support TRANSFER_DST swapchain images "
"(usage 0x%08x); this transfer-only present path cannot run here.",
capabilities.supportedUsageFlags);
return false;
}
uint32_t format_count = 0;
self->api.vkGetPhysicalDeviceSurfaceFormatsKHR(self->physical_device, self->surface, &format_count, nullptr);
if (format_count == 0) {
return false;
}
std::vector<VkSurfaceFormatKHR> formats(format_count);
self->api.vkGetPhysicalDeviceSurfaceFormatsKHR(self->physical_device, self->surface, &format_count,
formats.data());
VkSurfaceFormatKHR chosen = formats[0];
for (const VkSurfaceFormatKHR& candidate : formats) {
if (candidate.format == VK_FORMAT_R8G8B8A8_UNORM || candidate.format == VK_FORMAT_B8G8R8A8_UNORM) {
chosen = candidate;
break;
}
}
uint32_t present_mode_count = 0;
self->api.vkGetPhysicalDeviceSurfacePresentModesKHR(self->physical_device, self->surface,
&present_mode_count, nullptr);
std::vector<VkPresentModeKHR> present_modes(present_mode_count);
if (present_mode_count) {
self->api.vkGetPhysicalDeviceSurfacePresentModesKHR(self->physical_device, self->surface,
&present_mode_count, present_modes.data());
}
/* FIFO is always available and is the vsync-on mode. Without vsync prefer IMMEDIATE,
then MAILBOX. */
VkPresentModeKHR present_mode = VK_PRESENT_MODE_FIFO_KHR;
if (!self->vsync) {
for (VkPresentModeKHR candidate : present_modes) {
if (candidate == VK_PRESENT_MODE_IMMEDIATE_KHR) {
present_mode = candidate;
break;
}
if (candidate == VK_PRESENT_MODE_MAILBOX_KHR) {
present_mode = candidate;
}
}
}
/* ---- surface transform ---------------------------------------------------------
A landscape-locked Android app on a portrait-native panel gets
currentTransform = VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR. Passing that straight
through as preTransform (which is what this backend used to do) is a PROMISE to the
presentation engine that the app already rotated its content by 90 degrees, so the
compositor counter-rotates on scan-out. This backend blits an upright frame, so the
result was a picture turned 90 degrees on screen.
We ask for IDENTITY instead. Android's loader always advertises it
(supportedTransforms = 0x1ff on this device) and it makes the swapchain behave
exactly like the software blit bridge: buffers in window space, no buffer transform,
SurfaceFlinger applies the display rotation. Costs one composition pass.
See the imageExtent block below for the other half of that trade. */
VkSurfaceTransformFlagBitsKHR pre_transform = capabilities.currentTransform;
if ((capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) != 0) {
pre_transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
}
self->surface_transform = capabilities.currentTransform;
armsx_render_log("renderer",
"Vulkan: surface caps current_extent=%ux%u min=%ux%u max=%ux%u "
"current_transform=0x%02x supported_transforms=0x%04x pre_transform=0x%02x "
"min_image_count=%u usage=0x%08x",
capabilities.currentExtent.width, capabilities.currentExtent.height,
capabilities.minImageExtent.width, capabilities.minImageExtent.height,
capabilities.maxImageExtent.width, capabilities.maxImageExtent.height,
(unsigned)capabilities.currentTransform, (unsigned)capabilities.supportedTransforms,
(unsigned)pre_transform, capabilities.minImageCount,
capabilities.supportedUsageFlags);
/* ---- imageExtent ----------------------------------------------------------------
preTransform = IDENTITY means the presentation engine maps the swapchain image onto
the Surface with no buffer transform, so the image's pixels ARE the Surface's pixels
and armsx_render_compute_dst() can letterbox straight against them. That equivalence
only holds while the extent we create matches the Surface's own geometry.
currentExtent is not reliable for that on Android. It is whatever the last producer
asked of the ANativeWindow (the host's CPU blit bridge sets a downscaled framebuffer
size with ANativeWindow_setBuffersGeometry before we ever get here), and whether it
is expressed in window space or in pre-transform space is driver-dependent once
currentTransform is ROTATE_90/270. A portrait extent on a landscape Surface is
exactly the reported bug: the letterbox gets computed for a 1080x1920 rect — full
width, bars top and bottom — and SurfaceFlinger then squashes that rect into the
1920x1080 window, so the bars survive the trip and the picture comes out stretched.
So prefer the geometry the host published with the Surface itself, which is in the
app's own (orientation-locked) space; currentExtent stays as the fallback, and as the
retry if the driver refuses our extent. */
VkExtent2D reported = capabilities.currentExtent;
if (reported.width == 0xFFFFFFFFu || reported.height == 0xFFFFFFFFu) {
int w = 0;
int h = 0;
if (self->window) {
SDL_Vulkan_GetDrawableSize(self->window, &w, &h);
}
reported.width = static_cast<uint32_t>(std::max(w, 0));
reported.height = static_cast<uint32_t>(std::max(h, 0));
}
if (reported.width == 0 || reported.height == 0) {
/* Surface is not presentable right now (backgrounded / zero-sized). */
return false;
}
auto clamp_extent = [&capabilities](VkExtent2D value) {
value.width = std::clamp(value.width, capabilities.minImageExtent.width,
capabilities.maxImageExtent.width);
value.height = std::clamp(value.height, capabilities.minImageExtent.height,
capabilities.maxImageExtent.height);
return value;
};
reported = clamp_extent(reported);
/* Candidates, best first. Duplicates are dropped so the common case is one attempt. */
std::vector<VkExtent2D> candidates;
int host_width = 0;
int host_height = 0;
if (armsx_render_native_window_size(&host_width, &host_height)) {
const VkExtent2D host = clamp_extent({static_cast<uint32_t>(host_width),
static_cast<uint32_t>(host_height)});
if (host.width != reported.width || host.height != reported.height) {
armsx_render_log("renderer",
"Vulkan: host Surface is %dx%d but currentExtent is %ux%u; "
"presenting at the host geometry so the letterbox is computed in "
"the same space it is displayed in.",
host_width, host_height, reported.width, reported.height);
}
candidates.push_back(host);
}
if (candidates.empty() || candidates.front().width != reported.width ||
candidates.front().height != reported.height) {
candidates.push_back(reported);
}
uint32_t image_count = capabilities.minImageCount + 1;
if (capabilities.maxImageCount > 0 && image_count > capabilities.maxImageCount) {
image_count = capabilities.maxImageCount;
}
VkSwapchainKHR old_swapchain = self->swapchain;
VkExtent2D extent{0, 0};
VkSwapchainKHR created = VK_NULL_HANDLE;
/* A failed vkCreateSwapchainKHR still retires oldSwapchain, and a retired swapchain may
not be offered as oldSwapchain again — so only the first attempt gets it. */
VkSwapchainKHR reusable = old_swapchain;
for (const VkExtent2D& candidate : candidates) {
if (candidate.width == 0 || candidate.height == 0) {
continue;
}
VkSwapchainCreateInfoKHR info{};
info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
info.surface = self->surface;
info.minImageCount = image_count;
info.imageFormat = chosen.format;
info.imageColorSpace = chosen.colorSpace;
info.imageExtent = candidate;
info.imageArrayLayers = 1;
info.imageUsage = wanted_usage;
info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.preTransform = pre_transform;
info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
info.presentMode = present_mode;
info.clipped = VK_TRUE;
info.oldSwapchain = reusable;
reusable = VK_NULL_HANDLE;
const VkResult result = self->api.vkCreateSwapchainKHR(self->device, &info, nullptr, &created);
if (result == VK_SUCCESS) {
extent = candidate;
break;
}
armsx_render_log("renderer", "Vulkan: vkCreateSwapchainKHR(%ux%u) failed (VkResult %d)",
candidate.width, candidate.height, (int)result);
created = VK_NULL_HANDLE;
}
if (created == VK_NULL_HANDLE) {
/* Surface is not presentable right now (backgrounded / zero-sized), or the driver
refused every geometry we could offer it. Drop the old swapchain rather than leave
a retired handle behind for the next attempt to offer back — after the in-flight
frame that may still reference its images has retired. */
if (old_swapchain != VK_NULL_HANDLE && self->frame_fence != VK_NULL_HANDLE) {
self->api.vkWaitForFences(self->device, 1, &self->frame_fence, VK_TRUE, UINT64_MAX);
}
DestroySwapchain(self);
return false;
}
if (old_swapchain != VK_NULL_HANDLE) {
self->api.vkDestroySwapchainKHR(self->device, old_swapchain, nullptr);
}
self->swapchain = created;
self->swapchain_format = chosen.format;
self->swapchain_extent = extent;
uint32_t actual = 0;
self->api.vkGetSwapchainImagesKHR(self->device, self->swapchain, &actual, nullptr);
self->swapchain_images.resize(actual);
self->api.vkGetSwapchainImagesKHR(self->device, self->swapchain, &actual, self->swapchain_images.data());
self->swapchain_valid = true;
self->present_traces = 0;
armsx_render_log("renderer",
"Vulkan: swapchain %ux%u format=%d images=%u present_mode=%d "
"pre_transform=0x%02x current_transform=0x%02x",
extent.width, extent.height, (int)chosen.format, actual, (int)present_mode,
(unsigned)pre_transform, (unsigned)capabilities.currentTransform);
return true;
}
void DestroyStaging(VkRenderer* self) {
if (self->staging_mapped) {
self->api.vkUnmapMemory(self->device, self->staging_memory);
self->staging_mapped = nullptr;
}
if (self->staging_buffer != VK_NULL_HANDLE) {
self->api.vkDestroyBuffer(self->device, self->staging_buffer, nullptr);
self->staging_buffer = VK_NULL_HANDLE;
}
if (self->staging_memory != VK_NULL_HANDLE) {
self->api.vkFreeMemory(self->device, self->staging_memory, nullptr);
self->staging_memory = VK_NULL_HANDLE;
}
self->staging_size = 0;
}
void DestroyFrameImage(VkRenderer* self) {
if (self->frame_image != VK_NULL_HANDLE) {
self->api.vkDestroyImage(self->device, self->frame_image, nullptr);
self->frame_image = VK_NULL_HANDLE;
}
if (self->frame_memory != VK_NULL_HANDLE) {
self->api.vkFreeMemory(self->device, self->frame_memory, nullptr);
self->frame_memory = VK_NULL_HANDLE;
}
self->frame_width = 0;
self->frame_height = 0;
self->frame_image_initialized = false;
}
bool EnsureStaging(VkRenderer* self, VkDeviceSize size) {
if (self->staging_size >= size && self->staging_buffer != VK_NULL_HANDLE) {
return true;
}
DestroyStaging(self);
VkBufferCreateInfo info{};
info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
info.size = size;
info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (!Check(self->api.vkCreateBuffer(self->device, &info, nullptr, &self->staging_buffer), "vkCreateBuffer")) {
return false;
}
VkMemoryRequirements requirements{};
self->api.vkGetBufferMemoryRequirements(self->device, self->staging_buffer, &requirements);
const uint32_t type_index = FindMemoryType(self, requirements.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
if (type_index == UINT32_MAX) {
armsx_render_log("renderer", "Vulkan: no host-visible coherent memory type for the staging buffer.");
return false;
}
VkMemoryAllocateInfo alloc{};
alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
alloc.allocationSize = requirements.size;
alloc.memoryTypeIndex = type_index;
if (!Check(self->api.vkAllocateMemory(self->device, &alloc, nullptr, &self->staging_memory), "vkAllocateMemory") ||
!Check(self->api.vkBindBufferMemory(self->device, self->staging_buffer, self->staging_memory, 0),
"vkBindBufferMemory") ||
!Check(self->api.vkMapMemory(self->device, self->staging_memory, 0, VK_WHOLE_SIZE, 0, &self->staging_mapped),
"vkMapMemory")) {
return false;
}
self->staging_size = size;
return true;
}
bool EnsureFrameImage(VkRenderer* self, int width, int height) {
if (self->frame_image != VK_NULL_HANDLE && self->frame_width == width && self->frame_height == height) {
return true;
}
self->api.vkDeviceWaitIdle(self->device);
DestroyFrameImage(self);
VkImageCreateInfo info{};
info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
info.imageType = VK_IMAGE_TYPE_2D;
info.format = VK_FORMAT_R8G8B8A8_UNORM;
info.extent = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
info.mipLevels = 1;
info.arrayLayers = 1;
info.samples = VK_SAMPLE_COUNT_1_BIT;
info.tiling = VK_IMAGE_TILING_OPTIMAL;
/* SAMPLED is for the shader chain (frontend/render_shaders.cpp), which samples this
image directly at internal_scale 1 rather than copying it first. Vulkan's mandatory
format table guarantees optimal-tiling SAMPLED_IMAGE for R8G8B8A8_UNORM on every
implementation, so this cannot narrow device support, and an unused usage bit costs
nothing when no chain is active. */
info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
VK_IMAGE_USAGE_SAMPLED_BIT;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if (!Check(self->api.vkCreateImage(self->device, &info, nullptr, &self->frame_image), "vkCreateImage")) {
return false;
}
VkMemoryRequirements requirements{};
self->api.vkGetImageMemoryRequirements(self->device, self->frame_image, &requirements);
const uint32_t type_index = FindMemoryType(self, requirements.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (type_index == UINT32_MAX) {
armsx_render_log("renderer", "Vulkan: no device-local memory type for the frame image.");
return false;
}
VkMemoryAllocateInfo alloc{};
alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
alloc.allocationSize = requirements.size;
alloc.memoryTypeIndex = type_index;
if (!Check(self->api.vkAllocateMemory(self->device, &alloc, nullptr, &self->frame_memory), "vkAllocateMemory") ||
!Check(self->api.vkBindImageMemory(self->device, self->frame_image, self->frame_memory, 0),
"vkBindImageMemory")) {
return false;
}
self->frame_width = width;
self->frame_height = height;
self->frame_image_initialized = false;
return true;
}
void ImageBarrier(VkRenderer* self, VkCommandBuffer cmd, VkImage image, VkImageLayout old_layout,
VkImageLayout new_layout, VkAccessFlags src_access, VkAccessFlags dst_access,
VkPipelineStageFlags src_stage, VkPipelineStageFlags dst_stage) {
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = old_layout;
barrier.newLayout = new_layout;
barrier.srcAccessMask = src_access;
barrier.dstAccessMask = dst_access;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
self->api.vkCmdPipelineBarrier(cmd, src_stage, dst_stage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
}
/* ---- ops --------------------------------------------------------------------------------- */
const char* OpDriverName(armsx_renderer_t* base) {
return Self(base)->driver_name.c_str();
}
bool OpIsAccelerated(armsx_renderer_t*) {
return true;
}
SDL_Renderer* OpSdlHandle(armsx_renderer_t*) {
return nullptr;
}
void OpResize(armsx_renderer_t* base, int, int) {
Self(base)->swapchain_valid = false;
}
void OpOutputSize(armsx_renderer_t* base, int* width, int* height) {
VkRenderer* self = Self(base);
if (width) {
*width = static_cast<int>(self->swapchain_extent.width);
}
if (height) {
*height = static_cast<int>(self->swapchain_extent.height);
}
}
void OpSetVsync(armsx_renderer_t* base, bool enabled) {
VkRenderer* self = Self(base);
if (self->vsync == enabled) {
return;
}
self->vsync = enabled;
self->swapchain_valid = false; /* present mode is baked into the swapchain */
}
bool OpUploadFrame(armsx_renderer_t* base,
const void* pixels,
int width,
int height,
int pitch,
Uint32 sdl_format,
int dirty_first_row,
int dirty_last_row) {
VkRenderer* self = Self(base);
if (!pixels || width <= 0 || height <= 0 || pitch <= 0) {
return false;
}
if (sdl_format != SDL_PIXELFORMAT_BGR555 && sdl_format != SDL_PIXELFORMAT_RGB24) {
armsx_render_log("renderer", "Vulkan backend cannot upload %s", SDL_GetPixelFormatName(sdl_format));
return false;
}
if (!EnsureFrameImage(self, width, height)) {
return false;
}
const size_t dst_pitch = static_cast<size_t>(width) * 4u;
if (!EnsureStaging(self, static_cast<VkDeviceSize>(dst_pitch) * static_cast<VkDeviceSize>(height))) {
return false;
}
if (!self->frame_image_initialized) {
/* Undefined image contents: the whole thing has to be written this frame. */
dirty_first_row = 0;
dirty_last_row = height - 1;
}
if (dirty_last_row < dirty_first_row) {
self->dirty_last = -1;
self->source_format = sdl_format;
return true;
}
dirty_first_row = std::max(dirty_first_row, 0);
dirty_last_row = std::min(dirty_last_row, height - 1);
const int rows = dirty_last_row - dirty_first_row + 1;
auto* staging = static_cast<uint8_t*>(self->staging_mapped) +
(static_cast<size_t>(dirty_first_row) * dst_pitch);
const auto* source = static_cast<const uint8_t*>(pixels);
if (sdl_format == SDL_PIXELFORMAT_BGR555) {
Convert555Rows(source, pitch, width, dirty_first_row, rows, staging, dst_pitch);
} else {
ConvertRgb24Rows(source, pitch, width, dirty_first_row, rows, staging, dst_pitch);
}
self->source_format = sdl_format;
self->dirty_first = dirty_first_row;
self->dirty_last = dirty_last_row;
self->has_frame = true;
return true;
}
bool BeginFrame(VkRenderer* self, uint32_t* image_index) {
/* A new ANativeWindow means our VkSurfaceKHR wraps a destroyed object. Tear down the
swapchain FIRST (it references the surface), then the surface, then rebuild both. The
device, pipelines and all resident images survive, so a resume costs one surface plus
one swapchain — not a renderer restart. */
{
const unsigned long generation = armsx_render_native_window_generation();
if (generation != self->window_generation) {
void* const window = armsx_render_native_window();
self->api.vkDeviceWaitIdle(self->device);
DestroySwapchain(self);
self->swapchain_valid = false;
if (self->surface != VK_NULL_HANDLE) {
self->api.vkDestroySurfaceKHR(self->instance, self->surface, nullptr);
self->surface = VK_NULL_HANDLE;
}
/* Our own reference goes with the surface it was taken for. Dropped only AFTER
vkDestroySurfaceKHR, and cleared here rather than reassigned so the early return
below cannot leave OpShutdown holding a window this renderer no longer uses. */
if (self->native_window) {
ANativeWindow_release(self->native_window);
self->native_window = nullptr;
}
/* Backgrounded: no window to build on. Retry on a later frame and deliberately do
NOT consume the generation, so the eventual resume still triggers a rebuild. */
if (!window) {
return false;
}
VkAndroidSurfaceCreateInfoKHR info{};
info.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
info.window = static_cast<ANativeWindow*>(window);
if (!Check(self->api.vkCreateAndroidSurfaceKHR(self->instance, &info, nullptr,
&self->surface),
"vkCreateAndroidSurfaceKHR (rebuild)")) {
self->surface = VK_NULL_HANDLE;
return false;
}
/* Same contract as CreateSurface(): hold a reference for the lifetime of the
VkSurfaceKHR, and re-raise the claim so the host's CPU blit bridge stays parked.
(It is the backend's flag to hold; nothing else re-raises it after a resume.) */
ANativeWindow_acquire(static_cast<ANativeWindow*>(window));
self->native_window = static_cast<ANativeWindow*>(window);
armsx_render_set_native_window_claimed(true);
self->window_generation = generation;
armsx_render_log("renderer", "Vulkan: surface rebuilt after resume (#%ld) window=%p",
++self->surface_rebuilds, window);
}
}
if (!self->swapchain_valid && !CreateSwapchain(self)) {
return false;
}
self->api.vkWaitForFences(self->device, 1, &self->frame_fence, VK_TRUE, UINT64_MAX);
VkResult result = self->api.vkAcquireNextImageKHR(self->device, self->swapchain, UINT64_MAX,
self->acquire_semaphore, VK_NULL_HANDLE, image_index);
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
self->swapchain_valid = false;
if (!CreateSwapchain(self)) {
return false;
}
result = self->api.vkAcquireNextImageKHR(self->device, self->swapchain, UINT64_MAX,
self->acquire_semaphore, VK_NULL_HANDLE, image_index);
}
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
armsx_render_log("renderer", "Vulkan: vkAcquireNextImageKHR failed (VkResult %d)", (int)result);
self->swapchain_valid = false;
return false;
}
self->api.vkResetFences(self->device, 1, &self->frame_fence);
return true;
}
void OpPresent(armsx_renderer_t* base, const armsx_render_frame_params_t* params) {
VkRenderer* self = Self(base);
if (!self->has_frame || self->frame_image == VK_NULL_HANDLE) {
return;
}
if (!self->frame_image_initialized && self->dirty_last < self->dirty_first) {
/* Nothing has ever been written into the frame image; blitting from an UNDEFINED
layout would be undefined behaviour. */
return;
}
uint32_t image_index = 0;
if (!BeginFrame(self, &image_index) || image_index >= self->swapchain_images.size()) {
return;
}
VkCommandBuffer cmd = self->command_buffer;
self->api.vkResetCommandBuffer(cmd, 0);
VkCommandBufferBeginInfo begin{};
begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if (!Check(self->api.vkBeginCommandBuffer(cmd, &begin), "vkBeginCommandBuffer")) {
return;
}
/* 1. Staging -> frame image (dirty rows only). */
const bool needs_copy = self->dirty_last >= self->dirty_first;
if (needs_copy) {
ImageBarrier(self, cmd, self->frame_image,
self->frame_image_initialized ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
: VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
self->frame_image_initialized ? VK_ACCESS_TRANSFER_READ_BIT : 0,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
const int rows = self->dirty_last - self->dirty_first + 1;
VkBufferImageCopy copy{};
copy.bufferOffset = static_cast<VkDeviceSize>(self->dirty_first) *
static_cast<VkDeviceSize>(self->frame_width) * 4u;
copy.bufferRowLength = static_cast<uint32_t>(self->frame_width);
copy.bufferImageHeight = static_cast<uint32_t>(rows);
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageOffset = {0, self->dirty_first, 0};
copy.imageExtent = {static_cast<uint32_t>(self->frame_width), static_cast<uint32_t>(rows), 1};
self->api.vkCmdCopyBufferToImage(cmd, self->staging_buffer, self->frame_image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy);
ImageBarrier(self, cmd, self->frame_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT);
self->frame_image_initialized = true;
}
VkImage target = self->swapchain_images[image_index];
/* 2. Swapchain image -> TRANSFER_DST, clear the letterbox bars. */
ImageBarrier(self, cmd, target, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT);
VkClearColorValue clear{};
clear.float32[0] = 0.0f;
clear.float32[1] = 0.0f;
clear.float32[2] = 0.0f;
clear.float32[3] = 1.0f;
VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
self->api.vkCmdClearColorImage(cmd, target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &range);
/* 3. Scale-blit into the letterboxed destination rect. */
SDL_Rect dst{0, 0, 0, 0};
armsx_render_compute_dst(static_cast<int>(self->swapchain_extent.width),
static_cast<int>(self->swapchain_extent.height), self->frame_width,
self->frame_height, params, &dst);
if (self->present_traces < 3) {
self->present_traces++;
int host_width = 0;
int host_height = 0;
const bool have_host = armsx_render_native_window_size(&host_width, &host_height);
armsx_render_log("renderer",
"Vulkan present #%d surface=%dx%d swapchain=%ux%u current_transform=0x%02x "
"source=%dx%d aspect=%.4f stretch=%s dst=%d,%d %dx%d",
self->present_traces, have_host ? host_width : -1,
have_host ? host_height : -1, self->swapchain_extent.width,
self->swapchain_extent.height, (unsigned)self->surface_transform,
self->frame_width, self->frame_height, params ? params->aspect : 0.0f,
(params && params->stretch) ? "true" : "false", dst.x, dst.y, dst.w, dst.h);
}
if (dst.w > 0 && dst.h > 0) {
/* A RetroArch (.slangp) chain gets first refusal. It returns false whenever there
is no chain, the preset failed to compile, or librashader is unavailable — and
then the ordinary blit below runs, so a broken preset degrades to normal
presentation rather than a black screen. It is layout-transparent: both images
come back in the layouts they went in, so the barrier to PRESENT_SRC_KHR after
this block is correct either way. */
armsx_shader_vk_frame_t chain_frame{};
chain_frame.cmd = cmd;
chain_frame.source = self->frame_image;
chain_frame.source_format = VK_FORMAT_R8G8B8A8_UNORM;
chain_frame.source_width = self->frame_width;
chain_frame.source_height = self->frame_height;
chain_frame.target = target;
chain_frame.target_width = static_cast<int>(self->swapchain_extent.width);
chain_frame.target_height = static_cast<int>(self->swapchain_extent.height);
chain_frame.dst_x = dst.x;
chain_frame.dst_y = dst.y;
chain_frame.dst_w = dst.w;
chain_frame.dst_h = dst.h;
if (!armsx_shader_vk_render(&chain_frame)) {
/* [video] overscan_crop maps straight onto srcOffsets. [video] display_rotation
does NOT: vkCmdBlitImage cannot rotate and this backend deliberately hands the
compositor an upright frame (see the preTransform note above). Say so once and
present upright rather than present something wrong — the OpenGL and SDL
backends both do rotate. */
int sx0 = 0, sy0 = 0, sx1 = self->frame_width, sy1 = self->frame_height;
if (params && params->crop_w > 0 && params->crop_h > 0) {
sx0 = params->crop_x < 0 ? 0 : params->crop_x;
sy0 = params->crop_y < 0 ? 0 : params->crop_y;
if (sx0 > self->frame_width - 1) sx0 = self->frame_width - 1;
if (sy0 > self->frame_height - 1) sy0 = self->frame_height - 1;
sx1 = sx0 + params->crop_w;
sy1 = sy0 + params->crop_h;
if (sx1 > self->frame_width) sx1 = self->frame_width;
if (sy1 > self->frame_height) sy1 = self->frame_height;
}
if (params && (params->rotation & 3) && !self->rotation_warned) {
self->rotation_warned = true;
armsx_render_log("renderer",
"Vulkan present: display rotation is not supported by the "
"blit path; presenting upright. Use the OpenGL backend.");
}
VkImageBlit blit{};
blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
blit.srcOffsets[0] = {sx0, sy0, 0};
blit.srcOffsets[1] = {sx1, sy1, 1};
blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
blit.dstOffsets[0] = {dst.x, dst.y, 0};
blit.dstOffsets[1] = {dst.x + dst.w, dst.y + dst.h, 1};
self->api.vkCmdBlitImage(cmd, self->frame_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, target,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit,
(params && params->linear_filter) ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
}
}
ImageBarrier(self, cmd, target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_ACCESS_TRANSFER_WRITE_BIT, 0, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
if (!Check(self->api.vkEndCommandBuffer(cmd), "vkEndCommandBuffer")) {
return;
}
const VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
VkSubmitInfo submit{};
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit.waitSemaphoreCount = 1;
submit.pWaitSemaphores = &self->acquire_semaphore;
submit.pWaitDstStageMask = &wait_stage;
submit.commandBufferCount = 1;
submit.pCommandBuffers = &cmd;
submit.signalSemaphoreCount = 1;
submit.pSignalSemaphores = &self->release_semaphore;
bool submitted;
{
/* The shader chain is built on a worker thread which uploads its LUTs through THIS
queue. A VkQueue may not be used from two threads at once. Uncontended when nothing
is compiling, which is almost always. */
ShaderQueueGuard guard;
submitted = Check(self->api.vkQueueSubmit(self->queue, 1, &submit, self->frame_fence), "vkQueueSubmit");
}
if (!submitted) {
return;
}
VkPresentInfoKHR present{};
present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
present.waitSemaphoreCount = 1;
present.pWaitSemaphores = &self->release_semaphore;
present.swapchainCount = 1;
present.pSwapchains = &self->swapchain;
present.pImageIndices = &image_index;
/* VK_SUBOPTIMAL_KHR is the PERMANENT steady state here, not a transient one: asking for
an IDENTITY preTransform on a surface whose currentTransform is ROTATE_90 is exactly
what "suboptimal" means, and it is the trade we chose above. Recreating the swapchain
on it rebuilds every single frame and never converges (that thrash is itself a black
screen). Only OUT_OF_DATE — a genuinely stale surface — forces a rebuild. */
VkResult result;
{
ShaderQueueGuard guard;
result = self->api.vkQueuePresentKHR(self->queue, &present);
}
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
self->swapchain_valid = false;
} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
armsx_render_log("renderer", "Vulkan: vkQueuePresentKHR failed (VkResult %d)", (int)result);
self->swapchain_valid = false;
}
/* The dirty span has been consumed. */
self->dirty_last = -1;
self->dirty_first = 0;
}
void OpPresentBlank(armsx_renderer_t* base) {
VkRenderer* self = Self(base);
uint32_t image_index = 0;
if (!BeginFrame(self, &image_index) || image_index >= self->swapchain_images.size()) {
return;
}
VkCommandBuffer cmd = self->command_buffer;
self->api.vkResetCommandBuffer(cmd, 0);
VkCommandBufferBeginInfo begin{};
begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if (!Check(self->api.vkBeginCommandBuffer(cmd, &begin), "vkBeginCommandBuffer")) {
return;
}
VkImage target = self->swapchain_images[image_index];
ImageBarrier(self, cmd, target, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT);
VkClearColorValue clear{};
clear.float32[3] = 1.0f;
VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
self->api.vkCmdClearColorImage(cmd, target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &range);
ImageBarrier(self, cmd, target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_ACCESS_TRANSFER_WRITE_BIT, 0, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
if (!Check(self->api.vkEndCommandBuffer(cmd), "vkEndCommandBuffer")) {
return;
}
const VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
VkSubmitInfo submit{};
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit.waitSemaphoreCount = 1;
submit.pWaitSemaphores = &self->acquire_semaphore;
submit.pWaitDstStageMask = &wait_stage;
submit.commandBufferCount = 1;
submit.pCommandBuffers = &cmd;
submit.signalSemaphoreCount = 1;
submit.pSignalSemaphores = &self->release_semaphore;
bool submitted;
{
/* The shader chain is built on a worker thread which uploads its LUTs through THIS
queue. A VkQueue may not be used from two threads at once. Uncontended when nothing
is compiling, which is almost always. */
ShaderQueueGuard guard;
submitted = Check(self->api.vkQueueSubmit(self->queue, 1, &submit, self->frame_fence), "vkQueueSubmit");
}
if (!submitted) {
return;
}
VkPresentInfoKHR present{};
present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
present.waitSemaphoreCount = 1;
present.pWaitSemaphores = &self->release_semaphore;
present.swapchainCount = 1;
present.pSwapchains = &self->swapchain;
present.pImageIndices = &image_index;
VkResult result;
{
ShaderQueueGuard guard;
result = self->api.vkQueuePresentKHR(self->queue, &present);
}
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
self->swapchain_valid = false;
}
}
void OpShutdown(armsx_renderer_t* base) {
VkRenderer* self = Self(base);
/* BEFORE vkDestroyDevice, and before anything else is torn down: the shader chain owns
images, views, framebuffers, pipelines and descriptor pools created against this
VkDevice. Safe to call unconditionally — it is a no-op when nothing ever attached,
which includes every failure path out of armsx_render_create_vk(). */
armsx_shader_vk_detach();
if (self->device != VK_NULL_HANDLE && self->api.vkDeviceWaitIdle) {
self->api.vkDeviceWaitIdle(self->device);
DestroyStaging(self);
DestroyFrameImage(self);
DestroySwapchain(self);
if (self->frame_fence != VK_NULL_HANDLE) {
self->api.vkDestroyFence(self->device, self->frame_fence, nullptr);
}
if (self->acquire_semaphore != VK_NULL_HANDLE) {
self->api.vkDestroySemaphore(self->device, self->acquire_semaphore, nullptr);
}
if (self->release_semaphore != VK_NULL_HANDLE) {
self->api.vkDestroySemaphore(self->device, self->release_semaphore, nullptr);
}
if (self->command_pool != VK_NULL_HANDLE) {
self->api.vkDestroyCommandPool(self->device, self->command_pool, nullptr);
}
self->api.vkDestroyDevice(self->device, nullptr);
}
if (self->instance != VK_NULL_HANDLE) {
if (self->surface != VK_NULL_HANDLE && self->api.vkDestroySurfaceKHR) {
self->api.vkDestroySurfaceKHR(self->instance, self->surface, nullptr);
}
if (self->api.vkDestroyInstance) {
self->api.vkDestroyInstance(self->instance, nullptr);
}
}
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
if (self->native_window) {
ANativeWindow_release(self->native_window);
self->native_window = nullptr;
}
#endif
armsx_render_set_native_window_claimed(false);
armsx_render_set_active_name("");
CloseVulkanLibrary(self->library);
delete self;
}
const armsx_render_ops_t kVkOps = {
OpDriverName,
OpIsAccelerated,
OpSdlHandle,
OpResize,
OpOutputSize,
OpSetVsync,
OpUploadFrame,
OpPresent,
OpPresentBlank,
OpShutdown,
};
} // namespace
extern "C" {
armsx_renderer_t* armsx_render_create_vk(SDL_Window* window, const armsx_render_config_t* config) {
auto* self = new VkRenderer();
self->base.ops = &kVkOps;
self->base.impl = self;
self->base.backend = ARMSX_RENDER_BACKEND_VULKAN;
self->window = window;
self->vsync = config ? config->vsync : true;
self->driver_name = "Vulkan";
self->library = OpenVulkanLibrary();
if (!self->library) {
armsx_render_log("renderer", "Vulkan: no loader could be opened.");
delete self;
return nullptr;
}
if (!LoadLoaderFunctions(self) || !CreateInstance(self) || !CreateSurface(self) ||
!PickPhysicalDevice(self) || !CreateDeviceAndQueue(self) || !CreateFrameResources(self) ||
!CreateSwapchain(self)) {
OpShutdown(&self->base);
return nullptr;
}
/* Hand the shader layer the live device. Everything it needs is explicit here because
VkRenderer is namespace-private to this file; nothing about librashader leaks in.
vkGetInstanceProcAddr rather than a linked entry point is what keeps an adrenotools
custom driver substitutable all the way through the chain. */
armsx_shader_vk_device_t shader_device{};
shader_device.get_instance_proc_addr = self->api.vkGetInstanceProcAddr;
shader_device.instance = self->instance;
shader_device.physical_device = self->physical_device;
shader_device.device = self->device;
shader_device.queue = self->queue;
shader_device.queue_family = self->queue_family;
armsx_shader_vk_attach(&shader_device);
/* The name the OSD and the driver manager show. "custom driver" only ever appears when a
caller-supplied loader handle is what we are genuinely running on. */
const std::string name = "Vulkan (" + self->driver_name +
(g_custom_loader_active ? ", custom driver)" : ")");
armsx_render_set_active_name(name.c_str());
armsx_render_log("renderer", "Vulkan backend created: %s", name.c_str());
return &self->base;
}
void armsx_render_set_vulkan_loader(armsx_vk_library_open_fn open_fn, void* user) {
std::lock_guard<std::mutex> lock(g_loader_lock);
g_loader_fn = open_fn;
g_loader_user = user;
}
} // extern "C"
#else /* !ARMSX_ENABLE_VULKAN */
extern "C" {
armsx_renderer_t* armsx_render_create_vk(SDL_Window*, const armsx_render_config_t*) {
return nullptr;
}
void armsx_render_set_vulkan_loader(armsx_vk_library_open_fn, void*) {}
} // extern "C"
#endif