diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index e7312525d..82f52f9f7 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -1047,6 +1047,26 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna return dimension == Decoder::ImageDimension::Dim1D || dimension == Decoder::ImageDimension::Dim2D; }; + // The descriptor's type selects the address layout. A 2D or 3D candidate reads the leading + // coordinates of an equal or larger dimension, but an array with more than one slice reads + // the next as its slice. Derivative, LOD, clamp and mip operands shift with the dimension. + const auto leading = [&](Decoder::ImageDimension dimension, const DescriptorValue& value) { + const bool sliced = + static_cast((value.dwords[3] >> 28u) & 0xfu) == + Prospero::ImageType::kColor2DArray && + (value.dwords[4] & 0x1fffu) != ((value.dwords[4] >> 16u) & 0x1fffu); + return !sliced && + (dimension == Decoder::ImageDimension::Dim2D || dimension == base.dimension); + }; + const bool leading_coordinates = (base.dimension == Decoder::ImageDimension::Dim2D || + base.dimension == Decoder::ImageDimension::Dim3D) && + leading(image_class.dimension, snapshot.images[exemplar]) && + std::ranges::none_of(program.memory_info, [&](const MemoryInfo& memory) { + return memory.kind == ResourceKind::Image && memory.resource == root_index && + (memory.image_has_mip || (memory.image_sample_flags & + (Decoder::ImageSampleFlagLod | Decoder::ImageSampleFlagDerivative | + Decoder::ImageSampleFlagLodClamp)) != 0u); + }); for (uint32_t candidate = 0; candidate < specialization.images.size(); candidate++) { auto& image = specialization.images[candidate]; if (image.indirect_root != root_index) { @@ -1066,8 +1086,10 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna base.dimension == Decoder::ImageDimension::Dim2D && !image.cube && !image_class.cube && is_1d_or_2d(image.dimension) && is_1d_or_2d(image_class.dimension); + const bool prefix_coordinates = + leading_coordinates && leading(image.dimension, snapshot.images[candidate]); if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension)) && - !unfiltered_coordinates) || + !unfiltered_coordinates && !prefix_coordinates) || image.conversion_format != image_class.conversion_format || (!storage_swizzle && image.shader_swizzle != image_class.shader_swizzle)) { return SpecializationFail( @@ -1717,8 +1739,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime } uint32_t ImageHeapKind(const ImageResource& root, DescriptorValue& value) { - // Mirrors BuildResourceSpecialization for sampled roots: entries it would reject, and - // entries that need shader-side conversion, read as null instead of failing the draw. + // Mirrors BuildResourceSpecialization for sampled roots: entries it would reject, entries of + // another dimension and entries that need shader-side conversion read as null instead of + // failing the draw. const auto format = static_cast((value.dwords[1] >> 20u) & 0x1ffu); const auto tile = static_cast((value.dwords[3] >> 20u) & 0x1fu); const auto numeric_class = diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index dbd58fab2..6c54e30e9 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -18053,7 +18053,8 @@ public: image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DArray; if (image.resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled) { supported = supported || - image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1DArray; + image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1DArray || + image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim3D; } Require(test.name, "dispatch", supported, "unsupported image dimension needs a matching Vulkan test view"); @@ -41990,6 +41991,106 @@ void CheckWaterfallFloorImageTable(VulkanHarness &vulkan) { std::printf("[compute] %s ok\n", test.name); } +void CheckMixedDimensionImageTable(VulkanHarness &vulkan) { + using O = ShaderOpcode; + constexpr u32 base = 256u, table_offset = 0x100u, rows = 4u; + // Row 0 is a one-slice 2D array under a 3D sample: it reads the leading two coordinates and + // clamps the third to its slice. A second slice or a LOD operand would read the third instead, + // and a 3D row 0 under a 2D sample, or 2D rows under a 1D sample, would read a coordinate the + // sample does not supply. + constexpr std::array names{"MixedDimensionImageTable", "MixedDimensionImageTableLod", + "MixedDimensionImageTableSlices", "MixedDimensionImageTableFlat", + "MixedDimensionImageTableLine"}; + for (u32 variant = 0; variant < names.size(); ++variant) { + const bool lod = variant == 1u, slices = variant == 2u, flat = variant == 3u, + line = variant == 4u; + TestCase test; + test.name = names[variant]; + test.dispatch_x = rows; + test.bda_mappings = {{0u, 0u}}; + test.initial.resize((base + table_offset + rows * 32u) / 4u); + for (u32 row = 0; row < rows; ++row) { + auto image = MakeSampledTextureData(Prospero::BufferFormat::k32Float); + image[0] = row + 1u; + auto type = row == 0u ? Prospero::ImageType::kColor2DArray : Prospero::ImageType::kColor3D; + if (flat) type = row == 0u ? Prospero::ImageType::kColor3D : Prospero::ImageType::kColor2D; + if (line) type = row == 0u ? Prospero::ImageType::kColor1D : Prospero::ImageType::kColor2D; + image[3] = DstSel(4, 0, 0, 1) | (static_cast(type) << 28u); + image[4] = row == 0u && slices ? 1u : 0u; + std::copy_n(image.begin(), 8, test.initial.begin() + (base + table_offset + row * 32u) / 4u); + } + test.user_data[0] = base; + const auto output = MakeStructuredStorageBufferData(0u, rows * 4u); + std::copy_n(output.begin(), 4, test.user_data.begin() + 48); + test.has_user_data = test.has_compute_info = true; + test.compute_info.wave_size = 64u; + test.compute_info.threads_num[0] = test.compute_info.threads_num[1] = + test.compute_info.threads_num[2] = 1u; + test.compute_info.group_id[0] = true; + test.compute_info.workgroup_register = 44u; + u32 dim = 2u; + if (flat) dim = 1u; + if (line) dim = 0u; + auto &code = test.code; + AppendVMovLiteral(&code, 10, std::bit_cast(0.5f)); + AppendVMovLiteral(&code, 11, std::bit_cast(0.5f)); + AppendVMovLiteral(&code, 12, std::bit_cast(0.5f)); + AppendVMovLiteral(&code, 13, 0u); + code.insert(code.end(), {EncodeSop2(0x1e, 23, 44, InlineU32(5)), + EncodeSop2(0x02, 23, 23, 255), table_offset, EncodeSmem0(0x03, 8), EncodeSmem1(0, 23), + EncodeMimg0(lod ? 0x24 : 0x27, 1, 0, false, dim), EncodeMimg1(5, 10, 2, 13), + EncodeSop2(0x1e, 25, 44, InlineU32(2)), EncodeVop1(0x01, 30, 25)}); + AppendBufferStoreDword(&code, 5, 30); + AppendEnd(&code); + if (variant != 0u) { + ShaderRecompiler::CompileOptions options; + options.user_data = test.user_data; + options.input_info.compute = &test.compute_info; + auto translated = ShaderRecompiler::TranslateProgram(test.code, options); + const auto plan = LoadedResourcePlan(test.name, translated.program); + ShaderRecompiler::IR::ResourceSnapshot snapshot; + ShaderRecompiler::IR::ResourceSpecialization specialization; + const std::array workgroup_counts{rows, 1u, 1u}; + const ShaderRecompiler::IR::SrtRuntime runtime{ + .user_data = test.user_data, + .userdata = &test.initial, + .read_specialization_memory = ReadTestMemory, + .workgroup_counts = workgroup_counts, + }; + FakeGuestMemory fake(ReadTestMemory, runtime.userdata); + Require(test.name, "coordinate layout", + !ShaderRecompiler::IR::MaterializeResources(plan, runtime, snapshot, specialization), + "a candidate read a coordinate outside its layout"); + continue; + } + test.opcodes = {O::V_MOV_B32, O::S_LSHL_B32, O::S_ADD_I32, O::S_LOAD_DWORDX8, + O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + const auto compiled = CompileCase(test, vulkan.SubgroupSize()); + std::vector images; + for (const auto &descriptor : compiled.resources.images) { + const bool volume = + (descriptor.dwords[3] >> 28u) == static_cast(Prospero::ImageType::kColor3D); + images.push_back(vulkan.CreateImageMips(test.name, 1, 1, vk::Format::eR32Sfloat, + vk::ImageUsageFlagBits::eSampled, + {{std::bit_cast(static_cast(descriptor.dwords[0]))}}, 1, + vk::ImageLayout::eShaderReadOnlyOptimal, volume ? vk::ImageType::e3D : vk::ImageType::e2D, + volume ? vk::ImageViewType::e3D : vk::ImageViewType::e2D, 1)); + } + const auto sampler = vulkan.CreateSampler(test.name, 1); + auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size(), true); + vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, nullptr, nullptr, sampler, images); + std::vector expected; + for (u32 row = 0; row < rows; ++row) + expected.push_back(std::bit_cast(static_cast(row + 1u))); + CompareWords(test, "mixed dimension rows", expected, + vulkan.ReadBuffer(test.name, buffer, expected.size())); + vulkan.DestroyBuffer(&buffer); + vulkan.Device().destroySampler(sampler); + for (auto &image : images) vulkan.DestroyImage(&image); + std::printf("[compute] %s ok\n", test.name); + } +} + void CheckNestedPointerImageTable(VulkanHarness &vulkan) { using O = ShaderOpcode; constexpr u32 list_address = 160; @@ -52455,6 +52556,7 @@ int main(int argc, char **argv) { CheckPointerImageTable(vulkan); CheckWorkgroupDividedImageTable(vulkan); CheckWaterfallFloorImageTable(vulkan); + CheckMixedDimensionImageTable(vulkan); CheckNestedPointerImageTable(vulkan); CheckGuardedRecordImageTable(vulkan); CheckIndirectImageKeySwitch(vulkan); @@ -52635,6 +52737,7 @@ int main(int argc, char **argv) { CheckPointerImageTable(vulkan); CheckWorkgroupDividedImageTable(vulkan); CheckWaterfallFloorImageTable(vulkan); + CheckMixedDimensionImageTable(vulkan); CheckNestedPointerImageTable(vulkan); CheckGuardedRecordImageTable(vulkan); CheckIndirectImageKeySwitch(vulkan);