feat(video): add resizable aspect-ratio-aware window

This commit is contained in:
boykopovar 2026-09-23 14:27:47 +03:00
parent b1722c06e3
commit 282257fd98
16 changed files with 476 additions and 38 deletions

View File

@ -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`.

View File

@ -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

View File

@ -0,0 +1,31 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ASPECTFIT_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ASPECTFIT_HPP
#include <cstdint>
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

View File

@ -14,6 +14,7 @@ struct PresentationWindow {
void* context;
std::span<const char* const> 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;
};

View File

@ -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 <vulkan/vulkan.h>
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include <cstdint>
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

View File

@ -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<const std::byte> pixels);
void WaitPresented(std::uint64_t id);

View File

@ -0,0 +1,53 @@
#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <string>
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<double>(std::numeric_limits<std::uint32_t>::max()), "computed extent is out of range");
return static_cast<std::uint32_t>(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<double>(boundsWidth) / static_cast<double>(sourceWidth), static_cast<double>(boundsHeight) / static_cast<double>(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<std::uint32_t>(1, roundToExtent(sourceWidth * scale)), std::max<std::uint32_t>(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<std::int64_t>(destinationWidth) - static_cast<std::int64_t>(size.width)) / 2;
const auto y = (static_cast<std::int64_t>(destinationHeight) - static_cast<std::int64_t>(size.height)) / 2;
return AspectFitRect{static_cast<std::int32_t>(x), static_cast<std::int32_t>(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<std::uint32_t>(1, roundToExtent(static_cast<double>(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<std::uint32_t>(1, roundToExtent(static_cast<double>(width) * sourceHeight / sourceWidth));
}
}
}

View File

@ -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<VulkanDevice> 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());

View File

@ -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<PFN_vkDestroyImage>("vkDestroyImage")(context.device, sourceImage, nullptr);
if (sourceMemory != VK_NULL_HANDLE) context.Function<PFN_vkFreeMemory>("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<PFN_vkCreateImage>("vkCreateImage")(context.device, &imageInfo, nullptr, &sourceImage), "vkCreateImage presentation source");
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetImageMemoryRequirements>("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<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &sourceMemory), "vkAllocateMemory presentation source");
Graphics::Check(context.Function<PFN_vkBindImageMemory>("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<PFN_vkCmdPipelineBarrier>("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<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, uploadBuffer, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy);
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<std::int32_t>(sourceWidth), static_cast<std::int32_t>(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<std::int32_t>(rect.width), rect.y + static_cast<std::int32_t>(rect.height), 1};
context.Function<PFN_vkCmdBlitImage>("vkCmdBlitImage")(commands, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destinationImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
}
}

View File

@ -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 <SDL_loadso.h>
@ -68,6 +69,7 @@ struct VulkanDevice::State {
bool textureCompressionBC = false;
std::unique_ptr<Graphics::TextureDetiler> detiler;
VkPhysicalDeviceMeshShaderPropertiesEXT meshLimits{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT};
std::unique_ptr<PresentationScaler> scaler;
template<typename TFunction>
TFunction InstanceFunction(const char* name) const {
@ -133,6 +135,7 @@ struct VulkanDevice::State {
const auto idle = reinterpret_cast<PFN_vkDeviceWaitIdle>(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<PFN_vkWaitForFences>(deviceProc(device, "vkWaitForFences"))(device, 1, &presentFence, VK_TRUE, std::numeric_limits<std::uint64_t>::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<const char*> 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<PFN_vkCreateCommandPool>("vkCreateCommandPool")(state->device, &poolInfo, nullptr, &state->pool), "vkCreateCommandPool");
state->detiler = std::make_unique<Graphics::TextureDetiler>(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<PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR>("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<std::uint32_t>::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<std::uint32_t>::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<PFN_vkAllocateCommandBuffers>("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<void*>(state->acquireFence), reinterpret_cast<void*>(state->renderFence), reinterpret_cast<void*>(state->presentFence), reinterpret_cast<void*>(state->rendered), reinterpret_cast<void*>(state->clearCommands));
state->scaler = std::make_unique<PresentationScaler>(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<unsigned>(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<void*>(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<unsigned>(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<const std::byte> 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<unsigned>(opaque), pixels.size(), reinterpret_cast<void*>(state->swapchain), state->extent.width, state->extent.height, static_cast<unsigned>(state->presentQueued), static_cast<unsigned long long>(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<std::uint64_t>::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<PFN_vkCmdClearColorImage>("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<void*>(state->uploadBuffer), reinterpret_cast<void*>(barrier.image));
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {width, height, 1};
state->DeviceFunction<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, state->uploadBuffer, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy);
APS5_LOG_OUT("Recording swapchain scaled blit width=%u height=%u bytes=%zu buffer=%p image=%p", width, height, pixels.size(), reinterpret_cast<void*>(state->uploadBuffer), reinterpret_cast<void*>(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<PFN_vkCmdClearColorImage>("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;

View File

@ -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<SDL_Window*>(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<SDL_Window*>(context), &drawableWidth, &drawableHeight);
*width = drawableWidth > 0 ? static_cast<std::uint32_t>(drawableWidth) : 0;
*height = drawableHeight > 0 ? static_cast<std::uint32_t>(drawableHeight) : 0;
}, Width, Height};
AgcDriver::VulkanDevice device(&presentation);
auto vertex = LoadShader(directory / "Triangle.vert.spv");

View File

@ -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)

View File

@ -0,0 +1,36 @@
#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_DISPLAYWINDOW_HPP
#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_DISPLAYWINDOW_HPP
#include <cstdint>
#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

View File

@ -14,6 +14,7 @@
#include <stdexcept>
#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<std::shared_ptr<AgcDriver::IVideoOutput>, VIDEO_OUT_NUM_MAX> outputs;
std::shared_ptr<FlipQueue> flipQueue = std::make_shared<FlipQueue>();
SDL_Window* window = nullptr;
DisplayWindow window;
std::jthread presentThread;
std::jthread vblankThread;

View File

@ -0,0 +1,138 @@
#include "prx/libSceVideoOut/include/DisplayWindow.hpp"
#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp"
#include "SDL_vulkan.h"
#include <stdexcept>
#include <string>
#ifdef _WIN32
#include "SDL_syswm.h"
#include <windows.h>
#include <commctrl.h>
#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<std::uint32_t>(usable.w) || sourceHeight > static_cast<std::uint32_t>(usable.h))) {
const auto fitted = AgcDriver::ComputeContainSize_nid_postfix(sourceWidth, sourceHeight, static_cast<std::uint32_t>(usable.w), static_cast<std::uint32_t>(usable.h), false);
initialWidth = fitted.width;
initialHeight = fitted.height;
}
window = SDL_CreateWindow("PS5", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast<int>(initialWidth), static_cast<int>(initialHeight), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
require(window != nullptr, SDL_GetError());
SDL_SetWindowMinimumSize(window, static_cast<int>(DisplayWindowMinimumWidth), static_cast<int>(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<std::uint32_t>(drawableWidth) : 0;
height = drawableHeight > 0 ? static_cast<std::uint32_t>(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<SUBCLASSPROC>(&DisplayWindow::windowProc), DisplayWindowSubclassId, reinterpret_cast<DWORD_PTR>(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<SUBCLASSPROC>(&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*>(rect);
const auto currentWidth = static_cast<std::uint32_t>(bounds->right - bounds->left);
const auto currentHeight = static_cast<std::uint32_t>(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<LONG>(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<LONG>(height);
else bounds->bottom = bounds->top + static_cast<LONG>(height);
#else
static_cast<void>(edge);
static_cast<void>(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<void>(subclassId);
if (message == WM_SIZING) {
reinterpret_cast<const DisplayWindow*>(referenceData)->applyAspectRatio(wParam, reinterpret_cast<void*>(lParam));
}
return DefSubclassProc(static_cast<HWND>(hwnd), message, static_cast<WPARAM>(wParam), static_cast<LPARAM>(lParam));
#else
static_cast<void>(hwnd);
static_cast<void>(message);
static_cast<void>(wParam);
static_cast<void>(lParam);
static_cast<void>(subclassId);
static_cast<void>(referenceData);
return 0;
#endif
}

View File

@ -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<uint32_t>(std::numeric_limits<int>::max()) && req.height <= static_cast<uint32_t>(std::numeric_limits<int>::max()), "invalid window dimensions");
if (window == nullptr) {
window = SDL_CreateWindow("PS5", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast<int>(req.width), static_cast<int>(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<int>(req.width) || height != static_cast<int>(req.height)) SDL_SetWindowSize(window, static_cast<int>(req.width), static_cast<int>(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<const char*> 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<SDL_Window*>(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<SDL_Window*>(context), &drawableWidth, &drawableHeight);
*width = drawableWidth > 0 ? static_cast<std::uint32_t>(drawableWidth) : 0;
*height = drawableHeight > 0 ? static_cast<std::uint32_t>(drawableHeight) : 0;
}, req.width, req.height};
const auto gpuReady = [](void* context) {
auto& request = *static_cast<FlipRequest*>(context);