shader: share finite descriptor planning with GPU-selected inline samplers

This commit is contained in:
nmzik committed 2026-10-09 15:06:39 +00:00
1 parent bb5edbd490
commit 8c81386568
9 files changed
+383 -56

No files matched your search

+124
View File
@@ -2648,6 +2648,129 @@ void TestFiniteImagePhiCycle() {
}
}
void TestFiniteInlineSampler() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
Fixture fixture;
auto* entry = fixture.block;
auto* defined = fixture.AddBlock();
auto* merge = fixture.AddBlock();
entry->AddBranch(defined);
entry->AddBranch(merge);
defined->AddBranch(merge);
const auto lane = fixture.Emit(ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
entry->condition = fixture.Emit(ValueOpcode::IEqual32, {lane, Value(0u)});
entry->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = defined, .false_block = merge};
defined->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = merge};
merge->terminator.kind = CFG::TerminatorKind::Return;
fixture.block = merge;
auto& lod = merge->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
auto& filter = merge->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
lod.AddPhiOperand(entry, Value(0u));
lod.AddPhiOperand(defined, Value(0x00fff000u));
filter.AddPhiOperand(defined, Value(0x0a500000u));
filter.AddPhiOperand(entry, Value(0u));
const auto sampler = fixture.Sampler({Value(0u), Value(&lod), Value(&filter), Value(0u)});
const auto direct = fixture.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)});
const auto previous = fixture.Sampler(
{Value(0u), Value(0x00fff000u), Value(0x0a500000u), Value(0u)});
using F = Libs::Graphics::Prospero::BufferFormat;
for (const auto format : {F::k32Float, F::k32SInt}) {
const auto image = fixture.Image({Value(0x1000u), Value(uint32_t(format) << 20u),
Value(0u), Value(Libs::Graphics::DstSel(4, 5, 6, 7) |
(uint32_t(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u)),
Value(0u), Value(0u), Value(0u), Value(0u)});
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
if (format == F::k32Float)
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, previous, fixture.ImageAddress()},
fixture.AddMemory(memory, 0x440u));
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.AddMemory(memory, 0x1dd0u));
if (format == F::k32Float)
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, direct, fixture.ImageAddress()},
fixture.AddMemory(memory, 0x1dd8u));
}
fixture.PlanAndTrack();
const auto root = fixture.program.memory_info[1].sampler;
const auto source = fixture.program.info.samplers[root].source;
const auto& finite = fixture.program.descriptor_sources[source].indirect_descriptor;
Check(finite && finite->sources.size() == 2 && fixture.program.info.samplers.size() == 3,
"finite inline sampler lost either state or aliased the direct sampler");
const auto* key = sampler.Instruction()->Arg(0).Instruction();
Check(key->GetOpcode() == ValueOpcode::Phi && key->Parent() == merge &&
key->PhiBlock(0) == entry && key->PhiBlock(1) == defined &&
key->Arg(0).U32() == 0 && key->Arg(1).U32() == 1,
"inline sampler DWORD correlation did not preserve its GPU selector");
const auto plan = ExtractResourcePlan(fixture.program);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const SrtRuntime runtime{.read_specialization_memory = ReadTestMemory};
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"finite inline sampler could not materialize literal choices");
const auto candidates = fixture.program.info.samplers[root].indirect_resources;
Check(candidates.size() == 2 && snapshot.samplers[candidates[0]].dwords[1] == 0 &&
snapshot.samplers[candidates[0]].dwords[2] == 0 &&
snapshot.samplers[candidates[1]].dwords[1] == 0x00fff000u &&
snapshot.samplers[candidates[1]].dwords[2] == 0x0a500000u,
"finite sampler materialization mixed the two correlated DWORD states");
Check(candidates[1] == fixture.program.memory_info[0].sampler && candidates[1] < root,
"finite sampler did not reuse the earlier constant candidate");
ApplyResourceSpecialization(fixture.program, specialization);
for (const uint32_t operation : {1u, 3u}) {
const auto selected = fixture.program.memory_info[operation].sampler;
const auto& choices = fixture.program.info.samplers[selected].indirect_resources;
Check(choices.size() == 2, "sampler class specialization lost a finite candidate");
for (uint32_t ordinal = 0; ordinal < choices.size(); ++ordinal) {
const auto& choice = fixture.program.info.samplers[choices[ordinal]];
Check(choice.snapshot_index == candidates[ordinal] &&
choice.force_point_filtering == (operation == 3u) &&
choice.integer_border == (operation == 3u),
"finite sampler class remapping changed candidate identity or filtering");
}
}
Check(fixture.program.info.samplers[fixture.program.memory_info[2].sampler]
.indirect_resources.empty(),
"direct sampler was changed into a dynamic selection");
auto invalid = ExtractResourcePlan(fixture.program);
invalid.info.samplers[0].source = static_cast<uint32_t>(invalid.descriptor_sources.size());
Check(!MaterializeResources(invalid, runtime, snapshot, specialization),
"invalid sampler source bypassed materialization validation");
Fixture adjusted;
auto* initial = adjusted.block;
auto* alternate = adjusted.AddBlock();
auto* join = adjusted.AddBlock();
initial->AddBranch(alternate);
initial->AddBranch(join);
alternate->AddBranch(join);
initial->condition = adjusted.Emit(ValueOpcode::IEqual32,
{adjusted.Emit(ValueOpcode::LaneId), Value(0u)});
initial->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = alternate, .false_block = join};
alternate->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = join};
join->terminator.kind = CFG::TerminatorKind::Return;
auto& selected = join->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
selected.AddPhiOperand(initial, Value(0u));
selected.AddPhiOperand(alternate, Value(0x00fff000u));
adjusted.block = join;
const auto shared = adjusted.Sampler({Value(0u), Value(&selected), Value(0u), Value(0u)});
const auto image = adjusted.Image({Value(0u), Value(0u), Value(0u), Value(0u),
Value(0u), Value(0u), Value(0u), Value(0u)});
for (const uint32_t flags : {0u, uint32_t(Decoder::ImageSampleFlagAdjust)}) {
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_sample_flags = flags;
adjusted.Emit(ValueOpcode::ImageSampleRaw, {image, shared, adjusted.ImageAddress()},
adjusted.AddMemory(memory, 0x1dd0u));
}
CheckFatal([&] { adjusted.PlanAndTrack(); }, "not a valid runtime value",
"finite sampler planning accepted a shared SampleAdjust handle");
}
void TestFiniteImageBitScanSentinel() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
for (const bool nonzero : {false, true}) {
@@ -3608,6 +3731,7 @@ int main() {
Run("writable descriptor phi", TestWritableDescriptorPhi);
Run("conditional sampler phi", TestConditionalSamplerPhi);
Run("finite image phi cycle", TestFiniteImagePhiCycle);
Run("finite inline sampler", TestFiniteInlineSampler);
Run("finite image bit scan sentinel", TestFiniteImageBitScanSentinel);
Run("runtime-rooted loop", TestLoopCycleEnteredThroughRuntimeValue);
Run("invariant loop phi", TestInvariantLoopPhi);
+125 -5
View File
@@ -15356,7 +15356,8 @@ public:
const Image *storage_image = nullptr,
const Image *storage_image_uint = nullptr,
vk::Sampler sampler = nullptr,
std::span<const Image> sampled_resources = {}) {
std::span<const Image> sampled_resources = {},
std::span<const vk::Sampler> sampler_resources = {}) {
using Kind = ShaderRecompiler::IR::DescriptorBindingKind;
const auto &layout = compiled.program.bindings;
auto shader_data = compiled.packed_user_data;
@@ -15671,11 +15672,12 @@ public:
writes.push_back(write);
}
if (Count(Kind::Samplers) != 0) {
Require(test.name, "dispatch", sampler != nullptr,
"sampler descriptor requested but no sampler was provided");
Require(test.name, "dispatch", sampler_resources.empty()
? sampler != nullptr : sampler_resources.size() == Count(Kind::Samplers),
"sampler resources must match the native sampler array");
sampler_infos.resize(Count(Kind::Samplers));
for (auto &info : sampler_infos) {
info.sampler = sampler;
for (u32 index = 0; index < sampler_infos.size(); ++index) {
sampler_infos[index].sampler = sampler_resources.empty() ? sampler : sampler_resources[index];
}
vk::WriteDescriptorSet write{};
write.dstSet = descriptor_set;
@@ -34947,6 +34949,122 @@ void CheckIndirectBufferStore(VulkanHarness &vulkan) {
std::printf("[compute] %-32s ok\n", test.name);
}
void CheckFiniteInlineSamplerPhi(VulkanHarness &vulkan) {
TestCase test;
test.name = "FiniteInlineSamplerPhi";
auto &code = test.code;
code.insert(code.end(), {EncodeSop1(0x04, 36, InlineU32(0)),
EncodeSop1(0x04, 38, InlineU32(0)), EncodeSop1(0x04, 24, 126),
EncodeVop1(0x01, 10, 16), EncodeVopc(0xd5, InlineU32(0), 10)});
const auto skip_sampler = code.size();
code.push_back(EncodeSopp(0x08));
// Actual correlated inline sampler words from 905b2e2c: zero or {0,fff000,a500000,0}.
code.insert(code.end(), {EncodeSop2(0x24, 37, InlineU32(12), InlineU32(12)),
EncodeSop1(0x04, 38, 255), 0x0a500000u});
code[skip_sampler] = EncodeSopp(0x08, code.size() - skip_sampler - 1u);
code.push_back(EncodeSop1(0x04, 126, 24));
AppendVMovLiteral(&code, 20, std::bit_cast<u32>(0.5f));
AppendVMovLiteral(&code, 21, std::bit_cast<u32>(0.5f));
AppendVMovLiteral(&code, 1, 0x12345678u);
AppendVMovLiteral(&code, 2, 0x87654321u);
code.insert(code.end(), {EncodeVopc(0xd4, InlineU32(2), 10),
EncodeMimg0(0x27, 1), EncodeMimg1(1, 20, 0, 9),
EncodeMimg0(0x27, 1), EncodeMimg1(2, 20, 2, 9),
EncodeSop1(0x04, 126, 24), EncodeVop1(0x01, 30, 16),
EncodeVop2(0x1a, 31, InlineU32(3), 30),
EncodeMubuf0(0x1c), EncodeMubuf1(1, 12, 31),
EncodeMubuf0(0x1c, 4), EncodeMubuf1(2, 12, 31)});
AppendEnd(&code);
test.initial.assign(7, 0xa5a5a5a5u);
test.expected = {std::bit_cast<u32>(4.0f), 0xfffffff9u,
std::bit_cast<u32>(2.0f), 0xfffffff9u, 0x12345678u, 0x87654321u, 0xa5a5a5a5u};
test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32_32_32_32Float);
test.user_data[3] |= static_cast<u32>(Prospero::ImageType::kColor2D) << 28u;
const auto signed_image = MakeSampledTextureData(Prospero::BufferFormat::k32SInt);
std::copy_n(signed_image.begin(), 8, test.user_data.begin() + 8);
test.user_data[11] |= static_cast<u32>(Prospero::ImageType::kColor2D) << 28u;
test.user_data[50] = test.initial.size() * sizeof(u32);
test.user_data[51] = 3u << 28u;
test.has_user_data = true;
test.compute_info.threads_num[0] = test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1;
test.compute_info.wave_size = 32;
test.compute_info.group_id[0] = true;
test.compute_info.workgroup_register = 16;
test.has_compute_info = true;
test.dispatch_x = 3;
test.opcodes = {ShaderOpcode::S_MOV_B64, ShaderOpcode::S_BFM_B32,
ShaderOpcode::S_CBRANCH_EXECZ, ShaderOpcode::V_MOV_B32, ShaderOpcode::V_CMPX_NE_U32,
ShaderOpcode::V_CMPX_GT_U32, ShaderOpcode::V_LSHLREV_B32,
ShaderOpcode::IMAGE_SAMPLE, ShaderOpcode::BUFFER_STORE_DWORD, ShaderOpcode::S_ENDPGM};
test.required_spirv = {"OpPhi", "NonUniform", "OpImageSampleExplicitLod"};
auto compiled = CompileCase(test, vulkan.SubgroupSize());
std::set<u32> nonuniform_ids;
std::vector<std::pair<u32, u32>> sampled_ids;
for (size_t offset = 5; offset < compiled.spirv.size();) {
const auto words = std::span(compiled.spirv).subspan(offset, compiled.spirv[offset] >> 16u);
if (static_cast<spv::Op>(words[0] & 0xffffu) == spv::OpDecorate &&
words[2] == spv::DecorationNonUniform) nonuniform_ids.insert(words[1]);
if (static_cast<spv::Op>(words[0] & 0xffffu) == spv::OpSampledImage)
sampled_ids.emplace_back(words[2], words[4]);
offset += words.size();
}
Require(test.name, "dynamic sampler decorations", sampled_ids.size() >= 2 &&
std::ranges::all_of(sampled_ids, [&](const auto &ids) {
return nonuniform_ids.contains(ids.first) && nonuniform_ids.contains(ids.second);
}),
"fetched sampler or sampled image lost its NonUniform decoration");
const auto &samplers = compiled.program.info.samplers;
u32 roots = 0;
for (const auto &root : samplers) {
if (root.indirect_resources.empty()) continue;
++roots;
Require(test.name, "correlated sampler candidates", root.indirect_resources.size() == 2,
"finite sampler was folded or lost a correlated branch state");
for (const auto child : root.indirect_resources)
Require(test.name, "sampler class remapping", child < samplers.size() &&
samplers[child].force_point_filtering == root.force_point_filtering &&
samplers[child].integer_border == root.integer_border,
"a finite candidate retained another image class's sampler");
}
Require(test.name, "finite sampler class variants", roots == 2 && samplers.size() == 4 &&
compiled.resources.samplers.size() == 2,
"the two-state sampler did not retain both float and signed image classes");
constexpr std::array<u32, 4> zero_sampler{};
constexpr std::array<u32, 4> linear_sampler{0, 0x00fff000u, 0x0a500000u, 0};
Require(test.name, "inline sampler states",
std::ranges::all_of(compiled.resources.samplers, [&](const auto &value) {
return std::equal(zero_sampler.begin(), zero_sampler.end(), value.dwords.begin()) ||
std::equal(linear_sampler.begin(), linear_sampler.end(), value.dwords.begin());
}) && compiled.resources.samplers[0] != compiled.resources.samplers[1],
"descriptor words were mixed between the two native sampler states");
auto pixels = MakeRgbaImage(4, 4);
for (u32 y = 0; y < 4; ++y)
for (u32 x = 2; x < 4; ++x) SetRgbaPixel(&pixels, 4, x, y, std::bit_cast<u32>(4.0f), 0, 0, 0);
std::array<VulkanHarness::Image, 2> images{
vulkan.CreateImageMips(test.name, 4, 4, vk::Format::eR32G32B32A32Sfloat,
vk::ImageUsageFlagBits::eSampled, {pixels}, 4, vk::ImageLayout::eShaderReadOnlyOptimal,
vk::ImageType::e2D, vk::ImageViewType::e2D, 1),
vulkan.CreateImageMips(test.name, 4, 4, vk::Format::eR32Sint,
vk::ImageUsageFlagBits::eSampled, {std::vector<u32>(16, 0xfffffff9u)}, 1,
vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageType::e2D, vk::ImageViewType::e2D, 1)};
std::vector<vk::Sampler> native_samplers;
for (const auto &resource : samplers) {
ShaderSamplerResource descriptor{};
std::copy_n(compiled.resources.samplers[resource.snapshot_index].dwords.begin(), 4, descriptor.fields);
const auto filter = !resource.force_point_filtering && descriptor.XyMagFilter() == 1
? vk::Filter::eLinear : vk::Filter::eNearest;
native_samplers.push_back(vulkan.CreateSampler(test.name, 1, filter));
}
auto output = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size());
vulkan.Dispatch(test, compiled, output, nullptr, nullptr, nullptr, nullptr, nullptr, images, native_samplers);
CompareWords(test, "point, linear, remapped signed sampler and inactive output", test.expected,
vulkan.ReadBuffer(test.name, output, test.expected.size()));
vulkan.DestroyBuffer(&output);
for (const auto sampler : native_samplers) vulkan.Device().destroySampler(sampler);
for (auto &image : images) vulkan.DestroyImage(&image);
std::printf("[compute] %-32s ok\n", test.name);
}
void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
constexpr const char *name = "IndirectImageKeySwitch";
constexpr uint32_t mapping_capacity = 1793u;
@@ -43191,6 +43309,7 @@ int main(int argc, char **argv) {
CheckImageSamplerSpecialization();
VulkanHarness vulkan;
CheckIndirectImageKeySwitch(vulkan);
CheckFiniteInlineSamplerPhi(vulkan);
RunCase(&vulkan, ImageCubeGradientsPreserveDerivatives());
return 0;
}
@@ -43354,6 +43473,7 @@ int main(int argc, char **argv) {
CheckRuntimeBufferRecords(vulkan);
CheckComputeThreadDimensions(vulkan);
CheckIndirectImageKeySwitch(vulkan);
CheckFiniteInlineSamplerPhi(vulkan);
CheckWave64WholeWaveResults();
CheckPs5GameExampleImageClearRuntimeShape();
vulkan.CheckSchedulerTimeline();