diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index b09d3168c25..20d16b894a8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -61,11 +61,14 @@ struct GuestTextureResource { std::uint64_t dccAddress = 0; bool dccAlphaOnMsb = false; std::uint32_t minLod = 0; + bool emptyView = false; + std::uint32_t emptyBaseLevel = 0; }; float EffectiveMinLod(const GuestTextureResource& resource); GuestTextureResource DecodeTextureResource(std::span words); +void RequireLevelsPresent(const GuestTextureResource& resource); bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index fc8dfba9acf..6c80d04955f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -129,7 +129,7 @@ GuestTextureResource DecodeTextureResource(std::span words) } catch (const std::exception& error) { throw std::runtime_error(std::string(error.what()) + describe()); } - Require(baseLevel <= maxMip, "guest texture descriptor starts past the surface's last mip level" + describe()); + const bool emptyView = baseLevel > maxMip; // Views may name levels past MAX_MIP (a 512x512 view through 1x1 over a 9-level surface); the // hardware never addresses them, so the view ends at the surface's last level. lastLevel = std::min(lastLevel, maxMip); @@ -164,7 +164,9 @@ GuestTextureResource DecodeTextureResource(std::span words) result.depthOrLastArray = depth; result.baseArray = baseArray; result.mipCount = maxMip + 1u; - result.baseLevel = baseLevel; + result.baseLevel = emptyView ? maxMip : baseLevel; + result.emptyView = emptyView; + result.emptyBaseLevel = emptyView ? baseLevel : 0u; result.lastLevel = lastLevel; result.tileMode = tileMode; result.dimension = dimension; @@ -184,6 +186,11 @@ float EffectiveMinLod(const GuestTextureResource& resource) { return std::min(static_cast(resource.minLod) / 256.0f, static_cast(resource.lastLevel)); } +void RequireLevelsPresent(const GuestTextureResource& resource) { + if (!resource.emptyView) return; + throw std::runtime_error("AGC graphics: guest texture descriptor starts past the surface's last mip level (format " + std::to_string(resource.format) + ", " + std::to_string(resource.width) + "x" + std::to_string(resource.height) + ", mip " + std::to_string(resource.emptyBaseLevel) + " of " + std::to_string(resource.mipCount) + ")"); +} + bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension) { switch (shape) { case ShaderRecompiler::DescriptorImageShape::Image1D: return dimension == TextureDimension::k1D; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index cfd280eb397..d3d0e5cd06c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -512,6 +512,14 @@ std::array SurfaceKey(const Context& context, const GuestTextu } // `guestBytes` is the surface size when the caller described the surface already (0: described here). +void NoteDroppedStores(const GuestTextureResource& resource) { + static std::mutex mutex; + static std::set> reported; + std::lock_guard lock(mutex); + if (!reported.insert({resource.baseAddress, resource.emptyBaseLevel}).second) return; + std::fprintf(stderr, "[gpu] storage image view of surface 0x%llx starts at mip %u, past its last mip %u: the shader's stores to it are dropped\n", static_cast(resource.baseAddress), resource.emptyBaseLevel, resource.mipCount - 1u); +} + std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes) { if (DepthSurfaceAt(resource.baseAddress)) { char text[112]; @@ -1630,6 +1638,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); const auto resource = DecodeTextureResource(words); + RequireLevelsPresent(resource); const VkComponentMapping components{ComponentSwizzleFor(resource.dstSelX), ComponentSwizzleFor(resource.dstSelY), ComponentSwizzleFor(resource.dstSelZ), ComponentSwizzleFor(resource.dstSelW)}; if (textureIndex >= textures.size() || cachedTexture(context, words, resource, components, 0, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element)) != textures[textureIndex]) return false; ++textureIndex; @@ -2453,6 +2462,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); const auto* record = nextRecord(); const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words); + RequireLevelsPresent(resource); const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray; if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); const VkComponentMapping components{ComponentSwizzleFor(resource.dstSelX), ComponentSwizzleFor(resource.dstSelY), ComponentSwizzleFor(resource.dstSelZ), ComponentSwizzleFor(resource.dstSelW)}; @@ -2494,7 +2504,11 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind storageKeys.push_back(resource.dccAddress); storageFirstLayer.push_back(firstLayer); // Images the shader only reads have nothing to store back. - storageWritten.push_back(element >= binding.imageWritten.size() || binding.imageWritten[element]); + if (resource.emptyView) { + Require(sameAsPrevious == false && mipOffset == 0, "AGC graphics: dynamic-mip storage writes to an image view past its surface's last mip level are not implemented"); + NoteDroppedStores(resource); + } + storageWritten.push_back(!resource.emptyView && (element >= binding.imageWritten.size() || binding.imageWritten[element])); describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); item.imageAllocations.push_back(storageTextures.size() - 1); } diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 092a548c0cc..1c9398f260f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -269,6 +269,22 @@ void RunGuestTextureResourceTests() { const auto partial = DecodeTextureResource(pack(partialMips)); Require(partial.lastLevel == 1 && partial.mipCount == 3, "a view over part of the mip chain decoded wrongly"); + Fields pastLast = base; + pastLast.width = 64; + pastLast.height = 64; + pastLast.maxMip = 5; + pastLast.baseLevel = 6; + pastLast.lastLevel = 6; + const auto empty = DecodeTextureResource(pack(pastLast)); + Require(empty.emptyView && empty.emptyBaseLevel == 6, "a view starting past MAX_MIP was not marked empty"); + Require(empty.baseLevel == 5 && empty.lastLevel == 5 && empty.mipCount == 6, "an empty view was not clamped to the surface's last mip level"); + reject([&] { RequireLevelsPresent(empty); }, "starts past the surface's last mip level"); + pastLast.baseLevel = 5; + pastLast.lastLevel = 5; + const auto lastLevel = DecodeTextureResource(pack(pastLast)); + Require(!lastLevel.emptyView && lastLevel.baseLevel == 5 && lastLevel.emptyBaseLevel == 0, "a view of the surface's last mip level was marked empty"); + RequireLevelsPresent(lastLevel); + std::array shortWords{}; reject([&] { DecodeTextureResource(shortWords); }, "8 dwords"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmptyMipView.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmptyMipView.cpp new file mode 100644 index 00000000000..d43c795d568 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmptyMipView.cpp @@ -0,0 +1,127 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 8; +constexpr std::uint32_t Side = 8; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Stored = 0x12345678u; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +alignas(4096) std::array Texels{}; + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Stored, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(256) constexpr std::array LoadCode{ + 0x7e040300, 0x7e060280, 0xf0000108, 0x00020102, 0xbf8c3f70, 0xf0200108, 0x00020102, 0xbf810000, +}; + +std::array TextureDescriptor(std::uint32_t baseLevel, std::uint32_t lastLevel, std::uint32_t maxMip) { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (baseLevel << 12u) | (lastLevel << 16u) | (Type2D << 28u), + 0u, + maxMip << 4u, + 0u, + 0u, + }; +} + +bool RunStore(AgcDriver::VulkanDevice& device, std::uint32_t baseLevel) { + Texels.fill(Untouched); + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(baseLevel, baseLevel, 1u); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + const std::span code(StoreCode); + 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; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::FlushCachedTextures(device.Device()); + } + device.WaitIdle(); + return std::any_of(Texels.begin(), Texels.end(), [](std::uint32_t texel) { return texel != Untouched; }); +} + +void RunLoad(AgcDriver::VulkanDevice& device) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(2u, 2u, 1u); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + const std::span code(LoadCode); + 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; + try { + static_cast(ShaderRecompiler::Recompile(request)); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find("past its surface's last mip level") != std::string_view::npos, std::string("load from an empty view failed for another reason: ") + error.what()); + return; + } + Require(false, "load from an image view past the surface's last mip level compiled"); +} + +void RegisterTexels(bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, Texels.data(), sizeof(Texels), true, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, Texels.data()); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + RegisterTexels(true); + Require(RunStore(*device, 1u), "store through the surface's last mip level did not reach the texels"); + Require(!RunStore(*device, 2u), "store through a view past the surface's last mip level changed the texels"); + RunLoad(*device); + RegisterTexels(false); + std::puts("empty mip view tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index bbe52b016e7..59c33458483 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -52,6 +52,7 @@ struct ImageResource { bool r128 = false; bool depthBits = false; bool depthUnorm16 = false; + bool empty = false; std::uint32_t indirectRoot = NoIndirectImage; std::uint32_t indirectMappingOffset = 0; std::uint32_t indirectSearchIterations = 0; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index 0bbff3eb447..3cd9685f976 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -31,6 +31,7 @@ struct ResourceSpecialization { bool fmask = false; bool depthBits = false; bool depthUnorm16 = false; + bool empty = false; bool operator==(const Image& other) const; }; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 73ffff5612c..d83e42366e1 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -44,6 +44,7 @@ struct DecodedImage { bool fmask = false; bool depthBits = false; bool depthUnorm16 = false; + bool empty = false; }; ShaderBufferResource decodeBufferDescriptor(const DescriptorValue& value) { @@ -57,6 +58,18 @@ ShaderBufferResource decodeBufferDescriptor(const DescriptorValue& value) { return result; } +std::uint32_t imageBaseLevel(const DescriptorValue& descriptor) { + return (descriptor.dwords[3] >> 12u) & 0xfu; +} + +std::uint32_t imageLastLevel(const DescriptorValue& descriptor) { + return (descriptor.dwords[3] >> 16u) & 0xfu; +} + +std::uint32_t imageMaxMip(const DescriptorValue& descriptor) { + return (descriptor.dwords[5] >> 4u) & 0xfu; +} + bool nullImageDescriptor(const DescriptorValue& descriptor) { return descriptor.dwords[0] == 0u && (descriptor.dwords[1] & 0xffu) == 0u; } @@ -111,9 +124,9 @@ bool validImageDescriptor(const DescriptorValue& descriptor, bool r128) { return false; } if (type == ImageType::Color2DMsaa || type == ImageType::Color2DMsaaArray) { - const auto baseLevel = (descriptor.dwords[3] >> 12u) & 0xfu; - const auto fragments = (descriptor.dwords[3] >> 16u) & 0xfu; - const auto maxMip = (descriptor.dwords[5] >> 4u) & 0xfu; + const auto baseLevel = imageBaseLevel(descriptor); + const auto fragments = imageLastLevel(descriptor); + const auto maxMip = imageMaxMip(descriptor); return baseLevel == 0u && fragments >= 1u && fragments <= 3u && (r128 || maxMip == fragments); } return true; @@ -123,8 +136,8 @@ std::uint32_t storageMipCount(const ImageResource& base, const DescriptorValue& if (base.mipMode != ImageMipMode::DynamicStorage || nullImageDescriptor(descriptor)) { return 1u; } - const auto mipBase = (descriptor.dwords[3] >> 12u) & 0xfu; - const auto mipLast = (descriptor.dwords[3] >> 16u) & 0xfu; + const auto mipBase = imageBaseLevel(descriptor); + const auto mipLast = imageLastLevel(descriptor); return mipBase <= mipLast ? mipLast - mipBase + 1u : 0u; } @@ -140,6 +153,9 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag decoded.cube = false; return decoded; } + if (descriptor.dwordCount == 8u && base.mipMode != ImageMipMode::DynamicStorage) { + decoded.empty = imageBaseLevel(descriptor) > imageMaxMip(descriptor); + } if (base.resourceClass == ImageResourceClass::None || (base.atomic && base.resourceClass != ImageResourceClass::Storage)) { throw std::runtime_error("image resource has an invalid class"); } @@ -533,6 +549,7 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s entry.fmask = decoded.fmask; entry.depthBits = decoded.depthBits; entry.depthUnorm16 = decoded.depthUnorm16; + entry.empty = decoded.empty; result.images.push_back(entry); } @@ -619,6 +636,7 @@ void ResourceMaterializer::Apply(IrProgram& program, const ResourceSpecializatio image.cube = source.cube; image.depthBits = source.depthBits; image.depthUnorm16 = source.depthUnorm16; + image.empty = source.empty; image.indirectResources.clear(); } for (std::uint32_t index = 0; index < images.size(); index++) { @@ -884,7 +902,7 @@ bool ResourceSpecialization::Buffer::operator==(const Buffer& other) const { } bool ResourceSpecialization::Image::operator==(const Image& other) const { - return numericClass == other.numericClass && dimension == other.dimension && mipCount == other.mipCount && conversionFormat == other.conversionFormat && shaderSwizzle == other.shaderSwizzle && indirectRoot == other.indirectRoot && indirectMappingOffset == other.indirectMappingOffset && indirectSearchIterations == other.indirectSearchIterations && cube == other.cube && fmask == other.fmask && depthBits == other.depthBits && depthUnorm16 == other.depthUnorm16; + return numericClass == other.numericClass && dimension == other.dimension && mipCount == other.mipCount && conversionFormat == other.conversionFormat && shaderSwizzle == other.shaderSwizzle && indirectRoot == other.indirectRoot && indirectMappingOffset == other.indirectMappingOffset && indirectSearchIterations == other.indirectSearchIterations && cube == other.cube && fmask == other.fmask && depthBits == other.depthBits && depthUnorm16 == other.depthUnorm16 && empty == other.empty; } bool ResourceSpecialization::operator==(const ResourceSpecialization& other) const { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 506203b727d..d39f5e25559 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -69,7 +69,7 @@ static_assert(sizeof(FragmentParameter) == 12, "FragmentParameter changed: updat static_assert(sizeof(CompiledShaderInfo) == 304, "CompiledShaderInfo changed: update the info encoder"); static_assert(sizeof(ShaderInfo) == 200, "ShaderInfo changed: update the info encoder"); static_assert(sizeof(BufferResource) == 36, "BufferResource changed: update the info encoder"); -static_assert(sizeof(ImageResource) == 80, "ImageResource changed: update the info encoder"); +static_assert(sizeof(ImageResource) == 88, "ImageResource changed: update the info encoder"); static_assert(sizeof(SamplerResource) == 12, "SamplerResource changed: update the info encoder"); static_assert(sizeof(SampledResourcePair) == 12, "SampledResourcePair changed: update the info encoder"); static_assert(sizeof(StageInput) == 56, "StageInput changed: update the info encoder"); @@ -79,7 +79,7 @@ static_assert(sizeof(IrDescriptorBinding) == 32, "IrDescriptorBinding changed: u static_assert(sizeof(BindingAllocationResult) == 120, "BindingAllocationResult changed: update the allocation encoder"); static_assert(sizeof(ResourceSpecialization) == 72, "ResourceSpecialization changed: update BuildKey"); static_assert(sizeof(ResourceSpecialization::Buffer) == 16, "ResourceSpecialization::Buffer changed: update BuildKey"); -static_assert(sizeof(ResourceSpecialization::Image) == 36, "ResourceSpecialization::Image changed: update BuildKey"); +static_assert(sizeof(ResourceSpecialization::Image) == 40, "ResourceSpecialization::Image changed: update BuildKey"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); #endif @@ -401,6 +401,7 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { out.Value(image.r128); out.Value(image.depthBits); out.Value(image.depthUnorm16); + out.Value(image.empty); out.Value(image.indirectRoot); out.Value(image.indirectMappingOffset); out.Value(image.indirectSearchIterations); @@ -488,6 +489,7 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { in.Value(image.r128); in.Value(image.depthBits); in.Value(image.depthUnorm16); + in.Value(image.empty); in.Value(image.indirectRoot); in.Value(image.indirectMappingOffset); in.Value(image.indirectSearchIterations); @@ -800,6 +802,7 @@ void BuildKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, c out.Value(image.fmask); out.Value(image.depthBits); out.Value(image.depthUnorm16); + out.Value(image.empty); }); writer.Values(std::span(specialization.boundDescriptors)); const auto& switches = switchKey(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index b0fee41fa6e..2bb59cdbe4b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -149,6 +149,9 @@ std::uint32_t LoadSampledImageDescriptor(SpirvEmitterState& state, std::uint32_t if (imageResource.resourceClass != ImageResourceClass::Sampled) { FailEmit("sampled image descriptor requested for a non-sampled image"); } + if (imageResource.empty) { + FailEmit("reading an image view that starts past its surface's last mip level is not implemented"); + } const auto kind = DescriptorBindingForImage(imageResource); const auto arrayIndex = ResourceForDescriptor(state, kind, resource); const auto variable = state.imageVariables.at(ImageBindingIndex(kind)); @@ -197,6 +200,9 @@ std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint3 if (image.resourceClass != ImageResourceClass::Storage) { FailEmit("storage image descriptor requested for a non-storage image"); } + if (image.empty) { + FailEmit("reading an image view that starts past its surface's last mip level is not implemented"); + } const auto kind = DescriptorBindingForImage(image); const auto arrayIndex = ResourceForDescriptor(state, kind, resource); const auto pointerType = state.module.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, ImageType(state, image)); @@ -209,6 +215,10 @@ void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std if (image.resourceClass != ImageResourceClass::Storage) { FailEmit("storage image write requested for a non-storage image"); } + if (image.empty) { + if (image.atomic) FailEmit("atomics on an image view that starts past its surface's last mip level are not implemented"); + return; + } if (!image.atomic) { state.module.EmitCapability(spv::CapabilityStorageImageWriteWithoutFormat); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index fa678defea8..aabda952a47 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -63,4 +63,5 @@ Throughout the project, every function at every stage either **does exactly what - [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates only the 101-key (US) arrangement and throws for the 106-key (Japanese) one; Ctrl and Alt do not change the character. - [libSceAudiodec](../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. - [libScePngEnc](../../core/libs/prx/libScePngEnc/Export.cpp) honours `filter_type` only as all filters (adaptive) or a single filter: the [PNG encoder](../../core/Decoder/Png/src/Png.cpp) (stb) cannot restrict adaptive filtering to a subset, so the first filter in the mask is used. +- A texture view whose BASE_LEVEL is past the descriptor's MAX_MIP ([GuestTextureResource](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp)) is treated as a view of no level: [image stores to it are compiled out](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp) and the binding is the surface's last level, never written back; reads, samples and atomics throw. The hardware's behaviour for such a view is not confirmed by AMD documentation (KytyPS5 instead adds the missing level to its host image). PPSA21567 runs one pass of a per-level compute chain into level 6 of a 6-level surface - [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations; its parameter ABI remains unverified.