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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UTjpbMX7BUFMZZaxJE98zV
This commit is contained in:
parent
faf6a11701
commit
bafca2ffda
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<float> SmallFloat(std::uint32_t code, std::uint32_t bits) {
|
||||
std::optional<float> 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<float> SmallFloat(std::uint32_t code, std::uint32_t bits) {
|
|||
return negative ? -std::numeric_limits<float>::infinity() : std::numeric_limits<float>::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<float>(mantissa) / static_cast<float>(1u << mantissaBits), -14);
|
||||
return negative ? -value : value;
|
||||
}
|
||||
const float value = std::ldexp(1.0f + static_cast<float>(mantissa) / static_cast<float>(1u << mantissaBits), static_cast<int>(exponent) - 15);
|
||||
return negative ? -value : value;
|
||||
|
|
@ -925,7 +926,7 @@ std::optional<float> 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<const std::uint32_t, 4> pattern, VkClearColorValue& clear) {
|
||||
bool FillClearColor(VkFormat format, std::uint32_t elementBytes, std::span<const std::uint32_t, 4> pattern, VkClearColorValue& clear, bool denormals = false) {
|
||||
ClearLayout layout{};
|
||||
if (elementBytes == 0 || elementBytes > 16 || 16 % elementBytes != 0 || !ClearLayoutFor(format, layout)) return false;
|
||||
std::array<std::byte, 16> bytes{};
|
||||
|
|
@ -961,13 +962,13 @@ bool FillClearColor(VkFormat format, std::uint32_t elementBytes, std::span<const
|
|||
if ((exponent == 0xffu && mantissa != 0) || (exponent == 0 && mantissa != 0)) return false;
|
||||
clear.float32[channel.component] = std::bit_cast<float>(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<std::pair<VkDevice, VkFormat>, 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<std::uint32_t, 4> pattern{};
|
||||
VkClearColorValue clear{};
|
||||
std::uint32_t elementBits = 0;
|
||||
constexpr std::array<std::uint32_t, 4> 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<std::size_t>((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<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
|
||||
if (memory) context.Function<PFN_vkFreeMemory>("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<PFN_vkCreateImage>("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage denormal clear probe");
|
||||
VkMemoryRequirements requirements{};
|
||||
context.Function<PFN_vkGetImageMemoryRequirements>("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<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory denormal clear probe");
|
||||
Check(context.Function<PFN_vkBindImageMemory>("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<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toClear);
|
||||
context.Function<PFN_vkCmdClearColorImage>("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<PFN_vkCmdPipelineBarrier>("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<PFN_vkCmdCopyImageToBuffer>("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<const std::uint32_t, 4> 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<std::uint64_t>(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<StorageTexture>(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<PFN_vkCmdCopyImageToBuffer>("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<std::uint32_t, 4> 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<std::uint32_t, 4> 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<std::uint32_t, 4> 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<Device> 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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue