mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2026-03-28 22:49:52 -06:00
874 lines
32 KiB
C++
874 lines
32 KiB
C++
// Copyright Citra Emulator Project / Azahar Emulator Project
|
|
// Licensed under GPLv2 or any later version
|
|
// Refer to the license.txt file included.
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
#include <vector>
|
|
#include <boost/container/static_vector.hpp>
|
|
#include <fmt/format.h>
|
|
|
|
#include "citra_libretro/environment.h"
|
|
#include "citra_libretro/libretro_vk.h"
|
|
#include "common/assert.h"
|
|
#include "common/logging/log.h"
|
|
#include "common/settings.h"
|
|
#include "core/frontend/emu_window.h"
|
|
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
|
|
|
#include <vk_mem_alloc.h>
|
|
|
|
static const struct retro_hw_render_interface_vulkan* vulkan_intf;
|
|
|
|
namespace LibRetro {
|
|
|
|
const VkApplicationInfo* GetVulkanApplicationInfo() {
|
|
static VkApplicationInfo app_info{VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
|
app_info.pApplicationName = "Azahar";
|
|
app_info.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
|
|
app_info.pEngineName = "Azahar";
|
|
app_info.engineVersion = VK_MAKE_VERSION(1, 0, 0);
|
|
// Request Vulkan 1.1 for better compatibility (especially on Android)
|
|
// Extensions can be used for features beyond 1.1
|
|
app_info.apiVersion = VK_API_VERSION_1_1;
|
|
return &app_info;
|
|
}
|
|
|
|
void AddExtensionIfAvailable(std::vector<const char*>& enabled_exts,
|
|
const std::vector<VkExtensionProperties>& available_exts,
|
|
const char* ext_name) {
|
|
// Check if already in the list
|
|
for (const char* ext : enabled_exts) {
|
|
if (ext && !strcmp(ext, ext_name)) {
|
|
return; // Already enabled
|
|
}
|
|
}
|
|
|
|
// Check if available
|
|
for (const auto& ext : available_exts) {
|
|
if (!strcmp(ext.extensionName, ext_name)) {
|
|
enabled_exts.push_back(ext_name);
|
|
LOG_INFO(Render_Vulkan, "Enabling Vulkan extension: {}", ext_name);
|
|
return;
|
|
}
|
|
}
|
|
|
|
LOG_DEBUG(Render_Vulkan, "Vulkan extension {} not available", ext_name);
|
|
}
|
|
|
|
bool CreateVulkanDevice(struct retro_vulkan_context* context, VkInstance instance,
|
|
VkPhysicalDevice gpu, VkSurfaceKHR surface,
|
|
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
|
const char** required_device_extensions,
|
|
unsigned num_required_device_extensions,
|
|
const char** required_device_layers, unsigned num_required_device_layers,
|
|
const VkPhysicalDeviceFeatures* required_features) {
|
|
|
|
LOG_INFO(Render_Vulkan, "CreateDevice callback invoked - negotiating Vulkan device creation");
|
|
|
|
// Get available extensions for this physical device
|
|
uint32_t ext_count = 0;
|
|
PFN_vkEnumerateDeviceExtensionProperties vkEnumerateDeviceExtensionProperties =
|
|
(PFN_vkEnumerateDeviceExtensionProperties)get_instance_proc_addr(
|
|
instance, "vkEnumerateDeviceExtensionProperties");
|
|
|
|
vkEnumerateDeviceExtensionProperties(gpu, nullptr, &ext_count, nullptr);
|
|
std::vector<VkExtensionProperties> available_exts(ext_count);
|
|
if (ext_count > 0) {
|
|
vkEnumerateDeviceExtensionProperties(gpu, nullptr, &ext_count, available_exts.data());
|
|
}
|
|
|
|
// Start with frontend's required extensions
|
|
std::vector<const char*> enabled_exts;
|
|
enabled_exts.reserve(num_required_device_extensions + 10);
|
|
for (unsigned i = 0; i < num_required_device_extensions; i++) {
|
|
if (required_device_extensions[i]) {
|
|
enabled_exts.push_back(required_device_extensions[i]);
|
|
}
|
|
}
|
|
|
|
// Add extensions we want (if available)
|
|
AddExtensionIfAvailable(enabled_exts, available_exts, VK_KHR_SWAPCHAIN_EXTENSION_NAME);
|
|
AddExtensionIfAvailable(enabled_exts, available_exts, VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME);
|
|
AddExtensionIfAvailable(enabled_exts, available_exts,
|
|
VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
|
|
AddExtensionIfAvailable(enabled_exts, available_exts,
|
|
VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME);
|
|
AddExtensionIfAvailable(enabled_exts, available_exts, VK_EXT_TOOLING_INFO_EXTENSION_NAME);
|
|
|
|
// These are beneficial but blacklisted on some platforms due to driver bugs
|
|
// For now, let the Instance class handle these decisions
|
|
// AddExtensionIfAvailable(enabled_exts, available_exts,
|
|
// VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME);
|
|
// AddExtensionIfAvailable(enabled_exts, available_exts,
|
|
// VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME);
|
|
|
|
// Merge frontend's required features with our baseline
|
|
VkPhysicalDeviceFeatures merged_features{};
|
|
if (required_features) {
|
|
// Copy all frontend requirements
|
|
for (unsigned i = 0; i < sizeof(VkPhysicalDeviceFeatures) / sizeof(VkBool32); i++) {
|
|
if (reinterpret_cast<const VkBool32*>(required_features)[i]) {
|
|
reinterpret_cast<VkBool32*>(&merged_features)[i] = VK_TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Query actual device features so we only request what's supported
|
|
PFN_vkGetPhysicalDeviceFeatures vkGetPhysicalDeviceFeatures =
|
|
(PFN_vkGetPhysicalDeviceFeatures)get_instance_proc_addr(instance,
|
|
"vkGetPhysicalDeviceFeatures");
|
|
VkPhysicalDeviceFeatures device_features{};
|
|
vkGetPhysicalDeviceFeatures(gpu, &device_features);
|
|
|
|
// Request features we want, gated by actual device support
|
|
if (device_features.geometryShader)
|
|
merged_features.geometryShader = VK_TRUE;
|
|
if (device_features.logicOp)
|
|
merged_features.logicOp = VK_TRUE;
|
|
if (device_features.samplerAnisotropy)
|
|
merged_features.samplerAnisotropy = VK_TRUE;
|
|
if (device_features.fragmentStoresAndAtomics)
|
|
merged_features.fragmentStoresAndAtomics = VK_TRUE;
|
|
if (device_features.shaderClipDistance)
|
|
merged_features.shaderClipDistance = VK_TRUE;
|
|
|
|
// Find queue family with graphics support
|
|
PFN_vkGetPhysicalDeviceQueueFamilyProperties vkGetPhysicalDeviceQueueFamilyProperties =
|
|
(PFN_vkGetPhysicalDeviceQueueFamilyProperties)get_instance_proc_addr(
|
|
instance, "vkGetPhysicalDeviceQueueFamilyProperties");
|
|
|
|
uint32_t queue_family_count = 0;
|
|
vkGetPhysicalDeviceQueueFamilyProperties(gpu, &queue_family_count, nullptr);
|
|
std::vector<VkQueueFamilyProperties> queue_families(queue_family_count);
|
|
vkGetPhysicalDeviceQueueFamilyProperties(gpu, &queue_family_count, queue_families.data());
|
|
|
|
uint32_t graphics_queue_family = VK_QUEUE_FAMILY_IGNORED;
|
|
for (uint32_t i = 0; i < queue_family_count; i++) {
|
|
if (queue_families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
|
|
graphics_queue_family = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (graphics_queue_family == VK_QUEUE_FAMILY_IGNORED) {
|
|
LOG_CRITICAL(Render_Vulkan, "No graphics queue family found!");
|
|
return false;
|
|
}
|
|
|
|
// Create device
|
|
const float queue_priority = 1.0f;
|
|
VkDeviceQueueCreateInfo queue_info{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
|
queue_info.queueFamilyIndex = graphics_queue_family;
|
|
queue_info.queueCount = 1;
|
|
queue_info.pQueuePriorities = &queue_priority;
|
|
|
|
VkDeviceCreateInfo device_info{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
|
|
device_info.queueCreateInfoCount = 1;
|
|
device_info.pQueueCreateInfos = &queue_info;
|
|
device_info.enabledExtensionCount = static_cast<uint32_t>(enabled_exts.size());
|
|
device_info.ppEnabledExtensionNames = enabled_exts.data();
|
|
device_info.enabledLayerCount = num_required_device_layers;
|
|
device_info.ppEnabledLayerNames = required_device_layers;
|
|
device_info.pEnabledFeatures = &merged_features;
|
|
|
|
PFN_vkCreateDevice vkCreateDevice =
|
|
(PFN_vkCreateDevice)get_instance_proc_addr(instance, "vkCreateDevice");
|
|
|
|
VkDevice device = VK_NULL_HANDLE;
|
|
VkResult result = vkCreateDevice(gpu, &device_info, nullptr, &device);
|
|
if (result != VK_SUCCESS) {
|
|
LOG_CRITICAL(Render_Vulkan, "vkCreateDevice failed: {}", static_cast<int>(result));
|
|
return false;
|
|
}
|
|
|
|
// Get the queue
|
|
PFN_vkGetDeviceQueue vkGetDeviceQueue =
|
|
(PFN_vkGetDeviceQueue)get_instance_proc_addr(instance, "vkGetDeviceQueue");
|
|
|
|
VkQueue queue = VK_NULL_HANDLE;
|
|
vkGetDeviceQueue(device, graphics_queue_family, 0, &queue);
|
|
|
|
// Fill in the context for the frontend
|
|
context->gpu = gpu;
|
|
context->device = device;
|
|
context->queue = queue;
|
|
context->queue_family_index = graphics_queue_family;
|
|
context->presentation_queue = queue; // Same queue for LibRetro
|
|
context->presentation_queue_family_index = graphics_queue_family;
|
|
|
|
LOG_INFO(Render_Vulkan,
|
|
"Vulkan device created successfully via negotiation interface (GPU: {}, Queue "
|
|
"Family: {})",
|
|
static_cast<void*>(gpu), graphics_queue_family);
|
|
|
|
return true;
|
|
}
|
|
|
|
void VulkanResetContext() {
|
|
LibRetro::GetHWRenderInterface((void**)&vulkan_intf);
|
|
|
|
// Initialize dispatcher with LibRetro's function pointers
|
|
VULKAN_HPP_DEFAULT_DISPATCHER.init(vulkan_intf->get_instance_proc_addr);
|
|
|
|
vk::Instance vk_instance{vulkan_intf->instance};
|
|
VULKAN_HPP_DEFAULT_DISPATCHER.init(vk_instance);
|
|
}
|
|
|
|
} // namespace LibRetro
|
|
|
|
namespace Vulkan {
|
|
|
|
std::shared_ptr<Common::DynamicLibrary> OpenLibrary(
|
|
[[maybe_unused]] Frontend::GraphicsContext* context) {
|
|
// the frontend takes care of this, we'll get the instance later
|
|
return std::make_shared<Common::DynamicLibrary>();
|
|
}
|
|
|
|
vk::SurfaceKHR CreateSurface(vk::Instance instance, const Frontend::EmuWindow& emu_window) {
|
|
// LibRetro cores don't use surfaces - we render to our own output texture
|
|
// This function should not be called in LibRetro mode
|
|
LOG_WARNING(Render_Vulkan, "CreateSurface called in LibRetro mode - this should not happen");
|
|
return VK_NULL_HANDLE;
|
|
}
|
|
|
|
vk::UniqueInstance CreateInstance([[maybe_unused]] const Common::DynamicLibrary& library,
|
|
[[maybe_unused]] Frontend::WindowSystemType window_type,
|
|
[[maybe_unused]] bool enable_validation,
|
|
[[maybe_unused]] bool dump_command_buffers) {
|
|
// LibRetro cores don't create instances - frontend handles this
|
|
LOG_WARNING(Render_Vulkan, "CreateInstance called in LibRetro mode - this should not happen");
|
|
return vk::UniqueInstance{};
|
|
}
|
|
|
|
DebugCallback CreateDebugCallback(vk::Instance instance, bool& debug_utils_supported) {
|
|
// LibRetro handles debugging, return empty callback
|
|
debug_utils_supported = false;
|
|
return {};
|
|
}
|
|
|
|
LibRetroVKInstance::LibRetroVKInstance(Frontend::EmuWindow& window,
|
|
[[maybe_unused]] u32 physical_device_index)
|
|
: Instance(Instance::NoInit{}) {
|
|
// Ensure LibRetro interface is available
|
|
if (!vulkan_intf) {
|
|
LOG_CRITICAL(Render_Vulkan, "LibRetro Vulkan interface not initialized!");
|
|
throw std::runtime_error("LibRetro Vulkan interface not available");
|
|
}
|
|
|
|
// Initialize basic Vulkan objects from LibRetro
|
|
physical_device = vulkan_intf->gpu;
|
|
if (!physical_device) {
|
|
LOG_CRITICAL(Render_Vulkan, "LibRetro provided invalid physical device!");
|
|
throw std::runtime_error("Invalid physical device from LibRetro");
|
|
}
|
|
|
|
// Get device properties and features
|
|
properties = physical_device.getProperties();
|
|
|
|
const std::vector extensions = physical_device.enumerateDeviceExtensionProperties();
|
|
available_extensions.reserve(extensions.size());
|
|
for (const auto& extension : extensions) {
|
|
available_extensions.emplace_back(extension.extensionName.data());
|
|
}
|
|
|
|
// Get queues from LibRetro
|
|
graphics_queue = vulkan_intf->queue;
|
|
queue_family_index = vulkan_intf->queue_index;
|
|
present_queue = graphics_queue; // Same queue for LibRetro
|
|
|
|
if (!graphics_queue) {
|
|
LOG_CRITICAL(Render_Vulkan, "LibRetro provided invalid graphics queue!");
|
|
throw std::runtime_error("Invalid graphics queue from LibRetro");
|
|
}
|
|
|
|
// Initialize Vulkan HPP dispatcher with LibRetro's device
|
|
VULKAN_HPP_DEFAULT_DISPATCHER.init(vk::Device{vulkan_intf->device});
|
|
|
|
// Now run device capability detection with dispatcher initialized
|
|
CreateDevice();
|
|
|
|
// LibRetro-specific: Validate function pointers are actually available
|
|
// LibRetro's device may not have loaded all extension functions even if extensions are
|
|
// available
|
|
if (extended_dynamic_state) {
|
|
if (!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetCullModeEXT ||
|
|
!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetDepthTestEnableEXT ||
|
|
!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetDepthWriteEnableEXT ||
|
|
!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetFrontFaceEXT) {
|
|
LOG_WARNING(Render_Vulkan, "Extended dynamic state function pointers not available in "
|
|
"LibRetro context, disabling");
|
|
extended_dynamic_state = false;
|
|
}
|
|
}
|
|
|
|
if (timeline_semaphores) {
|
|
if (!VULKAN_HPP_DEFAULT_DISPATCHER.vkGetSemaphoreCounterValueKHR) {
|
|
LOG_WARNING(Render_Vulkan, "Timeline semaphore function pointers not available in "
|
|
"LibRetro context, disabling");
|
|
timeline_semaphores = false;
|
|
}
|
|
}
|
|
|
|
// Initialize subsystems
|
|
CreateAllocator();
|
|
CreateFormatTable();
|
|
CollectToolingInfo();
|
|
CreateCustomFormatTable();
|
|
CreateAttribTable();
|
|
|
|
LOG_INFO(Render_Vulkan, "LibRetro Vulkan Instance initialized successfully");
|
|
LOG_INFO(Render_Vulkan, "Device: {} ({})", properties.deviceName.data(), GetVendorName());
|
|
LOG_INFO(Render_Vulkan, "Driver: {}", GetDriverVersionName());
|
|
}
|
|
|
|
vk::Instance LibRetroVKInstance::GetInstance() const {
|
|
return vk::Instance{vulkan_intf->instance};
|
|
}
|
|
|
|
vk::Device LibRetroVKInstance::GetDevice() const {
|
|
return vk::Device{vulkan_intf->device};
|
|
}
|
|
|
|
// ============================================================================
|
|
// PresentWindow Implementation (LibRetro version)
|
|
// ============================================================================
|
|
|
|
PresentWindow::PresentWindow(Frontend::EmuWindow& emu_window_, const Instance& instance_,
|
|
Scheduler& scheduler_, [[maybe_unused]] bool low_refresh_rate)
|
|
: emu_window{emu_window_}, instance{instance_}, scheduler{scheduler_},
|
|
graphics_queue{instance.GetGraphicsQueue()} {
|
|
const vk::Device device = instance.GetDevice();
|
|
|
|
LOG_INFO(Render_Vulkan, "Initializing LibRetro PresentWindow");
|
|
|
|
// Create command pool for frame operations
|
|
const vk::CommandPoolCreateInfo pool_info = {
|
|
.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer |
|
|
vk::CommandPoolCreateFlagBits::eTransient,
|
|
.queueFamilyIndex = instance.GetGraphicsQueueFamilyIndex(),
|
|
};
|
|
command_pool = device.createCommandPool(pool_info);
|
|
|
|
// Create render pass for LibRetro output
|
|
present_renderpass = CreateRenderpass();
|
|
|
|
// Start with initial dimensions from layout
|
|
const auto& layout = emu_window.GetFramebufferLayout();
|
|
CreateOutputTexture(layout.width, layout.height);
|
|
CreateFrameResources();
|
|
|
|
LOG_INFO(Render_Vulkan, "LibRetro PresentWindow initialized with {}x{}", layout.width,
|
|
layout.height);
|
|
}
|
|
|
|
PresentWindow::~PresentWindow() {
|
|
const vk::Device device = instance.GetDevice();
|
|
|
|
LOG_DEBUG(Render_Vulkan, "Destroying LibRetro PresentWindow");
|
|
|
|
// Wait for any pending operations
|
|
WaitPresent();
|
|
device.waitIdle();
|
|
|
|
// Destroy frame resources
|
|
DestroyFrameResources();
|
|
|
|
// Destroy output texture
|
|
DestroyOutputTexture();
|
|
|
|
// Destroy Vulkan objects
|
|
if (command_pool) {
|
|
device.destroyCommandPool(command_pool);
|
|
}
|
|
if (present_renderpass) {
|
|
device.destroyRenderPass(present_renderpass);
|
|
}
|
|
}
|
|
|
|
void PresentWindow::CreateOutputTexture(u32 width, u32 height) {
|
|
if (width == 0 || height == 0) {
|
|
LOG_ERROR(Render_Vulkan, "Invalid output texture dimensions: {}x{}", width, height);
|
|
return;
|
|
}
|
|
|
|
// Destroy existing texture if dimensions changed
|
|
if (output_image && (output_width != width || output_height != height)) {
|
|
DestroyOutputTexture();
|
|
}
|
|
|
|
// Skip if already created with correct dimensions
|
|
if (output_image && output_width == width && output_height == height) {
|
|
return;
|
|
}
|
|
|
|
const vk::Device device = instance.GetDevice();
|
|
output_width = width;
|
|
output_height = height;
|
|
|
|
// Create output image with LibRetro requirements
|
|
const vk::ImageCreateInfo image_info = {
|
|
.imageType = vk::ImageType::e2D,
|
|
.format = output_format,
|
|
.extent = {width, height, 1},
|
|
.mipLevels = 1,
|
|
.arrayLayers = 1,
|
|
.samples = vk::SampleCountFlagBits::e1,
|
|
.tiling = vk::ImageTiling::eOptimal,
|
|
.usage = vk::ImageUsageFlagBits::eColorAttachment | // For rendering
|
|
vk::ImageUsageFlagBits::eTransferSrc | // Required by LibRetro
|
|
vk::ImageUsageFlagBits::eSampled | // Required by LibRetro
|
|
vk::ImageUsageFlagBits::eTransferDst, // For clearing
|
|
.sharingMode = vk::SharingMode::eExclusive,
|
|
.initialLayout = vk::ImageLayout::eUndefined,
|
|
};
|
|
|
|
// Create image with VMA - using budget-aware allocation like standalone version
|
|
VmaAllocationCreateInfo alloc_info = {};
|
|
alloc_info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
|
alloc_info.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
|
|
|
|
VkImage vk_image;
|
|
const VkResult result = vmaCreateImage(instance.GetAllocator(),
|
|
reinterpret_cast<const VkImageCreateInfo*>(&image_info),
|
|
&alloc_info, &vk_image, &output_allocation, nullptr);
|
|
|
|
if (result != VK_SUCCESS) {
|
|
LOG_CRITICAL(Render_Vulkan, "Failed to create output image: {}", static_cast<int>(result));
|
|
throw std::runtime_error("Failed to create LibRetro output texture");
|
|
}
|
|
|
|
output_image = vk::Image{vk_image};
|
|
|
|
// Create image view
|
|
output_view_create_info = {
|
|
.image = output_image,
|
|
.viewType = vk::ImageViewType::e2D,
|
|
.format = output_format,
|
|
.components =
|
|
{
|
|
.r = vk::ComponentSwizzle::eIdentity,
|
|
.g = vk::ComponentSwizzle::eIdentity,
|
|
.b = vk::ComponentSwizzle::eIdentity,
|
|
.a = vk::ComponentSwizzle::eIdentity,
|
|
},
|
|
.subresourceRange =
|
|
{
|
|
.aspectMask = vk::ImageAspectFlagBits::eColor,
|
|
.baseMipLevel = 0,
|
|
.levelCount = 1,
|
|
.baseArrayLayer = 0,
|
|
.layerCount = 1,
|
|
},
|
|
};
|
|
output_image_view = device.createImageView(output_view_create_info);
|
|
|
|
LOG_DEBUG(Render_Vulkan, "Created LibRetro output texture: {}x{}", width, height);
|
|
}
|
|
|
|
void PresentWindow::DestroyOutputTexture() {
|
|
if (!output_image) {
|
|
return;
|
|
}
|
|
|
|
const vk::Device device = instance.GetDevice();
|
|
|
|
if (output_image_view) {
|
|
device.destroyImageView(output_image_view);
|
|
output_image_view = nullptr;
|
|
}
|
|
|
|
if (output_allocation) {
|
|
vmaDestroyImage(instance.GetAllocator(), static_cast<VkImage>(output_image),
|
|
output_allocation);
|
|
output_allocation = {};
|
|
}
|
|
|
|
output_image = nullptr;
|
|
output_width = 0;
|
|
output_height = 0;
|
|
}
|
|
|
|
vk::RenderPass PresentWindow::CreateRenderpass() {
|
|
const vk::AttachmentDescription color_attachment = {
|
|
.format = output_format,
|
|
.samples = vk::SampleCountFlagBits::e1,
|
|
.loadOp = vk::AttachmentLoadOp::eClear,
|
|
.storeOp = vk::AttachmentStoreOp::eStore,
|
|
.stencilLoadOp = vk::AttachmentLoadOp::eDontCare,
|
|
.stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
|
|
.initialLayout = vk::ImageLayout::eUndefined,
|
|
.finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal, // Ready for LibRetro
|
|
};
|
|
|
|
const vk::AttachmentReference color_ref = {
|
|
.attachment = 0,
|
|
.layout = vk::ImageLayout::eColorAttachmentOptimal,
|
|
};
|
|
|
|
const vk::SubpassDescription subpass = {
|
|
.pipelineBindPoint = vk::PipelineBindPoint::eGraphics,
|
|
.colorAttachmentCount = 1,
|
|
.pColorAttachments = &color_ref,
|
|
};
|
|
|
|
const vk::SubpassDependency dependency = {
|
|
.srcSubpass = VK_SUBPASS_EXTERNAL,
|
|
.dstSubpass = 0,
|
|
.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
|
|
.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
|
|
.srcAccessMask = {},
|
|
.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite,
|
|
};
|
|
|
|
const vk::RenderPassCreateInfo renderpass_info = {
|
|
.attachmentCount = 1,
|
|
.pAttachments = &color_attachment,
|
|
.subpassCount = 1,
|
|
.pSubpasses = &subpass,
|
|
.dependencyCount = 1,
|
|
.pDependencies = &dependency,
|
|
};
|
|
|
|
return instance.GetDevice().createRenderPass(renderpass_info);
|
|
}
|
|
|
|
void PresentWindow::CreateFrameResources() {
|
|
const vk::Device device = instance.GetDevice();
|
|
const u32 frame_count = 2; // Double buffering for LibRetro
|
|
|
|
// Destroy existing frames
|
|
DestroyFrameResources();
|
|
|
|
// Create frame pool
|
|
frame_pool.resize(frame_count);
|
|
|
|
// Allocate command buffers
|
|
const vk::CommandBufferAllocateInfo alloc_info = {
|
|
.commandPool = command_pool,
|
|
.level = vk::CommandBufferLevel::ePrimary,
|
|
.commandBufferCount = frame_count,
|
|
};
|
|
const std::vector command_buffers = device.allocateCommandBuffers(alloc_info);
|
|
|
|
// Initialize frames
|
|
for (u32 i = 0; i < frame_count; i++) {
|
|
Frame& frame = frame_pool[i];
|
|
frame.width = output_width;
|
|
frame.height = output_height;
|
|
frame.image = output_image; // All frames use the same output texture
|
|
frame.image_view = output_image_view;
|
|
frame.allocation = {}; // VMA allocation handled separately
|
|
frame.cmdbuf = command_buffers[i];
|
|
frame.render_ready = device.createSemaphore({});
|
|
frame.present_done = device.createFence({.flags = vk::FenceCreateFlagBits::eSignaled});
|
|
|
|
// Create framebuffer for this frame
|
|
const vk::FramebufferCreateInfo fb_info = {
|
|
.renderPass = present_renderpass,
|
|
.attachmentCount = 1,
|
|
.pAttachments = &output_image_view,
|
|
.width = output_width,
|
|
.height = output_height,
|
|
.layers = 1,
|
|
};
|
|
frame.framebuffer = device.createFramebuffer(fb_info);
|
|
}
|
|
|
|
LOG_DEBUG(Render_Vulkan, "Created {} frame resources for LibRetro", frame_count);
|
|
}
|
|
|
|
void PresentWindow::DestroyFrameResources() {
|
|
if (frame_pool.empty()) {
|
|
return;
|
|
}
|
|
|
|
const vk::Device device = instance.GetDevice();
|
|
|
|
for (auto& frame : frame_pool) {
|
|
if (frame.framebuffer) {
|
|
device.destroyFramebuffer(frame.framebuffer);
|
|
}
|
|
if (frame.render_ready) {
|
|
device.destroySemaphore(frame.render_ready);
|
|
}
|
|
if (frame.present_done) {
|
|
device.destroyFence(frame.present_done);
|
|
}
|
|
}
|
|
|
|
frame_pool.clear();
|
|
current_frame_index = 0;
|
|
}
|
|
|
|
Frame* PresentWindow::GetRenderFrame() {
|
|
if (frame_pool.empty()) {
|
|
LOG_ERROR(Render_Vulkan, "No frames available in LibRetro PresentWindow");
|
|
return nullptr;
|
|
}
|
|
|
|
// RetroArch may not call context_reset during fullscreen toggle, leaving us
|
|
// with a stale interface pointer that can crash
|
|
const struct retro_hw_render_interface_vulkan* current_intf = nullptr;
|
|
if (!LibRetro::GetHWRenderInterface((void**)¤t_intf) || !current_intf) {
|
|
LOG_ERROR(Render_Vulkan, "Failed to get current Vulkan interface");
|
|
return &frame_pool[current_frame_index];
|
|
}
|
|
|
|
// Update global interface if it changed
|
|
if (current_intf != vulkan_intf) {
|
|
LOG_INFO(Render_Vulkan, "Vulkan interface changed during runtime from {} to {}",
|
|
static_cast<const void*>(vulkan_intf), static_cast<const void*>(current_intf));
|
|
vulkan_intf = current_intf;
|
|
}
|
|
|
|
// LibRetro synchronization: Use LibRetro's wait mechanism instead of fences
|
|
if (vulkan_intf && vulkan_intf->wait_sync_index && vulkan_intf->handle) {
|
|
vulkan_intf->wait_sync_index(vulkan_intf->handle);
|
|
}
|
|
|
|
// Use LibRetro's sync index for frame selection if available
|
|
u32 frame_index = current_frame_index;
|
|
if (vulkan_intf && vulkan_intf->get_sync_index && vulkan_intf->handle) {
|
|
LOG_TRACE(Render_Vulkan, "Calling get_sync_index with handle: {}",
|
|
static_cast<void*>(vulkan_intf->handle));
|
|
|
|
const u32 sync_index = vulkan_intf->get_sync_index(vulkan_intf->handle);
|
|
frame_index = sync_index % frame_pool.size();
|
|
LOG_TRACE(Render_Vulkan, "LibRetro sync index: {}, using frame: {}", sync_index,
|
|
frame_index);
|
|
}
|
|
|
|
return &frame_pool[frame_index];
|
|
}
|
|
|
|
void PresentWindow::RecreateFrame(Frame* frame, u32 width, u32 height) {
|
|
if (!frame) {
|
|
LOG_ERROR(Render_Vulkan, "Invalid frame for recreation");
|
|
return;
|
|
}
|
|
|
|
if (frame->width == width && frame->height == height) {
|
|
return; // No change needed
|
|
}
|
|
|
|
LOG_DEBUG(Render_Vulkan, "Recreating LibRetro frame: {}x{} -> {}x{}", frame->width,
|
|
frame->height, width, height);
|
|
|
|
// Wait for frame to be idle
|
|
const vk::Device device = instance.GetDevice();
|
|
[[maybe_unused]] const vk::Result wait_result =
|
|
device.waitForFences(frame->present_done, VK_TRUE, UINT64_MAX);
|
|
|
|
// Recreate output texture with new dimensions
|
|
CreateOutputTexture(width, height);
|
|
|
|
// Recreate frame resources
|
|
CreateFrameResources();
|
|
|
|
LOG_INFO(Render_Vulkan, "LibRetro frame recreated for {}x{}", width, height);
|
|
}
|
|
|
|
void PresentWindow::Present(Frame* frame) {
|
|
if (!frame) {
|
|
LOG_ERROR(Render_Vulkan, "Cannot present null frame");
|
|
return;
|
|
}
|
|
|
|
if (!vulkan_intf) {
|
|
LOG_ERROR(Render_Vulkan, "LibRetro Vulkan interface not available for presentation");
|
|
return;
|
|
}
|
|
|
|
// CRITICAL: Use persistent struct to avoid stack lifetime issues!
|
|
// RetroArch may cache this pointer for frame duping during pause
|
|
persistent_libretro_image.image_view = static_cast<VkImageView>(frame->image_view);
|
|
persistent_libretro_image.image_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
persistent_libretro_image.create_info =
|
|
static_cast<VkImageViewCreateInfo>(output_view_create_info);
|
|
|
|
vulkan_intf->set_image(vulkan_intf->handle, &persistent_libretro_image, 0, nullptr,
|
|
instance.GetGraphicsQueueFamilyIndex());
|
|
|
|
// Call EmuWindow SwapBuffers to trigger LibRetro video frame submission
|
|
emu_window.SwapBuffers();
|
|
|
|
// LibRetro manages frame indices via sync_index, so we don't manually increment
|
|
// current_frame_index = (current_frame_index + 1) % frame_pool.size();
|
|
|
|
LOG_TRACE(Render_Vulkan, "Frame presented to LibRetro: {}x{}", frame->width, frame->height);
|
|
}
|
|
|
|
void PresentWindow::WaitPresent() {
|
|
if (frame_pool.empty()) {
|
|
return;
|
|
}
|
|
|
|
const vk::Device device = instance.GetDevice();
|
|
|
|
// Wait for all frames to complete
|
|
std::vector<vk::Fence> fences;
|
|
fences.reserve(frame_pool.size());
|
|
|
|
for (const auto& frame : frame_pool) {
|
|
fences.push_back(frame.present_done);
|
|
}
|
|
|
|
if (!fences.empty()) {
|
|
[[maybe_unused]] const vk::Result wait_result =
|
|
device.waitForFences(fences, VK_TRUE, UINT64_MAX);
|
|
}
|
|
}
|
|
|
|
void PresentWindow::NotifySurfaceChanged() {
|
|
// LibRetro doesn't use surfaces, so this is a no-op
|
|
LOG_DEBUG(Render_Vulkan, "Surface change notification ignored in LibRetro mode");
|
|
}
|
|
|
|
// ============================================================================
|
|
// MasterSemaphoreLibRetro Implementation
|
|
// ============================================================================
|
|
|
|
constexpr u64 FENCE_RESERVE = 8;
|
|
|
|
MasterSemaphoreLibRetro::MasterSemaphoreLibRetro(const Instance& instance_) : instance{instance_} {
|
|
const vk::Device device{instance.GetDevice()};
|
|
// Pre-allocate fence pool
|
|
for (u64 i = 0; i < FENCE_RESERVE; i++) {
|
|
free_queue.push_back(device.createFence({}));
|
|
}
|
|
// Start background wait thread
|
|
wait_thread = std::jthread([this](std::stop_token token) { WaitThread(token); });
|
|
}
|
|
|
|
MasterSemaphoreLibRetro::~MasterSemaphoreLibRetro() {
|
|
// wait_thread will be automatically stopped by jthread destructor
|
|
// Clean up remaining fences
|
|
const vk::Device device{instance.GetDevice()};
|
|
for (const auto& fence : free_queue) {
|
|
device.destroyFence(fence);
|
|
}
|
|
}
|
|
|
|
void MasterSemaphoreLibRetro::Refresh() {}
|
|
|
|
void MasterSemaphoreLibRetro::Wait(u64 tick) {
|
|
std::unique_lock lock{free_mutex};
|
|
free_cv.wait(lock, [this, tick] { return gpu_tick.load(std::memory_order_relaxed) >= tick; });
|
|
}
|
|
|
|
void MasterSemaphoreLibRetro::SubmitWork(vk::CommandBuffer cmdbuf, vk::Semaphore wait,
|
|
vk::Semaphore signal, u64 signal_value) {
|
|
if (!vulkan_intf) {
|
|
LOG_ERROR(Render_Vulkan, "LibRetro Vulkan interface not available for command submission");
|
|
return;
|
|
}
|
|
|
|
cmdbuf.end();
|
|
|
|
// Get a fence from the pool
|
|
const vk::Fence fence = GetFreeFence();
|
|
|
|
// Strip semaphores - RetroArch handles frame sync, we track resources internally
|
|
const vk::SubmitInfo submit_info = {
|
|
.waitSemaphoreCount = 0,
|
|
.pWaitSemaphores = nullptr,
|
|
.pWaitDstStageMask = nullptr,
|
|
.commandBufferCount = 1u,
|
|
.pCommandBuffers = &cmdbuf,
|
|
.signalSemaphoreCount = 0,
|
|
.pSignalSemaphores = nullptr,
|
|
};
|
|
|
|
// Use LibRetro's queue coordination
|
|
if (vulkan_intf->lock_queue) {
|
|
vulkan_intf->lock_queue(vulkan_intf->handle);
|
|
}
|
|
|
|
try {
|
|
// Submit with fence for internal resource tracking
|
|
vk::Queue queue{vulkan_intf->queue};
|
|
queue.submit(submit_info, fence);
|
|
|
|
if (vulkan_intf->unlock_queue) {
|
|
vulkan_intf->unlock_queue(vulkan_intf->handle);
|
|
}
|
|
} catch (vk::DeviceLostError& err) {
|
|
if (vulkan_intf->unlock_queue) {
|
|
vulkan_intf->unlock_queue(vulkan_intf->handle);
|
|
}
|
|
UNREACHABLE_MSG("Device lost during submit: {}", err.what());
|
|
} catch (...) {
|
|
if (vulkan_intf->unlock_queue) {
|
|
vulkan_intf->unlock_queue(vulkan_intf->handle);
|
|
}
|
|
throw;
|
|
}
|
|
|
|
// Enqueue fence for wait thread to process
|
|
{
|
|
std::scoped_lock lock{wait_mutex};
|
|
wait_queue.emplace(fence, signal_value);
|
|
wait_cv.notify_one();
|
|
}
|
|
}
|
|
|
|
void MasterSemaphoreLibRetro::WaitThread(std::stop_token token) {
|
|
const vk::Device device{instance.GetDevice()};
|
|
|
|
while (!token.stop_requested()) {
|
|
vk::Fence fence;
|
|
u64 signal_value;
|
|
|
|
// Wait for work
|
|
{
|
|
std::unique_lock lock{wait_mutex};
|
|
Common::CondvarWait(wait_cv, lock, token, [this] { return !wait_queue.empty(); });
|
|
if (token.stop_requested()) {
|
|
return;
|
|
}
|
|
std::tie(fence, signal_value) = wait_queue.front();
|
|
wait_queue.pop();
|
|
}
|
|
|
|
// Wait for fence (blocks only this background thread)
|
|
const vk::Result result = device.waitForFences(fence, true, UINT64_MAX);
|
|
if (result != vk::Result::eSuccess) {
|
|
LOG_ERROR(Render_Vulkan, "Fence wait failed: {}", vk::to_string(result));
|
|
}
|
|
|
|
// Reset fence and return to pool
|
|
device.resetFences(fence);
|
|
|
|
// Update GPU tick - signals main thread's Wait()
|
|
gpu_tick.store(signal_value, std::memory_order_release);
|
|
|
|
// Return fence to pool
|
|
{
|
|
std::scoped_lock lock{free_mutex};
|
|
free_queue.push_back(fence);
|
|
free_cv.notify_all();
|
|
}
|
|
}
|
|
}
|
|
|
|
vk::Fence MasterSemaphoreLibRetro::GetFreeFence() {
|
|
std::scoped_lock lock{free_mutex};
|
|
if (free_queue.empty()) {
|
|
// Pool exhausted - create new fence
|
|
return instance.GetDevice().createFence({});
|
|
}
|
|
|
|
const vk::Fence fence = free_queue.front();
|
|
free_queue.pop_front();
|
|
return fence;
|
|
}
|
|
|
|
// Factory function for scheduler to create LibRetro MasterSemaphore
|
|
std::unique_ptr<MasterSemaphore> CreateLibRetroMasterSemaphore(const Instance& instance) {
|
|
return std::make_unique<MasterSemaphoreLibRetro>(instance);
|
|
}
|
|
|
|
} // namespace Vulkan
|