graphics: share signed storage conversion and support R16 image writes

Co-authored-by: fxpw <fxpw@outlook.com>
This commit is contained in:
nmzik
2026-10-14 15:36:29 +02:00
parent f3c017b168
commit c88f05f7b5
7 changed files with 227 additions and 45 deletions
@@ -308,9 +308,8 @@ void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource,
const bool descriptor_ok = IsSupportedStorageTextureDescriptor(resource, descriptor);
const bool encoding_ok = IsSupportedStorageTextureEncoding(resource, descriptor);
const bool uint_resource = resource.numeric_class == Prospero::TextureNumericClass::Uint;
const bool raw_sint_storage = format == Prospero::BufferFormat::k32SInt && uint_resource &&
resource.written &&
(resource.atomic ? !resource.atomic64 : !resource.read);
const bool raw_sint_storage = uint_resource &&
ShaderRecompiler::IR::RawSignedStorageFormat(resource, format) != Prospero::BufferFormat::kInvalid;
const auto numeric_class =
Prospero::SampledTextureNumericClass(Prospero::RemapTextureFormat(format));
const bool raw_float_atomic = format == Prospero::BufferFormat::k32Float && uint_resource &&
@@ -588,10 +587,11 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
pixel_format = depth_format->depth_attachment_format;
}
}
const auto raw_sint_format = ShaderRecompiler::IR::RawSignedStorageFormat(resource, format);
const auto storage_view_format = resource.atomic64 ? vk::Format::eR64Uint
: storage && (resource.atomic ||
format == Prospero::BufferFormat::k32SInt)
? vk::Format::eR32Uint
: storage && resource.atomic ? vk::Format::eR32Uint
: raw_sint_format != Prospero::BufferFormat::kInvalid
? VulkanFormat(raw_sint_format)
: SrgbStorageViewFormat(pixel_format);
const auto view_format = storage && storage_view_format != vk::Format::eUndefined
? storage_view_format
@@ -385,9 +385,10 @@ Format::BufferFormatInfo ImageConversionFormat(const EmitterState& state,
const auto format = image.conversion_format;
if (format == Prospero::BufferFormat::kInvalid) return {};
const auto info = Format::GetFormatInfo(format);
EXIT_IF(Prospero::RemapTextureFormat(format) == format || info.component_count == 0u ||
const bool raw_signed = IR::RawSignedStorageFormat(image, format) != Prospero::BufferFormat::kInvalid;
EXIT_IF((Prospero::RemapTextureFormat(format) == format && !raw_signed) || info.component_count == 0u ||
info.component_count > 4u);
EXIT_IF(info.type != Format::ComponentType::Uscaled &&
EXIT_IF(!raw_signed && info.type != Format::ComponentType::Uscaled &&
((info.type != Format::ComponentType::Uint &&
info.type != Format::ComponentType::Unorm) || !info.packed_bitfield ||
info.byte_size != sizeof(uint32_t)));
@@ -610,15 +611,16 @@ uint32_t EmitOneDimensionalGatherLz(ValueEmitContext& ctx, const IR::MemoryInfo&
uint32_t StoreTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t data, bool integer) {
const auto swizzle = ctx.state.program.info.images[mem.resource].shader_swizzle;
const auto info = ImageConversionFormat(ctx.state, mem);
const bool packed = info.format != Prospero::BufferFormat::kInvalid;
EXIT_IF(packed && (!info.packed_bitfield ||
(info.type != Format::ComponentType::Uint &&
info.type != Format::ComponentType::Unorm)));
const bool converted = info.format != Prospero::BufferFormat::kInvalid;
const bool packed = info.packed_bitfield;
EXIT_IF(converted && info.type != Format::ComponentType::Sint &&
(!packed || (info.type != Format::ComponentType::Uint &&
info.type != Format::ComponentType::Unorm)));
const auto zero = ConstantU32(ctx.state, 0);
uint32_t values[4] {zero, zero, zero, zero};
auto packed_value = zero;
const auto dmask = mem.dmask != 0u ? mem.dmask : 1u;
for (uint32_t component = 0; component < (packed ? info.component_count : 4u); component++) {
for (uint32_t component = 0; component < (converted ? info.component_count : 4u); component++) {
const auto source = InverseSwizzle(swizzle, component);
const bool selected = source < 4u && ((dmask >> source) & 1u) != 0u;
uint32_t raw = zero;
@@ -638,9 +640,13 @@ uint32_t StoreTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t d
}
}
}
if (packed) {
if (converted) {
if (!selected) continue;
auto value = EncodeFormattedStoreComponent(ctx.state, info, component, raw, mem.data_bits);
if (!packed) {
values[component] = value;
continue;
}
if (info.type == Format::ComponentType::Uint) {
value = EmitUMin32(ctx.state, value,
ConstantU32(ctx.state, (1u << info.component_bits[component]) - 1u));
@@ -435,6 +435,13 @@ uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::Buffer
data = EmitBitCastU32F32(state, EmitF16BitsToF32(state, data));
}
}
if (info.type == Format::ComponentType::Sint && bits < 32u) {
const auto maximum = (1u << (bits - 1u)) - 1u;
data = EmitSMax32(state, data, ConstantU32(state, ~maximum));
data = EmitSMin32(state, data, ConstantU32(state, maximum));
return Binary(state, spv::OpBitwiseAnd, TypeU32(state), data,
ConstantU32(state, (1u << bits) - 1u));
}
const bool normalized = info.type == Format::ComponentType::Unorm ||
info.type == Format::ComponentType::Snorm;
const bool scaled = info.type == Format::ComponentType::Uscaled ||
@@ -169,6 +169,17 @@ struct ImageResource {
bool operator==(const ImageResource& other) const = default;
};
[[nodiscard]] constexpr Prospero::BufferFormat
RawSignedStorageFormat(const ImageResource& image, Prospero::BufferFormat format) {
if (image.resource_class != ImageResourceClass::Storage || !image.written)
return Prospero::BufferFormat::kInvalid;
if (format == Prospero::BufferFormat::k32SInt && (image.atomic ? !image.atomic64 : !image.read))
return Prospero::BufferFormat::k32UInt;
if (format == Prospero::BufferFormat::k16SInt && !image.read && !image.atomic)
return Prospero::BufferFormat::k16UInt;
return Prospero::BufferFormat::kInvalid;
}
struct SamplerResource {
uint32_t source = 0;
uint32_t first_use_pc = 0;
@@ -755,8 +755,9 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
if (storage || image.conversion_format != Prospero::BufferFormat::kInvalid) {
image.shader_swizzle = DescriptorImageSwizzle(descriptor);
}
const bool raw_sint_storage = storage && format == Prospero::BufferFormat::k32SInt &&
base.written && (base.atomic ? !base.atomic64 : !base.read);
const auto raw_sint_format = RawSignedStorageFormat(base, format);
const bool raw_sint_storage = raw_sint_format != Prospero::BufferFormat::kInvalid;
if (raw_sint_format == Prospero::BufferFormat::k16UInt) image.conversion_format = format;
image.numeric_class = Prospero::SampledTextureNumericClass(
Prospero::RemapTextureFormat(format));
if (!storage && format == Prospero::BufferFormat::k11_11_10UNorm) {
+77
View File
@@ -1164,6 +1164,82 @@ void TestMixedSamplerVariantsShareRuntimeDescriptor() {
"sampler variants retained stale or duplicated descriptors after refresh");
}
void TestSignedStorageSpecialization() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
using F = Libs::Graphics::Prospero::BufferFormat;
using N = Libs::Graphics::Prospero::TextureNumericClass;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
program.srt_plan_complete = program.resource_tracking_complete = true;
program.user_data_count = 1u;
auto &block = AddValueBlock(program);
DescriptorSource source;
source.dword_count = 8u;
source.dwords.fill(Value(0u));
source.dwords[0] = Value(0x100u);
source.dwords[1] = Value(&block.AppendNewInst(ValueOpcode::GetUserData, {Value(ScalarReg(0))}));
constexpr auto swizzle = Libs::Graphics::DstSel(5, 4, 1, 0);
source.dwords[3] = Value(swizzle |
(uint32_t(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u));
program.descriptor_sources.push_back(source);
program.info.images.push_back({.source = 0u, .resource_class = ImageResourceClass::Storage,
.dimension = Libs::Graphics::ShaderRecompiler::Decoder::ImageDimension::Dim2D, .written = true});
auto plan = ExtractResourcePlan(program);
std::array<uint32_t, 1> data{};
const SrtRuntime runtime{.user_data = data};
ResourceSnapshot snapshot;
ResourceSpecialization specialization, signed16;
for (const auto format : {F::k16SInt, F::k32SInt, F::k16SInt}) {
data[0] = uint32_t(format) << 20u;
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"write-only signed storage image failed materialization");
const auto conversion = format == F::k16SInt ? format : F::kInvalid;
const auto view = format == F::k16SInt ? F::k16UInt : F::k32UInt;
Check(specialization.images[0].numeric_class == N::Uint &&
specialization.images[0].conversion_format == conversion &&
specialization.images[0].shader_swizzle == swizzle &&
snapshot.images[0].dwords[1] == data[0] &&
RawSignedStorageFormat(plan.info.images[0], format) == view,
"signed storage specialization lost guest format, conversion, swizzle or raw view width");
if (format == F::k16SInt) {
if (signed16.images.empty()) signed16 = specialization;
else Check(specialization == signed16, "signed storage format refresh retained a stale specialization");
} else {
Check(specialization != signed16, "signed 16/32-bit stores shared incompatible conversion state");
}
ApplyResourceSpecialization(program, specialization);
const auto binding = DescriptorBindingForImage(program.info.images[0]);
Check(binding && ImageBindingResourceClass(*binding) == ImageResourceClass::Storage,
"signed storage conversion did not reuse the unsigned storage binding");
}
for (const auto format : {F::k16SInt, F::k32SInt}) {
data[0] = uint32_t(format) << 20u;
auto &image = plan.info.images[0];
image.read = true;
Check(RawSignedStorageFormat(image, format) == F::kInvalid &&
!MaterializeResources(plan, runtime, snapshot, specialization),
"signed raw storage view accepted a non-atomic read/write image");
image.atomic = true;
Check((RawSignedStorageFormat(image, format) != F::kInvalid) == (format == F::k32SInt) &&
MaterializeResources(plan, runtime, snapshot, specialization) == (format == F::k32SInt),
"signed storage conversion changed atomic format eligibility");
image.atomic64 = true;
Check(RawSignedStorageFormat(image, format) == F::kInvalid &&
!MaterializeResources(plan, runtime, snapshot, specialization),
"signed 16/32-bit image acquired a 64-bit atomic raw view");
image.resource_class = ImageResourceClass::Sampled;
image.written = image.atomic = image.atomic64 = false;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
specialization.images[0].numeric_class == N::Sint &&
specialization.images[0].conversion_format == F::kInvalid &&
RawSignedStorageFormat(image, format) == F::kInvalid,
"storage conversion changed ordinary signed image reads");
image.resource_class = ImageResourceClass::Storage;
image.written = true;
image.read = false;
}
}
void TestIndirectImageSamplerClasses(bool mixed) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
using Libs::Graphics::Prospero::BufferFormat;
@@ -1256,6 +1332,7 @@ int main() {
TestCyclicControlPredicatesRemainUnknown();
TestEmptyScalarBufferSpecialization();
TestMixedSamplerVariantsShareRuntimeDescriptor();
TestSignedStorageSpecialization();
for (const bool mixed : {false, true}) TestIndirectImageSamplerClasses(mixed);
std::puts("ResourceMaterializationTests: all cases passed");
return 0;
+109 -29
View File
@@ -37,6 +37,7 @@
#include "graphics/host_gpu/renderer/renderTarget.h"
#include "graphics/host_gpu/renderer/sync.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include <vulkan/vulkan_format_traits.hpp>
#include "graphics/presentation/window/windowInternal.h"
#include "graphics/shader/recompiler/BufferFormat.h"
#include "graphics/shader/recompiler/ShaderRecompiler.h"
@@ -2009,7 +2010,6 @@ public:
u32 height = 0;
u32 layers = 1;
u32 mip_levels = 1;
u32 dwords_per_pixel = 0;
};
[[nodiscard]] vk::Device Device() const { return m_device; }
@@ -15749,11 +15749,12 @@ public:
return std::max(value, 1u);
}
static size_t ImageMipDwordCount(u32 width, u32 height, u32 dwords_per_pixel,
static size_t ImageMipDwordCount(u32 width, u32 height, vk::Format format,
u32 level, u32 layers = 1) {
return static_cast<size_t>(MipExtent(width, level)) *
static_cast<size_t>(MipExtent(height, level)) * layers *
dwords_per_pixel;
const auto block = vk::blockExtent(format);
const auto blocks_x = (MipExtent(width, level) + block[0] - 1u) / block[0];
const auto blocks_y = (MipExtent(height, level) + block[1] - 1u) / block[1];
return (size_t{blocks_x} * blocks_y * layers * vk::blockSize(format) + 3u) / 4u;
}
Image CreateImageMips(const char *shader_name, u32 width, u32 height,
@@ -15772,7 +15773,8 @@ public:
ret.height = height;
ret.layers = layers;
ret.mip_levels = std::max<u32>(static_cast<u32>(initial_mips.size()), 1u);
ret.dwords_per_pixel = dwords_per_pixel;
Require(shader_name, "image format size", dwords_per_pixel == (vk::blockSize(format) + 3u) / 4u,
"image fixture word size disagrees with its native format");
vk::ImageCreateInfo image_info{};
image_info.sType = vk::StructureType::eImageCreateInfo;
@@ -15845,13 +15847,13 @@ public:
size_t total_dwords = 0;
for (u32 level = 0; level < ret.mip_levels; level++) {
total_dwords +=
ImageMipDwordCount(width, height, dwords_per_pixel, level, layers);
ImageMipDwordCount(width, height, format, level, layers);
}
std::vector<u32> contents(total_dwords, 0);
size_t offset = 0;
for (u32 level = 0; level < ret.mip_levels; level++) {
const auto level_dwords =
ImageMipDwordCount(width, height, dwords_per_pixel, level, layers);
ImageMipDwordCount(width, height, format, level, layers);
const auto &src = initial_mips[level];
for (size_t i = 0; i < src.size() && i < level_dwords; i++) {
contents[offset + i] = src[i];
@@ -15891,9 +15893,8 @@ public:
}
std::vector<u32> ReadImage(const char *shader_name, Image *image) {
const auto dword_count = static_cast<size_t>(image->width) *
static_cast<size_t>(image->height) *
image->layers * image->dwords_per_pixel;
const auto dword_count = ImageMipDwordCount(image->width, image->height,
image->format, 0, image->layers);
auto staging = CreateHostBuffer(shader_name, dword_count * sizeof(u32),
vk::BufferUsageFlagBits::eTransferDst, {});
@@ -19549,7 +19550,7 @@ private:
copies.push_back(copy);
offset += static_cast<vk::DeviceSize>(
ImageMipDwordCount(image->width, image->height,
image->dwords_per_pixel, level, image->layers) *
image->format, level, image->layers) *
sizeof(u32));
}
cmd.copyBufferToImage(staging, image->image,
@@ -31882,23 +31883,23 @@ TestCase TBufferLoadFormatXSintSignExtendsSubDword() {
TestCase TBufferStoreFormatXSintWritesSubDword() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovU32(&code, 20, 0);
AppendVMovLiteral(&code, 0, 0xffffff80u);
code.push_back(EncodeMtbuf0(0x04, 6, 0));
code.push_back(EncodeMtbuf1(0x04, 0, 0, 20));
AppendVMovU32(&code, 20, 4);
AppendVMovLiteral(&code, 1, 0xffff8001u);
code.push_back(EncodeMtbuf0(0x04, 12, 0));
code.push_back(EncodeMtbuf1(0x04, 1, 0, 20));
AppendEnd(&code);
return {"TBufferStoreFormatXSintWritesSubDword",
code,
{0x11223344u, 0x55667788u},
{0x11223380u, 0x55668001u},
{O::V_MOV_B32, O::TBUFFER_STORE_FORMAT_X, O::S_ENDPGM}};
TestCase test;
test.name = "TBufferStoreFormatXSintWritesSubDword";
constexpr std::array<u32, 6> inputs{0xffffff80u, 0xffffff7fu, 128u,
0xffff8001u, 0xffff7fffu, 32768u};
constexpr std::array<u32, 6> encoded{0x80u, 0x80u, 0x7fu, 0x8001u, 0x8000u, 0x7fffu};
test.initial.assign(inputs.size(), 0x11223344u);
test.expected = test.initial;
for (u32 index = 0; index < inputs.size(); ++index) {
AppendVMovU32(&test.code, 20, index * 4u);
AppendVMovLiteral(&test.code, 0, inputs[index]);
test.code.push_back(EncodeMtbuf0(0x04, index < 3u ? 6u : 12u, 0));
test.code.push_back(EncodeMtbuf1(0x04, 0, 0, 20));
test.expected[index] = (test.expected[index] & (index < 3u ? 0xffffff00u : 0xffff0000u)) | encoded[index];
}
AppendEnd(&test.code);
test.opcodes = {O::V_MOV_B32, O::TBUFFER_STORE_FORMAT_X, O::S_ENDPGM};
return test;
}
TestCase TBufferLoadFormatXy88IntegerComponents() {
@@ -38292,6 +38293,82 @@ TestCase ImageStoreR32FloatUsesFormatlessStorageImage() {
return test;
}
void CheckSigned16ImageStore(VulkanHarness &vulkan) {
constexpr std::array<int32_t, 8> values{INT32_MIN, -32769, -32768, -1, 0, 32767, 32768, INT32_MAX};
const u32 alignment = vulkan.RuntimeContext().GetPhysicalDeviceProperties()
.limits.minStorageBufferOffsetAlignment;
for (const u32 wave : {32u, 64u}) {
for (const bool d16 : {false, true}) {
for (const u32 component : {0u, 2u}) {
TestCase test;
test.name = "Signed16ImageAndBufferStore";
const u32 width = wave - 2u;
const u32 output = (wave * 4u + alignment - 1u) / alignment * alignment;
test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k16SInt);
test.user_data[1] |= ((width - 1u) & 3u) << 30u;
test.user_data[2] = ((width - 1u) >> 2u) | (1u << 14u);
test.has_user_data = test.has_compute_info = true;
test.image_descriptor_swizzle = component == 0u ? DstSel(4, 0, 0, 1) : DstSel(0, 0, 4, 1);
const std::array<u32, 8> descriptors{0x1000u, 4u << 16u, wave, 0x5204u,
0x2000u, 2u << 16u, wave, (u32(Prospero::BufferFormat::k16SInt) << 12u) | 4u};
std::copy(descriptors.begin(), descriptors.end(), test.user_data.begin() + 8u);
test.storage_buffer_ranges = {{0u, wave * 4u}, {output, wave * 2u}};
test.compute_info.wave_size = wave;
test.compute_info.threads_num[0] = wave;
test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1u;
test.compute_info.thread_ids_num = 1u;
test.compute_info.lds_size_dwords = 0u;
test.initial.assign(output / 4u + wave / 2u, 0xdeadbeefu);
for (u32 lane = 0; lane < wave; ++lane)
test.initial[lane] = d16 ? 0x7a5b0000u | (u32(values[lane % values.size()]) & 0xffffu)
: u32(values[lane % values.size()]);
test.expected = test.initial;
std::vector<u32> pixels(width, 0x13571357u), expected = pixels;
for (u32 lane = 0; lane < wave; ++lane) {
const auto input = values[lane % values.size()];
const auto value = d16 ? int32_t(std::bit_cast<int16_t>(uint16_t(input))) : input;
const u32 encoded = uint16_t(std::clamp(value, -32768, 32767));
const auto set_half = [&](std::vector<u32> &words, u32 index) {
const auto shift = (index & 1u) * 16u;
words[index / 2u] = (words[index / 2u] & ~(0xffffu << shift)) | (encoded << shift);
};
set_half(test.expected, output / 2u + lane);
if (lane < width) {
set_half(expected, lane);
if (lane & 1u) set_half(expected, width + lane);
}
}
auto &code = test.code;
code = {EncodeMubuf0(0x0c, 0, true, false), EncodeMubuf1(4, 2, 0),
EncodeSopp(0x0c, 0), EncodeMubuf0(d16 ? 0x84 : 0x04, 0, true, false),
EncodeMubuf1(4, 3, 0), EncodeVop1(0x01, 20, Vgpr(0))};
AppendVMovU32(&code, 21, 0u);
code.insert(code.end(), {EncodeMimg0(0x08, 1u << component),
EncodeMimg1(4, 20, 0, 0, false, d16)});
AppendVMovU32(&code, 21, 1u);
AppendSMovLiteral(&code, 126, 0xaaaaaaaau);
AppendSMovLiteral(&code, 127, wave == 64u ? 0xaaaaaaaau : 0u);
code.insert(code.end(), {EncodeMimg0(0x08, 1u << component),
EncodeMimg1(4, 20, 0, 0, false, d16)});
AppendEnd(&code);
test.opcodes = {ShaderOpcode::IMAGE_STORE};
const auto compiled = CompileCase(test, vulkan.SubgroupSize());
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size());
auto image = vulkan.CreateImage2D(test.name, width, 2u, vk::Format::eR16Sint,
vk::ImageUsageFlagBits::eStorage, pixels, 1u, vk::ImageLayout::eGeneral, vk::Format::eR16Uint);
vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, nullptr, &image);
CompareWords(test, "signed image saturation, sparse channels, masked lanes and OOB", expected,
vulkan.ReadImage(test.name, &image));
CompareWords(test, "shared buffer conversion and adjacent halfwords", test.expected,
vulkan.ReadBuffer(test.name, buffer, test.expected.size()));
vulkan.DestroyImage(&image);
vulkan.DestroyBuffer(&buffer);
}
}
}
std::printf("[compute] %-32s ok\n", "Signed16ImageAndBufferStore");
}
TestCase ImageStoreR32SintUsesRawUintView() {
using O = ShaderOpcode;
@@ -45517,6 +45594,7 @@ int main(int argc, char **argv) {
RunCase(&vulkan, BufferStoreFormatXRejectsPartialRecord());
RunCase(&vulkan, BufferStoreFormatXyRejectsPartialRecord());
RunCase(&vulkan, BufferFormatStoreVariants());
RunCase(&vulkan, TBufferStoreFormatXSintWritesSubDword());
RunCase(&vulkan, BufferStoreByteD16Hi(false));
RunCase(&vulkan, BufferStoreByteD16Hi(true));
return 0;
@@ -45634,6 +45712,7 @@ int main(int argc, char **argv) {
if (argc == 2 && std::strcmp(argv[1], "--packed-texture-only") == 0) {
VulkanHarness vulkan;
vulkan.CheckPackedTextureComponents();
CheckSigned16ImageStore(vulkan);
vulkan.CheckBgra16Readback();
RunCase(&vulkan, ImageLoadPackedUintUnpacksAndSwizzles());
RunCase(&vulkan, ImageLoadPackedUnorm<false, false>());
@@ -46414,6 +46493,7 @@ int main(int argc, char **argv) {
CheckTessellationPrograms();
CheckPixelParameterAliases();
CheckRectListShaders();
CheckSigned16ImageStore(vulkan);
CheckBoundedFormattedBufferCopy(vulkan);
CheckIndirectBufferStore(vulkan);
CheckBoundedRecordBufferAccess(vulkan);