shader: validate indirect image operands at each typed access

This commit is contained in:
nmzik 2026-10-12 20:12:09 +02:00
parent 0e9c77b0a2
commit f01a3750d4
6 changed files with 224 additions and 38 deletions

View File

@ -61,10 +61,10 @@ uint32_t AddressF32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::
: Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), value);
}
ImageSampleLayout Layout(const IR::MemoryInfo& mem) {
ImageSampleLayout Layout(const IR::MemoryInfo& mem, ImageDimension dimension) {
ImageSampleLayout layout;
uint32_t cursor = 0;
const auto& info = ImageDimensionInfoFor(mem.image_dimension);
const auto& info = ImageDimensionInfoFor(dimension);
if (HasFlag(mem, Decoder::ImageSampleFlagOffset)) layout.offset = cursor++;
if (HasFlag(mem, Decoder::ImageSampleFlagBias)) layout.bias = cursor++;
if (HasFlag(mem, Decoder::ImageSampleFlagCompare)) layout.dref = cursor++;
@ -80,6 +80,48 @@ ImageSampleLayout Layout(const IR::MemoryInfo& mem) {
return layout;
}
void ValidateIndirectImageOperands(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto op = inst.GetOpcode();
const auto& memory = ctx.Memory(inst);
const auto& images = ctx.state.program.info.images;
const auto& root = images.at(memory.resource);
if (root.indirect_root != memory.resource) return;
if (op != IR::ValueOpcode::ImageSampleRaw &&
op != IR::ValueOpcode::ImageGatherRaw && op != IR::ValueOpcode::ImageRead) {
ctx.Fail(inst, "requires an unsupported indirect image operation");
}
const auto& native = ImageDimensionInfoFor(memory.image_dimension);
const auto native_layout = Layout(memory, memory.image_dimension);
const bool sampling = op == IR::ValueOpcode::ImageSampleRaw ||
op == IR::ValueOpcode::ImageGatherRaw;
const auto validate = [&](const IR::ImageResource& image) {
const auto& candidate = ImageDimensionInfoFor(image.dimension);
// Sample/fetch ignore the native array slice for a non-array T# while
// keeping the original positions of later operands.
const bool projected_2d = memory.image_dimension == ImageDimension::Dim2DArray &&
image.dimension == ImageDimension::Dim2D;
bool compatible = projected_2d ||
(candidate.coordinate_components == native.coordinate_components &&
candidate.multisampled == native.multisampled);
if (sampling) {
compatible &= candidate.multisampled == 0u && native.multisampled == 0u;
compatible &= projected_2d || Layout(memory, image.dimension) == native_layout;
if (HasFlag(memory, Decoder::ImageSampleFlagDerivative)) {
compatible &= candidate.spatial_components == native.spatial_components;
}
if (op == IR::ValueOpcode::ImageGatherRaw ||
HasFlag(memory, Decoder::ImageSampleFlagCompare)) {
compatible &= image.dimension != ImageDimension::Dim3D;
}
}
if (!compatible) ctx.Fail(inst, "has incompatible image operand layout");
};
validate(root);
for (const auto resource: root.indirect_resources) {
if (resource != memory.resource) validate(images.at(resource));
}
}
uint32_t ZeroF32(EmitterState& state) {
return ConstantF32(state, 0);
}
@ -736,6 +778,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto& mem = ctx.Memory(inst);
const auto image_arg = inst.Arg(0);
ctx.ResourceIndex(image_arg, IR::ValueOpcode::GetImageResource);
ValidateIndirectImageOperands(ctx, inst);
const auto& image = state.program.info.images.at(mem.resource);
const auto* address = ctx.ImageAddress(inst.Arg(image_info.needs_sampler ? 2 : 1));
if (op == IR::ValueOpcode::ImageQueryDimensions) {
@ -817,7 +860,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
if (op == IR::ValueOpcode::ImageSampleRaw || op == IR::ValueOpcode::ImageGatherRaw) {
const auto dimension = image.dimension;
const auto& dimension_info = ImageDimensionInfoFor(dimension);
const auto layout = Layout(mem);
const auto layout = Layout(mem, mem.image_dimension);
const auto numeric_class = image.numeric_class;
const bool dref = HasFlag(mem, Decoder::ImageSampleFlagCompare);
IR::Value offset_value;

View File

@ -272,6 +272,8 @@ struct ImageSampleLayout {
uint32_t lod = NoImageComponent;
uint32_t grad_x = NoImageComponent;
uint32_t grad_y = NoImageComponent;
bool operator==(const ImageSampleLayout&) const = default;
};
struct F32Class {

View File

@ -621,10 +621,6 @@ 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 is_2d = [](Decoder::ImageDimension dimension) {
return dimension == Decoder::ImageDimension::Dim2D ||
dimension == Decoder::ImageDimension::Dim2DArray;
};
for (uint32_t candidate = 0; candidate < specialization.images.size(); candidate++) {
auto& image = specialization.images[candidate];
if (image.indirect_root != root_index) {
@ -638,10 +634,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
image.shader_swizzle = image_class.shader_swizzle;
image.cube = image_class.cube;
}
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))) ||
image.mip_count != image_class.mip_count ||
if (image.mip_count != image_class.mip_count ||
image.conversion_format != image_class.conversion_format ||
image.shader_swizzle != image_class.shader_swizzle) {
return SpecializationFail(
@ -1414,10 +1407,6 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
memory.sampler = sampler_plan.mapping[memory.sampler][type];
EXIT_IF(memory.sampler == UINT32_MAX);
}
EXIT_IF(image.indirect_root == memory.resource &&
inst.GetOpcode() != ValueOpcode::ImageSampleRaw &&
inst.GetOpcode() != ValueOpcode::ImageGatherRaw &&
inst.GetOpcode() != ValueOpcode::ImageRead);
}
}
for (auto& memory: memory_info) {

View File

@ -434,8 +434,18 @@ void TestBoundedImageViewEligibility() {
plan.descriptor_sources.push_back(source);
}
plan.descriptor_sources[image_source].indirect_descriptor->sources = std::move(selected);
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 2u &&
snapshot.images[0].dwords == descriptor(0x30u, Type::kColor2D) &&
snapshot.images[1].dwords == descriptor(0x30u, Type::kColor3D) &&
specialization.images[0].dimension == Decoder::ImageDimension::Dim2D &&
specialization.images[1].dimension == Decoder::ImageDimension::Dim3D,
"explicitly selected image dimensions were discarded before operand validation");
const auto converted = plan.descriptor_sources[image_source].indirect_descriptor->sources.back();
plan.descriptor_sources[converted].dwords[1] = Value(
static_cast<uint32_t>(Libs::Graphics::Prospero::BufferFormat::k11_11_10UInt) << 20u);
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"explicitly selected incompatible descriptor was silently normalized to null");
"explicitly selected incompatible conversion was silently normalized to null");
}
void TestWaterfallImageTable() {

View File

@ -1987,6 +1987,7 @@ public:
vk::ImageLayout layout = vk::ImageLayout::eUndefined;
u32 width = 0;
u32 height = 0;
u32 depth = 1;
u32 layers = 1;
u32 mip_levels = 1;
u32 dwords_per_pixel = 0;
@ -15451,11 +15452,12 @@ public:
u32 view_layers = 0,
vk::SampleCountFlagBits samples = vk::SampleCountFlagBits::e1,
vk::Format view_format = vk::Format::eUndefined,
u32 view_base_mip = 0) {
u32 view_base_mip = 0, u32 depth = 1) {
Image ret;
ret.format = format;
ret.width = width;
ret.height = height;
ret.depth = depth;
ret.layers = layers;
ret.mip_levels = std::max<u32>(static_cast<u32>(initial_mips.size()), 1u);
ret.dwords_per_pixel = dwords_per_pixel;
@ -15466,7 +15468,7 @@ public:
image_info.format = format;
image_info.extent.width = width;
image_info.extent.height = height;
image_info.extent.depth = 1;
image_info.extent.depth = depth;
image_info.mipLevels = ret.mip_levels;
image_info.arrayLayers = layers;
image_info.samples = samples;
@ -15531,13 +15533,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, dwords_per_pixel, level, layers) * MipExtent(depth, level);
}
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, dwords_per_pixel, level, layers) * MipExtent(depth, level);
const auto &src = initial_mips[level];
for (size_t i = 0; i < src.size() && i < level_dwords; i++) {
contents[offset + i] = src[i];
@ -15681,7 +15683,8 @@ public:
if (image.resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled) {
supported = supported ||
image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1D ||
image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1DArray;
image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1DArray ||
image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim3D;
}
Require(test.name, "dispatch", supported,
"unsupported image dimension needs a matching Vulkan test view");
@ -19202,11 +19205,11 @@ private:
copy.imageSubresource.layerCount = image->layers;
copy.imageExtent.width = MipExtent(image->width, level);
copy.imageExtent.height = MipExtent(image->height, level);
copy.imageExtent.depth = 1;
copy.imageExtent.depth = MipExtent(image->depth, level);
copies.push_back(copy);
offset += static_cast<vk::DeviceSize>(
ImageMipDwordCount(image->width, image->height,
image->dwords_per_pixel, level, image->layers) *
image->dwords_per_pixel, level, image->layers) * MipExtent(image->depth, level) *
sizeof(u32));
}
cmd.copyBufferToImage(staging, image->image,
@ -36134,6 +36137,9 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
program.info.images[root.indirect_resources[103]].dimension =
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
program.info.images[root.indirect_resources[103]].cube = true;
program.memory_info[0].image_dimension =
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
program.memory_info[0].image_address_components = 4;
program.shader_info_complete = false;
CollectShaderInfo(program, {.compute = &compute});
spirv = ShaderRecompiler::Spirv::EmitProgram(program, {.compute = &compute});
@ -36142,9 +36148,6 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
CountText(text, "OpImageSampleExplicitLod") == 3,
"one cube split the native arrays into per-image samples");
program.memory_info[0].image_dimension =
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
program.memory_info[0].image_address_components = 4;
for (u32 component = 0; component < 4u; component++) {
constexpr std::array values{1.375f, 1.625f, 10.0f, 2.0f};
address.SetArg(component, Value(std::bit_cast<u32>(values[component])));
@ -36198,6 +36201,83 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
Require(name, "mixed sample count", samples == 3u,
"mixed candidate switch did not retain every image");
}
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
enum class RejectedAccess { LodPosition, Derivatives, Gather, Depth, Multisampled, Query, Write, Atomic };
for (const auto [rejected, label] : {
std::pair{RejectedAccess::LodPosition, "IndirectImageLodPositionRejected"},
std::pair{RejectedAccess::Derivatives, "IndirectImageDerivativeLayoutRejected"},
std::pair{RejectedAccess::Gather, "IndirectImageVolumeGatherRejected"},
std::pair{RejectedAccess::Depth, "IndirectImageVolumeDrefRejected"},
std::pair{RejectedAccess::Multisampled, "IndirectImageMsaaSampleRejected"},
std::pair{RejectedAccess::Query, "IndirectImageQueryRejected"},
std::pair{RejectedAccess::Write, "IndirectImageWriteRejected"},
std::pair{RejectedAccess::Atomic, "IndirectImageAtomicRejected"}}) {
ExpectFatal(label, [&] {
using Dimension = ShaderRecompiler::Decoder::ImageDimension;
for (auto &resource : program.info.images) {
resource.dimension = Dimension::Dim2DArray;
resource.cube = false;
}
program.info.images[2].dimension = Dimension::Dim3D;
auto &mem = program.memory_info[0];
const auto position = std::ranges::find_if(*block, [&](const Inst &inst) { return &inst == &sample; });
if (rejected == RejectedAccess::LodPosition) {
// Four available operands do not make native x,y,LOD compatible with x,y,z,LOD.
mem.image_dimension = Dimension::Dim2D;
program.info.images[0].dimension = program.info.images[1].dimension = Dimension::Dim2D;
} else if (rejected == RejectedAccess::Derivatives) {
mem.image_sample_flags = ShaderRecompiler::Decoder::ImageSampleFlagDerivative;
mem.image_address_components = 9u; // Capacity alone must not change the native gradient layout.
} else if (rejected == RejectedAccess::Gather) {
mem.image_sample_flags = ShaderRecompiler::Decoder::ImageSampleFlagLevelZero;
mem.image_address_components = 3u;
const auto gather = block->PrependNewInst(position, ValueOpcode::ImageGatherRaw,
{Value(&image), Value(&sampler), Value(&address), Value(0u), Value(0u),
Value(0u), Value(0u), Value(true)}, memory_flag_bits);
sample.ReplaceUsesWith(Value(&*gather), false);
} else if (rejected == RejectedAccess::Depth) {
mem.image_sample_flags |= ShaderRecompiler::Decoder::ImageSampleFlagCompare;
mem.image_address_components = 5u;
for (auto &resource : program.info.images) resource.depth_compare = true;
program.info.samplers[0].depth_compare = true;
} else if (rejected == RejectedAccess::Multisampled) {
program.info.images[2].dimension = Dimension::Dim2DMsaaArray;
} else if (rejected == RejectedAccess::Query) {
const auto query = block->PrependNewInst(position, ValueOpcode::ImageQueryDimensions,
{Value(&image), Value(&address)}, memory_flag_bits);
sample.ReplaceUsesWith(Value(&*query), false);
} else {
sample_x.ReplaceUsesWith(Value(0u), false);
sample.Invalidate();
sampler.Invalidate();
program.info.samplers.clear();
program.info.sampled_pairs.clear();
mem.image_sample_flags = 0u;
mem.image_address_components = 3u;
const bool atomic = rejected == RejectedAccess::Atomic;
for (auto &resource : program.info.images) {
resource.resource_class = ImageResourceClass::Storage;
resource.numeric_class = atomic ? Prospero::TextureNumericClass::Uint
: Prospero::TextureNumericClass::Float;
resource.written = true;
resource.atomic = atomic;
}
if (atomic) {
block->PrependNewInst(position, ValueOpcode::ImageAtomicIAdd32,
{Value(&image), Value(&address), Value(1u), Value(true)}, memory_flag_bits);
} else {
const auto texel = block->PrependNewInst(position, ValueOpcode::CompositeConstructU32x4,
{Value(0u), Value(0u), Value(0u), Value(0u)});
block->PrependNewInst(position, ValueOpcode::ImageWrite,
{Value(&image), Value(&address), Value(&*texel), Value(true)}, memory_flag_bits);
}
}
program.shader_info_complete = false;
CollectShaderInfo(program, {.compute = &compute});
ShaderRecompiler::Spirv::EmitProgram(program, {.compute = &compute});
});
}
#endif
// Keep an unrelated native root between the null image and appended children,
// then select both sides of a cube boundary and the unmapped suffix on the GPU.
root.indirect_resources = {0u, 2u, 3u, 4u, 5u, 6u};
@ -36248,6 +36328,24 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
resource == 4u ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, layers));
}
const auto native_sampler = vulkan.CreateSampler(test.name);
const auto mip_sampler = vulkan.CreateSampler(test.name, 2u);
constexpr u32 volume_mip_bits = 0x43800001u;
constexpr u32 array_mip_bits = 0x42800001u;
const auto mip_pixels = [](u32 texels, u32 first) {
std::vector<u32> pixels(texels * 4u);
for (u32 texel = 0; texel < texels; ++texel)
std::fill_n(pixels.begin() + texel * 4u, 4u, first + texel);
return pixels;
};
auto volume_texture = vulkan.CreateImageMips(
test.name, 4, 4, vk::Format::eR32G32B32A32Sfloat, vk::ImageUsageFlagBits::eSampled,
{mip_pixels(4u * 4u * 8u, 0x44000001u), mip_pixels(2u * 2u * 4u, volume_mip_bits)}, 4u,
vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageType::e3D, vk::ImageViewType::e3D, 1u,
0u, 0u, vk::SampleCountFlagBits::e1, vk::Format::eUndefined, 0u, 8u);
auto array_texture = vulkan.CreateImageMips(
test.name, 4, 4, vk::Format::eR32G32B32A32Sfloat, vk::ImageUsageFlagBits::eSampled,
{mip_pixels(4u * 4u * 2u, 0x43000001u), mip_pixels(2u * 2u * 2u, array_mip_bits)}, 4u,
vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageType::e2D, vk::ImageViewType::e2DArray, 2u);
constexpr u32 uint_texel = 0x80000106u;
auto uint_texture = vulkan.CreateImageMips(
test.name, 2, 2, vk::Format::eR32G32B32A32Uint, vk::ImageUsageFlagBits::eSampled,
@ -36267,21 +36365,35 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
&integer_sampler_info, nullptr, &integer_border_sampler), "vkCreateSampler");
const std::array native_samplers{native_sampler, native_sampler, native_sampler,
integer_sampler, integer_sampler, integer_border_sampler};
enum class Access { Sample, Gather, GatherHomogeneous, MixedSample, MixedGather, MixedFetch, Fetch };
enum class Access { Sample, MixedDimensions, Gather, GatherHomogeneous, MixedSample, MixedGather, MixedFetch, Fetch };
auto *access = &sample;
bool uint_bound = false;
for (const auto mode : {Access::Sample, Access::Gather,
bool volume_bound = false;
for (const auto mode : {Access::Sample, Access::MixedDimensions, Access::Gather,
Access::GatherHomogeneous, Access::MixedSample,
Access::MixedGather, Access::MixedFetch, Access::Fetch}) {
const bool mixed = mode == Access::MixedSample || mode == Access::MixedGather || mode == Access::MixedFetch;
const bool dimensions = mode == Access::MixedDimensions;
const bool fetch = mode == Access::Fetch || mode == Access::MixedFetch;
const bool gather = mode == Access::Gather || mode == Access::GatherHomogeneous || mode == Access::MixedGather;
const bool homogeneous = mode == Access::GatherHomogeneous;
test.name = mixed ? (fetch ? "IndirectImageMixedFetch" : gather ? "IndirectImageMixedGather"
test.name = dimensions ? "IndirectImageArrayCubeVolumeLod"
: mixed ? (fetch ? "IndirectImageMixedFetch" : gather ? "IndirectImageMixedGather"
: "IndirectImageMixedSample")
: fetch ? "IndirectImageFetchContiguousRuns"
: homogeneous ? "IndirectImageGatherHomogeneous"
: gather ? "IndirectImageGatherContiguousRuns" : "IndirectImageContiguousRuns";
if (volume_bound != dimensions) {
std::swap(textures[6], volume_texture);
std::swap(textures[3], array_texture);
compiled.program.info.images[6].dimension = dimensions
? ShaderRecompiler::Decoder::ImageDimension::Dim3D
: ShaderRecompiler::Decoder::ImageDimension::Dim2D;
compiled.program.info.images[3].dimension = dimensions
? ShaderRecompiler::Decoder::ImageDimension::Dim2DArray
: ShaderRecompiler::Decoder::ImageDimension::Dim2D;
volume_bound = dimensions;
}
if (uint_bound != mixed) {
std::swap(textures[6], uint_texture);
compiled.program.info.images[6].numeric_class = mixed ? Prospero::TextureNumericClass::Uint
@ -36321,7 +36433,20 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
access->ReplaceUsesWith(Value(&read), false);
access = &read;
}
if (gather) {
if (dimensions) {
const auto position = std::ranges::find_if(*block, [&](const Inst &inst) { return &inst == &address; });
const auto parity = block->PrependNewInst(position, ValueOpcode::BitwiseAnd32, {Value(&key), Value(1u)});
const auto odd = block->PrependNewInst(position, ValueOpcode::INotEqual32, {Value(&*parity), Value(0u)});
const auto z = block->PrependNewInst(position, ValueOpcode::SelectU32,
{Value(&*odd), Value(std::bit_cast<u32>(0.75f)), Value(std::bit_cast<u32>(0.25f))});
const auto cube_lane = block->PrependNewInst(position, ValueOpcode::IEqual32, {Value(&key), Value(3u)});
const auto coordinate_z = block->PrependNewInst(position, ValueOpcode::SelectU32,
{Value(&*cube_lane), Value(0u), Value(&*z)});
address.SetArg(0, Value(std::bit_cast<u32>(1.25f)));
address.SetArg(1, Value(std::bit_cast<u32>(1.75f)));
address.SetArg(2, Value(&*coordinate_z));
address.SetArg(3, Value(std::bit_cast<u32>(1.0f)));
} else if (gather) {
compiled.program.info.images[4].cube = false;
auto &gather_memory = compiled.program.memory_info[0];
gather_memory.dmask = 1;
@ -36342,7 +36467,8 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
selected_resources = homogeneous ? std::vector<u32>{0u, 2u, 3u, 5u, 6u}
: root.indirect_resources;
compiled.resources.flattened_srt[6] = homogeneous ? 4u : ordinals[5];
compiled.resources.flattened_srt[7] = mixed ? 5u : ordinals[6];
compiled.resources.flattened_srt[7] = mixed || dimensions ? 5u : ordinals[6];
compiled.resources.flattened_srt[8] = dimensions ? 2u : ordinals[7];
if (fetch) {
sampler.Invalidate();
compiled.program.info.samplers.clear();
@ -36371,7 +36497,7 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
const auto image_opcode = fetch ? "OpImageFetch" : gather ? "OpImageGather"
: "OpImageSampleExplicitLod";
Require(test.name, "native array image runs", tools.Disassemble(compiled.spirv, &text) &&
CountText(text, image_opcode) == (homogeneous ? 1u : mixed ? 4u : 3u) * halves,
CountText(text, image_opcode) == (homogeneous ? 1u : dimensions ? 5u : mixed ? 4u : 3u) * halves,
"mixed runs expanded into per-image accesses");
if (homogeneous) {
Require(test.name, "homogeneous gather array", text.find("OpSwitch") == std::string::npos,
@ -36400,13 +36526,16 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
test.expected.assign(wave_size, 0u);
for (u32 lane = 0; lane < ordinals.size(); ++lane) {
const auto resource = selected_resources[compiled.resources.flattened_srt[1u + lane]];
test.expected[lane] = mixed && resource == 6u
test.expected[lane] = dimensions && resource == 6u
? volume_mip_bits + ((lane & 1u) ? 3u : 1u) * 4u + 3u
: dimensions && resource == 3u ? array_mip_bits + (lane & 1u) * 4u + 3u
: mixed && resource == 6u
? (fetch ? uint_texel + 10u : (lane & 1u) ? 1u : uint_texel)
: std::bit_cast<u32>(float(resource + (fetch && resource != 0u ? 10u : 0u)));
: std::bit_cast<u32>(float(resource + ((fetch || dimensions) && resource != 0u ? 10u : 0u)));
}
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size());
vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, nullptr, nullptr,
fetch ? vk::Sampler{} : native_sampler, textures,
fetch ? vk::Sampler{} : dimensions ? mip_sampler : native_sampler, textures,
mixed && !fetch ? std::span<const vk::Sampler>(native_samplers) : std::span<const vk::Sampler>{});
const auto actual = vulkan.ReadBuffer(test.name, buffer, test.expected.size());
vulkan.DestroyBuffer(&buffer);
@ -36417,7 +36546,10 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
vulkan.Device().destroySampler(integer_sampler);
vulkan.Device().destroySampler(integer_border_sampler);
vulkan.Device().destroySampler(native_sampler);
vulkan.Device().destroySampler(mip_sampler);
vulkan.DestroyImage(&uint_texture);
vulkan.DestroyImage(&volume_texture);
vulkan.DestroyImage(&array_texture);
for (auto &texture : textures) vulkan.DestroyImage(&texture);
}

View File

@ -14258,6 +14258,7 @@ void TestBoundedScalarMaterialImageKeys() {
bool clean = true;
bool mixed = false;
bool mixed_dimension = false;
bool mixed_conversion = false;
bool same_key = false;
uint32_t keys = 0;
uint32_t images = 0;
@ -14286,8 +14287,10 @@ void TestBoundedScalarMaterialImageKeys() {
std::ranges::fill(words, 0u);
words[0] = key;
const bool integer = key == 5573u || key == 5588u;
words[1] = static_cast<uint32_t>(integer && !(m.mixed && key == 5573u)
? Prospero::BufferFormat::k32UInt : Prospero::BufferFormat::k32Float) << 20u;
words[1] = static_cast<uint32_t>(m.mixed_conversion && key == 5573u
? Prospero::BufferFormat::k11_11_10UInt
: integer && !(m.mixed && key == 5573u)
? Prospero::BufferFormat::k32UInt : Prospero::BufferFormat::k32Float) << 20u;
words[3] = DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(m.mixed_dimension && key == 5573u
? Prospero::ImageType::kColor2D : Prospero::ImageType::kColor3D) << 28u);
@ -14344,8 +14347,15 @@ void TestBoundedScalarMaterialImageKeys() {
}) == 1,
"selected material fields lost their mixed float/uint image classes");
memory = {.mixed_dimension = true};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 4u &&
std::ranges::count_if(specialization.images, [](const auto& image) {
return image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2D;
}) == 1,
"selected material fields lost their per-candidate image dimensions");
memory = {.mixed_conversion = true};
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"selected material fields bypassed image coordinate compatibility");
"selected material fields bypassed image conversion compatibility");
memory = {};
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"native scalar material image selection did not recover after rejected candidates");