diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index e357ff77c4b4..6bc130edb084 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -61,6 +61,17 @@ class Texture { // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components); + struct DepthCopyTag {}; + Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag); + static VkFormat DepthCopyFormat(VkFormat colorFormat); + static bool CanDepthCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); + bool IsDepthCopy() const { return depthCopy; } + bool RefreshDepthCopy(); + struct DepthCopyCounts { + std::uint64_t textures; + std::uint64_t refreshes; + }; + static DepthCopyCounts TakeDepthCopyCounts(); static bool CanCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); ~Texture(); Texture(const Texture&) = delete; @@ -108,6 +119,11 @@ class Texture { std::shared_ptr source; std::shared_ptr storageSource; std::unique_ptr upload; + bool depthCopy = false; + bool copiedOnce = false; + std::uint64_t copiedVersion = 0; + VkFormat depthFormat = VK_FORMAT_UNDEFINED; + std::uint32_t copyWidth = 0, copyHeight = 0, copyLevels = 0, copyLayers = 0, copyElementBytes = 0; }; VkFormat AttachmentProxyFormat(const Context& context, VkFormat format); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 9ac9ff9bdc95..ac89460b78f4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -410,6 +410,14 @@ bool ClearedViewEnabled() { return !disabled; } +std::uint64_t DepthCopyMinBytes() { + static const std::uint64_t bytes = [] { + const char* text = std::getenv("APS5_DEPTH_COPY_MIN_KIB"); + return (text != nullptr ? std::strtoull(text, nullptr, 10) : 1024ull) << 10u; + }(); + return bytes; +} + // Surfaces whose storage image could not be made (no writable committed pages, say): remembered // so the sampled lookup does not throw and fall back on every use. A failure is keyed by the surface // (address and size: the heap reuses addresses) and holds only while the guest mappings are what @@ -537,8 +545,10 @@ std::shared_ptr cachedTexture(const Context& context, std::span= DepthCopyMinBytes() && Texture::DepthCopyFormat(ResolveTextureFormat(resource.format)) != VK_FORMAT_UNDEFINED && resource.dimension != TextureDimension::k3D; auto source = StorageTexture::FindPending(address, guestBytes); - if (source != nullptr && (depthCompare || !Texture::CanCopyFrom(*source, resource) || MetadataMoved(*source, resource))) source.reset(); + if (source != nullptr && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); + if (source != nullptr && MetadataMoved(*source, resource)) source.reset(); std::optional keys; bool clearThroughKeys = false; if (source != nullptr && resource.dccAddress != 0 && IsDccClear(source->FilledKeys())) { @@ -562,12 +572,13 @@ std::shared_ptr cachedTexture(const Context& context, std::span cachedTexture(const Context& context, std::span(context, source, resource, components, Texture::DepthCopyTag{}); + texture->RefreshDepthCopy(); + return texture; + } if (source != nullptr) return std::make_shared(context, source, resource, components); std::vector snapshot(bytes); ReadTextureSurface(resource, *keys, snapshot); @@ -618,6 +634,7 @@ std::shared_ptr cachedTexture(const Context& context, std::spansource) && (source != nullptr || *keys == DccKeys::Uncompressed) && StorageImageServesKeys(*it->source, resource.dccAddress)) { if (!GuestMemory::UnchangedSince(address, bytes, it->source->Generation())) it->source->Refresh(); + if (it->texture->IsDepthCopy()) it->texture->RefreshDepthCopy(); it->keys = *keys; touchTexture(cache, it); logLookup({it->texture.get(), resource, guestBytes, *keys, 0, it->source.get()}); @@ -650,7 +667,12 @@ std::shared_ptr cachedTexture(const Context& context, std::spanVersion(); - entry.texture = std::make_shared(context, source, resource, components); + if (depthCompare) { + entry.texture = std::make_shared(context, source, resource, components, Texture::DepthCopyTag{}); + entry.texture->RefreshDepthCopy(); + } else { + entry.texture = std::make_shared(context, source, resource, components); + } entry.accounted = source->Cached() ? 0u : heldBytes(*source); counters.fromStorage.fetch_add(1, std::memory_order_relaxed); } else { @@ -2313,6 +2335,9 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR } } pendingSerialSeen = EpochRevalidate() && StorageTexture::PendingSerial() == serialBefore ? serialBefore : 0; + for (const auto& texture : textures) { + if (texture != nullptr && texture->IsDepthCopy()) texture->RefreshDepthCopy(); + } return finish(fast, true); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 2e4b692a944e..bd296af03461 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -87,6 +87,7 @@ std::atomic pretestSkipped{0}, unregisteredDropped{0}, keyFlipKep // Images stored by a FlushPending after a hook skip of theirs (AccessKeptByCpu), of which by the // hook for the read site of the last skip; images evictStale dropped (the [hooksync] and [storage] lines). std::atomic flushedAfterSkip{0}, flushedAfterSkipSameSite{0}, staleEvicted{0}; +std::atomic depthCopyTextures{0}, depthCopyRefreshes{0}; std::atomic refreshesProved{0}; struct StorageTraffic { @@ -457,6 +458,159 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex } } +VkFormat Texture::DepthCopyFormat(VkFormat colorFormat) { + switch (colorFormat) { + case VK_FORMAT_R32_SFLOAT: return VK_FORMAT_D32_SFLOAT; + case VK_FORMAT_R16_UNORM: return VK_FORMAT_D16_UNORM; + default: return VK_FORMAT_UNDEFINED; + } +} + +bool Texture::CanDepthCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) { + if (!CanCopyFrom(source, descriptor) || descriptor.dimension == TextureDimension::k3D) return false; + if (DepthCopyFormat(ResolveTextureFormat(descriptor.format)) == VK_FORMAT_UNDEFINED) return false; + if (ResolveTextureFormat(source.Descriptor().format) != ResolveTextureFormat(descriptor.format)) return false; + const auto geometry = DescribeSurface(descriptor); + return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers && source.Descriptor().mipCount >= descriptor.mipCount; +} + +Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag) : context(context), storageSource(source), depthCopy(true) { + try { + Require(source != nullptr && CanDepthCopyFrom(*source, descriptor), "storage image cannot serve a depth copy of the sampled texture"); + const auto colorFormat = ResolveTextureFormat(descriptor.format); + depthFormat = DepthCopyFormat(colorFormat); + const auto geometry = DescribeSurface(descriptor); + copyWidth = descriptor.width; + copyHeight = descriptor.height; + copyLevels = descriptor.mipCount; + copyLayers = geometry.imageLayers; + copyElementBytes = BytesPerElement(descriptor.format); + APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u depth copy of storage image (vk=%d -> %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, static_cast(colorFormat), static_cast(depthFormat)); + + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; + imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.format = depthFormat; + imageInfo.extent = {descriptor.width, descriptor.height, 1u}; + imageInfo.mipLevels = copyLevels; + imageInfo.arrayLayers = copyLayers; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage depth copy"); + owned = std::make_shared(context, image, VK_NULL_HANDLE); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocationBytes = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory depth copy"); + Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory depth copy"); + + const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; + const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; + if (descriptor.dimension == TextureDimension::kCube) { + Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + } + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.format = depthFormat; + viewInfo.components = VkComponentMapping{}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; + VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; + ChainMinLod(context, descriptor, viewInfo, minLod); + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth copy"); + createFirstLayerView(descriptor, viewInfo); + static_cast(components); + depthCopyTextures.fetch_add(1, std::memory_order_relaxed); + } catch (...) { + release(); + throw; + } +} + +bool Texture::RefreshDepthCopy() { + if (!depthCopy || storageSource == nullptr) return false; + if (copiedOnce && storageSource->Version() == copiedVersion) return false; + std::uint64_t linearBytes = 0; + for (std::uint32_t level = 0; level < copyLevels; ++level) linearBytes += static_cast(std::max(copyWidth >> level, 1u)) * std::max(copyHeight >> level, 1u) * copyElementBytes * copyLayers; + auto linear = std::make_shared(context, static_cast(linearBytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::vector fromSource, toDepth; + std::uint64_t offset = 0; + for (std::uint32_t level = 0; level < copyLevels; ++level) { + const VkExtent3D extent{std::max(copyWidth >> level, 1u), std::max(copyHeight >> level, 1u), 1u}; + VkBufferImageCopy region{}; + region.bufferOffset = offset; + region.bufferRowLength = 0; + region.bufferImageHeight = 0; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, copyLayers}; + region.imageOffset = {0, 0, 0}; + region.imageExtent = extent; + fromSource.push_back(region); + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + toDepth.push_back(region); + offset += static_cast(extent.width) * extent.height * copyElementBytes * copyLayers; + } + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + VkCommandBuffer commands = VK_NULL_HANDLE; + if (recorder != nullptr) { + commands = recorder->Commands(); + recorder->Keep(linear); + recorder->Keep(owned); + recorder->Keep(storageSource); + } else { + batch = std::make_unique(context); + commands = batch->Handle(); + } + const auto barrier = context.Function("vkCmdPipelineBarrier"); + const VkMemoryBarrier produced{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT}; + VkImageMemoryBarrier toTransferDst{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransferDst.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toTransferDst.oldLayout = copiedOnce ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED; + toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.image = image; + toTransferDst.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, copyLevels, 0, copyLayers}; + barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &produced, 0, nullptr, 1, &toTransferDst); + context.Function("vkCmdCopyImageToBuffer")(commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, linear->Handle(), static_cast(fromSource.size()), fromSource.data()); + VkBufferMemoryBarrier linearReady{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; + linearReady.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + linearReady.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + linearReady.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + linearReady.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + linearReady.buffer = linear->Handle(); + linearReady.offset = 0; + linearReady.size = VK_WHOLE_SIZE; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearReady, 0, nullptr); + context.Function("vkCmdCopyBufferToImage")(commands, linear->Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(toDepth.size()), toDepth.data()); + VkImageMemoryBarrier toShaderRead{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + toShaderRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + toShaderRead.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toShaderRead.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toShaderRead.image = image; + toShaderRead.subresourceRange = toTransferDst.subresourceRange; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toShaderRead); + if (batch) batch->SubmitAndWait(); + copiedOnce = true; + copiedVersion = storageSource->Version(); + depthCopyRefreshes.fetch_add(1, std::memory_order_relaxed); + return true; +} + +Texture::DepthCopyCounts Texture::TakeDepthCopyCounts() { + return {depthCopyTextures.exchange(0, std::memory_order_relaxed), depthCopyRefreshes.exchange(0, std::memory_order_relaxed)}; +} + bool Texture::CanCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) { const auto& from = source.Descriptor(); if (IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format)) return false; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthCompareCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthCompareCopy.cpp new file mode 100644 index 000000000000..1db56eaa8733 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthCompareCopy.cpp @@ -0,0 +1,315 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Format16Unorm = 7; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::uint32_t LessEqual = 3; +constexpr std::uint32_t ClampEdge = 2; +constexpr std::size_t BlockBytes = 2u << 20u; + +struct Shape { + std::uint32_t format; + std::uint32_t width; + std::uint32_t height; + std::uint32_t layers; + std::uint32_t mips; + bool array; +}; + +alignas(256) constexpr std::array StoreHalfCode{ + 0x7e280d00, 0x102828ff, 0x3d000000, 0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array StoreQuarterCode{ + 0x7e280d00, 0x102828ff, 0x3c800000, 0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array StoreLayerMipCode{ + 0x7e280d00, 0x1028280e, 0x7e3c0300, 0x7e3e0280, 0x7e40020c, 0x7e42020d, 0xf0241128, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array SampleCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0bc0108, 0x00820401, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array SampleLayerMipCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0x7e080214, 0x7e0a0215, 0xbf8c3f70, 0xf0b00128, 0x00820601, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001060b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +std::span Input; +std::span Output; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "depth compare copy: cannot allocate the guest block"); + Require(AgcDriver::GuestMemory::Watched(reinterpret_cast(block), BlockBytes), "depth compare copy: the guest block is not write-watched"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(block), BlockBytes); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data, const Shape& shape) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (shape.format << 20u) | (((shape.width - 1u) & 3u) << 30u), + ((shape.width - 1u) >> 2u) | ((shape.height - 1u) << 14u), + 0xfacu | ((shape.mips - 1u) << 16u) | ((shape.array ? Type2DArray : Type2D) << 28u), + shape.array ? shape.layers - 1u : 0u, + (shape.mips - 1u) << 4u, + 0u, 0u, + }; +} + +std::array SamplerDescriptor(bool mips) { + return {ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u) | (LessEqual << 12u), mips ? 0xf00u << 12u : 0u, mips ? 1u << 26u : 0u, 0u}; +} + +std::uint32_t Bits(float value) { + std::uint32_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + return bits; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape, std::span code, std::uint32_t layer = 0, std::uint32_t mip = 0, float scale = 0.0f) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, shape); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = layer; + userData[13] = mip; + userData[14] = Bits(scale); + const auto result = Compile(device, code, userData); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +ShaderRecompiler::RecompileResult SampleProgram(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape, std::uint32_t layer = 0, std::uint32_t mip = 0) { + std::vector userData(24, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size_bytes())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size_bytes())); + const auto texture = TextureDescriptor(texels, shape); + const auto sampler = SamplerDescriptor(shape.mips > 1u); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + userData[20] = Bits(static_cast(layer)); + userData[21] = Bits(static_cast(mip)); + return Compile(device, shape.array ? std::span(SampleLayerMipCode) : std::span(SampleCode), userData); +} + +void Sample(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& program, const Shape& shape, float scale, const std::string& what, std::uint32_t layer = 0, std::uint32_t mip = 0) { + const auto width = std::max(shape.width >> mip, 1u); + const auto height = std::max(shape.height >> mip, 1u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float value = static_cast(tid) * scale; + Input[tid * 3u + 0u] = (tid & 1u) != 0u ? value - scale / 4.0f : value + scale / 4.0f; + Input[tid * 3u + 1u] = (static_cast(tid) + 0.5f) / static_cast(width); + Input[tid * 3u + 2u] = 0.5f / static_cast(height); + } + std::fill(Output.begin(), Output.end(), -1.0f); + const auto* code = shape.array ? SampleLayerMipCode.data() : SampleCode.data(); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(code)); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float expected = (tid & 1u) != 0u ? 1.0f : 0.0f; + Require(Output[tid] == expected, what + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected)); + } +} + +std::uint64_t SurfaceBytes(std::uint8_t* texels, const Shape& shape) { + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels, shape))); + Require(surface.guestBytes >= (1u << 20u) && surface.guestBytes <= BlockBytes, "depth compare copy: the surface does not fit the guest block or is below the depth copy floor"); + return surface.guestBytes; +} + +std::uint64_t PendingVersion(std::uint8_t* texels, std::uint64_t bytes, const std::string& what) { + const auto source = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), bytes); + Require(source != nullptr, what + ": no storage image holds the surface's results"); + return source->Version(); +} + +void Settle(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint64_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), bytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(texels), bytes); +} + +void RequireCounts(std::uint64_t textures, std::uint64_t refreshes, const std::string& what) { + const auto counts = AgcDriver::Graphics::Texture::TakeDepthCopyCounts(); + Require(counts.textures == textures && counts.refreshes == refreshes, what + ": " + std::to_string(counts.textures) + " depth copy textures and " + std::to_string(counts.refreshes) + " refreshes, expected " + std::to_string(textures) + " and " + std::to_string(refreshes)); +} + +void RequireNewerVersion(std::uint64_t before, std::uint64_t after, const std::string& what) { + Require(after > before, what + ": the storage image version stayed at " + std::to_string(before)); +} + +float Texel(const std::uint8_t* texels, std::uint32_t x) { + float value = 0.0f; + std::memcpy(&value, texels + static_cast(x) * sizeof(float), sizeof(float)); + return value; +} + +void RequireGuestTexels(const std::uint8_t* texels, float scale, const std::string& what) { + for (std::uint32_t x = 0; x < Threads; ++x) { + const float expected = static_cast(x) * scale; + Require(Texel(texels, x) == expected, what + ": texel " + std::to_string(x) + " is " + std::to_string(Texel(texels, x)) + ", expected " + std::to_string(expected)); + } +} + +void SingleLevelCase(AgcDriver::VulkanDevice& device, const Shape& shape, const std::string& name) { + GuestBlock block; + auto* texels = block.Data(); + const auto bytes = SurfaceBytes(texels, shape); + Store(device, texels, shape, StoreQuarterCode); + Settle(device, texels, bytes); + AgcDriver::Graphics::Texture::TakeDepthCopyCounts(); + Store(device, texels, shape, StoreHalfCode); + const auto program = SampleProgram(device, texels, shape); + Sample(device, program, shape, 1.0f / 32.0f, name + ": comparison sample of pending results"); + RequireCounts(1, 1, name + ": first comparison sample"); + const auto before = PendingVersion(texels, bytes, name); + Store(device, texels, shape, StoreQuarterCode); + RequireNewerVersion(before, PendingVersion(texels, bytes, name), name + ": second store"); + Sample(device, program, shape, 1.0f / 64.0f, name + ": comparison sample of the same descriptor set after a second store"); + RequireCounts(0, 1, name + ": comparison sample after a second store"); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), bytes, nullptr, "test"); + device.WaitIdle(); + if (shape.format == Format32Float) RequireGuestTexels(texels, 1.0f / 64.0f, name + ": flushed results"); +} + +void LayersAndMipsCase(AgcDriver::VulkanDevice& device) { + const Shape shape{Format32Float, 256, 256, 4, 3, true}; + const std::string name = "R32F 256x256, 4 layers, 3 mips"; + GuestBlock block; + auto* texels = block.Data(); + const auto bytes = SurfaceBytes(texels, shape); + Store(device, texels, shape, StoreLayerMipCode, 0, 0, 1.0f / 512.0f); + Settle(device, texels, bytes); + AgcDriver::Graphics::Texture::TakeDepthCopyCounts(); + Store(device, texels, shape, StoreLayerMipCode, 0, 0, 1.0f / 32.0f); + Store(device, texels, shape, StoreLayerMipCode, 2, 1, 1.0f / 64.0f); + Store(device, texels, shape, StoreLayerMipCode, 3, 2, 1.0f / 128.0f); + const auto first = SampleProgram(device, texels, shape, 0, 0); + const auto second = SampleProgram(device, texels, shape, 2, 1); + const auto third = SampleProgram(device, texels, shape, 3, 2); + Sample(device, first, shape, 1.0f / 32.0f, name + ": layer 0 mip 0", 0, 0); + Sample(device, second, shape, 1.0f / 64.0f, name + ": layer 2 mip 1", 2, 1); + Sample(device, third, shape, 1.0f / 128.0f, name + ": layer 3 mip 2", 3, 2); + RequireCounts(1, 1, name + ": first comparison samples"); + const auto before = PendingVersion(texels, bytes, name); + Store(device, texels, shape, StoreLayerMipCode, 2, 1, 1.0f / 256.0f); + RequireNewerVersion(before, PendingVersion(texels, bytes, name), name + ": second store"); + Sample(device, second, shape, 1.0f / 256.0f, name + ": layer 2 mip 1 after a second store", 2, 1); + Sample(device, first, shape, 1.0f / 32.0f, name + ": layer 0 mip 0 after a second store", 0, 0); + RequireCounts(0, 1, name + ": comparison samples after a second store"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock buffers; + Input = std::span(reinterpret_cast(buffers.Data()), Threads * 3u); + Output = std::span(reinterpret_cast(buffers.Data() + 4096u), Threads); + SingleLevelCase(*device, {Format32Float, 512, 512, 1, 1, false}, "R32F 512x512"); + SingleLevelCase(*device, {Format16Unorm, 1024, 512, 1, 1, false}, "R16 unorm 1024x512"); + LayersAndMipsCase(*device); + std::puts("depth compare copy tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 3d64959512cb..8c55773ae6d8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -516,6 +516,7 @@ Throughout the project, every function at every stage either **does exactly what - [kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) handles socket descriptors through the POSIX `fcntl` export (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`) and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use; the command semantics follow FreeBSD's [fcntl](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_descrip.c), and a closed descriptor returns `SCE_KERNEL_ERROR_EBADF`. File descriptors throw: their open flags are not tracked, and `F_DUPFD`, `F_GETFD` and `F_SETFD` are unsupported on all descriptors. The return values and error codes are unverified on a console - [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. +- [Comparison-sampled R32 float and R16 unorm textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert