From 6142e82aa05bfa88046983fcd06cbf6541eb3c0a Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Fri, 2 Oct 2026 10:33:05 +1300 Subject: [PATCH] perf(agc): keep the 10-bit scanout resident and present it with the guest path's pixels PPSA21564's final composite renders into 4K 10-bit scanout buffers (COLOR_2_10_10_10 with the alternate swap, A2R10G10B10). No guest texture format matched that color format, so the target stayed non-resident: every frame's composite draw ran synchronously (about 11 ms, 143 per 10 s at the title) and the flip read the frame back through guest memory. The blue-low 10-bit color target now keeps its bits in the storage image of its surface's red-low 2-10-10-10 format, as B8G8R8A8 targets map to R8G8B8A8. Presenting it: DisplayTexelFormat names a display buffer's texel order as the guest-memory path reads it (R8G8B8A8 base: red low, else blue low; the 10-bit variant: A2B10G10R10 or A2R10G10B10). ResidentPresentPath blits a resident image only when it holds the display's 8-bit texel format (an exact copy). A 10-bit image (whose blit would round where the guest path drops the low two bits), the other 8-bit order or an sRGB type is converted instead: GpuColorTransfer::DetileImage copies the image's texels raw and runs the guest path's own conversion shader on them, so both paths present the same pixels. The image's write-back is unchanged: its results stay pending like any resident target's and are stored when a reader of the memory flushes them. Tests: agc_driver_recorder_tests checks the format decision for both 8-bit and both 10-bit orders, and converts an image in each order, comparing it with the CPU decode and the GPU detile of the same words in guest memory. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 + .../Execution/include/DisplayFormat.hpp | 19 +++ .../Execution/include/VulkanDevice.hpp | 2 +- .../Execution/src/DisplayFormat.cpp | 42 +++++ .../Execution/src/VulkanDevice.cpp | 54 +++--- .../Graphics/include/GpuColorTransfer.hpp | 2 + .../Graphics/src/GpuColorTransfer.cpp | 26 ++- .../Graphics/src/TextureFormat.cpp | 1 + .../prx/libSceAgcDriver/tests/Recorder.cpp | 2 + .../libSceAgcDriver/tests/ResidentPresent.cpp | 159 ++++++++++++++++++ .../libSceAgcDriver/tests/ResidentPresent.hpp | 8 + 11 files changed, 283 insertions(+), 35 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/ResidentPresent.hpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 21b44c72a..b0b0993b7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -49,6 +49,7 @@ add_custom_command( set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayBuffer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayFormat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/DeviceAccess.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Diagnostics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Dispatch/DataHitVerification.cpp @@ -266,6 +267,7 @@ add_custom_command( add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL tests/Recorder.cpp + tests/ResidentPresent.cpp ${agcSampleLodShaderHeader} ) @@ -453,6 +455,7 @@ if(AGC_BUILD_VISUAL_TEST) add_test_executable(agc_driver_visual_test EXCLUDE_FROM_ALL tests/Visual.cpp Execution/src/VulkanDevice.cpp + Execution/src/DisplayFormat.cpp Execution/src/AspectFit.cpp Execution/src/PresentationScaler.cpp Execution/src/GuestMemory.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp new file mode 100644 index 000000000..fc18cff57 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp @@ -0,0 +1,19 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DISPLAYFORMAT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DISPLAYFORMAT_HPP + +#define VK_NO_PROTOTYPES +#include +#include + +namespace AgcDriver { + +VkFormat DisplayTexelFormat(std::uint64_t pixelFormat); +bool DisplayTenBit(std::uint64_t pixelFormat); +bool DisplayRedLow(std::uint64_t pixelFormat); + +enum class ResidentPresent : std::uint8_t { None, Blit, Convert }; +ResidentPresent ResidentPresentPath(VkFormat storage, std::uint64_t pixelFormat, bool blitSource); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 02cd2611c..4a7f70aee 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -304,7 +304,7 @@ private: // The tail of a dispatch's device call from the open batch's command buffer to the completion // registration: keeps, the template data refresh, barriers, bind, push, dispatch, marks. void recordDispatch(RecordedDispatch& record); - bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false); + bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false); struct State; std::unique_ptr state; std::uint64_t serial; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp new file mode 100644 index 000000000..7a013fdbe --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp @@ -0,0 +1,42 @@ +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" + +namespace AgcDriver { +namespace { + +constexpr std::uint64_t PixelFormatUnormBit = 0x0100000000000000ull; +constexpr std::uint64_t PixelFormatR8G8B8A8 = 0x8000000022000000ull; + +} + +bool DisplayTenBit(std::uint64_t pixelFormat) { + return (pixelFormat & PixelFormatUnormBit) != 0; +} + +bool DisplayRedLow(std::uint64_t pixelFormat) { + return (pixelFormat & ~PixelFormatUnormBit) == PixelFormatR8G8B8A8; +} + +VkFormat DisplayTexelFormat(std::uint64_t pixelFormat) { + const bool redLow = DisplayRedLow(pixelFormat); + if (DisplayTenBit(pixelFormat)) return redLow ? VK_FORMAT_A2B10G10R10_UNORM_PACK32 : VK_FORMAT_A2R10G10B10_UNORM_PACK32; + return redLow ? VK_FORMAT_R8G8B8A8_UNORM : VK_FORMAT_B8G8R8A8_UNORM; +} + +ResidentPresent ResidentPresentPath(VkFormat storage, std::uint64_t pixelFormat, bool blitSource) { + const bool tenBit = DisplayTenBit(pixelFormat); + if (!tenBit && blitSource && storage == DisplayTexelFormat(pixelFormat)) return ResidentPresent::Blit; + switch (storage) { + case VK_FORMAT_A2B10G10R10_UNORM_PACK32: + case VK_FORMAT_A2R10G10B10_UNORM_PACK32: + return tenBit ? ResidentPresent::Convert : ResidentPresent::None; + case VK_FORMAT_R8G8B8A8_UNORM: + case VK_FORMAT_R8G8B8A8_SRGB: + case VK_FORMAT_B8G8R8A8_UNORM: + case VK_FORMAT_B8G8R8A8_SRGB: + return tenBit ? ResidentPresent::None : ResidentPresent::Convert; + default: + return ResidentPresent::None; + } +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 2b859eb91..29361657c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -1688,33 +1689,26 @@ void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, } } -// Whether a resident image of the display buffer can be blitted to the swapchain as it is: one 2D -// level of the display's extent in a normalized 4-byte color format (a blit converts components, -// so an integer or float reinterpretation would present wrong values). `filter` gets the best -// filter the format allows. -bool ResidentPresentable(const Graphics::Context& context, const Graphics::StorageTexture& image, const DisplayBuffer& buffer, VkFilter& filter) { +ResidentPresent ResidentPresentable(const Graphics::Context& context, const Graphics::StorageTexture& image, const DisplayBuffer& buffer, VkFilter& filter) { const auto& descriptor = image.Descriptor(); - if (descriptor.width != buffer.width || descriptor.height != buffer.height || descriptor.mipCount != 1 || image.ImageLayers() != 1 || image.ImageDepth() != 1) return false; - if (image.GuestBytes() != DisplayBufferSize(buffer)) return false; + if (descriptor.width != buffer.width || descriptor.height != buffer.height || descriptor.mipCount != 1 || image.ImageLayers() != 1 || image.ImageDepth() != 1) return ResidentPresent::None; + if (image.GuestBytes() != DisplayBufferSize(buffer)) return ResidentPresent::None; const auto format = Graphics::StorageFormatForGuest(context, descriptor.format); - constexpr std::uint64_t tenBitFormat = 0x0100000000000000ull; - const bool tenBit = (buffer.pixelFormat & tenBitFormat) != 0; - const bool rgba = (buffer.pixelFormat & ~tenBitFormat) == 0x8000000022000000ull; - const auto displayFormat = tenBit ? VK_FORMAT_A2B10G10R10_UNORM_PACK32 : rgba ? VK_FORMAT_R8G8B8A8_UNORM : VK_FORMAT_B8G8R8A8_UNORM; - if (format != displayFormat) return false; VkFormatProperties properties{}; context.formatProperties(context.physical, format, &properties); - if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) == 0) return false; + const bool blitSource = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0; filter = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0 ? VK_FILTER_LINEAR : VK_FILTER_NEAREST; - return true; + return ResidentPresentPath(format, buffer.pixelFormat, blitSource); } -// The resident image a display buffer is blitted from, or null; `pending` tells a buffer without -// a pending image from one whose image is unsuitable. -std::shared_ptr PresentableResident(const Graphics::Context& context, const DisplayBuffer& buffer, VkFilter& filter, bool& pending) { +std::shared_ptr PresentableResident(const Graphics::Context& context, const DisplayBuffer& buffer, VkFilter& filter, bool& pending, bool& convert) { auto resident = Graphics::StorageTexture::FindPending(buffer.address, DisplayBufferSize(buffer)); pending = resident != nullptr; - if (resident != nullptr && !ResidentPresentable(context, *resident, buffer, filter)) resident.reset(); + convert = false; + if (resident == nullptr) return resident; + const auto path = ResidentPresentable(context, *resident, buffer, filter); + if (path == ResidentPresent::None) resident.reset(); + convert = path == ResidentPresent::Convert; return resident; } @@ -1763,9 +1757,10 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { const auto bytes = DisplayBufferSize(buffer); std::shared_ptr resident; VkFilter filter = VK_FILTER_LINEAR; + bool convert = false; if (!NoResidentPresent()) { bool pending = false; - resident = PresentableResident(graphicsContext(), buffer, filter, pending); + resident = PresentableResident(graphicsContext(), buffer, filter, pending, convert); if (!pending) { ++notPending; } else if (resident == nullptr) { @@ -1801,7 +1796,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { std::fprintf(stderr, "[flip] %llu presents from the resident image (%llu refreshed first), through guest memory: %llu not pending, %llu unsuitable; %llu GPU frame dumps\n", static_cast(residentPresents), static_cast(refreshedPresents), static_cast(notPending), static_cast(unsuitable), static_cast(gpuDumps)); } CaptureTrace::Log("present dump=%d address=%llx width=%u height=%u resident=%d generation=%llu", dumpFrame ? state->nextDumpIndex : -1, static_cast(buffer.address), buffer.width, buffer.height, resident != nullptr, static_cast(resident ? resident->Generation() : 0)); - if (!present(buffer.width, buffer.height, true, {}, &buffer, resident, filter, dumpFrame)) { + if (!present(buffer.width, buffer.height, true, {}, &buffer, resident, filter, dumpFrame, convert)) { // A dropped frame (swapchain out of date) keeps the dump numbering contiguous. if (dumpFrame) --dumps.dumped; return false; @@ -1846,7 +1841,7 @@ bool VulkanDevice::AcquireImage() { return true; } -bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame) { +bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert) { PerformanceTimer timing("Vulkan.Present"); APS5_LOG_OUT_DEBUG("present begin width=%u height=%u opaque=%u pixels=%zu", width, height, static_cast(opaque), pixels.size()); require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain"); @@ -1861,7 +1856,8 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu require(index < state->images.size() && index < state->rendered.size(), "acquired image index is out of range"); auto& rendered = state->rendered[index]; const bool clearOnly = pixels.empty() && display == nullptr; - const bool direct = resident != nullptr; + require(!residentConvert || (resident != nullptr && display != nullptr), "a converted resident presentation needs its image and display buffer"); + const bool direct = resident != nullptr && !residentConvert; // The scaler's source image, the color transfer's staging and the upload buffer are single // objects an in-flight blit through them may still read: the paths using or re-creating them // wait for every slot first. The game path did so before taking the mutex @@ -1942,14 +1938,13 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu residentBarrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS}; pipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); - if (!direct) state->scaler->RecordBlitInto(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, residentFilter); + if (residentConvert) { + state->colorTransfer->DetileImage(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, Graphics::ColorTileMode::RenderTarget, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat)); + state->scaler->RecordUpload(commands, state->colorTransfer->LinearBuffer()); + } } else { if (display != nullptr) { - // Bit 56 selects the 10-bit A2B10G10R10 variant; R8G8B8A8-ordered formats need red and blue - // swapped for the B8G8R8A8 swapchain. - constexpr std::uint64_t tenBitFormat = 0x0100000000000000ull; - const bool tenBit = (display->pixelFormat & tenBitFormat) != 0; - state->colorTransfer->Detile(commands, (display->pixelFormat & ~tenBitFormat) == 0x8000000022000000ull, tenBit); + state->colorTransfer->Detile(commands, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat)); } state->scaler->RecordUpload(commands, display != nullptr ? state->colorTransfer->LinearBuffer() : state->uploadBuffer); } @@ -2076,7 +2071,8 @@ bool VulkanDevice::PresentWaitsForSlots(const DisplayBuffer* buffer) const { if (NoResidentPresent()) return true; VkFilter filter = VK_FILTER_LINEAR; bool pending = false; - if (PresentableResident(graphicsContext(), *buffer, filter, pending) == nullptr) return true; + bool convert = false; + if (PresentableResident(graphicsContext(), *buffer, filter, pending, convert) == nullptr || convert) return true; return state->scaler == nullptr || state->scaler->SourceWidth() != buffer->width || state->scaler->SourceHeight() != buffer->height; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp index 63c5724fb..3d1aaf2ba 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp @@ -14,6 +14,7 @@ public: GpuColorTransfer& operator=(const GpuColorTransfer&) = delete; void Upload(std::uint64_t address, std::uint32_t width, std::uint32_t height, ColorTileMode mode); void Detile(VkCommandBuffer commands, bool swapRedBlue = false, bool tenBit = false); + void DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, ColorTileMode mode, bool swapRedBlue, bool tenBit); void Tile(VkCommandBuffer commands); void WriteBack(std::uint64_t address); VkBuffer LinearBuffer() const; @@ -22,6 +23,7 @@ public: private: void prepare(std::uint32_t width, std::uint32_t height, ColorTileMode mode); void convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit = false); + void dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource); void release() noexcept; Context context; VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp index 55c236b72..346ba590a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp @@ -104,12 +104,20 @@ void GpuColorTransfer::convert(VkCommandBuffer commands, bool toTiled, bool swap // The guest bytes move to video memory with one DMA copy; the shader's scattered accesses stay local. RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); CopyBuffer(context, commands, tiled->Handle(), 0, tiledDevice->Handle(), 0, tiledDevice->Size()); + dispatch(commands, toTiled, swapRedBlue, tenBit, mode == ColorTileMode::RenderTarget); + if (toTiled) { + CopyBuffer(context, commands, tiledDevice->Handle(), 0, tiled->Handle(), 0, tiledDevice->Size()); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + } +} + +void GpuColorTransfer::dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource) { VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; before.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; before.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; const auto barrier = context.Function("vkCmdPipelineBarrier"); barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr); - const std::array push{width, height, (width + 127u) / 128u, (toTiled ? 1u : 0u) | (swapRedBlue ? 2u : 0u) | (mode == ColorTileMode::RenderTarget ? 4u : 0u) | (tenBit ? 8u : 0u)}; + const std::array push{width, height, (width + 127u) / 128u, (toTiled ? 1u : 0u) | (swapRedBlue ? 2u : 0u) | (tiledSource ? 4u : 0u) | (tenBit ? 8u : 0u)}; context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr); context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), push.data()); @@ -118,10 +126,18 @@ void GpuColorTransfer::convert(VkCommandBuffer commands, bool toTiled, bool swap after.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; after.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; barrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &after, 0, nullptr, 0, nullptr); - if (toTiled) { - CopyBuffer(context, commands, tiledDevice->Handle(), 0, tiled->Handle(), 0, tiledDevice->Size()); - RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); - } +} + +void GpuColorTransfer::DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t newWidth, std::uint32_t newHeight, ColorTileMode newMode, bool swapRedBlue, bool tenBit) { + Require(commands != VK_NULL_HANDLE && image != VK_NULL_HANDLE, "color transfer image source is unavailable"); + prepare(newWidth, newHeight, newMode); + Require(tiledDevice->Size() >= static_cast(width) * height * 4u, "color transfer buffer is smaller than the image"); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {width, height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image, layout, tiledDevice->Handle(), 1, ©); + dispatch(commands, false, swapRedBlue, tenBit, false); } void GpuColorTransfer::Detile(VkCommandBuffer commands, bool swapRedBlue, bool tenBit) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index 31affce9c..56499ba2b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -123,6 +123,7 @@ std::optional FindGuestColorTargetFormat(VkFormat format, std::ui switch (format) { case VK_FORMAT_B8G8R8A8_UNORM: format = VK_FORMAT_R8G8B8A8_UNORM; break; case VK_FORMAT_B8G8R8A8_SRGB: format = VK_FORMAT_R8G8B8A8_SRGB; break; + case VK_FORMAT_A2R10G10B10_UNORM_PACK32: format = VK_FORMAT_A2B10G10R10_UNORM_PACK32; break; default: break; } return FindGuestTextureFormat(format, elementBytes); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 8ad28f8fe..3a28aad68 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -14,6 +14,7 @@ #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" +#include "ResidentPresent.hpp" #include "SampleLod_spv.h" #include #ifdef _WIN32 @@ -2288,6 +2289,7 @@ int main() { drawSnapshotReuseTests(device, recorder); drawSnapshotEvictionTests(device); drawInputReuseTests(device, recorder); + RunResidentPresentTests(device.GetContext()); storeRunTests(device, recorder); movedMetadataTests(device, recorder); keysFillTests(device, recorder); diff --git a/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp new file mode 100644 index 000000000..b535571d2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp @@ -0,0 +1,159 @@ +#include "ResidentPresent.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using AgcDriver::DisplayBuffer; +using AgcDriver::ResidentPresent; + +constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; +constexpr std::uint64_t Rgba8 = 0x8000000022000000ull; +constexpr std::uint64_t TenBit = 0x0100000000000000ull; + +void decisionTests() { + using AgcDriver::DisplayTexelFormat; + using AgcDriver::ResidentPresentPath; + Require(DisplayTexelFormat(Bgra8) == VK_FORMAT_B8G8R8A8_UNORM && DisplayTexelFormat(Rgba8) == VK_FORMAT_R8G8B8A8_UNORM, "8-bit display texel formats are wrong"); + Require(DisplayTexelFormat(Bgra8 | TenBit) == VK_FORMAT_A2R10G10B10_UNORM_PACK32 && DisplayTexelFormat(Rgba8 | TenBit) == VK_FORMAT_A2B10G10R10_UNORM_PACK32, "10-bit display texel formats are wrong"); + Require(ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, Bgra8, true) == ResidentPresent::Blit && ResidentPresentPath(VK_FORMAT_R8G8B8A8_UNORM, Rgba8, true) == ResidentPresent::Blit, "an 8-bit image in its display's order is not blitted"); + Require(ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, Bgra8, false) == ResidentPresent::Convert, "an 8-bit image that cannot be a blit source is not converted"); + Require(ResidentPresentPath(VK_FORMAT_R8G8B8A8_UNORM, Bgra8, true) == ResidentPresent::Convert && ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, Rgba8, true) == ResidentPresent::Convert, "an 8-bit image in the other order is not converted"); + Require(ResidentPresentPath(VK_FORMAT_R8G8B8A8_SRGB, Rgba8, true) == ResidentPresent::Convert, "an sRGB image is blitted (the blit would decode it)"); + for (const auto display : {Bgra8 | TenBit, Rgba8 | TenBit}) { + for (const auto storage : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) { + Require(ResidentPresentPath(storage, display, true) == ResidentPresent::Convert, "a 10-bit image is blitted (the blit rounds where the guest path truncates)"); + } + Require(ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, display, true) == ResidentPresent::None, "an 8-bit image presents a 10-bit display"); + } + Require(ResidentPresentPath(VK_FORMAT_A2B10G10R10_UNORM_PACK32, Bgra8, true) == ResidentPresent::None && ResidentPresentPath(VK_FORMAT_R16G16_SFLOAT, Bgra8, true) == ResidentPresent::None, "an image of another type presents an 8-bit display"); + const auto guest = FindGuestColorTargetFormat(VK_FORMAT_A2R10G10B10_UNORM_PACK32, 4); + Require(guest.has_value() && guest == FindGuestTextureFormat(VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4), "the blue-low 10-bit color target has no storage format"); +} + +std::uint32_t memoryType(const Context& context, std::uint32_t bits, VkMemoryPropertyFlags flags) { + for (std::uint32_t i = 0; i < context.memory.memoryTypeCount; ++i) { + if ((bits & (1u << i)) != 0 && (context.memory.memoryTypes[i].propertyFlags & flags) == flags) return i; + } + throw std::runtime_error("no device-local memory type for the test image"); +} + +void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat format) { + constexpr std::uint32_t width = 200; + constexpr std::uint32_t height = 130; + std::vector words(static_cast(width) * height * 4); + std::uint32_t seed = 0x9e3779b9u ^ static_cast(format); + for (std::size_t i = 0; i < words.size(); ++i) { + seed = seed * 1664525u + 1013904223u; + words[i] = static_cast(seed >> 24u); + } + const ColorTargetLayout layout(width, height, ColorTileMode::RenderTarget); + std::vector storage(layout.Bytes() + 65536); + auto* aligned = reinterpret_cast((reinterpret_cast(storage.data()) + 65535u) & ~std::uintptr_t{65535}); + std::span guest(aligned, layout.Bytes()); + layout.Tile(words, guest); + const DisplayBuffer display{reinterpret_cast(aligned), pixelFormat, width, height}; + Require(AgcDriver::DisplayBufferSize(display) == guest.size(), "test display buffer size differs from the color layout"); + const auto expected = AgcDriver::DecodeDisplayBuffer(display, guest); + + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = format; + imageInfo.extent = {width, height, 1}; + 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_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage resident present test"); + struct Release { + const Context& context; + VkImage& image; + VkDeviceMemory& memory; + ~Release() { + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + } release{context, image, memory}; + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = memoryType(context, requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory resident present test"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory resident present test"); + + Buffer upload(context, words.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(upload.Bytes().data(), words.data(), words.size()); + Buffer fromImage(context, expected.size(), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + Buffer fromGuest(context, expected.size(), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + GpuColorTransfer transfer(context); + const bool redLow = AgcDriver::DisplayRedLow(pixelFormat); + const bool tenBit = AgcDriver::DisplayTenBit(pixelFormat); + const auto barrier = context.Function("vkCmdPipelineBarrier"); + const auto readback = [&](VkCommandBuffer commands, Buffer& destination) { + const VkBufferCopy copy{0, 0, expected.size()}; + context.Function("vkCmdCopyBuffer")(commands, transfer.LinearBuffer(), destination.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT); + }; + transfer.Upload(display.address, width, height, ColorTileMode::RenderTarget); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toTransfer.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toTransfer.srcQueueFamilyIndex = toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransfer.image = image; + toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + barrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toTransfer); + VkBufferImageCopy region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {width, height, 1}; + context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + auto toGeneral = toTransfer; + toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + transfer.Detile(commands, redLow, tenBit); + readback(commands, fromGuest); + transfer.DetileImage(commands, image, VK_IMAGE_LAYOUT_GENERAL, width, height, ColorTileMode::RenderTarget, redLow, tenBit); + readback(commands, fromImage); + batch.SubmitAndWait(); + } + const auto name = std::string(tenBit ? "10-bit " : "8-bit ") + (redLow ? "red-low" : "blue-low") + " display from VkFormat " + std::to_string(static_cast(format)); + Require(std::equal(expected.begin(), expected.end(), fromGuest.Bytes().begin()), "the GPU detile differs from the CPU decode: " + name); + Require(std::equal(expected.begin(), expected.end(), fromImage.Bytes().begin()), "the resident image presents other pixels than guest memory: " + name); +} + +} + +void RunResidentPresentTests(const Context& context) { + decisionTests(); + conversionTest(context, Bgra8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32); + conversionTest(context, Rgba8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32); + conversionTest(context, Bgra8 | TenBit, VK_FORMAT_A2R10G10B10_UNORM_PACK32); + conversionTest(context, Bgra8, VK_FORMAT_R8G8B8A8_UNORM); + conversionTest(context, Rgba8, VK_FORMAT_B8G8R8A8_UNORM); + std::cout << "resident present format and conversion tests passed\n"; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.hpp b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.hpp new file mode 100644 index 000000000..2d0181303 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_RESIDENTPRESENT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_RESIDENTPRESENT_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" + +void RunResidentPresentTests(const AgcDriver::Graphics::Context& context); + +#endif