Emit coordinates for each indirect image candidate

Co-authored-by: Techx3 <56268269+Techx3@users.noreply.github.com>
This commit is contained in:
nmzik
2026-09-23 11:15:08 +04:00
parent 8a5e5c5476
commit eb8a5504a3
4 changed files with 162 additions and 16 deletions
@@ -63,10 +63,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, ImageDimension dimension) {
ImageSampleLayout Layout(const IR::MemoryInfo& mem) {
ImageSampleLayout layout;
uint32_t cursor = 0;
const auto& info = ImageDimensionInfoFor(dimension);
const auto& info = ImageDimensionInfoFor(mem.image_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++;
@@ -104,8 +104,7 @@ uint32_t CubeLayer(EmitterState& state, uint32_t value) {
}
uint32_t CoordF32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
uint32_t first, uint32_t components) {
const bool cube = ctx.state.program.info.images.at(mem.resource).cube;
uint32_t first, uint32_t components, bool cube = false) {
auto x = AddressF32(ctx, mem, address, first);
if (components == 1u) return x;
auto y = mem.image_address_components > first + 1u ? AddressF32(ctx, mem, address, first + 1u)
@@ -576,7 +575,8 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto lod = state.builder.AllocateId();
state.builder.AddFunction(
spv::OpImageQueryLod, TypeF32Vector(state, 2), lod, sampled,
CoordF32(ctx, mem, *address, 0, ImageDimensionInfoFor(dimension).spatial_components));
CoordF32(ctx, mem, *address, 0, ImageDimensionInfoFor(dimension).spatial_components,
image.cube));
uint32_t values[4] = {ConstantU32(state, 0), ConstantU32(state, 0), ConstantU32(state, 0),
ConstantU32(state, 0)};
for (uint32_t index = 0; index < 2u; index++) {
@@ -637,7 +637,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, dimension);
const auto layout = Layout(mem);
const auto numeric_class = image.numeric_class;
const bool dref = HasFlag(mem, Decoder::ImageSampleFlagCompare);
if (dref && state.program.info.images[mem.resource].conversion_format !=
@@ -645,9 +645,9 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
ctx.Fail(inst, "uses depth comparison with a packed integer image");
return;
}
const auto coord =
CoordF32(ctx, mem, *address, layout.coord, dimension_info.coordinate_components);
if (op == IR::ValueOpcode::ImageGatherRaw) {
const auto coord = CoordF32(ctx, mem, *address, layout.coord,
dimension_info.coordinate_components, image.cube);
if (HasFlag(mem, Decoder::ImageSampleFlagLod)) {
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
if (!warned.test_and_set(std::memory_order_relaxed)) {
@@ -752,6 +752,11 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
operands.push_back(AddressF32(ctx, mem, *address, layout.bias));
}
const auto EmitSample = [&](uint32_t resource) {
const auto& candidate = state.program.info.images[resource];
const auto coord =
CoordF32(ctx, mem, *address, layout.coord,
ImageDimensionInfoFor(candidate.dimension).coordinate_components,
candidate.cube);
const auto sampled = MakeSampledImage(state, resource, mem.sampler);
const auto sample = state.builder.AllocateId();
std::vector<uint32_t> sample_operands {result_type, sample, sampled, coord};
@@ -453,6 +453,10 @@ 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) {
@@ -466,12 +470,13 @@ 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 (image.numeric_class != image_class.numeric_class ||
image.dimension != image_class.dimension ||
(!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension))) ||
image.mip_count != image_class.mip_count ||
image.conversion_format != image_class.conversion_format ||
image.shader_swizzle != image_class.shader_swizzle ||
image.cube != image_class.cube) {
image.shader_swizzle != image_class.shader_swizzle) {
return SpecializationFail(
fmt::format("indirect image table at pc 0x{:08x} has incompatible candidates",
program.info.images[root_index].first_use_pc));
+34
View File
@@ -352,6 +352,40 @@ void TestInvariantIndirectImageMaterialization() {
dynamic_snapshot.images.size() == 2 &&
dynamic_specialization.images.size() == 2,
"dynamic indirect image table did not materialize");
const auto second_image = (0x2020u - memory.base) / 4u;
memory.words[second_image + 1u] |= 3u << 30u;
memory.words[second_image + 2u] = 3u << 14u;
memory.words[second_image + 3u] =
Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kCube) << 28u);
memory.words[second_image + 4u] = 11u;
ResourceSnapshot mixed_snapshot;
ResourceSpecialization mixed_specialization;
Check(MaterializeResources(resource_plan, runtime, mixed_snapshot,
mixed_specialization) &&
mixed_snapshot.images.size() == 2 &&
mixed_specialization.images.size() == 2 &&
mixed_specialization.images[0].dimension ==
Decoder::ImageDimension::Dim2D &&
!mixed_specialization.images[0].cube &&
mixed_specialization.images[1].dimension ==
Decoder::ImageDimension::Dim2DArray &&
mixed_specialization.images[1].cube &&
std::equal(mixed_snapshot.images[1].dwords.begin(),
mixed_snapshot.images[1].dwords.end(),
memory.words.begin() + second_image),
"mixed 2D and cube candidates were rejected or discarded");
memory.words[second_image + 1u] =
static_cast<uint32_t>(
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32UInt)
<< 20u |
(3u << 30u);
Check(!MaterializeResources(resource_plan, runtime, mixed_snapshot,
mixed_specialization),
"indirect images with different numeric classes were accepted");
for (const auto dword : {1u, 2u, 3u, 4u}) {
memory.words[second_image + dword] = image_descriptor[dword];
}
ApplyResourceSpecialization(fixture->program, dynamic_specialization);
Check(fixture->program.info.images.size() == 2 &&
fixture->program.info.images[0].indirect_root == 0 &&
+107 -5
View File
@@ -26174,6 +26174,48 @@ TestCase ImageSampleAndGather() {
return test;
}
TestCase ImageCubeGradientsPreserveDerivatives() {
using O = ShaderOpcode;
TestCase test;
test.name = "ImageCubeGradientsPreserveDerivatives";
for (u32 component = 0; component < 7u; component++) {
constexpr std::array values{0.125f, 0.0f, 0.0f, 0.125f,
1.375f, 1.625f, 10.0f};
AppendVMovLiteral(&test.code, 20u + component,
std::bit_cast<u32>(values[component]));
}
test.code.push_back(EncodeMimg0(0x22, 0x1, 0, false, 3));
test.code.push_back(EncodeMimg1(0, 20));
AppendStoreVgpr(&test.code, 0, 0);
AppendEnd(&test.code);
test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32_32_32_32Float);
test.user_data[1] |= 3u << 30u;
test.user_data[2] = 3u << 14u;
test.user_data[3] =
(static_cast<u32>(Prospero::ImageType::kCube) << 28u) | (2u << 16u);
test.user_data[4] = 11u;
test.user_data[5] = 2u << 4u;
test.user_data[50] = sizeof(u32);
test.has_user_data = true;
test.sampled_image_view_type = vk::ImageViewType::e2DArray;
test.sampled_image_layers = 12;
for (u32 mip = 0; mip < 3u; mip++) {
const auto layer_dwords = (4u >> mip) * (4u >> mip) * 4u;
auto &pixels = test.sampled_image_rgba_mips.emplace_back(layer_dwords * 12u);
for (u32 layer = 0; layer < 12u; layer++) {
std::fill_n(pixels.begin() + layer * layer_dwords, layer_dwords,
std::bit_cast<u32>(static_cast<float>(1u + layer + 100u * mip)));
}
}
// Guest face 10 is host layer 8. Unmodified gradients select mip 0;
// applying the cube coordinate bias to gradients instead selects mip 2.
test.expected = {std::bit_cast<u32>(9.0f)};
test.required_spirv = {"OpImageSampleExplicitLod", "Grad"};
return test;
}
TestCase ImageGatherLodApproximatesLevelZero() {
using O = ShaderOpcode;
std::vector<u32> code;
@@ -26549,6 +26591,63 @@ void CheckIndirectImageKeySwitch() {
CountText(text, "OpImageSampleExplicitLod") == 2 &&
CountText(text, "OpIEqual") == 11,
"dynamic image key did not use a compact two-sample switch");
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])));
}
root.indirect_resources = {0u, 1u, 2u};
program.info.images = {root, candidate, candidate};
for (const bool cube_first : {true, false}) {
for (u32 resource = 0; resource < 3u; resource++) {
auto &image_resource = program.info.images[resource];
image_resource.cube = resource == (cube_first ? 0u : 1u);
image_resource.dimension =
resource == (cube_first ? 1u : 0u)
? ShaderRecompiler::Decoder::ImageDimension::Dim2D
: ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
}
program.binding_layout_complete = false;
AllocateBindings(program);
spirv = ShaderRecompiler::Spirv::EmitProgram(program, {.compute = &compute});
ValidateSpirv(name, spirv);
Require(name, "mixed SPIR-V disassembly", tools.Disassemble(spirv, &text),
"failed to disassemble mixed indirect image shader");
Require(name, "mixed key switch",
CountText(text, "OpImageSampleExplicitLod") == 3 &&
CountText(text, "OpFSub") == 2,
"mixed candidates lost a sample or shared cube coordinate conversion");
std::vector<std::span<const u32>> definitions(spirv[3]);
u32 samples = 0;
for (size_t offset = 5; offset < spirv.size();) {
const auto words = std::span<const u32>(spirv).subspan(offset, spirv[offset] >> 16u);
const auto opcode = static_cast<spv::Op>(words[0] & 0xffffu);
if (opcode == spv::OpTypeVector) {
definitions[words[1]] = words;
} else if (opcode == spv::OpCompositeConstruct || opcode == spv::OpBitcast ||
opcode == spv::OpConstant) {
definitions[words[2]] = words;
} else if (opcode == spv::OpImageSampleExplicitLod) {
const auto coord = definitions[words[4]];
const auto lod = definitions[words[6]];
const auto components = samples == (cube_first ? 1u : 0u) ? 2u : 3u;
Require(name, "mixed coordinate and LOD layout",
!coord.empty() && definitions[coord[1]][3] == components &&
words[5] == spv::ImageOperandsLodMask && lod.size() == 4u &&
definitions[lod[3]].size() == 4u &&
definitions[lod[3]][3] == std::bit_cast<u32>(2.0f),
"candidate coordinates changed the instruction's LOD operand");
samples++;
}
offset += words.size();
}
Require(name, "mixed sample count", samples == 3u,
"mixed candidate switch did not retain every image");
}
}
TestCase ImageStoreMipSelectsPpsa01340Descriptor() {
@@ -27795,6 +27894,7 @@ std::vector<TestCase> MakeCases() {
AddCase(ImageGetResinfoDmaskWidthHeight);
AddCase(ImageGetResinfoDmaskMipLevels);
AddCase(ImageSampleAndGather);
AddCase(ImageCubeGradientsPreserveDerivatives);
AddCase(ImageGatherLodApproximatesLevelZero);
AddCase(ImageD16GatherPacksHalfPairs);
AddCase(ImageSampleA16SamplerCoordsOnGpu);
@@ -32727,6 +32827,13 @@ int main(int argc, char **argv) {
RunCase(nullptr, ImageSampleA16CompareBiasRdna2AddressOrder());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--indirect-image-only") == 0) {
CheckImageSamplerSpecialization();
CheckIndirectImageKeySwitch();
VulkanHarness vulkan;
RunCase(&vulkan, ImageCubeGradientsPreserveDerivatives());
return 0;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (argc == 2 && std::strcmp(argv[1], "--reverse-rt-death") == 0) {
RunReverseRenderTargetDeathCase();
@@ -32762,11 +32869,6 @@ int main(int argc, char **argv) {
RunCase(&vulkan, ImageStoreBgraUsesInverseSwizzle());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--indirect-image-only") == 0) {
CheckImageSamplerSpecialization();
CheckIndirectImageKeySwitch();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--storage-mip-host-only") == 0) {
VulkanHarness vulkan;
vulkan.CheckRenderExecutorStencilBindingDiscovery();