From 7c19c66def8fde929b9862dcf48a1d100c9fc49e Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Sat, 17 Oct 2026 13:06:53 -0400 Subject: [PATCH] fix(shader): fold texel offsets into base-level unnormalized samples An implicit-LOD or gradient sample with a texel offset through an unnormalized S# was refused at bind time with "unnormalized guest sampler is used by an implicit-LOD sample". PPSA01317 aborts on boot on such a pixel image_sample_o. The base-level path already samples these through OpImageSampleExplicitLod with LOD 0, which Vulkan allows for unnormalized samplers, but it dropped the offset. It now adds the offset to the coordinates in that branch, as the explicit-LOD offset fold does, so a sampler whose uses also include offsets takes the base-level path. A normalized S# keeps the offset operand. AI-assisted: yes --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 20 +++++++++++++++++++ .../IrMetadata/Resources.hpp | 2 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 51dee75d38..87d83cf38b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4272,6 +4272,26 @@ void verifyUnnormalizedSamplers() { 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"); + const std::array pixelOffsetCode{0x7e0202ffu, 0x00000201u, 0xf0c00f08u, 0x00400801u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; + pixel.shader = {ShaderStage::Fragment, 0x50000u, pixelOffsetCode, 0, {}}; + pixel.context.userData = pixelData; + pixel.target.supportedCapabilities = capabilities; + pixel.target.nonConstantImageOffsets = true; + const auto pixelOffsetUnnormalized = Recompile(pixel); + require(proven(pixelOffsetUnnormalized, true), "unnormalized texel offsets: a pixel image_sample_o through an unnormalized S# was not flagged"); + auditSpirv(pixelOffsetUnnormalized.spirv.Words()); + require(foldedSamples(pixelOffsetUnnormalized.spirv.Words()), "unnormalized texel offsets: a pixel image_sample_o offset was not added to the base-level coordinates"); + pixel.context.userData = normalizedPixelData; + const auto pixelOffsetNormalized = Recompile(pixel); + std::vector implicitMasks; + const auto offsetWords = pixelOffsetNormalized.spirv.Words(); + for (std::size_t cursor = 5; cursor < offsetWords.size();) { + const auto count = offsetWords[cursor] >> 16u; + require(count != 0 && count <= offsetWords.size() - cursor, "unnormalized texel offsets: truncated SPIR-V instruction"); + if ((offsetWords[cursor] & 0xffffu) == spv::OpImageSampleImplicitLod && count > 5u) implicitMasks.push_back(offsetWords[cursor + 5]); + cursor += count; + } + require(proven(pixelOffsetNormalized, false) && implicitMasks.size() == 1u && (implicitMasks[0] & (spv::ImageOperandsConstOffsetMask | spv::ImageOperandsOffsetMask)) != 0u, "unnormalized texel offsets: a normalized pixel image_sample_o is not an implicit-LOD sample with its offset"); } void verifyUnusedUnnormalizedSampler() { diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 85c985a879..4750cccb14 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -140,7 +140,7 @@ inline bool FoldsTexelOffsets(const SamplerResource& sampler) { } inline bool SamplesBaseLevelIfUnnormalized(const SamplerResource& sampler) { - return (sampler.uses & (SamplerUseImplicitLod | SamplerUseGradient)) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseImplicitLod | SamplerUseGradient | SamplerUseNonZeroLod)) == 0u && !sampler.depthCompare; + return (sampler.uses & (SamplerUseImplicitLod | SamplerUseGradient)) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseImplicitLod | SamplerUseGradient | SamplerUseNonZeroLod | SamplerUseOffset)) == 0u && !sampler.depthCompare; } struct SampledResourcePair { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index d2ce577837..8fe26ae49d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1465,7 +1465,7 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con 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(hasOffset ? FoldedOffsetCoord(ctx, access, setup) : setup.coord, spv::ImageOperandsLodMask, {ZeroF32(state)}, spv::OpImageSampleExplicitLod); }, [&] { return emitSample(setup.coord, operandMask, operands, opcode); });