diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index 85dc422ac..dc1428ed0 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -712,6 +712,8 @@ auto EmitImageAccess(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t resul const auto next_slot = run.slot_bias + ordinal; if (candidate.dimension == first.dimension && candidate.cube == first.cube && IR::DescriptorBindingForImage(first) == kind && + (inst.GetOpcode() != IR::ValueOpcode::ImageWrite || + candidate.shader_swizzle == first.shader_swizzle) && candidate.mip_count == 1u && first.mip_count == 1u && (ordinal == 1u || candidate.descriptor_index == next_slot)) { // Native roots precede appended children; only the root may have a slot gap. diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index 3d0cf961f..b79489055 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -158,6 +158,7 @@ struct ImageResource { bool written = false; bool atomic = false; bool atomic64 = false; + bool unfiltered_access_only = true; bool depth_compare = false; bool cube = false; bool r128 = false; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 737399ea6..4d9724e04 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -829,10 +829,17 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna return SpecializationFail("indirect image specialization has no typed candidate"); } const auto& image_class = specialization.images[exemplar]; + const auto& base = program.info.images[root_index]; + const bool storage_swizzle = base.unfiltered_access_only && + base.resource_class == ImageResourceClass::Storage; const auto is_2d = [](Decoder::ImageDimension dimension) { return dimension == Decoder::ImageDimension::Dim2D || dimension == Decoder::ImageDimension::Dim2DArray; }; + const auto is_1d_or_2d = [](Decoder::ImageDimension dimension) { + return dimension == Decoder::ImageDimension::Dim1D || + dimension == Decoder::ImageDimension::Dim2D; + }; for (uint32_t candidate = 0; candidate < specialization.images.size(); candidate++) { auto& image = specialization.images[candidate]; if (image.indirect_root != root_index) { @@ -848,10 +855,15 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna } const bool same_coordinates = image.dimension == image_class.dimension && image.cube == image_class.cube; - if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension))) || + const bool unfiltered_coordinates = base.unfiltered_access_only && + base.dimension == Decoder::ImageDimension::Dim2D && + !image.cube && !image_class.cube && is_1d_or_2d(image.dimension) && + is_1d_or_2d(image_class.dimension); + if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension)) && + !unfiltered_coordinates) || image.mip_count != image_class.mip_count || image.conversion_format != image_class.conversion_format || - image.shader_swizzle != image_class.shader_swizzle) { + (!storage_swizzle && image.shader_swizzle != image_class.shader_swizzle)) { return SpecializationFail( fmt::format("indirect image table at pc 0x{:08x} has incompatible candidates", program.info.images[root_index].first_use_pc)); @@ -1708,7 +1720,8 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization& EXIT_IF(image.indirect_root == memory.resource && inst.GetOpcode() != ValueOpcode::ImageSampleRaw && inst.GetOpcode() != ValueOpcode::ImageGatherRaw && - inst.GetOpcode() != ValueOpcode::ImageRead); + inst.GetOpcode() != ValueOpcode::ImageRead && + inst.GetOpcode() != ValueOpcode::ImageWrite); } } for (auto& memory: memory_info) { diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index a6e2b7f2d..17e771286 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -3495,6 +3495,7 @@ private: image.read = image.read || !write || atomic; image.written = image.written || write; image.atomic = image.atomic || atomic; + image.unfiltered_access_only &= op == ValueOpcode::ImageRead || op == ValueOpcode::ImageWrite; } uint32_t AddSampler(uint32_t source, uint32_t pc) { diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 3a9ef2023..f2767f1f4 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -206,7 +206,8 @@ bool ReadLinearTestMemory(void *userdata, uint64_t address, std::span std::unique_ptr MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t table_offset = 0, - uint32_t table_shader_offset = 0, bool nested = false) { + uint32_t table_shader_offset = 0, bool nested = false, + ValueOpcode access = ValueOpcode::ImageSampleRaw, bool finite_key = false) { auto fixture = std::make_unique(); std::array material_words; std::array heap_words; @@ -215,12 +216,14 @@ MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t ta heap_words[dword] = fixture->UserData(dword + 4u); } const auto material = fixture->Buffer(material_words, 0x10d8); - const auto heap = fixture->Buffer(heap_words, 0x10d8); + const auto heap = finite_key ? fixture->Address(heap_words[0], heap_words[1], 0x10d8) + : fixture->Buffer(heap_words, 0x10d8); const auto invocation = fixture->Emit( ValueOpcode::GetBuiltin, - {Value(static_cast(StageInputKind::GlobalInvocationId)), Value(0u)}); - const auto selector = fixture->Emit(ValueOpcode::ReadFirstLane, - {invocation, Value(true)}); + {Value(static_cast(finite_key ? StageInputKind::WorkgroupId + : StageInputKind::GlobalInvocationId)), Value(0u)}); + const auto selector = finite_key ? invocation + : fixture->Emit(ValueOpcode::ReadFirstLane, {invocation, Value(true)}); Value member; if (nested) { std::array root_words; @@ -240,24 +243,27 @@ MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t ta MemoryInfo material_scalar; material_scalar.kind = ResourceKind::ScalarBuffer; material_scalar.offset = 4u; - const auto key = - fixture->Emit(ValueOpcode::ReadConstBuffer, {material, member}, - fixture->AddMemory(material_scalar, 0x10d8)); + auto key = fixture->Emit(ValueOpcode::ReadConstBuffer, {material, member}, + fixture->AddMemory(material_scalar, 0x10d8)); + if (finite_key) key = fixture->Emit(ValueOpcode::BitFieldUExtract, + {key, Value(16u), Value(5u)}); auto heap_offset = fixture->Emit(ValueOpcode::IMul32, {key, Value(table_stride)}); if (table_shader_offset != 0u) heap_offset = fixture->Emit(ValueOpcode::IAdd32, {heap_offset, Value(table_shader_offset)}); std::array image_words; MemoryInfo heap_scalar; - heap_scalar.kind = ResourceKind::ScalarBuffer; + heap_scalar.kind = finite_key ? ResourceKind::ScalarAddress : ResourceKind::ScalarBuffer; for (uint32_t dword = 0; dword < image_words.size(); dword++) { auto component = heap_scalar; component.offset = table_offset + dword * sizeof(uint32_t); if (malformed && dword == image_words.size() - 1u) { component.offset += sizeof(uint32_t); } - image_words[dword] = - fixture->Emit(ValueOpcode::ReadConstBuffer, {heap, heap_offset}, - fixture->AddMemory(component, 0x10d8)); + const auto flags = fixture->AddMemory(component, 0x10d8); + image_words[dword] = finite_key + ? fixture->Emit(ValueOpcode::LoadAddressU32, + {heap, heap_offset, Value(0u), Value(true)}, flags) + : fixture->Emit(ValueOpcode::ReadConstBuffer, {heap, heap_offset}, flags); } if (nested) { const auto field = fixture->Emit(ValueOpcode::BitwiseAnd32, {image_words[4], Value(7u)}); @@ -269,12 +275,27 @@ MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t ta MemoryInfo sample; sample.kind = ResourceKind::Image; sample.image_dimension = Decoder::ImageDimension::Dim2D; - const auto sampled = fixture->Emit(ValueOpcode::ImageSampleRaw, - {image, sampler, fixture->ImageAddress(), Value(true)}, - fixture->AddMemory(sample, 0x10f0)); - const auto sampled_x = - fixture->Emit(ValueOpcode::CompositeExtractU32x4, {sampled, Value(0u)}); - fixture->Emit(ValueOpcode::ReferenceU32, {sampled_x}); + const auto address = fixture->ImageAddress(); + const auto flags = fixture->AddMemory(sample, 0x10f0); + Value result; + if (access == ValueOpcode::ImageWrite) { + const auto texel = fixture->Emit(ValueOpcode::CompositeConstructU32x4, + {Value(1u), Value(2u), Value(3u), Value(4u)}); + fixture->Emit(access, {image, address, texel, Value(true)}, flags); + } else if (access == ValueOpcode::ImageRead) { + result = fixture->Emit(access, {image, address, Value(true)}, flags); + } else if (access == ValueOpcode::ImageQueryDimensions) { + result = fixture->Emit(access, {image, address}, flags); + } else if (access == ValueOpcode::ImageAtomicIAdd32) { + result = fixture->Emit(access, {image, address, Value(1u), Value(true)}, flags); + } else { + result = fixture->Emit(access, {image, sampler, address, Value(true)}, flags); + } + if (!result.IsEmpty()) { + if (result.GetType() == Type::U32x4) + result = fixture->Emit(ValueOpcode::CompositeExtractU32x4, {result, Value(0u)}); + fixture->Emit(ValueOpcode::ReferenceU32, {result}); + } return fixture; } @@ -441,6 +462,113 @@ void TestBoundedImageViewEligibility() { "explicitly selected incompatible descriptor was silently normalized to null"); } +void TestExactUnfilteredImageCandidates() { + using F = Libs::Graphics::Prospero::BufferFormat; + using T = Libs::Graphics::Prospero::ImageType; + constexpr std::array placeholder{ + 0x50091c00u, 0x03800000u, 0u, 0x80000000u, 0u, 0x00700000u, 0u, 0u}; + for (const auto access : {ValueOpcode::ImageRead, ValueOpcode::ImageWrite, + ValueOpcode::ImageSampleRaw, ValueOpcode::ImageQueryDimensions, + ValueOpcode::ImageAtomicIAdd32}) { + auto fixture = MakeIndirectImageFixture(false, 32u, 0u, 0u, false, access, true); + fixture->PlanAndTrack(); + auto plan = ExtractResourcePlan(fixture->program); + const bool unfiltered = access == ValueOpcode::ImageRead || access == ValueOpcode::ImageWrite; + Check(plan.info.images[0].unfiltered_access_only == unfiltered, + "image access classification admitted a sample, query or atomic"); + const auto &source = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor; + Check(source.key_count.Resolve() == Value(32u), + "five-bit image selector did not preserve its exact 32-row domain"); + LinearTestMemory memory; + memory.fail_address = 0x3000u; // Selector payload stays on the GPU. + auto first = placeholder; + first[0] += 1u; + first[3] = Libs::Graphics::DstSel(4, 5, 6, 7) | (uint32_t(T::kColor2D) << 28u); + auto second = placeholder; + if (access == ValueOpcode::ImageAtomicIAdd32) { + first[1] = second[1] = uint32_t(F::k32UInt) << 20u; + second[3] |= Libs::Graphics::DstSel(4, 5, 6, 7); + } + const auto fill = [&] { + for (uint32_t row = 0; row < 32u; ++row) { + const auto &words = row == 31u ? second : first; + std::copy(words.begin(), words.end(), memory.words.begin() + 0x1000u / 4u + row * 8u); + } + }; + fill(); + std::array data{0x3000u, 4u << 16u, 32u, 0u, + 0x2000u, 0u, 0u, 0u}; + const SrtRuntime runtime{.user_data = data, .userdata = &memory, + .read_specialization_memory = ReadLinearTestMemory}; + ResourceSnapshot snapshot; + ResourceSpecialization specialization; + Check(MaterializeResources(plan, runtime, snapshot, specialization) == unfiltered, + "mixed 1D/2D candidates violated the all-uses access guard"); + if (!unfiltered) continue; + Check(snapshot.images.size() == 2u && snapshot.images[1].dwords == placeholder && + specialization.images[0].dimension == Decoder::ImageDimension::Dim2D && + specialization.images[1].dimension == Decoder::ImageDimension::Dim1D, + "a real 1D zero-swizzle descriptor was filtered, nulled or retyped"); + if (access == ValueOpcode::ImageWrite) + Check(specialization.images[1].shader_swizzle == 0u, + "storage placeholder lost its descriptor swizzle"); + const auto operation = std::ranges::find_if(*fixture->block, [&](const Inst &inst) { + return inst.GetOpcode() == access; + }); + const auto key = operation->Arg(0).ResolveInstruction()->Arg(0); + ApplyResourceSpecialization(fixture->program, specialization); + const auto &images = fixture->program.info.images; + const auto &selected = fixture->program.memory_info[operation->Flags().index]; + Check(images.size() == 2u && selected.resource == 0u && + selected.image_dimension == Decoder::ImageDimension::Dim2D && + images[0].indirect_resources == std::vector({0u, 1u}) && + images[1].indirect_root == 0u && + images[1].dimension == Decoder::ImageDimension::Dim1D && + images[1].unfiltered_access_only && + operation->Arg(0).ResolveInstruction()->Arg(0) == key && + (access != ValueOpcode::ImageWrite || images[1].shader_swizzle == 0u), + "applying mixed candidates lost GPU selection, typed child or storage swizzle"); + second[0] += 2u; + fill(); + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + snapshot.images[1].dwords[0] == second[0], + "mixed-dimension materialization reused a stale descriptor"); + plan.info.images[0].dimension = Decoder::ImageDimension::Dim1D; + Check(!MaterializeResources(plan, runtime, snapshot, specialization), + "encoded 1D payload was treated as having a second coordinate"); + plan.info.images[0].dimension = Decoder::ImageDimension::Dim2D; + second[1] = uint32_t(F::k11_11_10UInt) << 20u; + fill(); + Check(!MaterializeResources(plan, runtime, snapshot, specialization), + "mixed-dimension acceptance discarded conversion compatibility"); + if (access == ValueOpcode::ImageRead) { + first[1] = second[1]; + fill(); + Check(!MaterializeResources(plan, runtime, snapshot, specialization), + "sampled converted candidates accepted different shader swizzles"); + first[1] = placeholder[1]; + } + second[1] = placeholder[1]; + second[3] |= 1u << 16u; + fill(); + plan.info.images[0].mip_mode = ImageMipMode::Dynamic; + Check(!MaterializeResources(plan, runtime, snapshot, specialization), + "mixed-dimension acceptance discarded mip-count compatibility"); + } + // The same image may be merged from several uses; one query keeps the guard strict. + auto merged = MakeIndirectImageFixture(false, 32u, 0u, 0u, false, ValueOpcode::ImageRead, true); + const auto handle = std::ranges::find_if(*merged->block, [](const Inst &inst) { + return inst.GetOpcode() == ValueOpcode::GetImageResource; + }); + merged->Emit(ValueOpcode::ImageQueryDimensions, {Value(&*handle), merged->ImageAddress()}, + merged->AddMemory({.kind = ResourceKind::Image, + .image_dimension = Decoder::ImageDimension::Dim2D}, 0x1100)); + merged->PlanAndTrack(); + Check(merged->program.info.images.size() == 1u && + !merged->program.info.images[0].unfiltered_access_only, + "a later merged query left the unfiltered-use proof intact"); +} + void TestWaterfallImageTable() { namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; const auto make = [](bool equality, bool scalar) { @@ -6757,6 +6885,7 @@ int main() { Run("invariant indirect images", [] { TestInvariantIndirectImageMaterialization(true); }); Run("unbounded material image keys", [] { TestInvariantIndirectImageMaterialization(false); }); Run("bounded image view eligibility", TestBoundedImageViewEligibility); + Run("exact unfiltered image candidates", TestExactUnfilteredImageCandidates); Run("waterfall image table", TestWaterfallImageTable); Run("nested pointer image table", TestNestedPointerImageTable); Run("guarded direct image table", TestGuardedDirectImageTable); diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index f655fafdf..18a407715 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -16083,11 +16083,11 @@ public: }; for (const auto &image : compiled.program.info.images) { bool supported = + image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1D || image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2D || image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DArray; if (image.resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled) { supported = supported || - image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1D || image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1DArray; } Require(test.name, "dispatch", supported, @@ -38429,11 +38429,11 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) { resource == 4u ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, layers)); } const auto native_sampler = vulkan.CreateSampler(test.name); - enum class Access { Sample, Gather, GatherHomogeneous, Fetch }; + enum class Access { Sample, Gather, GatherHomogeneous, Fetch, Fetch1D }; auto *access = &sample; for (const auto mode : {Access::Sample, Access::Gather, - Access::GatherHomogeneous, Access::Fetch}) { - const bool fetch = mode == Access::Fetch; + Access::GatherHomogeneous, Access::Fetch, Access::Fetch1D}) { + const bool fetch = mode == Access::Fetch || mode == Access::Fetch1D; const bool gather = mode == Access::Gather || mode == Access::GatherHomogeneous; const bool homogeneous = mode == Access::GatherHomogeneous; if (mode == Access::Gather) { @@ -38472,6 +38472,7 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) { position, ValueOpcode::ImageRead, {Value(&image), Value(&address), Value(true)}, memory_flag_bits); access->ReplaceUsesWith(Value(&read), false); + access = &read; sampler.Invalidate(); compiled.program.info.samplers.clear(); compiled.program.info.sampled_pairs.clear(); @@ -38483,6 +38484,27 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) { address.SetArg(component, Value(0u)); // Native DIM=2DArray places LOD after the layer, even for a 2D candidate. address.SetArg(3, Value(1u)); + if (mode == Access::Fetch1D) { + test.name = "IndirectImageFetch1D2D"; + compiled.program.memory_info[0].image_dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; + compiled.program.memory_info[0].image_address_components = 3; + address.SetArg(2, Value(1u)); + address.SetArg(3, Value(0u)); + for (const u32 resource : {3u, 4u}) { + const bool one_d = resource == 3u; + compiled.program.info.images[resource].dimension = one_d + ? ShaderRecompiler::Decoder::ImageDimension::Dim1D + : ShaderRecompiler::Decoder::ImageDimension::Dim2D; + vulkan.DestroyImage(&textures[resource]); + textures[resource] = vulkan.CreateImageMips(test.name, 2, 1, vk::Format::eR32G32B32A32Sfloat, + vk::ImageUsageFlagBits::eSampled, + {std::vector(8u, std::bit_cast(float(resource))), + std::vector(4u, std::bit_cast(float(resource + 10u)))}, + 4u, vk::ImageLayout::eShaderReadOnlyOptimal, + one_d ? vk::ImageType::e1D : vk::ImageType::e2D, + one_d ? vk::ImageViewType::e1D : vk::ImageViewType::e2D, 1u); + } + } } compiled.program.shader_info_complete = false; CollectShaderInfo(compiled.program, {.compute = &compute}); @@ -38568,7 +38590,7 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) { } Require(test.name, "mixed numeric image result", raw_join && tools.Disassemble(mixed_spirv, &text) && - CountText(text, image_opcode) == 4u * halves && + CountText(text, image_opcode) == (mode == Access::Fetch1D ? 3u : 4u) * halves && CountText(text, "OpConvertUToF") == coordinate_conversions, "mixed image classes lost raw result bits or expanded per image"); typed.numeric_class = Prospero::TextureNumericClass::Float; @@ -38585,7 +38607,6 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) { // Reuse the GPU key map, including the separated root slot and unmapped suffix, // for storage writes. Every lane has a distinct texel address. - test.name = "IndirectImageStoreHomogeneous"; store.Invalidate(); for (auto &inst : *block) if (inst.GetOpcode() == ValueOpcode::ReferenceU32) inst.Invalidate(); @@ -38596,6 +38617,7 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) { store_memory.image_address_components = 2; store_memory.image_has_mip = false; address.SetArg(0, Value(&key)); + address.SetArg(2, Value(0u)); address.SetArg(3, Value(0u)); for (auto &resource : compiled.program.info.images) { resource.resource_class = ImageResourceClass::Storage; @@ -38613,45 +38635,61 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) { block->AppendNewInst(ValueOpcode::ImageWrite, {Value(&image), Value(&address), Value(&texel), Value(&write_exec)}, memory_flag_bits); EliminateDeadCode(compiled.program.blocks); - compiled.program.shader_info_complete = false; - CollectShaderInfo(compiled.program, {.compute = &compute}); - for (const u32 wave_size : {32u, 64u}) { - compute.wave_size = wave_size; - compute.threads_num[0] = wave_size; - compiled.program.wave_size = wave_size; - compiled.spirv = ShaderRecompiler::Spirv::EmitProgram(compiled.program, {.compute = &compute}); - compiled.packed_user_data.resize(compiled.program.bindings.ShaderDataDwords()); - ValidateSpirv(test.name, compiled.spirv); - const auto halves = wave_size > compute.host_subgroup_size ? 2u : 1u; - Require(test.name, "one native array write per wave half", - tools.Disassemble(compiled.spirv, &text) && CountText(text, "OpImageWrite") == halves && - text.find("OpSwitch") == std::string::npos && - text.find("StorageImageArrayNonUniformIndexing") != std::string::npos, - "homogeneous storage candidates lost their native array selection"); - std::vector> expected_images; - for (u32 resource = 0; resource < compiled.program.info.images.size(); ++resource) { - expected_images.emplace_back((wave_size + 2u) * 4u, 0x41000000u + resource); - textures.push_back(vulkan.CreateImage2D(test.name, wave_size + 2u, 1, - vk::Format::eR32G32B32A32Sfloat, vk::ImageUsageFlagBits::eStorage, - expected_images.back(), 4u, vk::ImageLayout::eGeneral)); + for (const bool mixed : {false, true}) { + test.name = mixed ? "IndirectImageStoreMixed" : "IndirectImageStoreHomogeneous"; + compiled.program.info.images[3].dimension = mixed + ? ShaderRecompiler::Decoder::ImageDimension::Dim1D + : ShaderRecompiler::Decoder::ImageDimension::Dim2D; + compiled.program.info.images[5].shader_swizzle = mixed ? DstSel(5, 4, 6, 7) + : ShaderImageIdentitySwizzle; + compiled.program.info.images[6].shader_swizzle = mixed ? 0u : ShaderImageIdentitySwizzle; + compiled.program.shader_info_complete = false; + CollectShaderInfo(compiled.program, {.compute = &compute}); + for (const u32 wave_size : {32u, 64u}) { + compute.wave_size = wave_size; + compute.threads_num[0] = wave_size; + compiled.program.wave_size = wave_size; + compiled.spirv = ShaderRecompiler::Spirv::EmitProgram(compiled.program, {.compute = &compute}); + compiled.packed_user_data.resize(compiled.program.bindings.ShaderDataDwords()); + ValidateSpirv(test.name, compiled.spirv); + const auto halves = wave_size > compute.host_subgroup_size ? 2u : 1u; + Require(test.name, "native array writes per dimension and swizzle run", + tools.Disassemble(compiled.spirv, &text) && + CountText(text, "OpImageWrite") == (mixed ? 5u : 1u) * halves && + (mixed || text.find("OpSwitch") == std::string::npos) && + text.find("StorageImageArrayNonUniformIndexing") != std::string::npos, + "storage candidates lost their typed native array runs"); + std::vector> expected_images; + for (u32 resource = 0; resource < compiled.program.info.images.size(); ++resource) { + expected_images.emplace_back((wave_size + 2u) * 4u, 0x41000000u + resource); + const bool one_d = compiled.program.info.images[resource].dimension == + ShaderRecompiler::Decoder::ImageDimension::Dim1D; + textures.push_back(vulkan.CreateImageMips(test.name, wave_size + 2u, 1, + vk::Format::eR32G32B32A32Sfloat, vk::ImageUsageFlagBits::eStorage, + {expected_images.back()}, 4u, vk::ImageLayout::eGeneral, + one_d ? vk::ImageType::e1D : vk::ImageType::e2D, + one_d ? vk::ImageViewType::e1D : vk::ImageViewType::e2D, 1u)); + } + test.initial = {0xdeadbeefu}; + for (u32 lane = 0; lane < wave_size; ++lane) { + const auto resource = root.indirect_resources[lane < ordinals.size() ? ordinals[lane] : 0u]; + if (lane == 2u) continue; + std::array written{0x3f800000u + lane, 0x40000000u, 0x40400000u, 0x40800000u}; + if (mixed && resource == 5u) std::swap(written[0], written[1]); + if (mixed && resource == 6u) written.fill(0u); + std::copy(written.begin(), written.end(), expected_images[resource].begin() + lane * 4u); + } + auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size()); + vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, nullptr, nullptr, nullptr, textures); + for (u32 resource = 0; resource < textures.size(); ++resource) { + CompareWords(test, "selected writes, inactive lane and neighboring texels", + expected_images[resource], vulkan.ReadImage(test.name, &textures[resource])); + vulkan.DestroyImage(&textures[resource]); + } + textures.clear(); + vulkan.DestroyBuffer(&buffer); + std::printf("[compute] %s wave=%u ok\n", test.name, wave_size); } - test.initial = {0xdeadbeefu}; - for (u32 lane = 0; lane < wave_size; ++lane) { - const auto resource = root.indirect_resources[lane < ordinals.size() ? ordinals[lane] : 0u]; - if (lane == 2u) continue; - const std::array written{0x3f800000u + lane, 0x40000000u, 0x40400000u, 0x40800000u}; - std::copy(written.begin(), written.end(), expected_images[resource].begin() + lane * 4u); - } - auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size()); - vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, nullptr, nullptr, nullptr, textures); - for (u32 resource = 0; resource < textures.size(); ++resource) { - CompareWords(test, "selected writes, inactive lane and neighboring texels", - expected_images[resource], vulkan.ReadImage(test.name, &textures[resource])); - vulkan.DestroyImage(&textures[resource]); - } - textures.clear(); - vulkan.DestroyBuffer(&buffer); - std::printf("[compute] %s wave=%u ok\n", test.name, wave_size); } }