From 1bb152d9769a454652e2658b2e35c429c96127ee Mon Sep 17 00:00:00 2001 From: baggins183 Date: Tue, 17 Mar 2026 12:47:19 -0700 Subject: [PATCH] IMAGE_STORE_MIP fallback (#4075) * fallback for IMAGE_STORE_MIP when not natively supported * Lod should be treated as absolute, independent of sharp's base_level (judging by other implemented instructions) * fix descriptor set layouts * dumb error * force fallback for testing * treat Lod as relative to base_level * optimization when lod index is constant --- .../backend/spirv/emit_spirv_image.cpp | 26 +++++-- .../backend/spirv/spirv_emit_context.cpp | 16 ++++- .../backend/spirv/spirv_emit_context.h | 2 +- src/shader_recompiler/ir/passes/ir_passes.h | 2 +- .../ir/passes/resource_tracking_pass.cpp | 59 +++++++++++++-- src/shader_recompiler/recompiler.cpp | 2 +- src/shader_recompiler/resource.h | 11 +++ src/shader_recompiler/specialization.h | 5 +- .../renderer_vulkan/vk_compute_pipeline.cpp | 6 +- .../renderer_vulkan/vk_graphics_pipeline.cpp | 6 +- .../renderer_vulkan/vk_pipeline_cache.cpp | 2 +- .../renderer_vulkan/vk_rasterizer.cpp | 71 ++++++++++++++----- 12 files changed, 168 insertions(+), 40 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp index e2a969b61..0b05dcef4 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp @@ -220,20 +220,33 @@ Id EmitImageGradient(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id Id EmitImageRead(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id lod, Id ms) { const auto& texture = ctx.images[handle & 0xFFFF]; - const Id image = ctx.OpLoad(texture.image_type, texture.id); const Id color_type = texture.data_types->Get(4); ImageOperands operands; operands.Add(spv::ImageOperandsMask::Sample, ms); Id texel; if (!texture.is_storage) { + const Id image = ctx.OpLoad(texture.image_type, texture.id); operands.Add(spv::ImageOperandsMask::Lod, lod); texel = ctx.OpImageFetch(color_type, image, coords, operands.mask, operands.operands); } else { + Id image_ptr = texture.id; if (ctx.profile.supports_image_load_store_lod) { operands.Add(spv::ImageOperandsMask::Lod, lod); } else if (Sirit::ValidId(lod)) { - LOG_WARNING(Render, "Image read with LOD not supported by driver"); +#if 1 + // It's confusing what interactions will cause this code path so leave it as + // unreachable until a case is found. + // Normally IMAGE_LOAD_MIP should translate -> OpImageFetch + UNREACHABLE_MSG("Unsupported ImageRead with Lod"); +#else + LOG_WARNING(Render, "Fallback for ImageRead with LOD"); + ASSERT(texture.mip_fallback_mode == MipStorageFallbackMode::DynamicIndex); + const Id single_image_ptr_type = + ctx.TypePointer(spv::StorageClass::UniformConstant, texture.image_type); + image_ptr = ctx.OpAccessChain(single_image_ptr_type, image_ptr, std::array{lod}); +#endif } + const Id image = ctx.OpLoad(texture.image_type, image_ptr); texel = ctx.OpImageRead(color_type, image, coords, operands.mask, operands.operands); } return texture.is_integer ? ctx.OpBitcast(ctx.F32[4], texel) : texel; @@ -242,15 +255,20 @@ Id EmitImageRead(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id lod void EmitImageWrite(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id lod, Id ms, Id color) { const auto& texture = ctx.images[handle & 0xFFFF]; - const Id image = ctx.OpLoad(texture.image_type, texture.id); + Id image_ptr = texture.id; const Id color_type = texture.data_types->Get(4); ImageOperands operands; operands.Add(spv::ImageOperandsMask::Sample, ms); if (ctx.profile.supports_image_load_store_lod) { operands.Add(spv::ImageOperandsMask::Lod, lod); } else if (Sirit::ValidId(lod)) { - LOG_WARNING(Render, "Image write with LOD not supported by driver"); + LOG_WARNING(Render, "Fallback for ImageWrite with LOD"); + ASSERT(texture.mip_fallback_mode == MipStorageFallbackMode::DynamicIndex); + const Id single_image_ptr_type = + ctx.TypePointer(spv::StorageClass::UniformConstant, texture.image_type); + image_ptr = ctx.OpAccessChain(single_image_ptr_type, image_ptr, std::array{lod}); } + const Id image = ctx.OpLoad(texture.image_type, image_ptr); const Id texel = texture.is_integer ? ctx.OpBitcast(color_type, color) : color; ctx.OpImageWrite(image, coords, texel, operands.mask, operands.operands); } diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index 261155ab5..c0e469964 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -961,23 +961,33 @@ void EmitContext::DefineImagesAndSamplers() { const auto nfmt = sharp.GetNumberFmt(); const bool is_integer = AmdGpu::IsInteger(nfmt); const bool is_storage = image_desc.is_written; + const MipStorageFallbackMode mip_fallback_mode = image_desc.mip_fallback_mode; const VectorIds& data_types = GetAttributeType(*this, nfmt); const Id sampled_type = data_types[1]; const Id image_type{ImageType(*this, image_desc, sampled_type)}; - const Id pointer_type{TypePointer(spv::StorageClass::UniformConstant, image_type)}; + + const u32 num_bindings = image_desc.NumBindings(info); + Id pointee_type = image_type; + if (mip_fallback_mode == MipStorageFallbackMode::DynamicIndex) { + pointee_type = TypeArray(pointee_type, ConstU32(num_bindings)); + } + + const Id pointer_type{TypePointer(spv::StorageClass::UniformConstant, pointee_type)}; const Id id{AddGlobalVariable(pointer_type, spv::StorageClass::UniformConstant)}; - Decorate(id, spv::Decoration::Binding, binding.unified++); + Decorate(id, spv::Decoration::Binding, binding.unified); + binding.unified += num_bindings; Decorate(id, spv::Decoration::DescriptorSet, 0U); + // TODO better naming for resources (flattened sharp_idx is not informative) Name(id, fmt::format("{}_{}{}", stage, "img", image_desc.sharp_idx)); images.push_back({ .data_types = &data_types, .id = id, .sampled_type = is_storage ? sampled_type : TypeSampledImage(image_type), - .pointer_type = pointer_type, .image_type = image_type, .view_type = sharp.GetViewType(image_desc.is_array), .is_integer = is_integer, .is_storage = is_storage, + .mip_fallback_mode = mip_fallback_mode, }); interfaces.push_back(id); } diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.h b/src/shader_recompiler/backend/spirv/spirv_emit_context.h index 9bb2b7d7a..a9c6f0968 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.h +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.h @@ -293,11 +293,11 @@ public: const VectorIds* data_types; Id id; Id sampled_type; - Id pointer_type; Id image_type; AmdGpu::ImageType view_type; bool is_integer = false; bool is_storage = false; + MipStorageFallbackMode mip_fallback_mode{}; }; enum class PointerType : u32 { diff --git a/src/shader_recompiler/ir/passes/ir_passes.h b/src/shader_recompiler/ir/passes/ir_passes.h index f103b6736..1b14a1c6b 100644 --- a/src/shader_recompiler/ir/passes/ir_passes.h +++ b/src/shader_recompiler/ir/passes/ir_passes.h @@ -19,7 +19,7 @@ void DeadCodeEliminationPass(IR::Program& program); void ConstantPropagationPass(IR::BlockList& program); void FlattenExtendedUserdataPass(IR::Program& program); void ReadLaneEliminationPass(IR::Program& program); -void ResourceTrackingPass(IR::Program& program); +void ResourceTrackingPass(IR::Program& program, const Profile& profile); void CollectShaderInfoPass(IR::Program& program, const Profile& profile); void LowerBufferFormatToRaw(IR::Program& program); void LowerFp64ToFp32(IR::Program& program); diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index 4c41e94e9..3b7888ab3 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -9,6 +9,7 @@ #include "shader_recompiler/ir/operand_helper.h" #include "shader_recompiler/ir/program.h" #include "shader_recompiler/ir/reinterpret.h" +#include "shader_recompiler/profile.h" #include "video_core/amdgpu/resource.h" namespace Shader::Optimization { @@ -255,7 +256,9 @@ public: u32 Add(const ImageResource& desc) { const u32 index{Add(image_resources, desc, [&desc](const auto& existing) { - return desc.sharp_idx == existing.sharp_idx && desc.is_array == existing.is_array; + return desc.sharp_idx == existing.sharp_idx && desc.is_array == existing.is_array && + desc.mip_fallback_mode == existing.mip_fallback_mode && + desc.constant_mip_index == existing.constant_mip_index; })}; auto& image = image_resources[index]; image.is_atomic |= desc.is_atomic; @@ -529,14 +532,21 @@ void PatchBufferSharp(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& inst.SetArg(0, ir.Imm32(buffer_binding)); } -void PatchImageSharp(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& descriptors) { +void PatchImageSharp(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& descriptors, + const Profile& profile) { // Read image sharp. const auto inst_info = inst.Flags(); const IR::Inst* image_handle = inst.Arg(0).InstRecursive(); const auto tsharp = TrackSharp(image_handle, block, inst_info.pc); const bool is_atomic = IsImageAtomicInstruction(inst); const bool is_written = inst.GetOpcode() == IR::Opcode::ImageWrite || is_atomic; - const ImageResource image_res = { + const bool is_storage = + inst.GetOpcode() == IR::Opcode::ImageRead || inst.GetOpcode() == IR::Opcode::ImageWrite; + // ImageRead with !is_written gets emitted as OpImageFetch with LOD operand, doesn't + // need fallback (TODO is this 100% true?) + const bool needs_mip_storage_fallback = + inst_info.has_lod && is_written && !profile.supports_image_load_store_lod; + ImageResource image_res = { .sharp_idx = tsharp, .is_depth = bool(inst_info.is_depth), .is_atomic = is_atomic, @@ -544,9 +554,42 @@ void PatchImageSharp(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& .is_written = is_written, .is_r128 = bool(inst_info.is_r128), }; + auto image = image_res.GetSharp(info); ASSERT(image.GetType() != AmdGpu::ImageType::Invalid); + if (needs_mip_storage_fallback) { + // If the mip level to IMAGE_(LOAD/STORE)_MIP is a constant, set up ImageResource + // so that we will only bind a single level. + // If index is dynamic, we will bind levels as an array + const auto view_type = image.GetViewType(image_res.is_array); + + IR::Inst* body = inst.Arg(1).InstRecursive(); + const auto lod_arg = [&] -> IR::Value { + switch (view_type) { + case AmdGpu::ImageType::Color1D: // x, [lod] + return body->Arg(1); + case AmdGpu::ImageType::Color1DArray: // x, slice, [lod] + case AmdGpu::ImageType::Color2D: // x, y, [lod] + return body->Arg(2); + case AmdGpu::ImageType::Color2DArray: // x, y, slice, [lod] + case AmdGpu::ImageType::Color3D: // x, y, z, [lod] + return body->Arg(3); + case AmdGpu::ImageType::Color2DMsaa: + case AmdGpu::ImageType::Color2DMsaaArray: + default: + UNREACHABLE_MSG("Invalid image type {}", view_type); + } + }(); + + if (lod_arg.IsImmediate()) { + image_res.mip_fallback_mode = MipStorageFallbackMode::ConstantIndex; + image_res.constant_mip_index = lod_arg.U32(); + } else { + image_res.mip_fallback_mode = MipStorageFallbackMode::DynamicIndex; + } + } + // Patch image instruction if image is FMask. if (AmdGpu::IsFmask(image.GetDataFmt())) { ASSERT_MSG(!is_written, "FMask storage instructions are not supported"); @@ -1080,7 +1123,11 @@ void PatchImageArgs(IR::Block& block, IR::Inst& inst, Info& info) { const auto has_ms = view_type == AmdGpu::ImageType::Color2DMsaa || view_type == AmdGpu::ImageType::Color2DMsaaArray; ASSERT(!inst_info.has_lod || !has_ms); - const auto lod = inst_info.has_lod ? IR::U32{arg} : IR::U32{}; + // If we are binding a single mip level as fallback, drop the argument + const auto lod = + (inst_info.has_lod && image_res.mip_fallback_mode != MipStorageFallbackMode::ConstantIndex) + ? IR::U32{arg} + : IR::U32{}; const auto ms = has_ms ? IR::U32{arg} : IR::U32{}; const auto is_storage = image_res.is_written; @@ -1111,7 +1158,7 @@ void PatchImageArgs(IR::Block& block, IR::Inst& inst, Info& info) { } } -void ResourceTrackingPass(IR::Program& program) { +void ResourceTrackingPass(IR::Program& program, const Profile& profile) { // Iterate resource instructions and patch them after finding the sharp. auto& info = program.info; @@ -1122,7 +1169,7 @@ void ResourceTrackingPass(IR::Program& program) { if (IsBufferInstruction(inst)) { PatchBufferSharp(*block, inst, info, descriptors); } else if (IsImageInstruction(inst)) { - PatchImageSharp(*block, inst, info, descriptors); + PatchImageSharp(*block, inst, info, descriptors, profile); } } } diff --git a/src/shader_recompiler/recompiler.cpp b/src/shader_recompiler/recompiler.cpp index f4fa45afc..d6efa2890 100644 --- a/src/shader_recompiler/recompiler.cpp +++ b/src/shader_recompiler/recompiler.cpp @@ -80,7 +80,7 @@ IR::Program TranslateProgram(const std::span& code, Pools& pools, Inf Shader::Optimization::RingAccessElimination(program, runtime_info); Shader::Optimization::ReadLaneEliminationPass(program); Shader::Optimization::FlattenExtendedUserdataPass(program); - Shader::Optimization::ResourceTrackingPass(program); + Shader::Optimization::ResourceTrackingPass(program, profile); Shader::Optimization::LowerBufferFormatToRaw(program); Shader::Optimization::SharedMemorySimplifyPass(program, profile); Shader::Optimization::SharedMemoryToStoragePass(program, runtime_info, profile); diff --git a/src/shader_recompiler/resource.h b/src/shader_recompiler/resource.h index 5ae3179f6..82a861e2a 100644 --- a/src/shader_recompiler/resource.h +++ b/src/shader_recompiler/resource.h @@ -71,6 +71,8 @@ struct BufferResource { }; using BufferResourceList = boost::container::static_vector; +enum class MipStorageFallbackMode : u32 { None, DynamicIndex, ConstantIndex }; + struct ImageResource { u32 sharp_idx; bool is_depth{}; @@ -78,6 +80,8 @@ struct ImageResource { bool is_array{}; bool is_written{}; bool is_r128{}; + MipStorageFallbackMode mip_fallback_mode{}; + u32 constant_mip_index{}; constexpr AmdGpu::Image GetSharp(const auto& info) const noexcept { AmdGpu::Image image{}; @@ -102,6 +106,13 @@ struct ImageResource { } return image; } + + u32 NumBindings(const auto& info) const { + const AmdGpu::Image tsharp = GetSharp(info); + return (mip_fallback_mode == MipStorageFallbackMode::DynamicIndex) + ? (tsharp.last_level - tsharp.base_level + 1) + : 1; + } }; using ImageResourceList = boost::container::static_vector; diff --git a/src/shader_recompiler/specialization.h b/src/shader_recompiler/specialization.h index 4f6bb44bf..fa14583af 100644 --- a/src/shader_recompiler/specialization.h +++ b/src/shader_recompiler/specialization.h @@ -52,6 +52,8 @@ struct ImageSpecialization { bool is_srgb = false; AmdGpu::CompMapping dst_select{}; AmdGpu::NumberConversion num_conversion{}; + // FIXME any pipeline cache changes needed? + u32 num_bindings = 0; bool operator==(const ImageSpecialization&) const = default; }; @@ -133,7 +135,7 @@ struct StageSpecialization { } }); ForEachSharp(binding, images, info->images, - [](auto& spec, const auto& desc, AmdGpu::Image sharp) { + [&](auto& spec, const auto& desc, AmdGpu::Image sharp) { spec.type = sharp.GetViewType(desc.is_array); spec.is_integer = AmdGpu::IsInteger(sharp.GetNumberFmt()); spec.is_storage = desc.is_written; @@ -144,6 +146,7 @@ struct StageSpecialization { spec.is_srgb = sharp.GetNumberFmt() == AmdGpu::NumberFormat::Srgb; } spec.num_conversion = sharp.GetNumberConversion(); + spec.num_bindings = desc.NumBindings(*info); }); ForEachSharp(binding, fmasks, info->fmasks, [](auto& spec, const auto& desc, AmdGpu::Image sharp) { diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp index eecd416d1..ba0a3afa2 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp @@ -48,13 +48,15 @@ ComputePipeline::ComputePipeline(const Instance& instance, Scheduler& scheduler, }); } for (const auto& image : info->images) { + const u32 num_bindings = image.NumBindings(*info); bindings.push_back({ - .binding = binding++, + .binding = binding, .descriptorType = image.is_written ? vk::DescriptorType::eStorageImage : vk::DescriptorType::eSampledImage, - .descriptorCount = 1, + .descriptorCount = num_bindings, .stageFlags = vk::ShaderStageFlagBits::eCompute, }); + binding += num_bindings; } for (const auto& sampler : info->samplers) { bindings.push_back({ diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index 242c9b6f2..bc9ef571b 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -457,13 +457,15 @@ void GraphicsPipeline::BuildDescSetLayout(bool preloading) { }); } for (const auto& image : stage->images) { + const u32 num_bindings = image.NumBindings(*stage); bindings.push_back({ - .binding = binding++, + .binding = binding, .descriptorType = image.is_written ? vk::DescriptorType::eStorageImage : vk::DescriptorType::eSampledImage, - .descriptorCount = 1, + .descriptorCount = num_bindings, .stageFlags = stage_bit, }); + binding += num_bindings; } for (const auto& sampler : stage->samplers) { bindings.push_back({ diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 1b0af1d17..fdf6b3f2d 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -246,7 +246,7 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_, .support_fp32_denorm_flush = bool(vk12_props.shaderDenormFlushToZeroFloat32), .support_fp32_round_to_zero = bool(vk12_props.shaderRoundingModeRTZFloat32), .support_legacy_vertex_attributes = instance_.IsLegacyVertexAttributesSupported(), - .supports_image_load_store_lod = instance_.IsImageLoadStoreLodSupported(), + .supports_image_load_store_lod = /*instance_.IsImageLoadStoreLodSupported()*/ false, // TEST .supports_native_cube_calc = instance_.IsAmdGcnShaderSupported(), .supports_trinary_minmax = instance_.IsAmdShaderTrinaryMinMaxSupported(), // TODO: Emitted bounds checks cause problems with phi control flow; needs to be fixed. diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 737c9feed..80af19372 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -662,6 +662,13 @@ void Rasterizer::BindBuffers(const Shader::Info& stage, Shader::Backend::Binding void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindings& binding) { image_bindings.clear(); + const u32 first_image_idx = image_infos.size(); + // For loading/storing to explicit mip levels, when no native instruction support, bind an array + // of descriptors consecutively, 1 for each mip level. The shader can index this with LOD + // operand. + // This array holds the size of each consecutive array with the number of bindings consumed. + // This is currently always 1 for anything other than mip fallback arrays. + boost::container::small_vector image_descriptor_array_sizes; for (const auto& image_desc : stage.images) { const auto tsharp = image_desc.GetSharp(stage); @@ -671,25 +678,43 @@ void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindin if (tsharp.GetDataFmt() == AmdGpu::DataFormat::FormatInvalid) { image_bindings.emplace_back(std::piecewise_construct, std::tuple{}, std::tuple{}); + image_descriptor_array_sizes.push_back(1); continue; } - auto& [image_id, desc] = image_bindings.emplace_back(std::piecewise_construct, std::tuple{}, - std::tuple{tsharp, image_desc}); - image_id = texture_cache.FindImage(desc); - auto* image = &texture_cache.GetImage(image_id); - if (image->depth_id) { - // If this image has an associated depth image, it's a stencil attachment. - // Redirect the access to the actual depth-stencil buffer. - image_id = image->depth_id; - image = &texture_cache.GetImage(image_id); + const Shader::MipStorageFallbackMode mip_fallback_mode = image_desc.mip_fallback_mode; + const u32 num_bindings = image_desc.NumBindings(stage); + + for (auto i = 0; i < num_bindings; i++) { + auto& [image_id, desc] = image_bindings.emplace_back( + std::piecewise_construct, std::tuple{}, std::tuple{tsharp, image_desc}); + + if (mip_fallback_mode == Shader::MipStorageFallbackMode::ConstantIndex) { + ASSERT(num_bindings == 1); + desc.view_info.range.base.level += image_desc.constant_mip_index; + desc.view_info.range.extent.levels = 1; + } else if (mip_fallback_mode == Shader::MipStorageFallbackMode::DynamicIndex) { + desc.view_info.range.base.level += i; + desc.view_info.range.extent.levels = 1; + } + + image_id = texture_cache.FindImage(desc); + auto* image = &texture_cache.GetImage(image_id); + if (image->depth_id) { + // If this image has an associated depth image, it's a stencil attachment. + // Redirect the access to the actual depth-stencil buffer. + image_id = image->depth_id; + image = &texture_cache.GetImage(image_id); + } + if (image->binding.is_bound) { + // The image is already bound. In case if it is about to be used as storage we + // need to force general layout on it. + image->binding.force_general |= image_desc.is_written; + } + image->binding.is_bound = 1u; } - if (image->binding.is_bound) { - // The image is already bound. In case if it is about to be used as storage we need - // to force general layout on it. - image->binding.force_general |= image_desc.is_written; - } - image->binding.is_bound = 1u; + + image_descriptor_array_sizes.push_back(num_bindings); } // Second pass to re-bind images that were updated after binding @@ -749,16 +774,26 @@ void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindin image_infos.emplace_back(VK_NULL_HANDLE, *image_view.image_view, image.backing->state.layout); } + } + u32 image_info_idx = first_image_idx; + u32 image_binding_idx = 0; + for (u32 array_size : image_descriptor_array_sizes) { + const auto& [_, desc] = image_bindings[image_binding_idx]; + const bool is_storage = desc.type == VideoCore::TextureCache::BindingType::Storage; set_writes.push_back({ .dstSet = VK_NULL_HANDLE, - .dstBinding = binding.unified++, + .dstBinding = binding.unified, .dstArrayElement = 0, - .descriptorCount = 1, + .descriptorCount = array_size, .descriptorType = is_storage ? vk::DescriptorType::eStorageImage : vk::DescriptorType::eSampledImage, - .pImageInfo = &image_infos.back(), + .pImageInfo = &image_infos[image_info_idx], }); + + image_info_idx += array_size; + image_binding_idx += array_size; + binding.unified += array_size; } for (const auto& sampler : stage.samplers) {