From bafca2ffda5a783e5dfeba23813dcda71051e667 Mon Sep 17 00:00:00 2001 From: Raihan Abdul Majid Date: Fri, 9 Oct 2026 14:21:06 +0700 Subject: [PATCH] perf(agc): fill-clear small-float denormal patterns on devices whose clears keep them FillClearColor refused half and B10G11R11 patterns with a denormal code, since an implementation may flush them in a clear, so such fills over a resident image took the CPU path: PPSA01325 fast-clears its 3840x2160 R16G16_SFLOAT target 0x515390000 with a denormal half every frame, a 33 MiB guest write and re-upload (about 85 ms). ClearKeepsDenormals probes the device once per format: a 1x1 image cleared with denormal codes (half 0x0001, 0x83ff, 0x0200, 0x8001; 11/10-bit 0x001) and read back. When the bits survive, FillClear takes the refused pattern again with SmallFloat decoding denormals exactly. NaNs, float32 denormals and the SNORM minimum stay refused. agc_driver_recorder_tests clears an R16G16_SFLOAT resident image with a normal, a denormal and a NaN pattern: the denormal is cleared exactly when the probe says the device keeps it, and the NaN is refused. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UTjpbMX7BUFMZZaxJE98zV --- .../Graphics/include/Texture.hpp | 1 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 103 ++++++++++++++++-- .../prx/libSceAgcDriver/tests/Recorder.cpp | 96 ++++++++++++++++ 3 files changed, 193 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 0f6efa20f..2fa7b67e5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -35,6 +35,7 @@ bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat); // StorageTexture::upload); false for integer formats and non-clear keys. bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, DccKeys keys); std::uint64_t SampledTextureMemory(); +bool ClearKeepsDenormals(const Context& context, VkFormat format); // A sampled texture's own VkImage with its memory, shared with the recorder while a recorded upload // still writes it (see the snapshot constructor), so the texture may go before the batch completes. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index cbf7cdfa1..71720b8b5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -902,7 +902,7 @@ bool ClearLayoutFor(VkFormat format, ClearLayout& layout) { // A float of `bits` (16: IEEE half; 11 and 10: the unsigned 5-bit-exponent floats of // B10G11R11) as a float32, or nullopt for a NaN, a denormal or (unsigned) an infinity. -std::optional SmallFloat(std::uint32_t code, std::uint32_t bits) { +std::optional SmallFloat(std::uint32_t code, std::uint32_t bits, bool denormals = false) { const std::uint32_t mantissaBits = bits == 16 ? 10u : bits - 5u; const bool negative = bits == 16 && (code >> 15u) != 0; const std::uint32_t exponent = (code >> mantissaBits) & 0x1fu; @@ -912,8 +912,9 @@ std::optional SmallFloat(std::uint32_t code, std::uint32_t bits) { return negative ? -std::numeric_limits::infinity() : std::numeric_limits::infinity(); } if (exponent == 0) { - if (mantissa != 0) return std::nullopt; - return negative ? -0.0f : 0.0f; + if (mantissa != 0 && !denormals) return std::nullopt; + const float value = std::ldexp(static_cast(mantissa) / static_cast(1u << mantissaBits), -14); + return negative ? -value : value; } const float value = std::ldexp(1.0f + static_cast(mantissa) / static_cast(1u << mantissaBits), static_cast(exponent) - 15); return negative ? -value : value; @@ -925,7 +926,7 @@ std::optional SmallFloat(std::uint32_t code, std::uint32_t bits) { // to the same code), float ones the decoded value. Codes a clear might store differently are // refused: NaNs and denormals (an implementation may canonicalize or flush them) and the most // negative SNORM code (stored as its neighbour, the same -1.0). -bool FillClearColor(VkFormat format, std::uint32_t elementBytes, std::span pattern, VkClearColorValue& clear) { +bool FillClearColor(VkFormat format, std::uint32_t elementBytes, std::span pattern, VkClearColorValue& clear, bool denormals = false) { ClearLayout layout{}; if (elementBytes == 0 || elementBytes > 16 || 16 % elementBytes != 0 || !ClearLayoutFor(format, layout)) return false; std::array bytes{}; @@ -961,13 +962,13 @@ bool FillClearColor(VkFormat format, std::uint32_t elementBytes, std::span(code); } else { - const auto value = SmallFloat(code, channel.bits); + const auto value = SmallFloat(code, channel.bits, denormals); if (!value) return false; clear.float32[channel.component] = *value; } break; case ClearKind::UFloat: { - const auto value = SmallFloat(code, channel.bits); + const auto value = SmallFloat(code, channel.bits, denormals); if (!value) return false; clear.float32[channel.component] = *value; break; @@ -1000,6 +1001,93 @@ bool AdjacentGenerationEnabled() { } +bool ClearKeepsDenormals(const Context& context, VkFormat format) { + static std::mutex mutex; + static std::map, bool> known; + std::lock_guard lock(mutex); + if (const auto found = known.find({context.device, format}); found != known.end()) return found->second; + ClearLayout layout{}; + bool smallFloats = ClearLayoutFor(format, layout) && (layout.kind == ClearKind::Float || layout.kind == ClearKind::UFloat); + std::array pattern{}; + VkClearColorValue clear{}; + std::uint32_t elementBits = 0; + constexpr std::array halfCodes{0x0001u, 0x83ffu, 0x0200u, 0x8001u}; + for (std::uint32_t i = 0; smallFloats && i < layout.channels; ++i) { + const auto& channel = layout.channel[i]; + if (channel.bits == 32) { + smallFloats = false; + break; + } + const auto code = channel.bits == 16 ? halfCodes[i] : 1u; + pattern[channel.offset / 32u] |= code << (channel.offset % 32u); + clear.float32[channel.component] = *SmallFloat(code, channel.bits, true); + elementBits = std::max(elementBits, channel.offset + channel.bits); + } + bool keeps = false; + if (smallFloats) { + const auto elementBytes = static_cast((elementBits + 7u) / 8u); + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + 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}; + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {1, 1, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage denormal clear probe"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &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); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory denormal clear probe"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory denormal clear probe"); + Buffer readback(context, elementBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + CommandBatch batch(context); + const auto commands = batch.Handle(); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageMemoryBarrier toClear{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toClear.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toClear.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toClear.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toClear.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toClear.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toClear.image = image; + toClear.subresourceRange = range; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toClear); + context.Function("vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &range); + VkImageMemoryBarrier toCopy = toClear; + toCopy.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toCopy.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toCopy.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toCopy.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toCopy); + VkBufferImageCopy region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {1, 1, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.Handle(), 1, ®ion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + keeps = std::memcmp(readback.Bytes().data(), pattern.data(), elementBytes) == 0; + } + known.emplace(std::pair{context.device, format}, keeps); + return keeps; +} + VkImageView StorageTexture::createView(std::uint32_t mip, bool firstLayer, VkFormat format) const { Require(mip < descriptor.mipCount, "storage texture mip level is outside the texture"); Require(!firstLayer || descriptor.dimension == TextureDimension::k2DArray, "a first-layer storage view needs a 2D array surface"); @@ -3019,7 +3107,8 @@ bool StorageTexture::FillClear(std::span pattern, std::u } } traceRefusal{*this, pattern, layer, refusal}; VkClearColorValue clearValue{}; - if (!FillClearColor(storageFormat, BytesPerElement(descriptor.format), pattern, clearValue)) { + const auto elementBytes = BytesPerElement(descriptor.format); + if (!FillClearColor(storageFormat, elementBytes, pattern, clearValue) && !(ClearKeepsDenormals(context, storageFormat) && FillClearColor(storageFormat, elementBytes, pattern, clearValue, true))) { refusal = "pattern"; return false; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index cd9c0223a..f9c681cce 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -11,6 +11,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -3654,6 +3655,100 @@ void keysFillTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +void denormalClearTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if (base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: denormal fill clears not tested\n"; + return; + } + constexpr std::uint32_t side = 64; + constexpr std::size_t bytes = 65536; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the denormal clear block"); + std::memset(block, 0x55, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + } + } unregister{base, block, address}; + if (HostImportFor(base, address, bytes) == nullptr) { + std::cout << "host import of the denormal clear block refused: denormal fill clears not tested\n"; + return; + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 29; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(ResolveTextureFormat(resource.format) == VK_FORMAT_R16G16_SFLOAT, "guest format 29 is not R16G16_SFLOAT"); + const bool keeps = ClearKeepsDenormals(context, VK_FORMAT_R16G16_SFLOAT); + std::cout << "denormal half clears are " << (keeps ? "kept" : "flushed") << " by this device\n"; + Require(ClearKeepsDenormals(context, VK_FORMAT_R16G16_SFLOAT) == keeps, "the denormal clear probe changed its answer"); + Require(!ClearKeepsDenormals(context, VK_FORMAT_R32G32B32A32_SFLOAT), "a format without small floats probed as keeping denormals"); + { + auto image = std::make_shared(context, detiler, resource, 0); + const auto holds = [&](std::uint32_t texel) { + Buffer readback(context, side * side * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {side, side, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + const auto pixels = readback.Bytes(); + for (std::size_t at = 0; at + 4 <= pixels.size(); at += 4) { + std::uint32_t value = 0; + std::memcpy(&value, pixels.data() + at, 4); + if (value != texel) return false; + } + return true; + }; + const char* refusal = nullptr; + const std::array normal{0x3c00bc00u, 0x3c00bc00u, 0x3c00bc00u, 0x3c00bc00u}; + Require(image->FillClear(normal, StorageTexture::WholeImage, refusal), "a normal half pattern was refused"); + Require(holds(0x3c00bc00u), "a normal half fill clear stored other bits"); + const std::array denormal{0x83ff0001u, 0x83ff0001u, 0x83ff0001u, 0x83ff0001u}; + refusal = nullptr; + const bool cleared = image->FillClear(denormal, StorageTexture::WholeImage, refusal); + Require(cleared == keeps, keeps ? "a denormal half pattern was refused on a device whose clears keep denormals" : "a denormal half pattern was cleared on a device whose clears flush them"); + if (cleared) Require(holds(0x83ff0001u), "a denormal half fill clear stored other bits"); + const std::array nan{0x7e007e00u, 0x7e007e00u, 0x7e007e00u, 0x7e007e00u}; + refusal = nullptr; + Require(!image->FillClear(nan, StorageTexture::WholeImage, refusal), "a NaN half pattern was cleared"); + } + recorder.Sync(); +} + int main(int argc, char** argv) { try { std::unique_ptr created; @@ -3724,6 +3819,7 @@ int main(int argc, char** argv) { movedMetadataTests(device, recorder); viewPastLastMipTests(device, recorder); keysFillTests(device, recorder); + denormalClearTests(device, recorder); unitShadowTests(device, recorder); storageRefreshTests(device, recorder, false); storageRefreshTests(device, recorder, true);