test(agc): pin the unset-format T# rule to image types and sampled reads

This commit is contained in:
mixyt committed 2026-11-02 15:28:37 +00:00
1 parent 0dd1814129
commit e1eda1a2ae
3 files changed
+256 -2

No files matched your search

@@ -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<std::byte, 4096> guest{};
const auto base = reinterpret_cast<std::uint64_t>(guest.data());
const auto words = [](std::uint32_t format, std::uint64_t baseAddress) {
return std::vector<std::uint32_t>{
static_cast<std::uint32_t>(baseAddress >> 8u),
static_cast<std::uint32_t>((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<VkDevice>();
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<VkDevice>();
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<VkDevice>();
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<VkDevice>();
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<VkDevice>();
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<void>(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();
@@ -158,6 +158,23 @@ void RunGuestTextureResourceTests() {
const std::array<std::uint32_t, 8> 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<void>(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");
@@ -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<std::uint32_t, 8> measuredImage{0xffffffffu, 0xffffffffu, 0x00000019u, 0xc0017901u, 0x00000342u, 0xc8000000u, 0xc0064900u, 0x0000062fu};
const auto materialize = [](const std::array<std::uint32_t, 8>& words, const ImageResource& image) {
IrResourcePlan plan;
plan.srtPlanComplete = true;
plan.resourceTrackingComplete = true;
plan.userDataCount = 0u;
plan.materializationSources = {0u};
std::vector<std::unique_ptr<IrValue>> values;
DescriptorSource source;
source.dwordCount = static_cast<std::uint32_t>(words.size());
for (std::uint32_t i = 0u; i < words.size(); ++i) {
auto value = std::make_unique<IrValue>(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<void>(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<void>(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<std::uint32_t> 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<void>(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();