From ac3711ce7c4bd02db203751d433e622c0aae149e Mon Sep 17 00:00:00 2001 From: nmzik Date: Sun, 18 Oct 2026 20:50:03 +0200 Subject: [PATCH] shader: select the primary mip for linear explicit-LOD gathers --- .../backend/spirv/spirvEmitterImage.cpp | 12 ++++++--- .../ir/passes/ResourceMaterialization.cpp | 10 +++++--- tests/ResourceTrackingTests.cpp | 25 +++++++++++-------- tests/ShaderRecompilerComputeTests.cpp | 17 +++++++++---- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index 35b6e4102..2722fbba5 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -97,10 +97,14 @@ uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memory ConstantF32Value(state, 1.0f / 256.0f)); }; const auto sampler_control = ctx.Arg(inst, 6); + const auto mip_filter = Extract(sampler_control, 26, 2); const auto preclamp = Binary(state, spv::OpINotEqual, TypeBool(state), Extract(sampler_control, 28, 1), ConstantU32(state, 0)); - const auto half = ConstantF32Value(state, 0.5f); const auto zero = ZeroF32(state); + // Point selects the nearest mip; linear gathers return the lower, primary mip only. + const auto rounding = Select(state, TypeF32(state), + Binary(state, spv::OpIEqual, TypeBool(state), mip_filter, ConstantU32(state, 1)), + ConstantF32Value(state, 0.5f), zero); const auto base = Unary(state, spv::OpConvertUToF, TypeF32(state), Extract(ctx.Arg(inst, 4), 12, 4)); const auto minimum = Binary(state, spv::OpFSub, TypeF32(state), @@ -108,19 +112,19 @@ uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memory const auto last = ConstantF32Value(state, static_cast(mip_count - 1u)); auto lod = Binary(state, spv::OpFAdd, TypeF32(state), AddressF32(ctx, mem, address, layout.lod), - Select(state, TypeF32(state), preclamp, half, zero)); + Select(state, TypeF32(state), preclamp, rounding, zero)); lod = EmitGlsl( state, lod, LodFixed(ctx.Arg(inst, 5), 0), LodFixed(ctx.Arg(inst, 5), 12)); // T# minimum LOD is in physical mip space and applies after the S# view-relative clamp. lod = EmitGlsl(state, lod, minimum); lod = EmitGlsl(state, lod, zero, last); lod = Binary(state, spv::OpFAdd, TypeF32(state), lod, - Select(state, TypeF32(state), preclamp, zero, half)); + Select(state, TypeF32(state), preclamp, zero, rounding)); // The clamped value is nonnegative, so conversion performs the integer mip rounding. const auto mip = EmitUMin32(state, Unary(state, spv::OpConvertFToU, TypeU32(state), lod), ConstantU32(state, mip_count - 1u)); const auto mip_none = Binary(state, spv::OpIEqual, TypeBool(state), - Extract(sampler_control, 26, 2), ConstantU32(state, 0)); + mip_filter, ConstantU32(state, 0)); return Select(state, TypeU32(state), mip_none, ConstantU32(state, 0), mip); } diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 0427353cc..f6395ee1f 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -1671,11 +1671,13 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime if (program.info.samplers[base_index].gather_lod) { const auto control = snapshot.samplers[i].dwords[2]; const auto filter = (control >> 26u) & 3u; - // MipNone always selects the base level. Explicit point gathers currently require - // encoded-zero primary and secondary bias; linear primary-mip selection is unsupported. - if (filter > 1u || (filter == 1u && (control & 0xfffffu) != 0u)) { + const auto perf_mip = (snapshot.samplers[i].dwords[1] >> 24u) & 15u; + // MipNone always selects the base level. Point/linear selection requires zero + // primary/secondary bias; linear mip snapping is not implemented. + if (filter > 2u || (filter != 0u && (control & 0xfffffu) != 0u) || + (filter == 2u && perf_mip != 0u)) { return SpecializationFail( - "explicit-LOD gather requires mip filtering None or Point with zero LOD biases"); + "explicit-LOD gather requires None, unbiased Point, or unbiased Linear without mip snapping"); } } } diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 034022ddc..45fa82a6f 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -5177,19 +5177,24 @@ void TestGatherLodSamplerValidation() { Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u; user_data[3] = Libs::Graphics::DstSel(4, 5, 6, 7) | (static_cast(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u); - user_data[9] = 0xfff000u; constexpr std::array sampler_controls{ 0u, 0x4001u, 1u << 26u, (1u << 26u) | 256u, - (1u << 26u) | (1u << 14u), 2u << 26u, 3u << 26u}; + (1u << 26u) | (1u << 14u), 2u << 26u, (2u << 26u) | 256u, + (2u << 26u) | (1u << 14u), 0x0a500000u, 0x1a500000u, 3u << 26u}; for (const auto control : sampler_controls) { - user_data[10] = control; - ResourceSnapshot snapshot; - ResourceSpecialization specialization; - const bool supported = !explicit_lod || (control >> 26u) == 0 || - control == (1u << 26u); - Check(MaterializeResources(plan, {.user_data = user_data}, snapshot, - specialization) == supported, - "explicit gather accepted an unsupported sampler or rejected a valid one"); + for (const uint32_t perf_mip : {0u, 1u}) { + user_data[9] = 0xfff000u | (perf_mip << 24u); + user_data[10] = control; + ResourceSnapshot snapshot; + ResourceSpecialization specialization; + const auto filter = (control >> 26u) & 3u; + const bool supported = !explicit_lod || filter == 0 || + (filter <= 2 && (control & 0xfffffu) == 0 && + (filter != 2 || perf_mip == 0)); + Check(MaterializeResources(plan, {.user_data = user_data}, snapshot, + specialization) == supported, + "explicit gather accepted an unsupported sampler or rejected a valid one"); + } } } } diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index a3e9959c2..055e3e9f7 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -37524,11 +37524,12 @@ template TestCase ImageGatherLodPerLane() { "ImageGatherLodSamplerBounds", "ImageGatherLodTextureMinimum", "ImageGatherLodMipNone", "ImageGatherLodPointPostclampMaximum", "ImageGatherLodPointPreclampMaximum", "ImageGatherLodPointPostclampMinimum", - "ImageGatherLodPointPreclampMinimum"}; + "ImageGatherLodPointPreclampMinimum", "ImageGatherLodLinearPrimary", + "ImageGatherLodLinearIgnoresPreclamp"}; constexpr std::array lods = mode < 5 ? std::array{-1.0f, 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 9.0f} : std::array{-1.0f, 0.0f, 0.25f, 0.75f, 1.25f, 1.75f, 2.75f, 9.0f}; - constexpr std::array, 9> selected_mips{{ + constexpr std::array, 11> selected_mips{{ {0, 0, 1, 2, 3, 4, 4, 4}, {1, 1, 2, 3, 4, 4, 4, 4}, {2, 2, 2, 3, 3, 3, 3, 3}, @@ -37538,9 +37539,11 @@ template TestCase ImageGatherLodPerLane() { {0, 0, 0, 1, 1, 1, 1, 1}, {2, 2, 2, 2, 2, 2, 3, 4}, {1, 1, 1, 1, 1, 2, 3, 4}, + {0, 0, 0, 0, 1, 1, 2, 4}, + {0, 0, 0, 0, 1, 1, 2, 4}, }}; constexpr u32 lanes = lods.size(); - constexpr u32 sampler_min = mode == 2 ? 256 : mode >= 7 ? 448 : 0; + constexpr u32 sampler_min = mode == 2 ? 256 : mode == 7 || mode == 8 ? 448 : 0; constexpr u32 sampler_max = mode == 2 ? 512 : mode == 3 ? 256 : mode == 5 || mode == 6 ? 448 : 1024; TestCase test; @@ -37585,8 +37588,8 @@ template TestCase ImageGatherLodPerLane() { test.user_data[9] = sampler_min | (sampler_max << 12u); // Point preclamp selects whether the half-level rounding adjustment precedes // or follows the clamps. The streaming T# minimum follows the sampler bounds. - test.user_data[10] = mode == 4 ? 0x4001u : (1u << 26u); - if constexpr (mode == 6 || mode == 8) { + test.user_data[10] = mode == 4 ? 0x4001u : mode >= 9 ? 0x0a500000u : (1u << 26u); + if constexpr (mode == 6 || mode == 8 || mode == 10) { test.user_data[10] |= 1u << 28u; } test.user_data[18] = lanes * sizeof(u32); @@ -42828,6 +42831,8 @@ std::vector MakeCases() { AddCase(ImageGatherLodPerLane<6>); AddCase(ImageGatherLodPerLane<7>); AddCase(ImageGatherLodPerLane<8>); + AddCase(ImageGatherLodPerLane<9>); + AddCase(ImageGatherLodPerLane<10>); AddCase(ImageD16GatherPacksHalfPairs); AddCase(ImageSampleA16SamplerCoordsOnGpu); AddCase(ImageSampleOpcodeAliasUsesNormalCoords); @@ -48658,6 +48663,8 @@ int main(int argc, char **argv) { RunCase(&vulkan, ImageGatherLodPerLane<6>()); RunCase(&vulkan, ImageGatherLodPerLane<7>()); RunCase(&vulkan, ImageGatherLodPerLane<8>()); + RunCase(&vulkan, ImageGatherLodPerLane<9>()); + RunCase(&vulkan, ImageGatherLodPerLane<10>()); RunCase(&vulkan, ImageStoreSingleMipBounds<0>()); RunCase(&vulkan, ImageStoreSingleMipBounds<1>()); RunCase(&vulkan, ImageStoreSingleMipBounds<2>());