#include "SDL.h" #include "SDL_error.h" #include "SDL_events.h" #include "SDL_gamecontroller.h" #include "SDL_hints.h" #include "SDL_joystick.h" #include "SDL_keyboard.h" #include "SDL_keycode.h" #include "SDL_mouse.h" #include "SDL_pixels.h" #include "SDL_rwops.h" #include "SDL_stdinc.h" #include "SDL_surface.h" #include "SDL_thread.h" #include "SDL_touch.h" #include "SDL_video.h" #include "SDL_vulkan.h" #include "common/assert.h" #include "common/common.h" #include "common/emulatorConfig.h" #include "common/file.h" #include "common/logging/log.h" #include "common/profiler.h" #include "common/stringUtils.h" #include "common/subsystems.h" #include "common/systemInfo.h" #include "common/threads.h" #include "common/timer.h" #include "graphics/host_gpu/graphicContext.h" #include "graphics/host_gpu/renderer/render.h" #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/host_gpu/transfer.h" #include "graphics/host_gpu/vma.h" #include "graphics/host_gpu/vulkanCommon.h" #include "graphics/presentation/renderDoc.h" #include "graphics/presentation/videoOut.h" #include "graphics/presentation/window.h" #include "graphics/presentation/window/windowInternal.h" #include "libs/controller.h" #include "loader/systemContent.h" #include #include #include #include #include #include #include #include #include #include #include // IWYU pragma: no_include #define KYTY_ENABLE_DEBUG_PRINTF #define KYTY_DBG_INPUT namespace Libs::Graphics { struct PreparedFrame { VulkanImage image {VulkanImageType::Unknown}; std::unique_ptr present_commands; bool busy = false; }; class PreparedFramePool { public: void EnsureCapacity(uint32_t count, vk::Format format) { if (count == 0 || format == vk::Format::eUndefined) { EXIT("prepared-frame pool requires at least one frame\n"); } Common::LockGuard lock(m_mutex); m_format = format; while (m_frames.size() < count) { auto frame = std::make_unique(); m_free.push_back(frame.get()); m_frames.push_back(std::move(frame)); } } vk::Format GetFormat() { Common::LockGuard lock(m_mutex); if (m_format == vk::Format::eUndefined) { EXIT("prepared-frame pool has no presentation format\n"); } return m_format; } PreparedFrame* Acquire() { m_mutex.Lock(); if (m_frames.empty()) { EXIT("prepared-frame pool was used before swapchain initialization\n"); } while (m_free.empty()) { m_available.Wait(&m_mutex); } auto* frame = m_free.front(); m_free.pop_front(); if (frame->busy) { EXIT("prepared-frame pool returned an invalid frame\n"); } frame->busy = true; m_mutex.Unlock(); // The producer only waits here. Reset stays on the presentation thread that owns the // allocating Vulkan command pool. if (frame->present_commands != nullptr) { frame->present_commands->WaitForFenceOnly(); } return frame; } void Release(PreparedFrame* frame) { if (frame == nullptr) { EXIT("cannot release a null prepared frame\n"); } Common::LockGuard lock(m_mutex); if (!frame->busy) { EXIT("prepared frame was released twice\n"); } frame->busy = false; m_free.push_back(frame); m_available.Signal(); } private: Common::Mutex m_mutex; Common::CondVar m_available; std::vector> m_frames; std::deque m_free; vk::Format m_format = vk::Format::eUndefined; }; static PreparedFramePool* GetPreparedFramePool() { static auto* pool = new PreparedFramePool; return pool; } static void ConfigurePreparedFrame(PreparedFrame& frame, vk::Extent2D extent, vk::Format format) { if (extent.width == 0 || extent.height == 0 || format == vk::Format::eUndefined) { EXIT("unsupported prepared frame, extent=%ux%u format=%d\n", extent.width, extent.height, static_cast(format)); } auto& graphics = g_window_ctx->graphic_ctx; auto& dst = frame.image; const bool compatible = dst.image != nullptr && dst.extent.width == extent.width && dst.extent.height == extent.height && dst.format == format; if (compatible) { return; } if (dst.image != nullptr) { EXIT_IF(!dst.view_cache.views.empty()); graphics.DeleteImage(dst); dst.memory = {}; } dst.extent = extent; dst.format = format; dst.layers = 1; dst.mip_levels = 1; dst.layout = vk::ImageLayout::eUndefined; dst.memory.property = vk::MemoryPropertyFlagBits::eDeviceLocal; vk::ImageCreateInfo create {}; create.sType = vk::StructureType::eImageCreateInfo; create.imageType = vk::ImageType::e2D; create.extent = {dst.extent.width, dst.extent.height, 1}; create.mipLevels = 1; create.arrayLayers = 1; create.format = dst.format; create.tiling = vk::ImageTiling::eOptimal; create.initialLayout = vk::ImageLayout::eUndefined; create.usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst; create.sharingMode = vk::SharingMode::eExclusive; create.samples = vk::SampleCountFlagBits::e1; if (!graphics.CreateImage(create, dst)) { EXIT("failed to allocate prepared presentation image, extent=%ux%u format=%d\n", dst.extent.width, dst.extent.height, static_cast(dst.format)); } } VulkanSwapchain::~VulkanSwapchain() = default; [[maybe_unused]] static vk::SwapchainKHR VulkanCreateSwapchainInternal( vk::Device device, vk::SurfaceKHR surface, uint32_t width, uint32_t height, uint32_t image_count, SurfaceCapabilities& r, vk::Format& swapchain_format, vk::Extent2D& swapchain_extent, std::unique_ptr& swapchain_images, std::unique_ptr& swapchain_image_views, uint32_t& swapchain_images_count) { EXIT_IF(device == nullptr); EXIT_IF(surface == nullptr); EXIT_NOT_IMPLEMENTED(r.formats.empty()); vk::Extent2D extent {}; extent.width = std::clamp(width, r.capabilities.minImageExtent.width, r.capabilities.maxImageExtent.width); extent.height = std::clamp(height, r.capabilities.minImageExtent.height, r.capabilities.maxImageExtent.height); image_count = std::clamp(image_count, r.capabilities.minImageCount, r.capabilities.maxImageCount); vk::SwapchainCreateInfoKHR create_info {}; create_info.sType = vk::StructureType::eSwapchainCreateInfoKHR; create_info.pNext = nullptr; create_info.flags = {}; create_info.surface = surface; create_info.minImageCount = image_count; if (r.format_unorm_bgra32) { create_info.imageFormat = vk::Format::eB8G8R8A8Unorm; create_info.imageColorSpace = vk::ColorSpaceKHR::eSrgbNonlinear; } else if (r.format_srgb_bgra32) { create_info.imageFormat = vk::Format::eB8G8R8A8Srgb; create_info.imageColorSpace = vk::ColorSpaceKHR::eSrgbNonlinear; } else { const auto& format = r.formats[0]; create_info.imageFormat = format.format; create_info.imageColorSpace = format.colorSpace; } create_info.imageExtent = extent; create_info.imageArrayLayers = 1; create_info.imageUsage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferDst; create_info.imageSharingMode = vk::SharingMode::eExclusive; create_info.preTransform = r.capabilities.currentTransform; create_info.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque; create_info.presentMode = vk::PresentModeKHR::eFifo; create_info.clipped = VK_TRUE; create_info.oldSwapchain = nullptr; swapchain_format = create_info.imageFormat; swapchain_extent = extent; vk::SwapchainKHR swapchain = nullptr; RequireVulkanSuccess(device.createSwapchainKHR(&create_info, nullptr, &swapchain), "vkCreateSwapchainKHR"); EXIT_IF(swapchain == nullptr); auto images = EnumerateVulkan( "vkGetSwapchainImagesKHR", [&](uint32_t* count, vk::Image* values) { return device.getSwapchainImagesKHR(swapchain, count, values); }); EXIT_NOT_IMPLEMENTED(images.empty()); swapchain_images_count = static_cast(images.size()); swapchain_images = std::make_unique(images.size()); std::copy(images.begin(), images.end(), swapchain_images.get()); swapchain_image_views = std::make_unique(swapchain_images_count); for (uint32_t i = 0; i < swapchain_images_count; i++) { vk::ImageViewCreateInfo create_info {}; create_info.sType = vk::StructureType::eImageViewCreateInfo; create_info.pNext = nullptr; create_info.flags = {}; create_info.image = (swapchain_images)[i]; create_info.viewType = vk::ImageViewType::e2D; create_info.format = swapchain_format; create_info.components.r = vk::ComponentSwizzle::eIdentity; create_info.components.g = vk::ComponentSwizzle::eIdentity; create_info.components.b = vk::ComponentSwizzle::eIdentity; create_info.components.a = vk::ComponentSwizzle::eIdentity; create_info.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; create_info.subresourceRange.baseArrayLayer = 0; create_info.subresourceRange.baseMipLevel = 0; create_info.subresourceRange.layerCount = 1; create_info.subresourceRange.levelCount = 1; RequireVulkanSuccess( device.createImageView(&create_info, nullptr, &((swapchain_image_views)[i])), "vkCreateImageView"); EXIT_IF((swapchain_image_views)[i] == nullptr); } return swapchain; } VulkanSwapchain* VulkanCreateSwapchain(uint32_t image_count) { auto& graphics = g_window_ctx->graphic_ctx; EXIT_IF(graphics.screen_width == 0); EXIT_IF(graphics.screen_height == 0); Common::LockGuard lock(g_window_ctx->mutex); auto swapchain_owner = std::make_unique(); auto* s = swapchain_owner.get(); s->swapchain = VulkanCreateSwapchainInternal( graphics.device, g_window_ctx->surface, graphics.screen_width, graphics.screen_height, image_count, *g_window_ctx->surface_capabilities, s->swapchain_format, s->swapchain_extent, s->swapchain_images, s->swapchain_image_views, s->swapchain_images_count); if (s->swapchain == nullptr) { EXIT("Could not create swapchain"); } s->current_index = static_cast(-1); s->present_frame = 0; vk::SemaphoreCreateInfo semaphore_info {}; semaphore_info.sType = vk::StructureType::eSemaphoreCreateInfo; semaphore_info.pNext = nullptr; semaphore_info.flags = {}; s->image_acquired_semaphores = std::make_unique(s->swapchain_images_count); s->render_complete_semaphores = std::make_unique(s->swapchain_images_count); for (uint32_t i = 0; i < s->swapchain_images_count; i++) { auto result = graphics.device.createSemaphore(&semaphore_info, nullptr, &s->image_acquired_semaphores[i]); EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess); result = graphics.device.createSemaphore(&semaphore_info, nullptr, &s->render_complete_semaphores[i]); EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess); } GetPreparedFramePool()->EnsureCapacity(s->swapchain_images_count, s->swapchain_format); return swapchain_owner.release(); } static void VulkanDeleteSwapchain(VulkanSwapchain* s) { if (s == nullptr) { return; } auto swapchain_owner = std::unique_ptr(s); auto& graphics = g_window_ctx->graphic_ctx; Transfer::WaitForQueueIdle(); if (s->image_acquired_semaphores != nullptr) { for (uint32_t i = 0; i < s->swapchain_images_count; i++) { if (s->image_acquired_semaphores[i] != nullptr) { graphics.device.destroySemaphore(s->image_acquired_semaphores[i], nullptr); } } } if (s->render_complete_semaphores != nullptr) { for (uint32_t i = 0; i < s->swapchain_images_count; i++) { if (s->render_complete_semaphores[i] != nullptr) { graphics.device.destroySemaphore(s->render_complete_semaphores[i], nullptr); } } } if (s->swapchain_image_views != nullptr) { for (uint32_t i = 0; i < s->swapchain_images_count; i++) { if (s->swapchain_image_views[i] != nullptr) { graphics.device.destroyImageView(s->swapchain_image_views[i], nullptr); } } } if (s->swapchain != nullptr) { graphics.device.destroySwapchainKHR(s->swapchain, nullptr); } } static void VulkanRefreshSurfaceSize() { int width = 0; int height = 0; SDL_Vulkan_GetDrawableSize(g_window_ctx->window, &width, &height); if (width > 0 && height > 0) { g_window_ctx->graphic_ctx.screen_width = static_cast(width); g_window_ctx->graphic_ctx.screen_height = static_cast(height); } VulkanGetSurfaceCapabilities(g_window_ctx->graphic_ctx.physical_device, g_window_ctx->surface, *g_window_ctx->surface_capabilities); } static void VulkanRecreateSwapchain() { LOGF("Recreating Vulkan swapchain\n"); VulkanRefreshSurfaceSize(); VulkanDeleteSwapchain(g_window_ctx->swapchain); g_window_ctx->swapchain = VulkanCreateSwapchain(2); } PreparedFrame& WindowPrepareFrame(CommandBuffer& buffer, VideoOutVulkanImage& image) { KYTY_PROFILER_FUNCTION(); EXIT_IF(buffer.IsInvalid()); if (image.format == vk::Format::eUndefined) { EXIT("unsupported presentation source, image=%p\n", static_cast(&image)); } auto* frame = GetPreparedFramePool()->Acquire(); Common::LockGuard render_lock(GetRenderContext().GetMutex()); GetRenderContext().GetTextureCache().RefreshVideoOut(image); ConfigurePreparedFrame(*frame, image.extent, image.format); ImageImageCopy copy {image}; copy.width = image.extent.width; copy.height = image.extent.height; const std::array copies {copy}; Transfer::CopyImage(buffer, copies, frame->image, vk::ImageLayout::eTransferSrcOptimal); return *frame; } PreparedFrame& WindowPrepareBlankFrame(CommandBuffer& buffer, uint32_t width, uint32_t height, bool opaque) { KYTY_PROFILER_FUNCTION(); EXIT_IF(buffer.IsInvalid()); auto* pool = GetPreparedFramePool(); auto format = pool->GetFormat(); auto* frame = pool->Acquire(); Common::LockGuard render_lock(GetRenderContext().GetMutex()); ConfigurePreparedFrame(*frame, {width, height}, format); vk::ClearColorValue clear {}; clear.float32[3] = opaque ? 1.0f : 0.0f; Transfer::ClearColorImage(buffer, frame->image, clear); return *frame; } void WindowPresentFrame(PreparedFrame& frame) { KYTY_PROFILER_FUNCTION(); struct ReleaseScope { PreparedFrame* frame; ~ReleaseScope() { GetPreparedFramePool()->Release(frame); } }; ReleaseScope release {&frame}; if (g_window_ctx->window_hidden) { WindowUpdateIcon(); SDL_ShowWindow(g_window_ctx->window); g_window_ctx->window_hidden = false; VulkanRecreateSwapchain(); } auto* swapchain = g_window_ctx->swapchain; const auto present_frame = swapchain->present_frame; EXIT_IF(present_frame >= swapchain->swapchain_images_count); swapchain->current_index = static_cast(-1); auto result = g_window_ctx->graphic_ctx.device.acquireNextImageKHR( swapchain->swapchain, UINT64_MAX, swapchain->image_acquired_semaphores[present_frame], nullptr, &swapchain->current_index); switch (result) { case vk::Result::eSuccess: break; case vk::Result::eSuboptimalKHR: LOGF("vkAcquireNextImageKHR returned vk::Result::eSuboptimalKHR\n"); break; case vk::Result::eErrorOutOfDateKHR: LOGF("vkAcquireNextImageKHR returned vk::Result::eErrorOutOfDateKHR\n"); VulkanRecreateSwapchain(); return; default: EXIT("vkAcquireNextImageKHR failed: %s\n", VulkanToString(result).c_str()); } EXIT_NOT_IMPLEMENTED(swapchain->current_index == static_cast(-1)); if (frame.present_commands == nullptr) { frame.present_commands = std::make_unique(); } frame.present_commands->WaitForFenceAndReset(); auto& buffer = *frame.present_commands; auto vk_buffer = buffer.Handle(); buffer.Begin(); Transfer::BlitToSwapchain(buffer, frame.image, *swapchain); vk::ImageMemoryBarrier pre_present_barrier {}; pre_present_barrier.sType = vk::StructureType::eImageMemoryBarrier; pre_present_barrier.pNext = nullptr; pre_present_barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; pre_present_barrier.dstAccessMask = vk::AccessFlagBits::eMemoryRead; pre_present_barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal; pre_present_barrier.newLayout = vk::ImageLayout::ePresentSrcKHR; pre_present_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; pre_present_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; pre_present_barrier.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; pre_present_barrier.subresourceRange.baseMipLevel = 0; pre_present_barrier.subresourceRange.levelCount = 1; pre_present_barrier.subresourceRange.baseArrayLayer = 0; pre_present_barrier.subresourceRange.layerCount = 1; pre_present_barrier.image = swapchain->swapchain_images[swapchain->current_index]; vk_buffer.pipelineBarrier(vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eBottomOfPipe, vk::DependencyFlags {}, 0, nullptr, 0, nullptr, 1, &pre_present_barrier); buffer.End(); auto render_complete_semaphore = swapchain->render_complete_semaphores[swapchain->current_index]; const vk::PipelineStageFlags wait_stage = vk::PipelineStageFlagBits::eTransfer; buffer.ExecuteWithSemaphore(swapchain->image_acquired_semaphores[present_frame], wait_stage, render_complete_semaphore); vk::PresentInfoKHR present; present.sType = vk::StructureType::ePresentInfoKHR; present.pNext = nullptr; present.swapchainCount = 1; present.pSwapchains = &swapchain->swapchain; present.pImageIndices = &swapchain->current_index; present.pWaitSemaphores = &render_complete_semaphore; present.waitSemaphoreCount = 1; present.pResults = nullptr; auto& graphics = g_window_ctx->graphic_ctx; { Common::LockGuard lock(graphics.queue_mutex); result = graphics.queue.presentKHR(&present); } switch (result) { case vk::Result::eSuccess: break; case vk::Result::eSuboptimalKHR: LOGF("vkQueuePresentKHR returned vk::Result::eSuboptimalKHR\n"); break; case vk::Result::eErrorOutOfDateKHR: LOGF("vkQueuePresentKHR returned vk::Result::eErrorOutOfDateKHR\n"); VulkanRecreateSwapchain(); return; default: EXIT("vkQueuePresentKHR failed: %s\n", VulkanToString(result).c_str()); } swapchain->present_frame = (present_frame + 1u) % swapchain->swapchain_images_count; RenderDocOnPresent(); WindowUpdateTitle(); } } // namespace Libs::Graphics