From 282257fd98d445591741ac8f4dc57117363554c6 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Wed, 23 Sep 2026 14:27:47 +0300 Subject: [PATCH] feat(video): add resizable aspect-ratio-aware window --- README.md | 3 +- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 + .../Execution/include/AspectFit.hpp | 31 ++++ .../Execution/include/Presentation.hpp | 1 + .../Execution/include/PresentationScaler.hpp | 35 +++++ .../Execution/include/VulkanDevice.hpp | 1 + .../Execution/src/AspectFit.cpp | 53 +++++++ .../libSceAgcDriver/Execution/src/Driver.cpp | 26 ++-- .../Execution/src/PresentationScaler.cpp | 93 ++++++++++++ .../Execution/src/VulkanDevice.cpp | 52 +++++-- .../libs/prx/libSceAgcDriver/tests/Visual.cpp | 6 + core/libs/prx/libSceVideoOut/CMakeLists.txt | 4 + .../libSceVideoOut/include/DisplayWindow.hpp | 36 +++++ .../libSceVideoOut/include/VideoOutDriver.hpp | 3 +- .../prx/libSceVideoOut/src/DisplayWindow.cpp | 138 ++++++++++++++++++ .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 28 ++-- 16 files changed, 476 insertions(+), 38 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/AspectFit.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/src/AspectFit.cpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp create mode 100644 core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp create mode 100644 core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp diff --git a/README.md b/README.md index ac9e59a0c..037d4c25a 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,7 @@ Execution reaches `_start`, [stack unwinding](core/libs/prx/libc/src/exception/U The [shader recompiler](core/shader/recompiler/Recompiler.cpp) successfully produces validated via [Spirv-Tools](3rdparty/SPIRV-Tools) SPIR-V. -The application reaches the render loop and **successfully presents its first frame**: the application logo is visible on screen. - +The application reaches the render loop and **successfully presents its first frame** (logo). Current exception: `AGC graphics: unsupported primitive type 17`. diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6d4b66018..8bf0dcbbd 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -54,6 +54,8 @@ add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/BdaFeatures.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Pm4.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/VulkanDevice.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetTransfer.cpp @@ -279,6 +281,8 @@ if(AGC_BUILD_VISUAL_TEST) add_executable(agc_driver_visual_test EXCLUDE_FROM_ALL tests/Visual.cpp Execution/src/VulkanDevice.cpp + Execution/src/AspectFit.cpp + Execution/src/PresentationScaler.cpp Execution/src/GuestMemory.cpp Execution/src/BdaFeatures.cpp Graphics/src/Draw.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/AspectFit.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/AspectFit.hpp new file mode 100644 index 000000000..572879f78 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/AspectFit.hpp @@ -0,0 +1,31 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ASPECTFIT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ASPECTFIT_HPP + +#include + +namespace AgcDriver { + +struct AspectFitRect { + std::int32_t x; + std::int32_t y; + std::uint32_t width; + std::uint32_t height; +}; + +struct AspectFitSize { + std::uint32_t width; + std::uint32_t height; +}; + +extern "C" { + +double ComputeContainScale_nid_postfix(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t boundsWidth, std::uint32_t boundsHeight, bool allowUpscale); +AspectFitSize ComputeContainSize_nid_postfix(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t boundsWidth, std::uint32_t boundsHeight, bool allowUpscale); +AspectFitRect ComputeContainRect_nid_postfix(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t destinationWidth, std::uint32_t destinationHeight); +std::uint32_t ComputeWidthForHeight_nid_postfix(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t height); +std::uint32_t ComputeHeightForWidth_nid_postfix(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t width); + +} +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp index 6b5dd6ebf..c51d31eab 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp @@ -14,6 +14,7 @@ struct PresentationWindow { void* context; std::span extensions; VkSurfaceKHR (*createSurface)(void* context, VkInstance instance); + void (*getDrawableSize)(void* context, std::uint32_t* width, std::uint32_t* height); std::uint32_t width; std::uint32_t height; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp new file mode 100644 index 000000000..00c5666a3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp @@ -0,0 +1,35 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTATIONSCALER_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTATIONSCALER_HPP + +#define VK_NO_PROTOTYPES +#include +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include + +namespace AgcDriver { + +class PresentationScaler { +public: + PresentationScaler(const Graphics::Context& context, VkFormat sourceFormat, VkFormat destinationFormat); + ~PresentationScaler(); + PresentationScaler(const PresentationScaler&) = delete; + PresentationScaler& operator=(const PresentationScaler&) = delete; + + void EnsureSourceImage(std::uint32_t width, std::uint32_t height); + void RecordUpload(VkCommandBuffer commands, VkBuffer uploadBuffer); + void RecordBlit(VkCommandBuffer commands, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight); + +private: + void release() noexcept; + + Graphics::Context context; + VkFormat sourceFormat; + std::uint32_t sourceWidth = 0; + std::uint32_t sourceHeight = 0; + VkImage sourceImage = VK_NULL_HANDLE; + VkDeviceMemory sourceMemory = VK_NULL_HANDLE; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index ea60a2379..2e94955d3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -20,6 +20,7 @@ public: void WaitIdle(); void* Window() const; void Resize(std::uint32_t width, std::uint32_t height); + bool Presentable() const; std::uint64_t PresentClear(std::uint32_t width, std::uint32_t height, bool opaque); std::uint64_t PresentPixels(std::uint32_t width, std::uint32_t height, std::span pixels); void WaitPresented(std::uint64_t id); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/AspectFit.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/AspectFit.cpp new file mode 100644 index 000000000..e5d919d9f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/AspectFit.cpp @@ -0,0 +1,53 @@ +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" +#include +#include +#include +#include +#include + +namespace AgcDriver { +namespace { + +void require(bool condition, const char* reason) { + if (!condition) throw std::runtime_error(std::string("AspectFit: ") + reason); +} + +std::uint32_t roundToExtent(double value) { + require(value >= 0.0 && value <= static_cast(std::numeric_limits::max()), "computed extent is out of range"); + return static_cast(std::lround(value)); +} + +} +extern "C" { + +double ComputeContainScale_nid_postfix(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t boundsWidth, std::uint32_t boundsHeight, bool allowUpscale) { + require(sourceWidth != 0 && sourceHeight != 0, "source extent must be non-zero"); + require(boundsWidth != 0 && boundsHeight != 0, "bounds extent must be non-zero"); + const auto scale = std::min(static_cast(boundsWidth) / static_cast(sourceWidth), static_cast(boundsHeight) / static_cast(sourceHeight)); + return allowUpscale ? scale : std::min(scale, 1.0); +} + +AspectFitSize ComputeContainSize_nid_postfix(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t boundsWidth, std::uint32_t boundsHeight, bool allowUpscale) { + const auto scale = ComputeContainScale_nid_postfix(sourceWidth, sourceHeight, boundsWidth, boundsHeight, allowUpscale); + return AspectFitSize{std::max(1, roundToExtent(sourceWidth * scale)), std::max(1, roundToExtent(sourceHeight * scale))}; +} + +AspectFitRect ComputeContainRect_nid_postfix(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t destinationWidth, std::uint32_t destinationHeight) { + const auto size = ComputeContainSize_nid_postfix(sourceWidth, sourceHeight, destinationWidth, destinationHeight, true); + const auto x = (static_cast(destinationWidth) - static_cast(size.width)) / 2; + const auto y = (static_cast(destinationHeight) - static_cast(size.height)) / 2; + return AspectFitRect{static_cast(x), static_cast(y), size.width, size.height}; +} + +std::uint32_t ComputeWidthForHeight_nid_postfix(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t height) { + require(sourceWidth != 0 && sourceHeight != 0, "source extent must be non-zero"); + return std::max(1, roundToExtent(static_cast(height) * sourceWidth / sourceHeight)); +} + +std::uint32_t ComputeHeightForWidth_nid_postfix(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t width) { + require(sourceWidth != 0 && sourceHeight != 0, "source extent must be non-zero"); + return std::max(1, roundToExtent(static_cast(width) * sourceHeight / sourceWidth)); +} + +} +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index ed6770b8d..13d25bb4d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -186,8 +186,10 @@ public: void Present(const PresentationWindow& window, const DisplayBuffer* buffer, bool opaque, void (*gpuReady)(void*), void* context) { CheckFailure(); require(gpuReady != nullptr && context != nullptr, "missing GPU completion callback"); + require(window.getDrawableSize != nullptr, "missing window drawable size query"); std::shared_ptr presenting; std::uint64_t id = 0; + bool presented = false; try { { std::lock_guard lock(gpuMutex); @@ -197,18 +199,24 @@ public: } require(device->Window() == window.context, "presentation window does not match device surface"); presenting = device; - presenting->Resize(window.width, window.height); - if (buffer != nullptr) { - require(buffer->width == window.width && buffer->height == window.height, "display buffer extent differs from output"); - presenting->WaitIdle(); - const auto pixels = ReadDisplayBuffer(*buffer); - id = presenting->PresentPixels(window.width, window.height, pixels); - } else { - id = presenting->PresentClear(window.width, window.height, opaque); + std::uint32_t drawableWidth = 0; + std::uint32_t drawableHeight = 0; + window.getDrawableSize(window.context, &drawableWidth, &drawableHeight); + presenting->Resize(drawableWidth, drawableHeight); + if (presenting->Presentable()) { + if (buffer != nullptr) { + require(buffer->width == window.width && buffer->height == window.height, "display buffer extent differs from output"); + presenting->WaitIdle(); + const auto pixels = ReadDisplayBuffer(*buffer); + id = presenting->PresentPixels(window.width, window.height, pixels); + } else { + id = presenting->PresentClear(window.width, window.height, opaque); + } + presented = true; } } gpuReady(context); - presenting->WaitPresented(id); + if (presented) presenting->WaitPresented(id); CheckFailure(); } catch (...) { ReportFailure(std::current_exception()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp new file mode 100644 index 000000000..fedb9a12b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp @@ -0,0 +1,93 @@ +#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" + +namespace AgcDriver { + +PresentationScaler::PresentationScaler(const Graphics::Context& context, VkFormat sourceFormat, VkFormat destinationFormat) : context(context), sourceFormat(sourceFormat) { + Graphics::Require(context.formatProperties != nullptr, "missing Vulkan format property resolver"); + VkFormatProperties sourceProperties{}; + context.formatProperties(context.physical, sourceFormat, &sourceProperties); + Graphics::Require((sourceProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0, "presentation source format does not support blit sources"); + Graphics::Require((sourceProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0, "presentation source format does not support linear blit filtering"); + VkFormatProperties destinationProperties{}; + context.formatProperties(context.physical, destinationFormat, &destinationProperties); + Graphics::Require((destinationProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT) != 0, "presentation destination format does not support blit targets"); +} + +PresentationScaler::~PresentationScaler() { + release(); +} + +void PresentationScaler::release() noexcept { + if (sourceImage != VK_NULL_HANDLE) context.Function("vkDestroyImage")(context.device, sourceImage, nullptr); + if (sourceMemory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, sourceMemory, nullptr); + sourceImage = VK_NULL_HANDLE; + sourceMemory = VK_NULL_HANDLE; + sourceWidth = 0; + sourceHeight = 0; +} + +void PresentationScaler::EnsureSourceImage(std::uint32_t width, std::uint32_t height) { + Graphics::Require(width != 0 && height != 0, "presentation source extent must be non-zero"); + if (sourceImage != VK_NULL_HANDLE && sourceWidth == width && sourceHeight == height) return; + release(); + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = sourceFormat; + imageInfo.extent = {width, height, 1u}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Graphics::Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &sourceImage), "vkCreateImage presentation source"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, sourceImage, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Graphics::Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &sourceMemory), "vkAllocateMemory presentation source"); + Graphics::Check(context.Function("vkBindImageMemory")(context.device, sourceImage, sourceMemory, 0), "vkBindImageMemory presentation source"); + sourceWidth = width; + sourceHeight = height; +} + +void PresentationScaler::RecordUpload(VkCommandBuffer commands, VkBuffer uploadBuffer) { + Graphics::Require(sourceImage != VK_NULL_HANDLE, "presentation source image has not been created"); + const auto pipelineBarrier = context.Function("vkCmdPipelineBarrier"); + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = sourceImage; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {sourceWidth, sourceHeight, 1u}; + context.Function("vkCmdCopyBufferToImage")(commands, uploadBuffer, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); +} + +void PresentationScaler::RecordBlit(VkCommandBuffer commands, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight) { + Graphics::Require(sourceImage != VK_NULL_HANDLE, "presentation source image has not been created"); + const auto rect = ComputeContainRect_nid_postfix(sourceWidth, sourceHeight, destinationWidth, destinationHeight); + VkImageBlit blit{}; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + blit.srcOffsets[0] = {0, 0, 0}; + blit.srcOffsets[1] = {static_cast(sourceWidth), static_cast(sourceHeight), 1}; + blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + blit.dstOffsets[0] = {rect.x, rect.y, 0}; + blit.dstOffsets[1] = {rect.x + static_cast(rect.width), rect.y + static_cast(rect.height), 1}; + context.Function("vkCmdBlitImage")(commands, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destinationImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 9a187ca7c..800a8cfe7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1,6 +1,7 @@ #include "BdaAbi.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libc/include/General.hpp" #include @@ -68,6 +69,7 @@ struct VulkanDevice::State { bool textureCompressionBC = false; std::unique_ptr detiler; VkPhysicalDeviceMeshShaderPropertiesEXT meshLimits{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT}; + std::unique_ptr scaler; template TFunction InstanceFunction(const char* name) const { @@ -133,6 +135,7 @@ struct VulkanDevice::State { const auto idle = reinterpret_cast(deviceProc(device, "vkDeviceWaitIdle"))(device); if (idle != VK_SUCCESS && idle != VK_ERROR_DEVICE_LOST) std::terminate(); detiler.reset(); + scaler.reset(); if (presentQueued && idle != VK_ERROR_DEVICE_LOST) { const auto result = reinterpret_cast(deviceProc(device, "vkWaitForFences"))(device, 1, &presentFence, VK_TRUE, std::numeric_limits::max()); if (result != VK_SUCCESS && result != VK_ERROR_DEVICE_LOST) std::terminate(); @@ -186,7 +189,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u std::vector instanceExtensions; if (window != nullptr) { APS5_LOG_OUT("Presentation window context=%p extent=%ux%u extensions=%zu", window->context, window->width, window->height, window->extensions.size()); - require(window->context && window->createSurface && window->width && window->height, "invalid window descriptor"); + require(window->context && window->createSurface && window->getDrawableSize && window->width && window->height, "invalid window descriptor"); instanceExtensions.assign(window->extensions.begin(), window->extensions.end()); instanceExtensions.push_back(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME); instanceExtensions.push_back(VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME); @@ -408,12 +411,17 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u check(state->DeviceFunction("vkCreateCommandPool")(state->device, &poolInfo, nullptr, &state->pool), "vkCreateCommandPool"); state->detiler = std::make_unique(graphicsContext()); if (window != nullptr) { + require(window->getDrawableSize != nullptr, "missing window drawable size query"); + std::uint32_t drawableWidth = 0; + std::uint32_t drawableHeight = 0; + window->getDrawableSize(window->context, &drawableWidth, &drawableHeight); + require(drawableWidth != 0 && drawableHeight != 0, "window has a zero drawable size at creation"); VkSurfaceCapabilitiesKHR surface{}; check(state->InstanceFunction("vkGetPhysicalDeviceSurfaceCapabilitiesKHR")(selected, state->surface, &surface), "vkGetPhysicalDeviceSurfaceCapabilitiesKHR"); - state->extent = {window->width, window->height}; - APS5_LOG_OUT("Surface capabilities requested=%ux%u min=%ux%u max=%ux%u minImages=%u maxImages=%u usage=0x%x", window->width, window->height, surface.minImageExtent.width, surface.minImageExtent.height, surface.maxImageExtent.width, surface.maxImageExtent.height, surface.minImageCount, surface.maxImageCount, surface.supportedUsageFlags); - require(surface.currentExtent.width == std::numeric_limits::max() || (surface.currentExtent.width == window->width && surface.currentExtent.height == window->height), "window extent differs from requested output"); - require(window->width >= surface.minImageExtent.width && window->width <= surface.maxImageExtent.width && window->height >= surface.minImageExtent.height && window->height <= surface.maxImageExtent.height, "unsupported output extent"); + state->extent = {drawableWidth, drawableHeight}; + APS5_LOG_OUT("Surface capabilities drawable=%ux%u min=%ux%u max=%ux%u minImages=%u maxImages=%u usage=0x%x", drawableWidth, drawableHeight, surface.minImageExtent.width, surface.minImageExtent.height, surface.maxImageExtent.width, surface.maxImageExtent.height, surface.minImageCount, surface.maxImageCount, surface.supportedUsageFlags); + require(surface.currentExtent.width == std::numeric_limits::max() || (surface.currentExtent.width == drawableWidth && surface.currentExtent.height == drawableHeight), "window extent differs from the real drawable size"); + require(drawableWidth >= surface.minImageExtent.width && drawableWidth <= surface.maxImageExtent.width && drawableHeight >= surface.minImageExtent.height && drawableHeight <= surface.maxImageExtent.height, "unsupported output extent"); require((surface.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0, "surface does not support transfer destination images"); require((surface.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) != 0, "opaque composition is unavailable"); require((surface.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) != 0, "identity surface transform is unavailable"); @@ -455,6 +463,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u allocation.commandBufferCount = 1; check(state->DeviceFunction("vkAllocateCommandBuffers")(state->device, &allocation, &state->clearCommands), "vkAllocateCommandBuffers"); APS5_LOG_OUT("Presentation resources ready acquireFence=%p renderFence=%p presentFence=%p semaphore=%p commands=%p", reinterpret_cast(state->acquireFence), reinterpret_cast(state->renderFence), reinterpret_cast(state->presentFence), reinterpret_cast(state->rendered), reinterpret_cast(state->clearCommands)); + state->scaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); } } @@ -473,6 +482,10 @@ void* VulkanDevice::Window() const { void VulkanDevice::Resize(std::uint32_t width, std::uint32_t height) { APS5_LOG_OUT("Resize requested=%ux%u current=%ux%u presentQueued=%u", width, height, state->extent.width, state->extent.height, static_cast(state->presentQueued)); require(state->swapchain != VK_NULL_HANDLE && !state->presentQueued, "cannot resize an unavailable or pending swapchain"); + if (width == 0 || height == 0) { + state->extent = {0, 0}; + return; + } if (state->extent.width == width && state->extent.height == height) return; WaitIdle(); VkSurfaceCapabilitiesKHR surface{}; @@ -506,6 +519,10 @@ void VulkanDevice::Resize(std::uint32_t width, std::uint32_t height) { APS5_LOG_OUT("Resize complete swapchain=%p extent=%ux%u images=%u", reinterpret_cast(state->swapchain), state->extent.width, state->extent.height, count); } +bool VulkanDevice::Presentable() const { + return state->extent.width != 0 && state->extent.height != 0; +} + std::uint64_t VulkanDevice::PresentClear(std::uint32_t width, std::uint32_t height, bool opaque) { APS5_LOG_OUT("PresentClear width=%u height=%u opaque=%u", width, height, static_cast(opaque)); return present(width, height, opaque, {}); @@ -521,7 +538,7 @@ std::uint64_t VulkanDevice::PresentPixels(std::uint32_t width, std::uint32_t hei std::uint64_t VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels) { APS5_LOG_OUT("present begin width=%u height=%u opaque=%u pixels=%zu swapchain=%p extent=%ux%u queued=%u id=%llu", width, height, static_cast(opaque), pixels.size(), reinterpret_cast(state->swapchain), state->extent.width, state->extent.height, static_cast(state->presentQueued), static_cast(state->presentId)); require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain"); - require(width == state->extent.width && height == state->extent.height, "output resize is not implemented"); + require(state->extent.width != 0 && state->extent.height != 0, "output window is minimized"); require(!state->presentQueued, "previous presentation has not completed"); require(state->presentId != std::numeric_limits::max(), "presentation ID overflow"); if (!pixels.empty()) state->Upload(pixels); @@ -559,11 +576,24 @@ std::uint64_t VulkanDevice::present(std::uint32_t width, std::uint32_t height, b clear.float32[3] = opaque ? 1.0f : 0.0f; state->DeviceFunction("vkCmdClearColorImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &barrier.subresourceRange); } else { - APS5_LOG_OUT("Recording swapchain upload width=%u height=%u bytes=%zu buffer=%p image=%p", width, height, pixels.size(), reinterpret_cast(state->uploadBuffer), reinterpret_cast(barrier.image)); - VkBufferImageCopy copy{}; - copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - copy.imageExtent = {width, height, 1}; - state->DeviceFunction("vkCmdCopyBufferToImage")(commands, state->uploadBuffer, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + APS5_LOG_OUT("Recording swapchain scaled blit width=%u height=%u bytes=%zu buffer=%p image=%p", width, height, pixels.size(), reinterpret_cast(state->uploadBuffer), reinterpret_cast(barrier.image)); + require(state->scaler != nullptr, "presentation scaler is unavailable"); + state->scaler->EnsureSourceImage(width, height); + state->scaler->RecordUpload(commands, state->uploadBuffer); + VkClearColorValue letterbox{}; + letterbox.float32[3] = 1.0f; + state->DeviceFunction("vkCmdClearColorImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &letterbox, 1, &barrier.subresourceRange); + VkImageMemoryBarrier letterboxBarrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + letterboxBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + letterboxBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + letterboxBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + letterboxBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + letterboxBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + letterboxBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + letterboxBarrier.image = barrier.image; + letterboxBarrier.subresourceRange = barrier.subresourceRange; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &letterboxBarrier); + state->scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height); } barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/Visual.cpp b/core/libs/prx/libSceAgcDriver/tests/Visual.cpp index 3c0a34659..8e62ed74c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Visual.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Visual.cpp @@ -39,6 +39,12 @@ void Run(SDL_Window* window, const std::filesystem::path& directory, bool verify VkSurfaceKHR surface = VK_NULL_HANDLE; Require(SDL_Vulkan_CreateSurface(static_cast(context), instance, &surface) == SDL_TRUE, SDL_GetError()); return surface; + }, [](void* context, std::uint32_t* width, std::uint32_t* height) { + int drawableWidth = 0; + int drawableHeight = 0; + SDL_Vulkan_GetDrawableSize(static_cast(context), &drawableWidth, &drawableHeight); + *width = drawableWidth > 0 ? static_cast(drawableWidth) : 0; + *height = drawableHeight > 0 ? static_cast(drawableHeight) : 0; }, Width, Height}; AgcDriver::VulkanDevice device(&presentation); auto vertex = LoadShader(directory / "Triangle.vert.spv"); diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 8ee5513c0..fc0636c16 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(libSceVideoOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/Buffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Output.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/VideoOutDriver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/DisplayWindow.cpp ) target_include_directories(libSceVideoOut PRIVATE @@ -11,6 +12,9 @@ target_include_directories(libSceVideoOut PRIVATE ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include ) target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libc libkernel SDL2-static) +if(WIN32) + target_link_libraries(libSceVideoOut PRIVATE comctl32) +endif() set_target_properties(libSceVideoOut PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(libSceVideoOut) diff --git a/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp b/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp new file mode 100644 index 000000000..666b9c5b6 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp @@ -0,0 +1,36 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_DISPLAYWINDOW_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_DISPLAYWINDOW_HPP + +#include +#include "SDL.h" + +inline constexpr std::uint32_t DisplayWindowMinimumWidth = 320; +inline constexpr std::uint32_t DisplayWindowMinimumHeight = 180; + +class DisplayWindow { +public: + DisplayWindow() = default; + ~DisplayWindow(); + DisplayWindow(const DisplayWindow&) = delete; + DisplayWindow& operator=(const DisplayWindow&) = delete; + + void Ensure(std::uint32_t sourceWidth, std::uint32_t sourceHeight); + void Destroy() noexcept; + SDL_Window* Handle() const; + void DrawableSize(std::uint32_t& width, std::uint32_t& height) const; + +private: + void create(std::uint32_t sourceWidth, std::uint32_t sourceHeight); + void updateAspectRatio(std::uint32_t sourceWidth, std::uint32_t sourceHeight); + void installSubclass(); + void removeSubclass() noexcept; + void applyAspectRatio(std::uintptr_t edge, void* rect) const; + + static std::intptr_t __stdcall windowProc(void* hwnd, unsigned int message, std::uintptr_t wParam, std::intptr_t lParam, std::uintptr_t subclassId, std::uintptr_t referenceData); + + SDL_Window* window = nullptr; + std::uint32_t aspectWidth = 0; + std::uint32_t aspectHeight = 0; +}; + +#endif diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 72f562e56..6a6a049fd 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -14,6 +14,7 @@ #include #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "SDL.h" #include "SceTypes.hpp" @@ -182,7 +183,7 @@ private: std::array, VIDEO_OUT_NUM_MAX> outputs; std::shared_ptr flipQueue = std::make_shared(); - SDL_Window* window = nullptr; + DisplayWindow window; std::jthread presentThread; std::jthread vblankThread; diff --git a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp new file mode 100644 index 000000000..179483edf --- /dev/null +++ b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp @@ -0,0 +1,138 @@ +#include "prx/libSceVideoOut/include/DisplayWindow.hpp" +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" +#include "SDL_vulkan.h" +#include +#include + +#ifdef _WIN32 +#include "SDL_syswm.h" +#include +#include +#endif + +namespace { + +void require(bool condition, const char* reason) { + if (!condition) throw std::runtime_error(std::string("DisplayWindow: ") + reason); +} + +#ifdef _WIN32 +constexpr UINT_PTR DisplayWindowSubclassId = 0x41505335u; +#endif + +} + +DisplayWindow::~DisplayWindow() { + Destroy(); +} + +void DisplayWindow::Ensure(std::uint32_t sourceWidth, std::uint32_t sourceHeight) { + require(sourceWidth != 0 && sourceHeight != 0, "source extent must be non-zero"); + if (window == nullptr) create(sourceWidth, sourceHeight); + updateAspectRatio(sourceWidth, sourceHeight); +} + +void DisplayWindow::create(std::uint32_t sourceWidth, std::uint32_t sourceHeight) { + SDL_Rect usable{}; + require(SDL_GetDisplayUsableBounds(0, &usable) == 0, SDL_GetError()); + std::uint32_t initialWidth = sourceWidth; + std::uint32_t initialHeight = sourceHeight; + if (usable.w > 0 && usable.h > 0 && (sourceWidth > static_cast(usable.w) || sourceHeight > static_cast(usable.h))) { + const auto fitted = AgcDriver::ComputeContainSize_nid_postfix(sourceWidth, sourceHeight, static_cast(usable.w), static_cast(usable.h), false); + initialWidth = fitted.width; + initialHeight = fitted.height; + } + window = SDL_CreateWindow("PS5", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast(initialWidth), static_cast(initialHeight), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); + require(window != nullptr, SDL_GetError()); + SDL_SetWindowMinimumSize(window, static_cast(DisplayWindowMinimumWidth), static_cast(DisplayWindowMinimumHeight)); + installSubclass(); +} + +void DisplayWindow::updateAspectRatio(std::uint32_t sourceWidth, std::uint32_t sourceHeight) { + aspectWidth = sourceWidth; + aspectHeight = sourceHeight; +} + +void DisplayWindow::Destroy() noexcept { + if (window == nullptr) return; + removeSubclass(); + SDL_DestroyWindow(window); + window = nullptr; +} + +SDL_Window* DisplayWindow::Handle() const { + return window; +} + +void DisplayWindow::DrawableSize(std::uint32_t& width, std::uint32_t& height) const { + if (window == nullptr) { + width = 0; + height = 0; + return; + } + int drawableWidth = 0; + int drawableHeight = 0; + SDL_Vulkan_GetDrawableSize(window, &drawableWidth, &drawableHeight); + width = drawableWidth > 0 ? static_cast(drawableWidth) : 0; + height = drawableHeight > 0 ? static_cast(drawableHeight) : 0; +} + +void DisplayWindow::installSubclass() { +#ifdef _WIN32 + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + require(SDL_GetWindowWMInfo(window, &info) == SDL_TRUE, SDL_GetError()); + require(info.subsystem == SDL_SYSWM_WINDOWS, "unsupported window subsystem"); + const auto attached = SetWindowSubclass(info.info.win.window, reinterpret_cast(&DisplayWindow::windowProc), DisplayWindowSubclassId, reinterpret_cast(this)); + require(attached != FALSE, "SetWindowSubclass failed"); +#endif +} + +void DisplayWindow::removeSubclass() noexcept { +#ifdef _WIN32 + if (window == nullptr) return; + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + if (SDL_GetWindowWMInfo(window, &info) != SDL_TRUE) return; + if (info.subsystem != SDL_SYSWM_WINDOWS) return; + RemoveWindowSubclass(info.info.win.window, reinterpret_cast(&DisplayWindow::windowProc), DisplayWindowSubclassId); +#endif +} + +void DisplayWindow::applyAspectRatio(std::uintptr_t edge, void* rect) const { +#ifdef _WIN32 + if (aspectWidth == 0 || aspectHeight == 0) return; + auto* bounds = static_cast(rect); + const auto currentWidth = static_cast(bounds->right - bounds->left); + const auto currentHeight = static_cast(bounds->bottom - bounds->top); + if (edge == WMSZ_TOP || edge == WMSZ_BOTTOM) { + const auto width = AgcDriver::ComputeWidthForHeight_nid_postfix(aspectWidth, aspectHeight, currentHeight); + bounds->right = bounds->left + static_cast(width); + return; + } + const auto height = AgcDriver::ComputeHeightForWidth_nid_postfix(aspectWidth, aspectHeight, currentWidth); + if (edge == WMSZ_TOPLEFT || edge == WMSZ_TOPRIGHT) bounds->top = bounds->bottom - static_cast(height); + else bounds->bottom = bounds->top + static_cast(height); +#else + static_cast(edge); + static_cast(rect); +#endif +} + +std::intptr_t __stdcall DisplayWindow::windowProc(void* hwnd, unsigned int message, std::uintptr_t wParam, std::intptr_t lParam, std::uintptr_t subclassId, std::uintptr_t referenceData) { +#ifdef _WIN32 + static_cast(subclassId); + if (message == WM_SIZING) { + reinterpret_cast(referenceData)->applyAspectRatio(wParam, reinterpret_cast(lParam)); + } + return DefSubclassProc(static_cast(hwnd), message, static_cast(wParam), static_cast(lParam)); +#else + static_cast(hwnd); + static_cast(message); + static_cast(wParam); + static_cast(lParam); + static_cast(subclassId); + static_cast(referenceData); + return 0; +#endif +} diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 204dcde4c..11e65c25e 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -182,9 +182,9 @@ void VideoOutDriver::Shutdown() { flipQueue->changed.notify_all(); presentThread.join(); vblankThread.join(); - if (window != nullptr) { - AgcDriverReleaseWindow_nid_postfix(window); - SDL_DestroyWindow(window); + if (window.Handle() != nullptr) { + AgcDriverReleaseWindow_nid_postfix(window.Handle()); + window.Destroy(); } SDL_QuitSubSystem(SDL_INIT_VIDEO); stopped = true; @@ -323,24 +323,22 @@ void VideoOutDriver::processFlip(FlipRequest& req) { checkConfig(*req.cfg); } require(req.width != 0 && req.height != 0 && req.width <= static_cast(std::numeric_limits::max()) && req.height <= static_cast(std::numeric_limits::max()), "invalid window dimensions"); - if (window == nullptr) { - window = SDL_CreateWindow("PS5", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast(req.width), static_cast(req.height), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN); - if (window == nullptr) throw std::runtime_error(std::string("SDL_CreateWindow failed: ") + SDL_GetError()); - } else { - int width = 0; - int height = 0; - SDL_GetWindowSize(window, &width, &height); - if (width != static_cast(req.width) || height != static_cast(req.height)) SDL_SetWindowSize(window, static_cast(req.width), static_cast(req.height)); - } + window.Ensure(req.width, req.height); unsigned extensionCount = 0; - if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr)) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); + if (!SDL_Vulkan_GetInstanceExtensions(window.Handle(), &extensionCount, nullptr)) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); std::vector extensions(extensionCount); - if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, extensions.data())) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); + if (!SDL_Vulkan_GetInstanceExtensions(window.Handle(), &extensionCount, extensions.data())) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); extensions.resize(extensionCount); - const AgcDriver::PresentationWindow target{window, extensions, [](void* context, VkInstance instance) { + const AgcDriver::PresentationWindow target{window.Handle(), extensions, [](void* context, VkInstance instance) { VkSurfaceKHR surface = VK_NULL_HANDLE; if (!SDL_Vulkan_CreateSurface(static_cast(context), instance, &surface)) throw std::runtime_error(std::string("SDL_Vulkan_CreateSurface failed: ") + SDL_GetError()); return surface; + }, [](void* context, std::uint32_t* width, std::uint32_t* height) { + int drawableWidth = 0; + int drawableHeight = 0; + SDL_Vulkan_GetDrawableSize(static_cast(context), &drawableWidth, &drawableHeight); + *width = drawableWidth > 0 ? static_cast(drawableWidth) : 0; + *height = drawableHeight > 0 ? static_cast(drawableHeight) : 0; }, req.width, req.height}; const auto gpuReady = [](void* context) { auto& request = *static_cast(context);