diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2603c7fb2c..e9ca57674b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3,11 +3,13 @@ #include "BdaTests.hpp" #include "GraphicsTests.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -6982,6 +6984,161 @@ void resourceTests() { } } +void unsetFormatStorageImageTests() { + using ShaderRecompiler::DescriptorBinding; + using ShaderRecompiler::DescriptorImageShape; + using ShaderRecompiler::DescriptorKind; + using ShaderRecompiler::DescriptorRole; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + alignas(256) std::array guest{}; + const auto base = reinterpret_cast(guest.data()); + const auto words = [](std::uint32_t format, std::uint64_t baseAddress) { + return std::vector{ + static_cast(baseAddress >> 8u), + static_cast((baseAddress >> 40u) & 0xffu) | ((format & 0x1ffu) << 20u) | (3u << 30u), + 3u | (15u << 14u), + 0xfacu | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; + }; + const auto storage = [&](std::uint32_t format, std::uint64_t baseAddress) { + DescriptorBinding binding{}; + binding.kind = DescriptorKind::StorageImage; + binding.role = DescriptorRole::GuestImages; + binding.binding = ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; + binding.count = 1; + binding.imageWritten = {true}; + binding.guestDescriptor = words(format, baseAddress); + return binding; + }; + for (const std::uint32_t format : {0u, 0x1ffu}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.limits.maxPerStageDescriptorStorageImages = 8; + context.limits.maxDescriptorSetStorageImages = 8; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + vertex.bindings.push_back(storage(format, base)); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { + AgcDriver::Graphics::TextureDetiler detiler(context); + context.detiler = &detiler; + expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); }, "unsupported guest texture format"); + } + Require(mock.live == 0, "a rejected unset-format storage image leaked Vulkan objects"); + } + { + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.limits.maxPerStageDescriptorSampledImages = 8; + context.limits.maxDescriptorSetSampledImages = 8; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + auto sampled = storage(0u, base); + sampled.kind = DescriptorKind::SampledImage; + sampled.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + sampled.imageWritten.clear(); + sampled.imageShape = DescriptorImageShape::Image2D; + sampled.imageSamplers = {0}; + vertex.bindings.push_back(sampled); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { + AgcDriver::Graphics::TextureDetiler detiler(context); + context.detiler = &detiler; + AgcDriver::Graphics::TextureCache textureCache(context); + context.textureCache = &textureCache; + AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); + } + Require(mock.live == 0, "a sampled unset-format image leaked Vulkan objects"); + } + { + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.limits.maxPerStageDescriptorSampledImages = 8; + context.limits.maxDescriptorSetSampledImages = 8; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + auto sampled = storage(0u, base); + sampled.kind = DescriptorKind::SampledImage; + sampled.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + sampled.imageWritten.clear(); + sampled.imageShape = DescriptorImageShape::Image1DArray; + sampled.imageSamplers = {0}; + sampled.guestDescriptor[3] = (sampled.guestDescriptor[3] & ~(0xfu << 28u)) | (12u << 28u); + vertex.bindings.push_back(sampled); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { + AgcDriver::Graphics::TextureDetiler detiler(context); + context.detiler = &detiler; + AgcDriver::Graphics::TextureCache textureCache(context); + context.textureCache = &textureCache; + AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); + } + Require(mock.live == 0, "a sampled TYPE 12 image with an unset format leaked Vulkan objects"); + } + for (const std::uint32_t format : {0u, 0x1ffu}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.limits.maxPerStageDescriptorSampledImages = 8; + context.limits.maxDescriptorSetSampledImages = 8; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + auto sampled = storage(format, base); + sampled.kind = DescriptorKind::SampledImage; + sampled.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + sampled.imageWritten.clear(); + sampled.imageShape = DescriptorImageShape::Image2D; + sampled.imageSamplers = {0}; + sampled.guestDescriptor[3] = (sampled.guestDescriptor[3] & ~(0xfu << 28u)) | (2u << 28u); + vertex.bindings.push_back(sampled); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { + AgcDriver::Graphics::TextureDetiler detiler(context); + context.detiler = &detiler; + AgcDriver::Graphics::TextureCache textureCache(context); + context.textureCache = &textureCache; + expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); }, "unsupported image type 2"); + } + Require(mock.live == 0, "a rejected reserved-type sampled image leaked Vulkan objects"); + } + { + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.limits.maxPerStageDescriptorStorageImages = 8; + context.limits.maxDescriptorSetStorageImages = 8; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + vertex.bindings.push_back(storage(0u, 0)); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { + AgcDriver::Graphics::TextureDetiler detiler(context); + context.detiler = &detiler; + AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); + } + Require(mock.live == 0, "a null storage image with an unset format leaked Vulkan objects"); + } + { + ShaderRecompiler::RecompileResult compiled; + compiled.bindings.push_back(storage(Format8888UNorm, base)); + bool bounded = false; + const auto ranges = AgcDriver::DriverDetail::DispatchWriteRanges(compiled, bounded); + Require(ranges.size() == 1 && ranges[0].first == base && ranges[0].second == base + 4096u, "a written storage image did not contribute its whole surface to the dispatch write ranges"); + compiled.bindings.front().guestDescriptor = words(0u, base); + expectFailure([&] { static_cast(AgcDriver::DriverDetail::DispatchWriteRanges(compiled, bounded)); }, "unsupported guest texture format"); + compiled.bindings.front().guestDescriptor = words(0u, 0u); + Require(AgcDriver::DriverDetail::DispatchWriteRanges(compiled, bounded).empty(), "a null written storage image contributed dispatch write ranges"); + } +} + void descriptorCacheTests() { mock = MockVulkan{}; auto context = mockContext(); @@ -13321,6 +13478,7 @@ int main(int argc, char** argv) { descriptorPoolTests(); pushConstantTests(); resourceTests(); + unsetFormatStorageImageTests(); oversizedDescriptorTests(); viewAtRangeLimitTests(); guestBufferViewBytesTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 54799df27a..2854acd563 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -158,6 +158,23 @@ void RunGuestTextureResourceTests() { const std::array reportedStorage{0x04efd000u, 0x00040011u, 0x00017a00u, 0x00005204u, 0x0af00000u, 0u, 0x00000612u, 0x00fff000u}; Require(IsNullTextureResource(reportedStorage), "the type-0 words of a written storage slot were not classified as null"); reject([&] { static_cast(DecodeTextureResource(reportedStorage)); }, "guest texture descriptor"); + Fields unsetFormat = base; + unsetFormat.format = 0u; + unsetFormat.typeRaw = 12u; + Require(!IsNullTextureResource(pack(unsetFormat)), "a zero-format texture was classified as null for every consumer"); + Require(IsNullSampledTextureResource(pack(unsetFormat)), "a zero-format image-type texture was not classified as null for a sampled consumer"); + reject([] { ResolveTextureFormat(0u); }, "unsupported guest texture format"); + unsetFormat.format = 0x1ffu; + Require(!IsNullTextureResource(pack(unsetFormat)), "an all-ones-format texture was classified as null for every consumer"); + Require(IsNullSampledTextureResource(pack(unsetFormat)), "an all-ones-format image-type texture was not classified as null for a sampled consumer"); + reject([] { ResolveTextureFormat(0x1ffu); }, "unsupported guest texture format"); + unsetFormat.typeRaw = 2u; + unsetFormat.format = 0u; + Require(!IsNullSampledTextureResource(pack(unsetFormat)), "a reserved type-2 texture with a zero format was classified as null for a sampled consumer"); + unsetFormat.format = 0x1ffu; + Require(!IsNullSampledTextureResource(pack(unsetFormat)), "a reserved type-2 texture with an all-ones format was classified as null for a sampled consumer"); + unsetFormat.typeRaw = 15u; + Require(!IsNullSampledTextureResource(pack(unsetFormat)), "a type-15 texture with an unset format was classified as null for a sampled consumer"); auto result = DecodeTextureResource(pack(base)); Require(result.baseAddress == (base.base40 << 8u), "decoded base address changed"); Require(result.width == 16 && result.height == 16, "decoded width or height changed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 82f5087838..a17078c1b1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2104,7 +2104,7 @@ void verifyTypeZeroImageMaterialization() { { auto tornT = oldT; - tornT[1] &= ~(0x1ffu << 20u); + tornT[3] = (tornT[3] & ~(0xfu << 28u)) | (2u << 28u); srt.fill(0u); std::copy(tornT.begin(), tornT.end(), srt.begin()); std::copy(outputV.begin(), outputV.end(), srt.begin() + 16u); @@ -2145,7 +2145,7 @@ void verifyTypeZeroImageMaterialization() { { auto tornT = oldT; - tornT[1] &= ~(0x1ffu << 20u); + tornT[3] = (tornT[3] & ~(0xfu << 28u)) | (2u << 28u); srt.fill(0u); std::copy(tornT.begin(), tornT.end(), srt.begin()); std::copy(outputV.begin(), outputV.end(), srt.begin() + 16u); @@ -2366,6 +2366,68 @@ void verifyTypeZeroImageMaterialization() { } } +void verifyUnsetFormatImageMaterialization() { + using namespace ShaderRecompiler; + constexpr std::array measuredImage{0xffffffffu, 0xffffffffu, 0x00000019u, 0xc0017901u, 0x00000342u, 0xc8000000u, 0xc0064900u, 0x0000062fu}; + const auto materialize = [](const std::array& words, const ImageResource& image) { + IrResourcePlan plan; + plan.srtPlanComplete = true; + plan.resourceTrackingComplete = true; + plan.userDataCount = 0u; + plan.materializationSources = {0u}; + std::vector> values; + DescriptorSource source; + source.dwordCount = static_cast(words.size()); + for (std::uint32_t i = 0u; i < words.size(); ++i) { + auto value = std::make_unique(IrOpcode::Void, IrType::U32, i); + value->SetImmediateU32(words[i]); + source.dwords[i] = value.get(); + values.push_back(std::move(value)); + } + plan.descriptorSources.push_back(source); + plan.info.images.push_back(image); + plan.info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); + ResourceSnapshot snapshot; + ResourceMaterializer{}.Materialize(plan, SrtRuntime{}, snapshot); + return snapshot; + }; + ImageResource sampled; + sampled.source = 0u; + sampled.resourceClass = ImageResourceClass::Sampled; + sampled.dimension = RdnaImageDimension::Dim1DArray; + sampled.read = true; + for (const std::uint32_t format : {0u, 0x1ffu}) { + auto words = measuredImage; + words[1] = (words[1] & ~(0x1ffu << 20u)) | (format << 20u); + const auto snapshot = materialize(words, sampled); + require(snapshot.images.size() == 1u && snapshot.images[0].dwordCount == 8u && snapshot.images[0].dwords == words, + "an unset-format measured image-type T# was rejected or altered during materialization"); + for (const bool atomic : {false, true}) { + auto storage = sampled; + storage.resourceClass = ImageResourceClass::Storage; + storage.read = false; + storage.written = !atomic; + storage.atomic = atomic; + bool rejected = false; + try { + static_cast(materialize(words, storage)); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()).find("image descriptor") != std::string_view::npos; + } + require(rejected, atomic ? "an unset-format atomic image descriptor was accepted" : "an unset-format writable image descriptor was accepted"); + } + } + const auto expectInvalidType = [&](std::uint32_t type, const char* message) { + auto words = measuredImage; + words[1] &= ~(0x1ffu << 20u); + words[3] = (words[3] & ~(0xfu << 28u)) | (type << 28u); + expectFailure([&] { static_cast(materialize(words, sampled)); }, "runtime image descriptor is invalid", message); + }; + expectInvalidType(2u, "a reserved TYPE 2 T# with an unset format was accepted as a null image"); + expectInvalidType(7u, "a reserved TYPE 7 T# with an unset format was accepted as a null image"); + expectInvalidType(15u, "an unimplemented TYPE 15 T# with an unset format was accepted as a null image"); +} + void verifyOutOfRangeConstantReads() { std::vector code{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf4000400u, 0xfa000050u, 0xf4080a00u, 0xfa000080u, 0xbf8cc07fu, 0xf42c0814u, 0xfa000000u, 0xbf8cc07fu, 0xbf068010u, 0xbf850008u}; for (std::uint32_t dword = 0; dword < 8u; dword++) { @@ -2773,6 +2835,22 @@ void verifyGuardedDescriptorSources() { AgcDriver::ShaderMemory memory({}); expectFailure([&] { static_cast(memory.Capture(request)); }, "runtime image descriptor is invalid", "guarded sources: an invalid T# behind a taken branch was accepted"); } + auto unsetFormat = valid; + unsetFormat[1] = valid[1] & ~(0x1ffu << 20u); + fill(unsetFormat, 1u); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && capture->snapshot.images[0].dwords == unsetFormat, "guarded sources: a T# with an unset format behind a taken branch was rejected or altered"); + } + auto filledFormat = valid; + filledFormat[1] = valid[1] | (0x1ffu << 20u); + fill(filledFormat, 1u); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && capture->snapshot.images[0].dwords == filledFormat, "guarded sources: a T# with an all-ones format behind a taken branch was rejected or altered"); + } auto typeOne = valid; typeOne[3] = 0xfacu | (1u << 28u); fill(typeOne, 1u); @@ -10894,6 +10972,7 @@ int main(int argc, char** argv) { verifyLayoutPair(); verifySignedSrtComparison(); verifyTypeZeroImageMaterialization(); + verifyUnsetFormatImageMaterialization(); verifyOutOfRangeConstantReads(); verifySignedMinimum(); verifyMayEvaluate();