From 79837b423dd035b34b2dc476e1d2dce8ae0d18bf Mon Sep 17 00:00:00 2001 From: nmzik Date: Thu, 22 Oct 2026 19:15:32 +0200 Subject: [PATCH] graphics: support compute stores to 2D MSAA images --- CMakeLists.txt | 2 + .../host_gpu/renderer/image/image.cpp | 3 +- .../host_gpu/renderer/image/imageView.h | 1 + .../renderer/pipeline/descriptors.cpp | 8 +- .../presentation/window/vulkanWindow.cpp | 7 ++ .../backend/spirv/spirvEmitterAnalysis.cpp | 14 ++- .../backend/spirv/spirvEmitterImage.cpp | 5 +- .../backend/spirv/spirvEmitterInternal.h | 3 +- src/graphics/shader/recompiler/ir/ShaderIR.h | 14 +-- tests/ResourceTrackingTests.cpp | 30 +++--- tests/ShaderRecompilerComputeTests.cpp | 96 +++++++++++++++++++ 11 files changed, 156 insertions(+), 27 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b1b00492c..5503e6e9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -680,6 +680,8 @@ if(BUILD_TESTING) COMMAND $ --wait-until-safe-for-rendering-only) add_test(NAME depth_bits COMMAND $ --depth-bits-only) + add_test(NAME msaa_storage + COMMAND $ --msaa-storage-only) add_test(NAME pm4_context_state COMMAND $ --context-state-only) add_test(NAME gpu_tiler diff --git a/src/graphics/host_gpu/renderer/image/image.cpp b/src/graphics/host_gpu/renderer/image/image.cpp index c2e0c9cdf..240fe0f14 100644 --- a/src/graphics/host_gpu/renderer/image/image.cpp +++ b/src/graphics/host_gpu/renderer/image/image.cpp @@ -90,7 +90,8 @@ ImageCreateFlags(const GraphicContext& graphics, const ImageInfo& info, vk::Imag if (HasFormatFeature(properties, vk::FormatFeatureFlagBits::eColorAttachment)) { usage |= vk::ImageUsageFlagBits::eColorAttachment; } - if (info.samples == 1) { + if (graphics.GetPhysicalDeviceProperties().limits.storageImageSampleCounts & + static_cast(info.samples)) { usage |= vk::ImageUsageFlagBits::eStorage; } return usage; diff --git a/src/graphics/host_gpu/renderer/image/imageView.h b/src/graphics/host_gpu/renderer/image/imageView.h index a246f9677..5242cdc96 100644 --- a/src/graphics/host_gpu/renderer/image/imageView.h +++ b/src/graphics/host_gpu/renderer/image/imageView.h @@ -101,6 +101,7 @@ IsSupportedStorageImageResource(const ShaderRecompiler::IR::ImageResource& resou (resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1D || resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1DArray || resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2D || + resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DMsaa || resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim3D || resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DArray) && ((resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::None && diff --git a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp index aed048652..fa91258d5 100644 --- a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp @@ -254,6 +254,10 @@ static bool IsSupportedStorageTextureDescriptor(const ShaderRecompiler::IR::Imag const bool is_3d = resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim3D && descriptor.Type() == Prospero::ImageType::kColor3D && descriptor.BaseArray5() == 0; + const bool is_2d_msaa = + resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DMsaa && + descriptor.Type() == Prospero::ImageType::kColor2DMsaa && descriptor.Depth() == 0 && + descriptor.BaseArray5() == 0; TileTextureBlockLayout tile_layout {}; bool supported_tile = false; switch (tile) { @@ -284,8 +288,8 @@ static bool IsSupportedStorageTextureDescriptor(const ShaderRecompiler::IR::Imag const bool supported_swizzle = IsValidImageSwizzle(swizzle) && (swizzle == DstSel(4, 5, 6, 7) || !resource.read || resource.atomic); - return (is_1d || is_1d_array || is_2d || is_2d_array || is_3d) && supported_tile && - descriptor.BaseLevel() <= descriptor.LastLevel() && + return (is_1d || is_1d_array || is_2d || is_2d_array || is_3d || is_2d_msaa) && + supported_tile && descriptor.BaseLevel() <= descriptor.LastLevel() && descriptor.MinLod() == 0 && supported_swizzle && descriptor.BCSwizzle() == 0 && !descriptor.MsaaDepth(); } diff --git a/src/graphics/presentation/window/vulkanWindow.cpp b/src/graphics/presentation/window/vulkanWindow.cpp index 14f19c0e3..f3a6a0ef8 100644 --- a/src/graphics/presentation/window/vulkanWindow.cpp +++ b/src/graphics/presentation/window/vulkanWindow.cpp @@ -609,6 +609,8 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics, device_features.depthClamp = VK_TRUE; #endif device_features.shaderStorageImageWriteWithoutFormat = VK_TRUE; + device_features.shaderStorageImageMultisample = + supported_features2.features.shaderStorageImageMultisample; device_features.shaderImageGatherExtended = VK_TRUE; device_features.independentBlend = VK_TRUE; device_features.dualSrcBlend = VK_TRUE; @@ -623,6 +625,11 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics, device_features.multiViewport = VK_TRUE; device_features.fillModeNonSolid = VK_TRUE; device_features.vertexPipelineStoresAndAtomics = VK_TRUE; + // Storage image sample counts beyond 1x require the feature. + if (!device_features.shaderStorageImageMultisample) { + graphics.physical_device_properties.limits.storageImageSampleCounts = + vk::SampleCountFlagBits::e1; + } graphics.sample_rate_shading_enabled = true; device_features.shaderInt64 = VK_TRUE; device_features.shaderFloat64 = diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp index 91c47e1dc..74196f266 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp @@ -121,6 +121,9 @@ uint32_t ImageType(EmitterState& state, const IR::ImageResource& image) { EXIT("invalid image resource class"); } const auto& info = ImageDimensionInfoFor(image.dimension); + if (sampled == 2 && info.multisampled != 0u) { + state.builder.RequireCapability(spv::CapabilityStorageImageMultisample); + } const auto scalar_type = image.atomic64 ? TypeU64(state) : ImageScalarType(state, image.numeric_class); return state.builder.Type(spv::OpTypeImage, scalar_type, @@ -213,15 +216,20 @@ uint32_t ImageDescriptorPointer(EmitterState& state, uint32_t resource, uint32_t } void EmitStorageImageWrite(EmitterState& state, uint32_t resource, uint32_t mip_lod, uint32_t coord, - uint32_t texel, uint32_t array_index) { + uint32_t texel, uint32_t array_index, uint32_t sample) { const auto& image = state.program.info.images.at(resource); EXIT_IF(image.resource_class != IR::ImageResourceClass::Storage); if (!image.atomic) { state.builder.RequireCapability(spv::CapabilityStorageImageWriteWithoutFormat); } const auto EmitWrite = [&](uint32_t mip) { - state.builder.AddFunction(spv::OpImageWrite, LoadImageDescriptor(state, resource, mip, array_index), - coord, texel); + const auto descriptor = LoadImageDescriptor(state, resource, mip, array_index); + if (sample == 0u) { + state.builder.AddFunction(spv::OpImageWrite, descriptor, coord, texel); + } else { + state.builder.AddFunction(spv::OpImageWrite, descriptor, coord, texel, + spv::ImageOperandsSampleMask, sample); + } }; if (image.mip_mode != IR::ImageMipMode::Dynamic) { EmitWrite(0); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index 66196c3cf..af6fc72c8 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -941,9 +941,12 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { const auto mip_lod = candidate.mip_mode == IR::ImageMipMode::Dynamic ? LodU32(ctx, mem, *address, mem.image_dimension) : 0u; const auto coord = CoordU32(ctx, mem, *address, candidate.dimension); + const auto& dimension_info = ImageDimensionInfoFor(candidate.dimension); + const auto sample = dimension_info.multisampled != 0u + ? AddressU32(ctx, mem, *address, dimension_info.coordinate_components) : 0u; const auto texel = StoreTexel(ctx, selected, ctx.Arg(inst, 2), candidate.numeric_class == Prospero::TextureNumericClass::Uint); - EmitStorageImageWrite(state, resource, mip_lod, coord, texel, array_index); + EmitStorageImageWrite(state, resource, mip_lod, coord, texel, array_index, sample); }); }); return; diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h index 293ab7098..15fd29009 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h @@ -339,8 +339,9 @@ uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampl uint32_t mip = 0, uint32_t array_index = 0, bool sampler_dynamic = false); +// sample is 0 (no id) for single-sampled images. void EmitStorageImageWrite(EmitterState& state, uint32_t resource, uint32_t mip_lod, uint32_t coord, - uint32_t texel, uint32_t array_index); + uint32_t texel, uint32_t array_index, uint32_t sample); spv::ExecutionModel ExecutionModelForStage(ShaderType stage); diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index 696f9f812..a65ab353d 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -343,7 +343,7 @@ struct StageOutput { inline constexpr uint32_t FirstImageBinding = 1u; inline constexpr uint32_t FirstComparisonImageBinding = 22u; inline constexpr uint32_t FirstStorageImageBinding = 29u; -inline constexpr uint32_t ImageBindingCount = 48u; +inline constexpr uint32_t ImageBindingCount = 50u; enum class DescriptorBindingKind : uint32_t { Buffers = 0u, @@ -357,8 +357,8 @@ enum class DescriptorBindingKind : uint32_t { Count, }; -static_assert(static_cast(DescriptorBindingKind::Samplers) == 49u); -static_assert(static_cast(DescriptorBindingKind::Count) == 56u); +static_assert(static_cast(DescriptorBindingKind::Samplers) == 51u); +static_assert(static_cast(DescriptorBindingKind::Count) == 58u); struct PushData { static constexpr uint32_t DwordCount = 32; @@ -419,8 +419,8 @@ DescriptorBindingForImage(const ImageResource& image) { constexpr uint32_t SampledUintBinding = 8u; constexpr uint32_t SampledSintBinding = 15u; constexpr uint32_t StorageFloatBinding = FirstStorageImageBinding; - constexpr uint32_t StorageUintBinding = StorageFloatBinding + 5u; - constexpr uint32_t AtomicUintBinding = StorageUintBinding + 5u; + constexpr uint32_t StorageUintBinding = StorageFloatBinding + 6u; + constexpr uint32_t AtomicUintBinding = StorageUintBinding + 6u; uint32_t base = 0; bool sampled = false; @@ -467,10 +467,10 @@ DescriptorBindingForImage(const ImageResource& image) { case Decoder::ImageDimension::Dim2D: dimension = 2u; break; case Decoder::ImageDimension::Dim2DArray: dimension = 3u; break; case Decoder::ImageDimension::Dim2DMsaa: - if (!sampled) { + if (image.atomic) { return std::nullopt; } - dimension = 4u; + dimension = sampled ? 4u : 5u; break; case Decoder::ImageDimension::Dim2DMsaaArray: if (!sampled) { diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 75fb86f29..c8de765a9 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -8547,16 +8547,16 @@ void TestShaderInfoCollection() { void TestImageBindingAbi() { using NumericClass = Libs::Graphics::Prospero::TextureNumericClass; - Check(ImageBindingCount == 48u && + Check(ImageBindingCount == 50u && static_cast(DescriptorBindingKind::Buffers) == 0u && - static_cast(DescriptorBindingKind::Samplers) == 49u && - static_cast(DescriptorBindingKind::Gds) == 50u && - static_cast(DescriptorBindingKind::BdaPagetable) == 51u && - static_cast(DescriptorBindingKind::FaultBuffer) == 52u && - static_cast(DescriptorBindingKind::FlattenedSrt) == 53u && - static_cast(DescriptorBindingKind::ShaderData) == 54u && - static_cast(DescriptorBindingKind::SharedMemory) == 55u && - static_cast(DescriptorBindingKind::Count) == 56u, + static_cast(DescriptorBindingKind::Samplers) == 51u && + static_cast(DescriptorBindingKind::Gds) == 52u && + static_cast(DescriptorBindingKind::BdaPagetable) == 53u && + static_cast(DescriptorBindingKind::FaultBuffer) == 54u && + static_cast(DescriptorBindingKind::FlattenedSrt) == 55u && + static_cast(DescriptorBindingKind::ShaderData) == 56u && + static_cast(DescriptorBindingKind::SharedMemory) == 57u && + static_cast(DescriptorBindingKind::Count) == 58u, "native descriptor binding anchors changed"); const std::array sampled_dimensions{ @@ -8615,6 +8615,8 @@ void TestImageBindingAbi() { CheckBinding(ImageResourceClass::Storage, numeric_class, dimension, false); } + CheckBinding(ImageResourceClass::Storage, numeric_class, + Decoder::ImageDimension::Dim2DMsaa, false); } for (const auto dimension : storage_dimensions) { CheckBinding(ImageResourceClass::Storage, NumericClass::Uint, dimension, @@ -8658,11 +8660,15 @@ void TestImageBindingAbi() { image.numeric_class = NumericClass::Sint; Check(Invalid(image), "signed storage image received a descriptor binding"); image.numeric_class = NumericClass::Float; - image.dimension = Decoder::ImageDimension::Dim2DMsaa; + image.dimension = Decoder::ImageDimension::Dim2DMsaaArray; Check(Invalid(image), - "multisampled storage image received a descriptor binding"); - image.dimension = Decoder::ImageDimension::Dim2D; + "multisampled storage array received a descriptor binding"); + image.dimension = Decoder::ImageDimension::Dim2DMsaa; + image.numeric_class = NumericClass::Uint; image.atomic = true; + Check(Invalid(image), "multisampled atomic image received a descriptor binding"); + image.numeric_class = NumericClass::Float; + image.dimension = Decoder::ImageDimension::Dim2D; Check(Invalid(image), "float atomic image received a descriptor binding"); } diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 1650bcb0e..c7606febd 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -12595,6 +12595,95 @@ public: std::printf("[gpu] %-32s ok\n", name); } + // Cyberpunk 2077 clears a rect of an MSAA color target from compute, one sample per store. + void CheckMsaaStorageClear() { + constexpr const char *name = "MsaaStorageClear"; + constexpr uintptr_t base = 0x0000000205000000ull; + constexpr uint64_t allocation_size = 0x10000; + // Captured CS 0xad6c86aca9c0916e: s[8:11] color, s[12:13] origin, s14 width, + // s15 height << 16 | samples. + std::vector clear{0xbfa00001u, 0xd7460001u, 0x04050611u, 0xd7460000u, 0x04010610u, + 0x7da21ef9u, 0x85060001u, 0x7da8000eu, 0xbf88000fu, 0x4a08000cu, + 0x4a0a020du, 0x996b800fu, 0xbeea0380u, 0xbf0a6b6au, 0xbf840009u, + 0x7e0c026au, 0x7e000208u, 0x7e020209u, 0x7e04020au, 0x7e06020bu, + 0x816a816au, 0xf0200f30u, 0x00000004u, 0xbf82fff5u, 0xbf810000u}; + ShaderMapUserData(reinterpret_cast(clear.data()), + {.type = Prospero::ShaderBinaryType::kCs, + .code_size_bytes = static_cast(clear.size() * sizeof(u32))}); + EnsureRuntimeContext(); + int64_t direct_offset = -1; + Require(name, "direct allocation", + Libs::LibKernel::Memory::KernelAllocateDirectMemory( + 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), + allocation_size, 0x10000, 0, &direct_offset) == 0, + "MSAA allocation failed"); + void *mapped = reinterpret_cast(base); + Require(name, "direct mapping", + Libs::LibKernel::Memory::KernelMapDirectMemory( + &mapped, allocation_size, 0x3, 0x10, direct_offset, 0x10000) == 0 && + mapped == reinterpret_cast(base), + "MSAA mapping failed"); + RenderContext context(m_runtime_context); + context.InitializeGpu(nullptr); + LibKernel::Memory::InstallGpuResources(&context); + context.GetGpu().SendCommandSync([&] { + CommandProcessor processor(context, 0); + processor.BufferInit(); + auto &scheduler = context.GetCommandScheduler(); + auto &textures = context.GetTextureCache(); + auto &shaders = processor.GetShCtx(); + context.MapMemory(base, allocation_size); + // 8x8 4x MSAA RGBA8: LAST_LEVEL and MAX_MIP hold log2(samples). + const ShaderTextureResource target{{static_cast(base >> 8u), + (static_cast(Prospero::BufferFormat::k8_8_8_8UNorm) << 20u) | (3u << 30u), + 1u | (7u << 14u), + DstSel(4, 5, 6, 7) | (2u << 16u) | + (static_cast(Prospero::TileMode::kRenderTarget) << 20u) | + (static_cast(Prospero::ImageType::kColor2DMsaa) << 28u), + 0, 0x00700020u, 0, 0}}; + ShaderRecompiler::IR::DescriptorValue value{}; + value.dword_count = 8; + std::copy_n(target.fields, 8, value.dwords.begin()); + ShaderRecompiler::IR::ImageResource resource{}; + resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage; + resource.numeric_class = Prospero::TextureNumericClass::Float; + resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2DMsaa; + resource.written = true; + const auto binding = + RenderExecutorTestAccess::ResolveTexture(context.GetRenderExecutor(), resource, value); + TextureCacheTestAccess::ClearImage(textures, scheduler.Current(), binding.image_id, + {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, {}); + shaders.SetCsShader({.data_addr = reinterpret_cast(clear.data()), + .num_thread_x = 8, .num_thread_y = 8, .num_thread_z = 1, + .wave_size = 64, .user_sgpr = 16, .tgid_x_en = true, + .tgid_y_en = true, .tidig_comp_cnt = 1}); + const std::array args{std::bit_cast(1.0f), 0, 0, std::bit_cast(1.0f), + 1, 1, 2, (2u << 16u) | 4u}; + for (u32 i = 0; i < 8; i++) { + shaders.SetCsUserSgpr(i, target.fields[i], HW::UserSgprType::Unknown); + shaders.SetCsUserSgpr(8u + i, args[i], HW::UserSgprType::Unknown); + } + processor.DispatchDirect(1, 1, 1, 0x41u); + for (const int row : {1, 2, 3}) { + const u32 inside = row == 3 ? 0u : 0xff0000ffu; + Require(name, "every sample in the rect", + ReadCachedTexel(name, context, binding.image_id, {0, row, 0}, {4, 1, 1}) == + std::vector{0, inside, inside, 0}, + "the compute clear missed samples or wrote outside its rect"); + } + context.UnmapMemory(base, allocation_size); + scheduler.Finish(); + }); + context.ShutdownGpu(); + LibKernel::Memory::InstallGpuResources(nullptr); + Require(name, "unmap", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0, + "MSAA mapping release failed"); + Require(name, "release", + Libs::LibKernel::Memory::KernelReleaseDirectMemory(direct_offset, allocation_size) == 0, + "MSAA allocation release failed"); + std::printf("[gpu] %-32s ok\n", name); + } + void CheckSampledDccClear() { constexpr const char *name = "SampledDccClear"; constexpr uintptr_t base = 0x0000000205000000ull; @@ -21003,6 +21092,7 @@ private: device_info.pNext = &conditional_rendering; vk::PhysicalDeviceFeatures device_features{}; device_features.shaderStorageImageWriteWithoutFormat = true; + device_features.shaderStorageImageMultisample = available_features.shaderStorageImageMultisample; device_features.shaderImageGatherExtended = true; device_features.sampleRateShading = true; device_features.shaderInt64 = true; @@ -49870,6 +49960,11 @@ int main(int argc, char **argv) { RunCase(&vulkan, ImageSamplePacked8()); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--msaa-storage-only") == 0) { + VulkanHarness vulkan; + vulkan.CheckMsaaStorageClear(); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--depth-bits-only") == 0) { VulkanHarness vulkan; vulkan.CheckDepthBitsCopy(); @@ -50768,6 +50863,7 @@ int main(int argc, char **argv) { vulkan.CheckRenderExecutorColorMetadataClear(); vulkan.CheckSampledDccClear(); vulkan.CheckDepthBitsCopy(); + vulkan.CheckMsaaStorageClear(); vulkan.CheckRenderExecutorColorStandardTileDiscovery(); vulkan.CheckRenderExecutorColorDepthTileDiscovery(); vulkan.CheckRenderExecutorStencilBindingDiscovery();