Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
103 changes: 96 additions & 7 deletions core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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{};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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, &region);
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");
Expand Down Expand Up @@ -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;
}
Expand Down
96 changes: 96 additions & 0 deletions core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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, &copy);
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;
Expand Down Expand Up @@ -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);
Expand Down
Loading