diff --git a/src/graphics/host_gpu/renderer/image/imageView.h b/src/graphics/host_gpu/renderer/image/imageView.h index d4e93aee7..4521f3b31 100644 --- a/src/graphics/host_gpu/renderer/image/imageView.h +++ b/src/graphics/host_gpu/renderer/image/imageView.h @@ -90,8 +90,11 @@ IsSupportedSampledDepthResource(const ShaderRecompiler::IR::ImageResource& resou if (resource.numeric_class != Prospero::TextureNumericClass::Float && resource.depth_compare) { return false; } - return resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::None && resource.read && - !resource.written && !resource.atomic; + return ((resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::None && + resource.mip_count == 1u) || + (resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::Dynamic && + resource.mip_count != 0u)) && + resource.read && !resource.written && !resource.atomic; } inline void ValidateStorageColorView(vk::Format image_format, vk::Format view_format, @@ -114,7 +117,7 @@ IsSupportedStorageImageResource(const ShaderRecompiler::IR::ImageResource& resou resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DArray) && ((resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::None && resource.mip_count == 1u) || - (resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::DynamicStorage && + (resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::Dynamic && resource.mip_count != 0u)) && resource.written && (!resource.atomic || diff --git a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp index f2c8adda5..87f8c18ff 100644 --- a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp @@ -568,7 +568,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR const auto physical_levels = multisampled ? 1u : static_cast(max_mip) + 1u; // IMAGE_STORE addresses BASE_LEVEL; only IMAGE_STORE_MIP selects other view mips. const bool single_storage_mip = - storage && resource.mip_mode != ShaderRecompiler::IR::ImageMipMode::DynamicStorage; + storage && resource.mip_mode != ShaderRecompiler::IR::ImageMipMode::Dynamic; const auto view_levels = multisampled || single_storage_mip ? 1u : static_cast(last_level - base_level) + 1u; @@ -891,7 +891,7 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) { auto& binding = images[i]; binding.mip_views.clear(); const auto& resource = program.info.images[i]; - if (resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::DynamicStorage) { + if (resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::Dynamic) { EXIT_IF(resource.mip_count == 0u || resource.mip_count != binding.desc.view_info.level_count); binding.mip_views.reserve(resource.mip_count); @@ -899,6 +899,8 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) { auto desc = binding.desc; desc.view_info.base_level += mip; desc.view_info.level_count = 1; + // The shader selects the mip after applying the guest minimum LOD. + desc.view_info.min_lod = 0; binding.mip_views.push_back(texture_cache.FindTexture(binding.image_id, desc)); } binding.image_view = binding.mip_views.front(); diff --git a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp index b3fc3a079..8b13bfbb3 100644 --- a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp @@ -41,7 +41,7 @@ void ValidateNativeProgram(const IR::Program& program) { Fail(program, "native shader plan has an invalid image class"); } present[static_cast(*kind)] = true; - const auto dynamic = program.info.images[i].mip_mode == IR::ImageMipMode::DynamicStorage; + const auto dynamic = program.info.images[i].mip_mode == IR::ImageMipMode::Dynamic; const auto count = dynamic ? program.info.images[i].mip_count : 1u; if (count == 0u || (!dynamic && program.info.images[i].mip_count != 1u)) { Fail(program, "native shader plan has an invalid image mip descriptor count"); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp index 179dda7cd..7c974bcd5 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp @@ -152,20 +152,11 @@ uint32_t ImageViewSizeType(EmitterState& state, ImageDimension dimension) { } } -uint32_t LoadSampledImageDescriptor(EmitterState& state, uint32_t resource) { - const auto& image_resource = state.program.info.images.at(resource); - EXIT_IF(image_resource.resource_class != IR::ImageResourceClass::Sampled); - const auto kind = IR::DescriptorBindingForImage(image_resource); - EXIT_IF(!kind.has_value()); - const auto array_index = ResourceForDescriptor(state, *kind, resource); - const auto variable = state.image_variables[IR::ImageBindingIndex(*kind)]; - const auto pointer_type = state.builder.Type( - spv::OpTypePointer, spv::StorageClassUniformConstant, ImageType(state, image_resource)); - const auto pointer = - DescriptorElementPointer(state, pointer_type, variable, array_index, *kind, resource, - "sampled image descriptor array was not emitted"); +uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mip) { + const auto pointer = ImageDescriptorPointer(state, resource, mip); const auto image = state.builder.AllocateId(); - state.builder.AddFunction(spv::OpLoad, ImageType(state, image_resource), image, pointer); + state.builder.AddFunction(spv::OpLoad, ImageType(state, state.program.info.images.at(resource)), + image, pointer); return image; } @@ -183,10 +174,10 @@ uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler) { return sampler_id; } -uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler) { +uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler_id, uint32_t mip) { const auto& image_resource = state.program.info.images.at(resource); - const auto image = LoadSampledImageDescriptor(state, resource); - const auto sampler_id = LoadSamplerDescriptor(state, sampler); + EXIT_IF(image_resource.resource_class != IR::ImageResourceClass::Sampled); + const auto image = LoadImageDescriptor(state, resource, mip); const auto sampled_image = state.builder.AllocateId(); const auto sampled_type = state.builder.Type(spv::OpTypeSampledImage, ImageType(state, image_resource)); @@ -194,17 +185,17 @@ uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampl return sampled_image; } -uint32_t StorageImageDescriptorPointer(EmitterState& state, uint32_t resource) { +uint32_t ImageDescriptorPointer(EmitterState& state, uint32_t resource, uint32_t mip) { const auto& image = state.program.info.images.at(resource); - EXIT_IF(image.resource_class != IR::ImageResourceClass::Storage); + EXIT_IF(mip >= image.mip_count); const auto kind = IR::DescriptorBindingForImage(image); EXIT_IF(!kind.has_value()); - const auto array_index = ResourceForDescriptor(state, *kind, resource); + const auto array_index = ResourceForDescriptor(state, *kind, resource) + mip; const auto pointer_type = state.builder.Type( spv::OpTypePointer, spv::StorageClassUniformConstant, ImageType(state, image)); const auto variable = state.image_variables[IR::ImageBindingIndex(*kind)]; return DescriptorElementPointer(state, pointer_type, variable, array_index, *kind, resource, - "storage image descriptor array was not emitted"); + "image descriptor array was not emitted"); } void EmitStorageImageWrite(EmitterState& state, uint32_t resource, uint32_t mip_lod, uint32_t coord, @@ -214,46 +205,15 @@ void EmitStorageImageWrite(EmitterState& state, uint32_t resource, uint32_t mip_ if (!image.atomic) { state.builder.RequireCapability(spv::CapabilityStorageImageWriteWithoutFormat); } - const auto kind = IR::DescriptorBindingForImage(image); - EXIT_IF(!kind.has_value()); - const auto array_index = ResourceForDescriptor(state, *kind, resource); - const auto image_type = ImageType(state, image); - const auto pointer_type = - state.builder.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, image_type); - const auto variable = state.image_variables[IR::ImageBindingIndex(*kind)]; - const auto LoadAt = [&](uint32_t array_index) { - const auto pointer = - DescriptorElementPointer(state, pointer_type, variable, array_index, *kind, resource, - "storage image descriptor array was not emitted"); - const auto descriptor = state.builder.AllocateId(); - state.builder.AddFunction(spv::OpLoad, image_type, descriptor, pointer); - return descriptor; + const auto EmitWrite = [&](uint32_t mip) { + state.builder.AddFunction(spv::OpImageWrite, LoadImageDescriptor(state, resource, mip), + coord, texel); }; - if (image.mip_mode != IR::ImageMipMode::DynamicStorage) { - state.builder.AddFunction(spv::OpImageWrite, LoadAt(array_index), coord, texel); + if (image.mip_mode != IR::ImageMipMode::Dynamic) { + EmitWrite(0); return; } - if (image.mip_count == 0u) { - ExitDescriptorBindingFailure(state, *kind, resource, - "dynamic storage image has no mip descriptors"); - } - - const auto merge_label = state.builder.AllocateId(); - std::vector labels(image.mip_count); - std::vector words {spv::OpSwitch, mip_lod, merge_label}; - for (uint32_t mip = 0; mip < image.mip_count; mip++) { - labels[mip] = state.builder.AllocateId(); - words.push_back(mip); - words.push_back(labels[mip]); - } - state.builder.AddFunction(spv::OpSelectionMerge, merge_label, spv::SelectionControlMaskNone); - state.builder.AddFunction(words); - for (uint32_t mip = 0; mip < image.mip_count; mip++) { - EmitLabel(state, labels[mip]); - state.builder.AddFunction(spv::OpImageWrite, LoadAt(array_index + mip), coord, texel); - state.builder.AddFunction(spv::OpBranch, merge_label); - } - EmitLabel(state, merge_label); + EmitImageMipSwitch(state, mip_lod, image.mip_count, 0, EmitWrite); } spv::ExecutionModel ExecutionModelForStage(ShaderType stage) { diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index eb008c54d..4dd0d5966 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -3,9 +3,7 @@ #include "graphics/shader/recompiler/frontend/decode/ImageOps.h" #include -#include #include -#include namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter { namespace { @@ -86,6 +84,46 @@ uint32_t ZeroF32(EmitterState& state) { return ConstantF32(state, 0); } +uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem, + const IR::Inst& address, const ImageSampleLayout& layout, uint32_t mip_count) { + auto& state = ctx.state; + const auto Extract = [&](uint32_t word, uint32_t first, uint32_t count) { + return EmitBitFieldUExtract(state, word, ConstantU32(state, first), + ConstantU32(state, count)); + }; + const auto LodFixed = [&](uint32_t word, uint32_t first) { + return Binary(state, spv::OpFMul, TypeF32(state), + Unary(state, spv::OpConvertUToF, TypeF32(state), Extract(word, first, 12)), + ConstantF32Value(state, 1.0f / 256.0f)); + }; + const auto sampler_control = ctx.Arg(inst, 6); + 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); + 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), + LodFixed(ctx.Arg(inst, 3), 8), base); + 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)); + 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)); + // 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)); + return Select(state, TypeU32(state), mip_none, ConstantU32(state, 0), mip); +} + uint32_t CubeAxis(EmitterState& state, uint32_t value) { return Binary(state, spv::OpFSub, TypeF32(state), value, ConstantF32(state, 0x3f800000u)); } @@ -252,7 +290,7 @@ uint32_t QueryDimensions(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address) { const auto dimension = ctx.state.program.info.images.at(mem.resource).dimension; const auto& info = ImageDimensionInfoFor(dimension); - const auto image = LoadSampledImageDescriptor(ctx.state, mem.resource); + const auto image = LoadImageDescriptor(ctx.state, mem.resource); const auto size = ctx.state.builder.AllocateId(); if (info.multisampled != 0u) { ctx.state.builder.AddFunction(spv::OpImageQuerySize, @@ -433,7 +471,7 @@ uint32_t EmitOneDimensionalGatherLz(ValueEmitContext& ctx, const IR::MemoryInfo& uint32_t coord, Prospero::TextureNumericClass numeric_class) { auto& state = ctx.state; state.builder.RequireCapability(spv::CapabilityImageQuery); - const auto image = LoadSampledImageDescriptor(state, mem.resource); + const auto image = LoadImageDescriptor(state, mem.resource); const auto width = state.builder.AllocateId(); state.builder.AddFunction(spv::OpImageQuerySizeLod, TypeU32(state), width, image, ConstantU32(state, 0)); @@ -446,7 +484,8 @@ uint32_t EmitOneDimensionalGatherLz(ValueEmitContext& ctx, const IR::MemoryInfo& Binary(state, spv::OpFMul, TypeF32(state), coord, width_f32), ConstantF32(state, 0x3f000000u))); - const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler); + const auto sampled = MakeSampledImage(state, mem.resource, + LoadSamplerDescriptor(state, mem.sampler)); const auto vector_type = ImageVectorType(state, numeric_class, 4); const auto scalar_type = ImageScalarType(state, numeric_class); const auto component = @@ -558,7 +597,8 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { if (op == IR::ValueOpcode::ImageQueryLod) { state.builder.RequireCapability(spv::CapabilityImageQuery); const auto dimension = image.dimension; - const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler); + const auto sampled = MakeSampledImage(state, mem.resource, + LoadSamplerDescriptor(state, mem.sampler)); const auto lod = state.builder.AllocateId(); state.builder.AddFunction( spv::OpImageQueryLod, TypeF32Vector(state, 2), lod, sampled, @@ -588,7 +628,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { EmitValueOrDefaultIfCondition( state, condition, TypeU32Vector(state, 4), ConstantU32CompositeZero(state, 4), [&]() { - const auto descriptor = LoadSampledImageDescriptor(state, mem.resource); + const auto descriptor = LoadImageDescriptor(state, mem.resource); const auto color = state.builder.AllocateId(); const auto coord = CoordU32(ctx, mem, *address, dimension); if (dimension_info.multisampled != 0u) { @@ -612,7 +652,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { const auto dimension = image.dimension; EmitIfCondition(state, ctx.Arg(inst, 3), [&]() { const auto mip_lod = - state.program.info.images[mem.resource].mip_mode == IR::ImageMipMode::DynamicStorage + state.program.info.images[mem.resource].mip_mode == IR::ImageMipMode::Dynamic ? LodU32(ctx, mem, *address, dimension) : 0u; const auto coord = CoordU32(ctx, mem, *address, dimension); @@ -635,13 +675,6 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { if (op == IR::ValueOpcode::ImageGatherRaw) { const auto coord = CoordF32(ctx, mem, *address, layout.coord, dimension_info.coordinate_components, image.cube); - if (HasFlag(mem, Decoder::ImageSampleFlagLod)) { - static std::atomic_flag warned = ATOMIC_FLAG_INIT; - if (!warned.test_and_set(std::memory_order_relaxed)) { - std::fputs("Warning: approximating IMAGE_GATHER4_L at mip level 0; explicit LOD is ignored.\n", - stderr); - } - } if (dimension == ImageDimension::Dim1D) { if (dref || !HasFlag(mem, Decoder::ImageSampleFlagLevelZero) || HasFlag(mem, Decoder::ImageSampleFlagOffset) || @@ -658,44 +691,48 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { ctx.Fail(inst, "has an unsupported 1D-array gather"); return; } - const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler); - const auto sample = state.builder.AllocateId(); - std::vector words; + const auto result_numeric_class = dref ? Prospero::TextureNumericClass::Float + : numeric_class; + const auto result_type = ImageVectorType(state, result_numeric_class, 4); + uint32_t component_or_dref; if (dref) { - auto dref_value = ZeroF32(state); - if (layout.dref != NoImageComponent) { - dref_value = AddressF32(ctx, mem, *address, layout.dref); - } - words = {spv::OpImageDrefGather, - TypeF32Vector(state, 4), - sample, - sampled, - coord, - dref_value}; + component_or_dref = layout.dref != NoImageComponent + ? AddressF32(ctx, mem, *address, layout.dref) + : ZeroF32(state); } else { - uint32_t component = 0; - if (ImageConversionFormat(state, mem).format == Prospero::BufferFormat::kInvalid) { - component = ImageGatherComponent(mem.dmask); - } - words = {spv::OpImageGather, - ImageVectorType(state, numeric_class, 4), - sample, - sampled, - coord, - ConstantU32(state, component)}; + component_or_dref = ConstantU32( + state, ImageConversionFormat(state, mem).format == Prospero::BufferFormat::kInvalid + ? ImageGatherComponent(mem.dmask) : 0u); } + uint32_t operand_mask = 0; + uint32_t offset = 0; if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) { - words.push_back(spv::ImageOperandsConstOffsetsMask); - words.push_back(HorizontalOffsets(state, dimension)); + operand_mask = spv::ImageOperandsConstOffsetsMask; + offset = HorizontalOffsets(state, dimension); } else if (layout.offset != NoImageComponent) { - words.push_back(spv::ImageOperandsOffsetMask); - words.push_back(PackedOffset(ctx, mem, *address, layout, dimension)); - } - state.builder.AddFunction(words); - auto result_numeric_class = numeric_class; - if (dref) { - result_numeric_class = Prospero::TextureNumericClass::Float; + operand_mask = spv::ImageOperandsOffsetMask; + offset = PackedOffset(ctx, mem, *address, layout, dimension); } + const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler); + const auto EmitGather = [&](uint32_t mip) { + const auto sampled = MakeSampledImage(state, mem.resource, sampler_id, mip); + const auto sample = state.builder.AllocateId(); + std::vector words { + dref ? spv::OpImageDrefGather : spv::OpImageGather, result_type, + sample, sampled, coord, component_or_dref}; + if (operand_mask != 0u) { + words.push_back(operand_mask); + words.push_back(offset); + } + state.builder.AddFunction(words); + return sample; + }; + const auto sample = image.mip_mode == IR::ImageMipMode::Dynamic && image.mip_count > 1u + ? EmitImageMipSwitch( + state, GatherMip(ctx, inst, mem, *address, layout, + image.mip_count), + image.mip_count, result_type, EmitGather) + : EmitGather(0); ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample), result_numeric_class, false, mem, true)); return; @@ -738,13 +775,14 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { operand_mask |= spv::ImageOperandsBiasMask; operands.push_back(AddressF32(ctx, mem, *address, layout.bias)); } + const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler); const auto EmitSample = [&](uint32_t resource) { const auto& candidate = state.program.info.images[resource]; const auto coord = CoordF32(ctx, mem, *address, layout.coord, ImageDimensionInfoFor(candidate.dimension).coordinate_components, candidate.cube); - const auto sampled = MakeSampledImage(state, resource, mem.sampler); + const auto sampled = MakeSampledImage(state, resource, sampler_id); const auto sample = state.builder.AllocateId(); std::vector sample_operands {result_type, sample, sampled, coord}; if (dref) { @@ -874,7 +912,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { spv::OpTypePointer, spv::StorageClassImage, image.atomic64 ? TypeScalarU64(state) : TypeU32(state)); state.builder.AddFunction(spv::OpImageTexelPointer, pointer_type, pointer, - StorageImageDescriptorPointer(state, mem.resource), + ImageDescriptorPointer(state, mem.resource), CoordU32(ctx, mem, *address, dimension), ConstantU32(state, 0)); if (op == IR::ValueOpcode::ImageAtomicFMin32 || diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h index 98643eb17..46cc62276 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -314,13 +315,14 @@ uint32_t ImageType(EmitterState& state, const IR::ImageResource& image); uint32_t ImageViewSizeType(EmitterState& state, ImageDimension dimension); -uint32_t LoadSampledImageDescriptor(EmitterState& state, uint32_t resource); +uint32_t ImageDescriptorPointer(EmitterState& state, uint32_t resource, uint32_t mip = 0); + +uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mip = 0); uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler); -uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler); - -uint32_t StorageImageDescriptorPointer(EmitterState& state, uint32_t resource); +uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler_id, + uint32_t mip = 0); void EmitStorageImageWrite(EmitterState& state, uint32_t resource, uint32_t mip_lod, uint32_t coord, uint32_t texel); @@ -514,6 +516,45 @@ uint32_t ConstantDeviceAddress(EmitterState& state, uint64_t value); uint32_t DeviceAddressFromWords(EmitterState& state, uint32_t low, uint32_t high); // These templates accept local lambdas from several emitter translation units. +template +auto EmitImageMipSwitch(EmitterState& state, uint32_t mip_lod, uint32_t mip_count, + uint32_t result_type, Fn&& emit) { + constexpr bool has_result = !std::is_void_v>; + EXIT_IF(mip_count == 0u); + const auto merge_label = state.builder.AllocateId(); + std::vector labels(mip_count); + std::vector words {spv::OpSwitch, mip_lod, merge_label}; + for (uint32_t mip = 0; mip < mip_count; mip++) { + labels[mip] = state.builder.AllocateId(); + words.push_back(mip); + words.push_back(labels[mip]); + } + if constexpr (has_result) { + words[2] = labels.front(); + } + state.builder.AddFunction(spv::OpSelectionMerge, merge_label, spv::SelectionControlMaskNone); + state.builder.AddFunction(words); + std::vector phi; + if constexpr (has_result) { + phi = {spv::OpPhi, result_type, state.builder.AllocateId()}; + } + for (uint32_t mip = 0; mip < mip_count; mip++) { + EmitLabel(state, labels[mip]); + if constexpr (has_result) { + phi.push_back(emit(mip)); + phi.push_back(state.current_label); + } else { + emit(mip); + } + state.builder.AddFunction(spv::OpBranch, merge_label); + } + EmitLabel(state, merge_label); + if constexpr (has_result) { + state.builder.AddFunction(phi); + return phi[2]; + } +} + template void EmitIfCondition(EmitterState& state, uint32_t condition, Fn&& fn) { const auto then_label = state.builder.AllocateId(); diff --git a/src/graphics/shader/recompiler/frontend/translate/Memory.cpp b/src/graphics/shader/recompiler/frontend/translate/Memory.cpp index 2b51cf59a..7c173041d 100644 --- a/src/graphics/shader/recompiler/frontend/translate/Memory.cpp +++ b/src/graphics/shader/recompiler/frontend/translate/Memory.cpp @@ -708,8 +708,16 @@ void Translator::IMAGE_GATHER(const Decoder::Instruction& inst) { const auto resource = GetImageResource(memory); const auto sampler = GetSamplerResource(memory); const auto address = MakeImageAddress(inst, MemorySourceAt(inst, 0)); - const auto result = ir.Emit(IR::ValueOpcode::ImageGatherRaw, {resource, sampler, address}, - AddMemoryInfo(memory, inst.pc)); + const bool has_lod = (memory.image_sample_flags & Decoder::ImageSampleFlagLod) != 0u; + // LOD selection consumes these words on the GPU after descriptor handles are stripped. + const auto result = ir.Emit( + IR::ValueOpcode::ImageGatherRaw, + {resource, sampler, address, + has_lod ? resource.Instruction()->Arg(1) : IR::Value(0u), + has_lod ? resource.Instruction()->Arg(3) : IR::Value(0u), + has_lod ? sampler.Instruction()->Arg(1) : IR::Value(0u), + has_lod ? sampler.Instruction()->Arg(2) : IR::Value(0u)}, + AddMemoryInfo(memory, inst.pc)); for (uint32_t index = 0; index < memory.data_dwords; index++) { WriteOperand(OffsetOperand(inst.dst, index), ir.Emit(IR::ValueOpcode::CompositeExtractU32x4, {result, IR::Value(index)})); diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index 0a296c5dc..211b1ebc6 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -114,7 +114,7 @@ struct BufferResource { bool operator==(const BufferResource& other) const = default; }; -enum class ImageMipMode { None, DynamicStorage }; +enum class ImageMipMode { None, Dynamic }; constexpr uint32_t ShaderImageIdentitySwizzle = 0x00000facu; @@ -151,6 +151,7 @@ struct SamplerResource { bool force_point_filtering = false; bool depth_compare = false; bool integer_border = false; + bool gather_lod = false; bool operator==(const SamplerResource& other) const = default; }; diff --git a/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc b/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc index f0213607f..986f8c930 100644 --- a/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc +++ b/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc @@ -297,7 +297,7 @@ VALUE_OPCODE(ImageQueryLod, U32x4, ImageResource, SamplerResource, ImageAddress) VALUE_OPCODE(ImageRead, U32x4, ImageResource, ImageAddress, U1) VALUE_OPCODE(ImageWrite, Void, ImageResource, ImageAddress, U32x4, U1) VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress) -VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress) +VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U32, U32, U32, U32) VALUE_OPCODE(ImageAtomicCompareSwap32, U32, ImageResource, ImageAddress, U32, U32, U1) VALUE_OPCODE(ImageAtomicSwap32, U32, ImageResource, ImageAddress, U32, U1) VALUE_OPCODE(ImageAtomicIAdd32, U32, ImageResource, ImageAddress, U32, U1) diff --git a/src/graphics/shader/recompiler/ir/passes/BindingLayout.cpp b/src/graphics/shader/recompiler/ir/passes/BindingLayout.cpp index 70ddbf4cd..ba4cb2799 100644 --- a/src/graphics/shader/recompiler/ir/passes/BindingLayout.cpp +++ b/src/graphics/shader/recompiler/ir/passes/BindingLayout.cpp @@ -126,7 +126,7 @@ void AllocateBindings(Program& program, uint32_t push_data_start_dword) { EXIT("shader binding layout failed: image %u has an unmapped binding class", i); } auto& resources = image_groups[group]; - const auto dynamic = program.info.images[i].mip_mode == ImageMipMode::DynamicStorage; + const auto dynamic = program.info.images[i].mip_mode == ImageMipMode::Dynamic; const auto count = dynamic ? program.info.images[i].mip_count : 1u; if (count == 0u || (!dynamic && program.info.images[i].mip_count != 1u)) { EXIT("shader binding layout failed: image %u has invalid specialized mip count %u", i, diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 694c14579..9449bcec7 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -124,8 +124,8 @@ bool DescriptorIsCube(const DescriptorValue& descriptor) { Prospero::ImageType::kCube; } -uint32_t StorageMipCount(const ImageResource& image, const DescriptorValue& descriptor) { - if (image.mip_mode != ImageMipMode::DynamicStorage || NullImageDescriptor(descriptor)) { +uint32_t ImageMipCount(const ImageResource& image, const DescriptorValue& descriptor) { + if (image.mip_mode != ImageMipMode::Dynamic || NullImageDescriptor(descriptor)) { return 1; } const auto base = (descriptor.dwords[3] >> 12u) & 0xfu; @@ -435,10 +435,10 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna (base.atomic && base.resource_class != ImageResourceClass::Storage)) { return SpecializationFail(fmt::format("image resource {} has an invalid class", i)); } - image.mip_count = StorageMipCount(base, descriptor); + image.mip_count = ImageMipCount(base, descriptor); if (image.mip_count == 0u) { return SpecializationFail( - fmt::format("storage image descriptor {} has an invalid mip range", i)); + fmt::format("image descriptor {} has an invalid mip range", i)); } if (NullImageDescriptor(descriptor)) { image.numeric_class = base.atomic ? Prospero::TextureNumericClass::Uint @@ -1077,6 +1077,16 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime if (!evaluate(program.info.samplers[i].source, snapshot.samplers[i])) { return false; } + if (program.info.samplers[i].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)) { + return SpecializationFail( + "explicit-LOD gather requires mip filtering None or Point with zero LOD biases"); + } + } } snapshot.user_data.assign(runtime.user_data.begin(), runtime.user_data.end()); return BuildResourceSpecialization(program, snapshot, specialization); diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index 1d26dd05f..33913c2dc 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -1727,9 +1727,11 @@ private: const auto resource_class = ImageOpcodeInfoOf(op).resource_class; const bool atomic64 = ImageOpcodeInfoOf(op).access == ImageAccess::Atomic && memory.data_bits == 64u; - const auto mip = resource_class == ImageResourceClass::Storage && memory.image_has_mip - ? ImageMipMode::DynamicStorage - : ImageMipMode::None; + const bool dynamic_mip = + (resource_class == ImageResourceClass::Storage && memory.image_has_mip) || + (op == ValueOpcode::ImageGatherRaw && + (memory.image_sample_flags & Decoder::ImageSampleFlagLod) != 0u); + const auto mip = dynamic_mip ? ImageMipMode::Dynamic : ImageMipMode::None; const bool depth = (memory.image_sample_flags & Decoder::ImageSampleFlagCompare) != 0; for (uint32_t i = 0; i < m_info.images.size(); i++) { auto& image = m_info.images[i]; @@ -1939,6 +1941,9 @@ private: if (sampler == UINT32_MAX) { Fail(flags.pc, "sampler resource limit exceeded"); } + m_info.samplers[sampler].gather_lod |= + op == ValueOpcode::ImageGatherRaw && + (memory.image_sample_flags & Decoder::ImageSampleFlagLod) != 0u; AddHandlePatch(sampler_handle, sampler, flags.pc); AddSampledPair(resource, sampler, flags.pc); } diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 7a78c271e..d6f9a1a6e 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -2032,6 +2032,58 @@ void TestFmaskLoadSpecialization() { "rebinding FMASK as a texture reused the metadata specialization"); } +void TestGatherLodSamplerValidation() { + for (const bool explicit_lod : {false, true}) { + for (const bool reverse : {false, true}) { + Fixture fixture; + std::array image_words; + std::array sampler_words; + for (uint32_t i = 0; i < image_words.size(); i++) + image_words[i] = fixture.UserData(i); + for (uint32_t i = 0; i < sampler_words.size(); i++) + sampler_words[i] = fixture.UserData(i + 8); + for (uint32_t i = 0; i < 2; i++) { + const bool lod = explicit_lod && i == (reverse ? 0u : 1u); + MemoryInfo memory; + memory.kind = ResourceKind::Image; + memory.image_dimension = Decoder::ImageDimension::Dim2D; + memory.image_sample_flags = lod ? Decoder::ImageSampleFlagLod + : Decoder::ImageSampleFlagLevelZero; + fixture.Emit(ValueOpcode::ImageGatherRaw, + {fixture.Image(image_words), fixture.Sampler(sampler_words), + fixture.ImageAddress(), image_words[1], image_words[3], + sampler_words[1], sampler_words[2]}, + fixture.AddMemory(memory, i * 4)); + } + fixture.PlanAndTrack(); + auto plan = ExtractResourcePlan(fixture.program); + Check(plan.info.samplers.size() == 1 && + plan.info.samplers[0].gather_lod == explicit_lod, + "shared sampler lost explicit gather validation or restricted LZ gather"); + std::array user_data{}; + user_data[0] = 0x1000u; + user_data[1] = static_cast( + 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}; + 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"); + } + } + } +} + void TestDynamicStorageMipTracking() { Fixture fixture; std::array image_words; @@ -2067,7 +2119,7 @@ void TestDynamicStorageMipTracking() { const auto &images = fixture.program.info.images; Check(images.size() == 2 && images[0].mip_mode == ImageMipMode::None && images[0].mip_count == 1 && - images[1].mip_mode == ImageMipMode::DynamicStorage && + images[1].mip_mode == ImageMipMode::Dynamic && images[1].mip_count == 1, "storage mip writes did not share one dynamic logical resource"); Check(plain.first.Instruction()->Flags() == 0 && @@ -3412,6 +3464,7 @@ int main() { Run("images and samplers", TestImagesSamplersAndAliases); Run("SampleAdjust sampler scratch", TestSampleAdjustSamplerScratch); Run("FMASK load specialization", TestFmaskLoadSpecialization); + Run("gather LOD sampler validation", TestGatherLodSamplerValidation); Run("dynamic storage mips", TestDynamicStorageMipTracking); Run("invariant indirect images", TestInvariantIndirectImageMaterialization); Run("guarded direct image table", TestGuardedDirectImageTable); diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 4eb50673f..e7e3cc0cb 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -1175,6 +1175,7 @@ struct TestCase { vk::ImageType sampled_image_type = vk::ImageType::e2D; vk::ImageViewType sampled_image_view_type = vk::ImageViewType::e2D; u32 sampled_image_layers = 1; + u32 sampled_image_view_base_mip = 0; u32 sampled_image_view_base_layer = 0; u32 sampled_image_view_layers = 0; std::vector storage_image_rgba; @@ -1207,7 +1208,7 @@ struct TestCase { std::vector expected_gds; std::vector> decoded_counts; std::vector> ir_counts; - u32 expected_storage_mip_descriptors = 0; + u32 expected_mip_descriptors = 0; std::optional> expected_buffer_resources; }; @@ -1517,28 +1518,26 @@ CompiledShader CompileCase(const TestCase &test, u32 host_subgroup_size = 64) { text + " count=" + std::to_string(actual) + ", expected=" + std::to_string(expected)); } - if (test.expected_storage_mip_descriptors != 0) { + if (test.expected_mip_descriptors != 0) { const auto image = std::ranges::find_if( result.program.info.images, [](const auto &resource) { return resource.mip_mode == - ShaderRecompiler::IR::ImageMipMode::DynamicStorage; + ShaderRecompiler::IR::ImageMipMode::Dynamic; }); - Require(test.name, "dynamic storage mip specialization", + Require(test.name, "dynamic mip specialization", image != result.program.info.images.end() && - image->mip_count == test.expected_storage_mip_descriptors, + image->mip_count == test.expected_mip_descriptors, "runtime descriptor range did not specialize the mip count"); const auto resource = static_cast(image - result.program.info.images.begin()); - const auto binding = std::ranges::find_if( - result.program.bindings.descriptors, [&](const auto &group) { - return std::ranges::find(group.resources, resource) != - group.resources.end(); - }); - Require(test.name, "dynamic storage mip binding", - binding != result.program.bindings.descriptors.end() && + const auto kind = ShaderRecompiler::IR::DescriptorBindingForImage(*image); + const auto *binding = kind ? ShaderRecompiler::IR::FindBinding( + result.program.bindings, *kind) : nullptr; + Require(test.name, "dynamic mip binding", + binding != nullptr && std::ranges::count(binding->resources, resource) == - test.expected_storage_mip_descriptors, - "dynamic storage image did not receive one descriptor per mip"); + test.expected_mip_descriptors, + "dynamic image did not receive one descriptor per mip"); } if (test.expand_shader_data_storage) { auto &block = *result.program.blocks.front(); @@ -11225,7 +11224,7 @@ public: "IMAGE_STORE expanded an inaccessible mip and shifted physical storage"); auto mipped_storage_resource = storage_resource; mipped_storage_resource.mip_mode = - ShaderRecompiler::IR::ImageMipMode::DynamicStorage; + ShaderRecompiler::IR::ImageMipMode::Dynamic; mipped_storage_resource.mip_count = 3; #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS ExpectFatal("MipViewPhysicalLayoutChange", [&] { @@ -11514,6 +11513,33 @@ public: views[2] != views[3] && views[1] == views.back(), "different texture clamps aliased or identical clamped views " "were not reused"); + ShaderRecompiler::IR::CompiledShaderInfo gather_info{}; + gather_info.stage = ShaderType::Compute; + gather_info.info = lod_program.program.info; + gather_info.info.images[0].mip_mode = + ShaderRecompiler::IR::ImageMipMode::Dynamic; + gather_info.info.images[0].mip_count = 4; + auto gather_snapshot = lod_program.resources; + gather_snapshot.images[0] = lod_descriptor; + ShaderStageRuntime gather_runtime{&gather_info, &gather_snapshot}; + PreparedBindings gather_bindings; + executor.PrepareBindings(gather_runtime, gather_bindings); + executor.RebindImages(gather_bindings); + const auto &gather_binding = gather_bindings.images[0]; + Require(lod_test.name, "dynamic sampled mip views", + gather_binding.desc.view_info.min_lod == 256u && + gather_binding.mip_views.size() == 4, + "dynamic gather lost the guest minimum or accessible mip range"); + for (uint32_t mip = 0; mip < 4; ++mip) { + auto single_mip = gather_binding.desc; + single_mip.view_info.base_level = mip; + single_mip.view_info.level_count = 1; + single_mip.view_info.min_lod = 0; + Require(lod_test.name, "gather minimum applied once", + gather_binding.mip_views[mip] == texture_cache.FindTexture( + gather_binding.image_id, single_mip), + "the gather mip view reapplied the shader's streaming clamp"); + } DestroyBuffer(&output); RenderExecutorTestAccess::ResetBindings(executor); } @@ -13786,7 +13812,8 @@ public: u32 layers, u32 view_base_layer = 0, u32 view_layers = 0, vk::SampleCountFlagBits samples = vk::SampleCountFlagBits::e1, - vk::Format view_format = vk::Format::eUndefined) { + vk::Format view_format = vk::Format::eUndefined, + u32 view_base_mip = 0) { Image ret; ret.format = format; ret.width = width; @@ -13845,8 +13872,10 @@ public: view_info.viewType = view_type; view_info.format = view_format == vk::Format::eUndefined ? format : view_format; view_info.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; - view_info.subresourceRange.baseMipLevel = 0; - view_info.subresourceRange.levelCount = ret.mip_levels; + Require(shader_name, "dispatch", view_base_mip < ret.mip_levels, + "image view base mip is out of bounds"); + view_info.subresourceRange.baseMipLevel = view_base_mip; + view_info.subresourceRange.levelCount = ret.mip_levels - view_base_mip; Require(shader_name, "dispatch", view_base_layer < layers, "image view base layer is out of bounds"); if (view_layers == 0) { @@ -14134,6 +14163,7 @@ public: std::vector writes; std::vector buffer_infos; std::vector sampled_infos; + std::vector sampled_mip_views; std::vector storage_infos; std::vector storage_uint_infos; std::vector storage_atomic_infos; @@ -14271,9 +14301,32 @@ public: "sampled image descriptor requested but no sampled image was " "provided"); sampled_infos.resize(sampled->resources.size()); - for (auto &info : sampled_infos) { + std::vector mip_indices(compiled.program.info.images.size()); + for (u32 slot = 0; slot < sampled_infos.size(); slot++) { + auto &info = sampled_infos[slot]; info.imageView = sampled_image->view; info.imageLayout = sampled_image->layout; + const auto resource = sampled->resources[slot]; + if (compiled.program.info.images[resource].mip_mode == + ShaderRecompiler::IR::ImageMipMode::Dynamic) { + const auto mip = test.sampled_image_view_base_mip + mip_indices[resource]++; + Require(test.name, "dispatch", mip < sampled_image->mip_levels, + "sampled mip descriptor exceeds the supplied image"); + vk::ImageViewCreateInfo view{}; + view.image = sampled_image->image; + view.viewType = test.sampled_image_view_type; + view.format = sampled_image->format; + view.subresourceRange = { + vk::ImageAspectFlagBits::eColor, mip, 1, + test.sampled_image_view_base_layer, + test.sampled_image_view_layers != 0 + ? test.sampled_image_view_layers + : sampled_image->layers - test.sampled_image_view_base_layer}; + RequireVk(test.name, "dispatch", + m_device.createImageView(&view, nullptr, &info.imageView), + "vkCreateImageView(sampled mip)"); + sampled_mip_views.push_back(info.imageView); + } } vk::WriteDescriptorSet write{}; write.sType = vk::StructureType::eWriteDescriptorSet; @@ -14428,6 +14481,9 @@ public: &barrier, 0, nullptr); } EndSubmitAndFree(test.name, "dispatch", cmd); + for (const auto view : sampled_mip_views) { + m_device.destroyImageView(view, nullptr); + } if (flattened_buffer.buffer != nullptr) { DestroyBuffer(&flattened_buffer); } @@ -17346,7 +17402,8 @@ void RunCase(VulkanHarness *vulkan, const TestCase &test) { sampled_dwords_per_pixel, vk::ImageLayout::eShaderReadOnlyOptimal, test.sampled_image_type, test.sampled_image_view_type, test.sampled_image_layers, test.sampled_image_view_base_layer, - test.sampled_image_view_layers); + test.sampled_image_view_layers, vk::SampleCountFlagBits::e1, + vk::Format::eUndefined, test.sampled_image_view_base_mip); } if (needs_storage_image) { const bool atomic64 = std::ranges::any_of( @@ -17365,7 +17422,8 @@ void RunCase(VulkanHarness *vulkan, const TestCase &test) { atomic64 ? vk::Format::eR64Uint : vk::Format::eR32Uint); } if (needs_sampler) { - sampler = vulkan->CreateNearestSampler(test.name, sampled_image.mip_levels); + sampler = vulkan->CreateNearestSampler( + test.name, sampled_image.mip_levels - test.sampled_image_view_base_mip); } vulkan->Dispatch(test, compiled, buffer, needs_gds ? &gds_buffer : nullptr, @@ -29981,34 +30039,135 @@ TestCase ImageCubeGradientsPreserveDerivatives() { return test; } -TestCase ImageGatherLodApproximatesLevelZero() { +TestCase ImageGatherExplicitLod() { using O = ShaderOpcode; std::vector code; - AppendVMovLiteral(&code, 20, 0x3f000000u); - AppendVMovLiteral(&code, 21, 0x3f000000u); - AppendVMovLiteral(&code, 22, 0x3f800000u); - code.push_back(EncodeMimg0(0x44, 0x1)); - code.push_back(EncodeMimg1(0, 20)); - code.push_back(EncodeMimg0(0x24, 0x1)); - code.push_back(EncodeMimg1(4, 20)); + // PPSA29343 uses NSA coordinates v81/v84 with its explicit LOD in v78. + AppendVMovLiteral(&code, 81, 0x3f000000u); + AppendVMovLiteral(&code, 84, 0x3f000000u); + AppendVMovLiteral(&code, 78, 0x3f800000u); + code.push_back(EncodeMimg0(0x44, 0x1, 1)); + code.push_back(EncodeMimg1(9, 81, 0, 2)); + code.push_back(84u | (78u << 8u)); + code.push_back(EncodeMimg0(0x24, 0x1, 1)); + code.push_back(EncodeMimg1(13, 81, 0, 2)); + code.push_back(84u | (78u << 8u)); for (u32 i = 0; i < 5u; i++) { - AppendStoreVgpr(&code, i, i); + AppendStoreVgpr(&code, i + 9, i); } AppendEnd(&code); TestCase test; - test.name = "ImageGatherLodApproximatesLevelZero"; + test.name = "ImageGatherExplicitLod"; test.code = std::move(code); test.opcodes = {O::V_MOV_B32, O::IMAGE_GATHER4_L, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; - // Explicit sampling proves mip 1 is accessible; the gather must still read mip 0. - test.expected = {0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x41000000u}; + // A gather's single-mip descriptors must not replace the ordinary sample's + // full mip chain when both instructions use the same guest descriptor. + test.expected = std::vector(5, 0x41000000u); test.sampled_image_rgba_mips = {std::vector(4 * 4 * 4, 0x3f800000u), std::vector(2 * 2 * 4, 0x41000000u)}; + test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32_32_32_32Float); + test.user_data[1] |= 3u << 30u; + test.user_data[2] = 3u << 14u; + test.user_data[3] |= 1u << 16u; + test.user_data[8] = 0x92u; + test.user_data[9] = 0xfff000u; + test.user_data[10] = 0x05000000u; // Captured point sampler, preclamp disabled. + test.user_data[50] = test.expected.size() * sizeof(u32); + test.user_data[51] = 3u << 28u; + test.has_user_data = true; + test.expected_mip_descriptors = 2; test.required_spirv = {"OpImageGather", "OpImageSampleExplicitLod"}; return test; } +template TestCase ImageGatherLodPerLane() { + using O = ShaderOpcode; + constexpr std::array names{ + "ImageGatherLodPerLane", "ImageGatherLodBaseMip", + "ImageGatherLodSamplerBounds", "ImageGatherLodTextureMinimum", + "ImageGatherLodMipNone", "ImageGatherLodPointPostclampMaximum", + "ImageGatherLodPointPreclampMaximum", "ImageGatherLodPointPostclampMinimum", + "ImageGatherLodPointPreclampMinimum"}; + 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{{ + {0, 0, 1, 2, 3, 4, 4, 4}, + {1, 1, 2, 3, 4, 4, 4, 4}, + {2, 2, 2, 3, 3, 3, 3, 3}, + {3, 3, 3, 3, 3, 3, 3, 3}, + {1, 1, 1, 1, 1, 1, 1, 1}, + {0, 0, 0, 1, 1, 2, 2, 2}, + {0, 0, 0, 1, 1, 1, 1, 1}, + {2, 2, 2, 2, 2, 2, 3, 4}, + {1, 1, 1, 1, 1, 2, 3, 4}, + }}; + constexpr u32 lanes = lods.size(); + constexpr u32 sampler_min = mode == 2 ? 256 : mode >= 7 ? 448 : 0; + constexpr u32 sampler_max = mode == 2 ? 512 : mode == 3 ? 256 + : mode == 5 || mode == 6 ? 448 : 1024; + TestCase test; + test.name = names[mode]; + test.image_width = test.image_height = 16; + test.sampled_image_view_base_mip = mode > 0 && mode < 5 ? 1 : 0; + for (u32 mip = 0; mip < 5; mip++) { + test.sampled_image_rgba_mips.emplace_back( + (16u >> mip) * (16u >> mip) * 4u, + std::bit_cast(static_cast(mip + 1))); + } + for (const auto lod : lods) { + test.initial.push_back(std::bit_cast(lod)); + } + test.expected = test.initial; + for (u32 component = 0; component < 4; component++) { + for (const auto mip : selected_mips[mode]) { + test.expected.push_back(std::bit_cast(static_cast(mip + 1))); + } + } + auto &code = test.code; + code.push_back(EncodeVop1(0x01, 16, Vgpr(0))); + code.push_back(EncodeVop2(0x1a, 31, InlineU32(2), 16)); + code.push_back(EncodeMubuf0(0x0c)); + code.push_back(EncodeMubuf1(22, 4, 31)); + AppendVMovLiteral(&code, 20, 0x3f000000u); + AppendVMovLiteral(&code, 21, 0x3f000000u); + code.push_back(EncodeMimg0(0x44, 0x1)); + code.push_back(EncodeMimg1(0, 20, 0, 2)); + for (u32 component = 0; component < 4; component++) { + AppendStoreVgprAtLaneDwordOffset(&code, component, 16, lanes * (component + 1)); + } + AppendEnd(&code); + test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::BUFFER_LOAD_DWORD, + O::IMAGE_GATHER4_L, O::V_ADD_NC_U32, O::BUFFER_STORE_DWORD, + O::S_ENDPGM}; + test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32_32_32_32Float); + test.user_data[1] |= (3u << 30u) | (mode == 3 ? 768u << 8u : 0u); + test.user_data[2] = 3u | (15u << 14u); + test.user_data[3] |= (test.sampled_image_view_base_mip << 12u) | (4u << 16u); + test.user_data[8] = 0x92u; + 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] |= 1u << 28u; + } + test.user_data[18] = lanes * sizeof(u32); + test.user_data[19] = 3u << 28u; + test.user_data[50] = test.expected.size() * sizeof(u32); + test.user_data[51] = 3u << 28u; + test.has_user_data = true; + test.compute_info.threads_num[0] = lanes; + test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1; + test.compute_info.thread_ids_num = 1; + test.has_compute_info = true; + test.expected_mip_descriptors = 5 - test.sampled_image_view_base_mip; + test.required_spirv = {"OpImageGather"}; + return test; +} + TestCase ImageD16GatherPacksHalfPairs() { using O = ShaderOpcode; @@ -30441,11 +30600,42 @@ TestCase ImageStoreMipSelectsPpsa01340Descriptor() { std::copy(descriptor.begin(), descriptor.end(), test.user_data.begin()); test.has_user_data = true; test.compile_only = true; - test.expected_storage_mip_descriptors = 3; + test.expected_mip_descriptors = 3; test.required_spirv = {"OpSwitch", "OpImageWrite"}; return test; } +template TestCase ImageStoreSingleMipBounds() { + using O = ShaderOpcode; + constexpr std::array names{"ImageStoreSingleMipValid", "ImageStoreSingleMipBeyondEnd", + "ImageStoreSingleMipNegative"}; + constexpr std::array lods{0u, 1u, UINT32_MAX}; + TestCase test; + test.name = names[mode]; + AppendVMovU32(&test.code, 20, 0); + AppendVMovU32(&test.code, 21, 0); + AppendVMovU32(&test.code, 22, lods[mode]); + for (u32 component = 0; component < 4; component++) { + AppendVMovLiteral(&test.code, component, 0x40000000u); + } + test.code.push_back(EncodeMimg0(0x09, 0xf)); + test.code.push_back(EncodeMimg1(0, 20)); + AppendEnd(&test.code); + test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE_MIP, O::S_ENDPGM}; + test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k32_32_32_32Float); + test.has_user_data = true; + test.storage_image_rgba = std::vector(4 * 4 * 4, 0x3f800000u); + test.expected_storage_image_rgba = test.storage_image_rgba; + if constexpr (mode == 0) { + std::fill_n(test.expected_storage_image_rgba.begin(), 4, 0x40000000u); + } + test.expected_mip_descriptors = 1; + if constexpr (mode == 0) { + test.required_spirv = {"OpImageWrite"}; + } + return test; +} + TestCase ImageStoreRgbOneUsesInverseSwizzle() { using O = ShaderOpcode; @@ -32009,7 +32199,16 @@ std::vector MakeCases() { AddCase(ImageGetResinfoDmaskMipLevels); AddCase(ImageSampleAndGather); AddCase(ImageCubeGradientsPreserveDerivatives); - AddCase(ImageGatherLodApproximatesLevelZero); + AddCase(ImageGatherExplicitLod); + AddCase(ImageGatherLodPerLane<0>); + AddCase(ImageGatherLodPerLane<1>); + AddCase(ImageGatherLodPerLane<2>); + AddCase(ImageGatherLodPerLane<3>); + AddCase(ImageGatherLodPerLane<4>); + AddCase(ImageGatherLodPerLane<5>); + AddCase(ImageGatherLodPerLane<6>); + AddCase(ImageGatherLodPerLane<7>); + AddCase(ImageGatherLodPerLane<8>); AddCase(ImageD16GatherPacksHalfPairs); AddCase(ImageSampleA16SamplerCoordsOnGpu); AddCase(ImageSampleOpcodeAliasUsesNormalCoords); @@ -32019,6 +32218,9 @@ std::vector MakeCases() { AddCase(ImageStoreVariants); AddCase(ImageD16StoreUnpacksHalfPairs); AddCase(ImageStoreMipSelectsPpsa01340Descriptor); + AddCase(ImageStoreSingleMipBounds<0>); + AddCase(ImageStoreSingleMipBounds<1>); + AddCase(ImageStoreSingleMipBounds<2>); AddCase(ImageStoreRgbOneUsesInverseSwizzle); AddCase(ImageStoreDuplicateSelectorUsesInverseSwizzle); AddCase(ImageStoreBgraUsesInverseSwizzle); @@ -33060,7 +33262,7 @@ void CheckSampledColorViews() { "atomic uint storage resource was rejected"); storage_resource.atomic = false; storage_resource.mip_mode = - ShaderRecompiler::IR::ImageMipMode::DynamicStorage; + ShaderRecompiler::IR::ImageMipMode::Dynamic; storage_resource.mip_count = 3; Require("SampledColorViews", "dynamic storage mip resource", IsSupportedStorageImageResource(storage_resource), @@ -33133,10 +33335,15 @@ void CheckSampledDepthResource() { !IsSupportedSampledDepthResource(resource), "atomic depth resource was accepted"); resource = basic; - resource.mip_mode = ShaderRecompiler::IR::ImageMipMode::DynamicStorage; - Require("SampledDepthResource", "dynamic mip rejected", - !IsSupportedSampledDepthResource(resource), - "dynamic-storage mip depth resource was accepted"); + resource.mip_mode = ShaderRecompiler::IR::ImageMipMode::Dynamic; + resource.mip_count = 3; + Require("SampledDepthResource", "dynamic mip read", + IsSupportedSampledDepthResource(resource), + "dynamic sampled depth mip resource was rejected"); + resource.depth_compare = true; + Require("SampledDepthResource", "dynamic comparison mip read", + IsSupportedSampledDepthResource(resource), + "dynamic comparison depth mip resource was rejected"); resource = basic; resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2DArray; Require("SampledDepthResource", "singleton array accepted", @@ -36946,6 +37153,23 @@ int main(int argc, char **argv) { RunCase(&vulkan, FlatVirtualAddressRebasesGuestAllocation()); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--image-gather-lod-only") == 0) { + VulkanHarness vulkan; + RunCase(&vulkan, ImageGatherExplicitLod()); + RunCase(&vulkan, ImageGatherLodPerLane<0>()); + RunCase(&vulkan, ImageGatherLodPerLane<1>()); + RunCase(&vulkan, ImageGatherLodPerLane<2>()); + RunCase(&vulkan, ImageGatherLodPerLane<3>()); + RunCase(&vulkan, ImageGatherLodPerLane<4>()); + RunCase(&vulkan, ImageGatherLodPerLane<5>()); + RunCase(&vulkan, ImageGatherLodPerLane<6>()); + RunCase(&vulkan, ImageGatherLodPerLane<7>()); + RunCase(&vulkan, ImageGatherLodPerLane<8>()); + RunCase(&vulkan, ImageStoreSingleMipBounds<0>()); + RunCase(&vulkan, ImageStoreSingleMipBounds<1>()); + RunCase(&vulkan, ImageStoreSingleMipBounds<2>()); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--wave64-only") == 0) { CheckWave64WholeWaveResults(); VulkanHarness vulkan; @@ -36982,7 +37206,7 @@ int main(int argc, char **argv) { RunCase(&vulkan, BranchVccnzUsesWaveMask()); RunCase(&vulkan, BranchVccnzUsesCarryProducedWaveMask()); RunCase(&vulkan, ImageSampleAndGather()); - RunCase(&vulkan, ImageGatherLodApproximatesLevelZero()); + RunCase(&vulkan, ImageGatherExplicitLod()); RunCase(&vulkan, SharedReturnKeepsSelectedValues()); RunCase(&vulkan, SiblingSharedExitKeepsCapturedConditions()); return 0; diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index 82a75b47d..6d3982450 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -1309,7 +1309,8 @@ void TestSpirvRequirementsAnalysis() { block->AppendNewInst(ValueOpcode::ImageQueryLod, {Value(0u), Value(0u), Value(0u)}); block->AppendNewInst(ValueOpcode::ImageGatherRaw, - {Value(0u), Value(0u), Value(0u)}); + {Value(0u), Value(0u), Value(0u), Value(0u), + Value(0u), Value(0u), Value(0u)}); auto &shared = block->AppendNewInst(ValueOpcode::LoadSharedU32, {Value(0u), Value(true)}); shared.SetFlags(MemoryFlags{.index = 0});