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);