graphics: support compute stores to 2D MSAA images

This commit is contained in:
nmzik
2026-10-22 19:15:32 +02:00
parent aafe014d0c
commit 79837b423d
11 changed files with 156 additions and 27 deletions
+2
View File
@@ -680,6 +680,8 @@ if(BUILD_TESTING)
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --wait-until-safe-for-rendering-only)
add_test(NAME depth_bits
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --depth-bits-only)
add_test(NAME msaa_storage
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --msaa-storage-only)
add_test(NAME pm4_context_state
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --context-state-only)
add_test(NAME gpu_tiler
@@ -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<vk::SampleCountFlagBits>(info.samples)) {
usage |= vk::ImageUsageFlagBits::eStorage;
}
return usage;
@@ -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 &&
@@ -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();
}
@@ -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 =
@@ -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);
@@ -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;
@@ -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);
+7 -7
View File
@@ -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<uint32_t>(DescriptorBindingKind::Samplers) == 49u);
static_assert(static_cast<uint32_t>(DescriptorBindingKind::Count) == 56u);
static_assert(static_cast<uint32_t>(DescriptorBindingKind::Samplers) == 51u);
static_assert(static_cast<uint32_t>(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) {
+18 -12
View File
@@ -8547,16 +8547,16 @@ void TestShaderInfoCollection() {
void TestImageBindingAbi() {
using NumericClass = Libs::Graphics::Prospero::TextureNumericClass;
Check(ImageBindingCount == 48u &&
Check(ImageBindingCount == 50u &&
static_cast<uint32_t>(DescriptorBindingKind::Buffers) == 0u &&
static_cast<uint32_t>(DescriptorBindingKind::Samplers) == 49u &&
static_cast<uint32_t>(DescriptorBindingKind::Gds) == 50u &&
static_cast<uint32_t>(DescriptorBindingKind::BdaPagetable) == 51u &&
static_cast<uint32_t>(DescriptorBindingKind::FaultBuffer) == 52u &&
static_cast<uint32_t>(DescriptorBindingKind::FlattenedSrt) == 53u &&
static_cast<uint32_t>(DescriptorBindingKind::ShaderData) == 54u &&
static_cast<uint32_t>(DescriptorBindingKind::SharedMemory) == 55u &&
static_cast<uint32_t>(DescriptorBindingKind::Count) == 56u,
static_cast<uint32_t>(DescriptorBindingKind::Samplers) == 51u &&
static_cast<uint32_t>(DescriptorBindingKind::Gds) == 52u &&
static_cast<uint32_t>(DescriptorBindingKind::BdaPagetable) == 53u &&
static_cast<uint32_t>(DescriptorBindingKind::FaultBuffer) == 54u &&
static_cast<uint32_t>(DescriptorBindingKind::FlattenedSrt) == 55u &&
static_cast<uint32_t>(DescriptorBindingKind::ShaderData) == 56u &&
static_cast<uint32_t>(DescriptorBindingKind::SharedMemory) == 57u &&
static_cast<uint32_t>(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");
}
+96
View File
@@ -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<u32> 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<uint64_t>(clear.data()),
{.type = Prospero::ShaderBinaryType::kCs,
.code_size_bytes = static_cast<u32>(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<void *>(base);
Require(name, "direct mapping",
Libs::LibKernel::Memory::KernelMapDirectMemory(
&mapped, allocation_size, 0x3, 0x10, direct_offset, 0x10000) == 0 &&
mapped == reinterpret_cast<void *>(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<u32>(base >> 8u),
(static_cast<u32>(Prospero::BufferFormat::k8_8_8_8UNorm) << 20u) | (3u << 30u),
1u | (7u << 14u),
DstSel(4, 5, 6, 7) | (2u << 16u) |
(static_cast<u32>(Prospero::TileMode::kRenderTarget) << 20u) |
(static_cast<u32>(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<uint64_t>(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<u32, 8> args{std::bit_cast<u32>(1.0f), 0, 0, std::bit_cast<u32>(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<u32>{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<false, true>());
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();