// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #include #include #include #include #include #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 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; } bool AddExtensionIfAvailable(std::vector& enabled_exts, const std::vector& 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 true; } } // 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 true; } } LOG_DEBUG(Render_Vulkan, "Vulkan extension {} not available", ext_name); return false; } 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 available_exts(ext_count); if (ext_count > 0) { vkEnumerateDeviceExtensionProperties(gpu, nullptr, &ext_count, available_exts.data()); } // Start with frontend's required extensions std::vector 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); AddExtensionIfAvailable(enabled_exts, available_exts, VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME); const bool has_custom_border_color = AddExtensionIfAvailable( enabled_exts, available_exts, VK_EXT_CUSTOM_BORDER_COLOR_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(required_features)[i]) { reinterpret_cast(&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 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; VkPhysicalDeviceFeatures2 enabled_features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2}; enabled_features.features = merged_features; VkPhysicalDeviceCustomBorderColorFeaturesEXT custom_border_color_features{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_FEATURES_EXT}; if (has_custom_border_color) { custom_border_color_features.customBorderColors = VK_TRUE; custom_border_color_features.customBorderColorWithoutFormat = VK_TRUE; custom_border_color_features.pNext = enabled_features.pNext; enabled_features.pNext = &custom_border_color_features; } VkDeviceCreateInfo device_info{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; device_info.queueCreateInfoCount = 1; device_info.pQueueCreateInfos = &queue_info; device_info.enabledExtensionCount = static_cast(enabled_exts.size()); device_info.ppEnabledExtensionNames = enabled_exts.data(); device_info.enabledLayerCount = num_required_device_layers; device_info.ppEnabledLayerNames = required_device_layers; device_info.pNext = &enabled_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(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(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 OpenLibrary( [[maybe_unused]] Frontend::GraphicsContext* context) { // the frontend takes care of this, we'll get the instance later return std::make_shared(); } 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(&image_info), &alloc_info, &vk_image, &output_allocation, nullptr); if (result != VK_SUCCESS) { LOG_CRITICAL(Render_Vulkan, "Failed to create output image: {}", static_cast(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(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(vulkan_intf), static_cast(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(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(frame->image_view); persistent_libretro_image.image_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; persistent_libretro_image.create_info = static_cast(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 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 CreateLibRetroMasterSemaphore(const Instance& instance) { return std::make_unique(instance); } } // namespace Vulkan