From 63ed0ffeaf02daaaf60a4f6aeb4f90db4d17c507 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Mon, 12 Oct 2026 22:58:16 -0400 Subject: [PATCH] feat(agc): sample the base level through unnormalized samplers The hardware ignores the LOD for unnormalized coordinates and samples the base level. A sampler used only by explicit-LOD, implicit-LOD, bias and gradient samples gets a pipeline specialization constant set from its unnormalized bit when the binding plan is prepared. When the constant is set, each of its samples is emitted as OpImageSampleExplicitLod with LOD 0, which is what Vulkan requires of unnormalized samplers. Otherwise the sample keeps its own form. Such shaders are no longer refused. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 31 ++++++++++++++++--- .../execution/ImageSampleUnnormalized.cpp | 10 ++++++ .../IrMetadata/Resources.hpp | 4 +++ .../src/DescriptorBindingBuilder.cpp | 3 +- .../recompiler/PipelineSpecialization.hpp | 1 + .../SpirvBackend/src/SpirvImageEmitter.cpp | 23 +++++++++----- 6 files changed, 60 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 4290c4a85..86c807bc7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2456,9 +2456,9 @@ void verifyUnnormalizedSamplers() { require(proven(plain, false), "unnormalized samplers: a normalized S# was flagged"); require(plain.variantId == accepted.variantId && plain.spirv.Words() == accepted.spirv.Words(), "unnormalized samplers: the unnormalized S# compiled another variant"); - for (const std::uint32_t mimg : {0xf0900f08u, 0xf0800f08u, 0xf0940f08u, 0xf0840f08u}) { + for (const std::uint32_t mimg : {0xf0900f08u, 0xf0800f08u, 0xf0940f08u, 0xf0840f08u, 0xf0880f08u}) { const auto result = recompile(program(mimg), userData(unnormalized)); - require(proven(result, true), "unnormalized samplers: a compute image_sample_l, image_sample, image_sample_b or image_sample_cl was not flagged"); + require(proven(result, true), "unnormalized samplers: a compute image_sample_l, image_sample, image_sample_b, image_sample_cl or image_sample_d was not flagged"); auditSpirv(result.spirv.Words()); } @@ -2468,7 +2468,6 @@ void verifyUnnormalizedSamplers() { reject(0xf0bc0f08u, imageData(unnormalized, Type2D, Format32Float, 0u), "unnormalized guest sampler is used with depth comparison, which is not implemented", "unnormalized samplers: image_sample_c_lz was accepted"); reject(0xf11c0108u, userData(unnormalized), "unnormalized guest sampler is used by a gather, which is not implemented", "unnormalized samplers: image_gather4_lz was accepted"); reject(0xf1800308u, userData(unnormalized), "unnormalized guest sampler is used by image_get_lod, which is not implemented", "unnormalized samplers: image_get_lod was accepted"); - reject(0xf0880f08u, userData(unnormalized), "unnormalized guest sampler is used by a sample with derivatives, which is not implemented", "unnormalized samplers: image_sample_d was accepted"); reject(0xf0800f09u, userData(unnormalized), "unnormalized guest sampler is used by an image_sample_*_a variant, which is not implemented", "unnormalized samplers: image_sample_a was accepted"); reject(0xf09c0f18u, imageData(unnormalized, TypeCube, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a cube T# was accepted"); reject(0xf09c0f10u, imageData(unnormalized, Type3D, Format8888UNorm, 3u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted"); @@ -2543,6 +2542,14 @@ void verifyUnnormalizedSamplers() { require(!masks.empty() && std::ranges::all_of(masks, [](std::uint32_t mask) { return (mask & spv::ImageOperandsOffsetMask) != 0u; }), "unnormalized texel offsets: a normalized computed offset lost its Offset operand"); } + const auto gradient = program(0xf0880f08u); + const auto gradientAddress = nextAddress += 0x1000u; + const auto gradientUnnormalized = recompile(gradient, userData(unnormalized), gradientAddress); + const auto gradientNormalized = recompile(gradient, userData(normalized), gradientAddress); + require(proven(gradientNormalized, false) && gradientNormalized.variantId == gradientUnnormalized.variantId && gradientNormalized.PipelineVariantId() != gradientUnnormalized.PipelineVariantId(), "unnormalized samplers: a normalized image_sample_d did not get its own specialization of the shared artifact"); + require(sampleMasks(gradientNormalized.spirv.Words()) == std::vector{spv::ImageOperandsGradMask}, "unnormalized samplers: a normalized image_sample_d lost its derivatives"); + require(sampleMasks(gradientUnnormalized.spirv.Words()) == std::vector{spv::ImageOperandsLodMask}, "unnormalized samplers: an unnormalized image_sample_d kept its derivatives"); + const std::array pixelCode{0xf0800f08u, 0x00400801u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; const auto pixelData = userData(unnormalized); RecompileRequest pixel{}; @@ -2556,7 +2563,23 @@ void verifyUnnormalizedSamplers() { pixel.target.subgroupSize = 64; pixel.layout.pushConstantSizeBytes = 128; pixel.useCache = false; - expectFailure([&] { static_cast(Recompile(pixel)); }, "unnormalized guest sampler is used by an implicit-LOD sample, which is not implemented", "unnormalized samplers: a pixel image_sample was accepted"); + const auto implicitSamples = [](const std::vector& words) { + std::size_t samples = 0; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + require(count != 0 && count <= words.size() - cursor, "unnormalized samplers: truncated SPIR-V instruction"); + if ((words[cursor] & 0xffffu) == spv::OpImageSampleImplicitLod) ++samples; + cursor += count; + } + return samples; + }; + const auto pixelUnnormalized = Recompile(pixel); + require(proven(pixelUnnormalized, true), "unnormalized samplers: a pixel image_sample through an unnormalized S# was not flagged"); + auditSpirv(pixelUnnormalized.spirv.Words()); + const auto normalizedPixelData = userData(normalized); + pixel.context.userData = normalizedPixelData; + const auto pixelNormalized = Recompile(pixel); + require(proven(pixelNormalized, false) && implicitSamples(pixelNormalized.spirv.Words()) == 1u, "unnormalized samplers: a pixel image_sample through a normalized S# is not an implicit-LOD sample"); } void verifyUnusedUnnormalizedSampler() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp index 9a5394069..88a589493 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp @@ -53,6 +53,12 @@ alignas(256) constexpr std::array LevelZeroCode{ 0x80000b01, 0xbf810000, }; +alignas(256) constexpr std::array GradientCode{ + 0x34020086, 0xe0301000, 0x80000601, 0xe0301004, 0x80000701, 0xbf8c3f70, 0x7e0402f6, 0x7e060280, + 0x7e080280, 0x7e0a02f6, 0xf0880f08, 0x00610802, 0xbf8c3f70, 0xe0701010, 0x80000801, 0xe0701014, + 0x80000901, 0xe0701018, 0x80000a01, 0xe070101c, 0x80000b01, 0xbf810000, +}; + alignas(256) constexpr std::array OffsetCode{ 0x34020086, 0xe0301008, 0x80000201, 0xe0301000, 0x80000301, 0xe0301004, 0x80000401, 0xbf8c3f70, 0x7e0a02ff, 0x402ccccd, 0x7e2802ff, 0x0000023d, 0xf0dc0f08, 0x00610802, 0xf0d00f08, 0x00610c02, @@ -75,6 +81,7 @@ struct Program { const Program Plain{Code, {"image_sample_lz", "image_sample_l 2.7", "image_sample"}, {Offset::None, Offset::None, Offset::None}}; const Program LevelZero{LevelZeroCode, {"image_sample_lz", "", ""}, {Offset::None, Offset::None, Offset::None}, 1}; +const Program Gradient{GradientCode, {"image_sample_d (4, 0, 0, 4)", "", ""}, {Offset::None, Offset::None, Offset::None}, 1}; const Program Offsets{OffsetCode, {"image_sample_lz_o", "image_sample_l_o 2.7", "image_sample_lz_o (-3, 2) NSA"}, {Offset::Lane, Offset::Lane, Offset::Constant}}; alignas(256) constexpr std::array ConstantCode{ @@ -313,6 +320,8 @@ int main() { Check(Plain, sampler, 0u, "first level of a 4-level image", coordinates, multiSamples); Require(multiSamples == singleSamples, std::string(sampler.name) + ": the first level of a 4-level image does not sample like a 1-level image"); Check(Offsets, sampler, 0u, "1-level image", offsetCoordinates, Run(*device, Offsets, single, sampler.words, offsetCoordinates)); + Check(Gradient, sampler, 0u, "1-level image", coordinates, Run(*device, Gradient, single, sampler.words, coordinates)); + Check(Gradient, sampler, 0u, "first level of a 4-level image", coordinates, Run(*device, Gradient, multi, sampler.words, coordinates)); if (!sampler.atBaseLevel) continue; const auto levelZeroSingle = Run(*device, LevelZero, single, sampler.words, coordinates); const auto levelZeroChain = Run(*device, LevelZero, chain, sampler.words, coordinates); @@ -324,6 +333,7 @@ int main() { ExpectFailure(*device, Plain, chain, Samplers[0].words, "single-level", "a 4-level view read by image_sample_l and image_sample"); ExpectFailure(*device, Plain, fromLevelOne, Samplers[0].words, "single-level", "a view starting at mip 1 read by image_sample_l and image_sample"); ExpectFailure(*device, Offsets, chain, Samplers[0].words, "single-level", "a 4-level view read with texel offsets"); + ExpectFailure(*device, Gradient, chain, Samplers[0].words, "single-level", "a 4-level view read by image_sample_d"); ExpectFailure(*device, LevelZero, chain, Samplers[2].words, "single-level", "a 4-level view through an S# with a minimum LOD and a LOD bias"); ExpectFailure(*device, Plain, single, {0x00008092u, 0x00fff000u, 0x05100000u, 0u}, "different minification", "unequal minification and magnification filters"); ExpectFailure(*device, Plain, single, {0x00008090u, 0x00fff000u, 0x05500000u, 0u}, "clamp mode 0", "wrap on X"); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 25e0805e5..79cbff90c 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -135,6 +135,10 @@ inline bool FoldsTexelOffsets(const SamplerResource& sampler) { return (sampler.uses & SamplerUseOffset) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseOffset | SamplerUseNonZeroLod)) == 0u && !sampler.depthCompare; } +inline bool SamplesBaseLevelIfUnnormalized(const SamplerResource& sampler) { + return (sampler.uses & (SamplerUseImplicitLod | SamplerUseGradient)) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseImplicitLod | SamplerUseGradient | SamplerUseNonZeroLod)) == 0u && !sampler.depthCompare; +} + struct SampledResourcePair { std::uint32_t image = 0; std::uint32_t sampler = 0; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 5951e7877..608136513 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -175,7 +175,7 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh continue; } const std::uint32_t unsupported = sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseOffset | SamplerUseNonZeroLod); - if (unsupported != 0u) { + if (unsupported != 0u && !SamplesBaseLevelIfUnnormalized(sampler)) { failUnnormalized(UnnormalizedUseReason(unsupported)); } if (sampler.depthCompare) { @@ -259,6 +259,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto unnormalized = ProveUnnormalized(info, snapshot); for (std::uint32_t index = 0; index < info.samplers.size(); ++index) { if (FoldsTexelOffsets(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::FoldOffsetBase + index, unnormalized.samplers[index] ? 1u : 0u}); + if (SamplesBaseLevelIfUnnormalized(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::UnnormalizedBaseLevelBase + index, unnormalized.samplers[index] ? 1u : 0u}); } std::vector compareStates(info.images.size()); for (std::uint32_t index = 0; index < info.images.size(); ++index) { diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index ddae3a272..9293e3f65 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -34,6 +34,7 @@ inline constexpr std::uint32_t DescriptorIndexStride = 128u; inline constexpr std::uint32_t ImageModeBase = 32768u; inline constexpr std::uint32_t ImageModeStride = 256u; inline constexpr std::uint32_t FoldOffsetBase = 57344u; +inline constexpr std::uint32_t UnnormalizedBaseLevelBase = 57408u; inline constexpr std::uint32_t DescriptorIndex(std::uint32_t binding, std::uint32_t element) { return DescriptorIndexBase + binding * DescriptorIndexStride + element; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index d380b33fc..dcecad865 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1474,10 +1474,10 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con operands.insert(operands.end() - ((operandMask & spv::ImageOperandsMinLodMask) != 0u ? 1 : 0), offset); } - const auto emitSample = [&](std::uint32_t coord, std::uint32_t mask, const std::vector& arguments) { + const auto emitSample = [&](std::uint32_t coord, std::uint32_t mask, const std::vector& arguments, std::uint32_t sampleOpcode) { const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler, access.slot); const auto sample = state.module.AllocateId(); - std::vector words = {opcode, resultType, sample, sampled, coord}; + std::vector words = {sampleOpcode, resultType, sample, sampled, coord}; if (setup.dref) { words.push_back(drefValue); } @@ -1489,10 +1489,19 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con return sample; }; std::uint32_t sample = 0; - if (!foldOffset) { - sample = emitSample(setup.coord, operandMask, operands); + const bool baseLevelIfUnnormalized = !setup.dref && SamplesBaseLevelIfUnnormalized(state.program.Info().samplers.at(mem.sampler)); + if (baseLevelIfUnnormalized) { + const auto selector = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::UnnormalizedBaseLevelBase + mem.sampler, 0u); + const auto unnormalized = Binary(state, spv::OpINotEqual, TypeBool(state), selector, ConstantU32(state, 0u)); + sample = EmitValueIfElse(state, unnormalized, resultType, [&] { + return emitSample(setup.coord, spv::ImageOperandsLodMask, {ZeroF32(state)}, spv::OpImageSampleExplicitLod); + }, [&] { + return emitSample(setup.coord, operandMask, operands, opcode); + }); + } else if (!foldOffset) { + sample = emitSample(setup.coord, operandMask, operands, opcode); } else if (!offsetOperand) { - sample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands); + sample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands, opcode); } else { const auto selector = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::FoldOffsetBase + mem.sampler, 0u); const auto folded = Binary(state, spv::OpINotEqual, TypeBool(state), selector, ConstantU32(state, 0u)); @@ -1502,11 +1511,11 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, folded, foldLabel, operandLabel); EmitLabel(state, foldLabel); - const auto foldSample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands); + const auto foldSample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands, opcode); const auto foldEnd = state.currentLabel; state.module.AddFunction(spv::OpBranch, merge); EmitLabel(state, operandLabel); - const auto operandSample = emitSample(setup.coord, operandMask, operands); + const auto operandSample = emitSample(setup.coord, operandMask, operands, opcode); const auto operandEnd = state.currentLabel; state.module.AddFunction(spv::OpBranch, merge); EmitLabel(state, merge);