From e8b24afecd20606ecb848275bf0a85263a29d3c6 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Fri, 9 Oct 2026 12:25:03 -0400 Subject: [PATCH 1/2] feat(agc): fold samples of views whose channels all select constants When every dst_sel of a sampled view selects constant 0 or 1, a sample or gather returns that constant whatever the texel, filter, LOD or coordinates. Such placeholder views can have a dimension the static runtime image interface has no mode for (a 3D view under a 2D sample, a 1D view under a 2D gather), and a 3D one paired with an unnormalized sampler fails the unnormalized proof. A direct sampled image whose accesses are all non-compare samples or gathers gets three constant runtime modes (float, uint, sint). The mode is selected when all four dst_sel are 0 or 1, unless the view is FMASK, depth bits, converted or sRGB-decoded. In it the emitter returns each channel's constant (the dmask channel's, in all four texels, for a gather) from the image's swizzle specialization constants, and the image is left unbound. ProveUnnormalized skips the mode after the run-time-selected check, so table roots still throw. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 5 ++ .../execution/ImageSampleUnnormalized.cpp | 59 ++++++++++++++++++- .../IrMetadata/Resources.hpp | 2 + .../src/DescriptorBindingBuilder.cpp | 7 ++- .../Optimization/src/ResourceMaterializer.cpp | 20 +++++++ .../Optimization/src/ResourceTracker.cpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 6 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 21 +++++++ 8 files changed, 116 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 08c42d610..34f8135f6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1564,6 +1564,11 @@ void verifyUnusedUnnormalizedSampler() { auto selected = info; selected.images[0].indirectRoot = 0u; expectFailure([&] { static_cast(populate(selected)); }, "unnormalized guest sampler samples an image selected at run time, which is not implemented", "unnormalized samplers: an image table root was accepted"); + snapshot.images[0].dwords[3] = 0xa0000041u; + const auto constant = populate(info); + require(constant.size() == 1u && constant[0].samplerUnnormalized == std::vector{true}, "unnormalized samplers: a 3D view whose channels select constants was bound or refused"); + expectFailure([&] { static_cast(populate(selected)); }, "unnormalized guest sampler samples an image selected at run time, which is not implemented", "unnormalized samplers: an image table root whose channels select constants was accepted"); + snapshot.images[0].dwords[3] = 0x90000facu; auto compared = info; compared.samplers[0].depthCompare = true; expectFailure([&] { static_cast(populate(compared)); }, "unnormalized guest sampler is used with depth comparison, which is not implemented", "unnormalized samplers: a depth-compare S# without live uses was accepted"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp index cc0385031..4d1274912 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp @@ -28,7 +28,12 @@ constexpr std::int32_t Width = 13; constexpr std::int32_t Height = 7; constexpr std::uint32_t StorageLevels = 4; constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t Format32SInt = 21; +constexpr std::uint32_t Type1D = 8; constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type3D = 10; +constexpr std::uint32_t OneZeroOneZero = 0x041u; constexpr std::uint8_t OtherLevelTexel = 0x40u; alignas(256) std::array Buffer{}; alignas(256) std::array SingleLevel{}; @@ -44,6 +49,14 @@ alignas(256) constexpr std::array Code{ constexpr std::array Instructions{"image_sample_lz", "image_sample_l 2.7", "image_sample"}; +alignas(256) constexpr std::array ConstantCode{ + 0x34020086, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xbf8c3f70, 0xf09c0608, 0x00610802, + 0xf11c0208, 0x00610a02, 0xf11c0408, 0x00610e02, 0xbf8c3f70, 0xe0701010, 0x80000801, 0xe0701014, + 0x80000901, 0xe0701018, 0x80000a01, 0xe070101c, 0x80000b01, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xe0701030, 0x80001001, 0xe0701034, + 0x80001101, 0xbf810000, +}; + struct SamplerCase { const char* name; std::array words; @@ -107,6 +120,12 @@ void FillTexture(std::span texels, std::uint32_t levels) { } } +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + 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}; @@ -128,7 +147,7 @@ std::array TextureDescriptor(const void* data, std::uint32_t l using Samples = std::vector>; -Samples Run(AgcDriver::VulkanDevice& device, const std::array& texture, const std::array& sampler, const std::vector& coordinates) { +Samples Run(AgcDriver::VulkanDevice& device, const std::array& texture, const std::array& sampler, const std::vector& coordinates, std::span code = Code) { Samples samples; for (std::size_t first = 0; first < coordinates.size(); first += Threads) { Buffer.fill(0xdeadbeefu); @@ -142,7 +161,6 @@ Samples Run(AgcDriver::VulkanDevice& device, const std::array& std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); - const std::span code(Code); 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{ @@ -231,6 +249,43 @@ int main() { ExpectFailure(*device, TextureDescriptor(MultiLevel.data(), 2u, 2u), Samplers[0].words, "single-level", "a 3-level view"); ExpectFailure(*device, single, {0x00008092u, 0x00fff000u, 0x05100000u, 0u}, "different minification", "unequal minification and magnification filters"); ExpectFailure(*device, single, {0x00008090u, 0x00fff000u, 0x05500000u, 0u}, "clamp mode 0", "wrap on X"); + const auto constantView = [&](std::uint32_t type, std::uint32_t format, std::uint32_t swizzle) { + auto descriptor = TextureDescriptor(SingleLevel.data(), 0u, 0u); + descriptor[1] = (descriptor[1] & ~(0x1ffu << 20u)) | (format << 20u); + descriptor[3] = swizzle | (type << 28u); + return descriptor; + }; + for (const auto& sampler : Samplers) { + for (const auto& sample : Run(*device, constantView(Type3D, Format8888UNorm, OneZeroOneZero), sampler.words, coordinates)) { + for (std::uint32_t index = 0; index < Results; ++index) { + const std::uint32_t expected = index % 2u == 0u ? 0x3f800000u : 0u; + Require(sample[index] == expected, std::string(sampler.name) + ": a 3D view whose channels select (1, 0, 1, 0) returned " + Hex(sample[index]) + " for result " + std::to_string(index)); + } + } + } + struct ConstantCase { + const char* name; + std::array texture; + std::uint32_t one; + }; + const std::array constants{{ + {"a 3D UNORM view selecting (1, 0, 1, 0)", constantView(Type3D, Format8888UNorm, OneZeroOneZero), 0x3f800000u}, + {"a 32_UINT view selecting (1, 0, 1, 0)", constantView(Type2D, Format32UInt, OneZeroOneZero), 1u}, + {"a 32_SINT view selecting (1, 0, 1, 0)", constantView(Type2D, Format32SInt, OneZeroOneZero), 1u}, + {"a 1D view selecting (0, 0, 0, 0)", constantView(Type1D, Format8888UNorm, 0u), 0u}, + }}; + const std::array constantInstructions{"image_sample_lz dmask 0x6", "image_gather4_lz dmask 0x2", "image_gather4_lz dmask 0x4"}; + for (const auto& constant : constants) { + auto normalized = Samplers[0].words; + normalized[0] &= ~0x8000u; + for (const auto& sample : Run(*device, constant.texture, normalized, coordinates, ConstantCode)) { + const std::array expected{0u, constant.one, 0u, 0u, 0u, 0u, constant.one, constant.one, constant.one, constant.one}; + for (std::uint32_t index = 0; index < expected.size(); ++index) { + const auto instruction = constantInstructions[index < 2u ? 0u : index < 6u ? 1u : 2u]; + Require(sample[index] == expected[index], std::string(instruction) + ", " + constant.name + ": result " + std::to_string(index) + " is " + Hex(sample[index]) + ", expected " + Hex(expected[index])); + } + } + } std::puts("image sample unnormalized tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index ec2d0c79f..9edaac96b 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -83,6 +83,8 @@ struct ImageResource { bool packed = false; bool fmaskCompatible = true; bool depthBitsCompatible = true; + bool constantSwizzle = false; + bool constantSwizzleCompatible = true; std::uint32_t byElements = 0; std::uint32_t byComponents = 0; IrBufferFormat packedFormat = IrBufferFormat::Invalid; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 0005d4c46..5a7a24e54 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -187,6 +187,9 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh if (image.indirectRoot != ImageResource::NoIndirectImage) { failUnnormalized("samples an image selected at run time"); } + if (image.constantSwizzle) { + continue; + } if ((image.dimension != RdnaImageDimension::Dim1D && image.dimension != RdnaImageDimension::Dim2D) || image.cube) { failUnnormalized("samples a 1D-array, 2D-array, 3D, cube or multisampled image"); } @@ -295,7 +298,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({PipelineSpecialization::ExportBase + target * 4u + component, (exportMappings[target] >> (component * 2u)) & 3u}); } std::vector samplerModes(info.samplers.size(), 0u); - const auto samplerMode = [](const ImageResource& image) { return image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid || image.depthBits ? 2u : 1u; }; + const auto samplerMode = [](const ImageResource& image) { return !image.constantSwizzle && (image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid || image.depthBits) ? 2u : 1u; }; for (const auto& pair : info.sampledPairs) { const auto& image = info.images.at(pair.image); if (image.indirectRoot == ImageResource::NoIndirectImage) samplerModes.at(pair.sampler) |= samplerMode(info.runtimeImageModes.at(pair.image).at(imageModes[pair.image])); @@ -343,7 +346,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l previous = resource; { const auto word = snapshot.images.at(resource).dwords[3]; - active = DescriptorBindingForImage(mode) == logical.kind && mode.packedFormat != IrBufferFormat::Fmask8_S2_F1 && (image.mipMode != ImageMipMode::DynamicStorage || mip <= ((word >> 16u) & 0xfu) - ((word >> 12u) & 0xfu)); + active = DescriptorBindingForImage(mode) == logical.kind && mode.packedFormat != IrBufferFormat::Fmask8_S2_F1 && !mode.constantSwizzle && (image.mipMode != ImageMipMode::DynamicStorage || mip <= ((word >> 16u) & 0xfu) - ((word >> 12u) & 0xfu)); } } else if (samplerHeap) { active = (samplerModes.at(resource) & (1u << (element & 1u))) != 0u; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index fe2061160..dea1dba13 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -703,6 +703,21 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe modes.push_back(mode); } } + if (image.constantSwizzleCompatible && !storage && !image.depthCompare && !image.packed && image.indirectRoot == ImageResource::NoIndirectImage) { + for (const auto numeric : {IrTextureNumericClass::Float, IrTextureNumericClass::Uint, IrTextureNumericClass::Sint}) { + auto mode = image; + mode.numericClass = numeric; + mode.conversionFormat = IrBufferFormat::Invalid; + mode.packedFormat = IrBufferFormat::Invalid; + mode.depthBits = false; + mode.depthUnorm16 = false; + mode.cube = false; + mode.mipCount = 1u; + mode.shaderSwizzle = ShaderImageIdentitySwizzle; + mode.constantSwizzle = true; + modes.push_back(mode); + } + } if (modes.empty()) throw std::runtime_error("image instruction has no supported runtime modes"); return modes; } @@ -727,6 +742,11 @@ std::uint32_t ResourceMaterializer::RuntimeImageMode(const ImageResource& image, } } if (decoded.mipCount > (image.mipMode == ImageMipMode::DynamicStorage ? RuntimeAbi::StorageHeapCapacity : 1u)) throw std::runtime_error("runtime storage image mip capacity exceeded"); + if ((descriptorImageSwizzle(descriptor) & 06666u) == 0u && !decoded.fmask && !decoded.depthBits && decoded.conversionFormat == IrBufferFormat::Invalid && !decoded.srgbDecode) { + for (std::uint32_t index = 0u; index < modes.size(); ++index) { + if (modes[index].constantSwizzle && modes[index].numericClass == decoded.numericClass) return index; + } + } for (std::uint32_t index = 0u; index < modes.size(); ++index) { const auto& mode = modes[index]; if (((mode.emulatedCompare & EmulatedCompare::Enabled) != 0u) != emulated) continue; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 85bda01e6..124eb2747 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1061,6 +1061,7 @@ private: image.srgbDecodeCompatible = image.srgbDecodeCompatible && !ImageOpcodeInfoOf(op).needsSampler; image.fmaskCompatible = image.fmaskCompatible && op == IrOpcode::ImageRead && memory.dataBits == 32u; image.depthBitsCompatible = image.depthBitsCompatible && memory.dataBits == 32u; + image.constantSwizzleCompatible = image.constantSwizzleCompatible && (op == IrOpcode::ImageSampleRaw || op == IrOpcode::ImageGatherRaw); if ((memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0u) { constexpr auto unsupported = RdnaImageSampleFlagLod | RdnaImageSampleFlagDerivative; if (op == IrOpcode::ImageGatherRaw || (memory.imageSampleFlags & unsupported) != 0u) image.emulatedCompare |= EmulatedCompare::Unsupported; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 40f10ab42..1f570b0ba 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -474,6 +474,8 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { out.Value(image.packed); out.Value(image.fmaskCompatible); out.Value(image.depthBitsCompatible); + out.Value(image.constantSwizzle); + out.Value(image.constantSwizzleCompatible); out.Value(image.byElements); out.Value(image.byComponents); out.Value(image.packedFormat); @@ -550,7 +552,7 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { in.Value(buffer.scalar); in.Value(buffer.typedAlignment); }); - reader.List(info.images, 72, [](Reader& in, ImageResource& image) { + reader.List(info.images, 74, [](Reader& in, ImageResource& image) { in.Value(image.source); in.Value(image.firstUsePc); in.Value(image.resourceClass); @@ -575,6 +577,8 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { in.Value(image.packed); in.Value(image.fmaskCompatible); in.Value(image.depthBitsCompatible); + in.Value(image.constantSwizzle); + in.Value(image.constantSwizzleCompatible); in.Value(image.byElements); in.Value(image.byComponents); in.Value(image.packedFormat); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index e9fde5d68..bcef5cda3 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1470,7 +1470,28 @@ TableSelection TableSlot(SpirvValueEmitContext& ctx, const IrValue& inst, const return EmitIndirectImageSelector(ctx, image, key); } +void EmitConstantSwizzleSample(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { + auto& state = ctx.state; + const auto numericClass = access.image.numericClass; + const bool gather = access.inst.Opcode() == IrOpcode::ImageGatherRaw; + const auto scalarType = ImageScalarType(state, numericClass); + const auto one = numericClass == IrTextureNumericClass::Float ? ConstantF32(state, 0x3f800000u) : numericClass == IrTextureNumericClass::Sint ? ConstantI32(state, 1) : ConstantU32(state, 1u); + const auto zero = SampledComponentZero(state, numericClass); + std::uint32_t channels[4] = {}; + for (std::uint32_t channel = 0; channel < 4u; channel++) { + const auto selector = RuntimeImageSwizzle(state, access.mem.resource, gather ? ImageGatherComponent(EffectiveDmask(access.mem)) : channel); + channels[channel] = Select(state, scalarType, Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, 1u)), one, zero); + } + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, ImageVectorType(state, numericClass, 4), value, channels[0], channels[1], channels[2], channels[3]); + ctx.Define(access.inst, ResultVector(ctx, access, value, numericClass, false, gather)); +} + void EmitSamplingOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { + if (access.image.constantSwizzle) { + EmitConstantSwizzleSample(ctx, access); + return; + } if (access.image.srgbDecode) { ctx.Fail(access.inst, "samples or gathers an sRGB image the device cannot sample, which is not implemented"); } From 995de0c81950f9bb337b35c1a0f8e578b9467f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 10 Oct 2026 01:22:28 +0200 Subject: [PATCH 2/2] fix(agc): bump the shader disk cache format version for the new image fields The ImageResource layout change needs its own version: main is already at 21. --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index a9262a584..f8254462b 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -12,7 +12,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 21; +inline constexpr std::uint32_t FormatVersion = 22; enum class LoadStatus { Loaded,