mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-27 05:16:56 +01:00
shader: fold reads through empty scalar descriptor tables
Co-authored-by: fxpw <fxpw@outlook.com>
This commit is contained in:
@@ -60,6 +60,15 @@ bool NullImageDescriptor(const DescriptorValue& descriptor) {
|
||||
return descriptor.dwords[0] == 0 && (descriptor.dwords[1] & 0xffu) == 0;
|
||||
}
|
||||
|
||||
bool NullBufferDescriptor(Value value) {
|
||||
const auto* handle = value.Resolve().TryInstruction();
|
||||
if (handle == nullptr || handle->GetOpcode() != ValueOpcode::GetBufferResource ||
|
||||
handle->NumArgs() != 4u) return false;
|
||||
for (size_t index = 0; index < 4u; ++index)
|
||||
if (handle->Arg(index).Resolve() != Value(0u)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ValidImageDescriptor(const DescriptorValue& descriptor, bool r128 = false) {
|
||||
const auto& words = descriptor.dwords;
|
||||
// Reject texture descriptors with nonzero reserved bits.
|
||||
@@ -1378,6 +1387,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
base.formatted ? descriptor.Format() : Prospero::BufferFormat::kInvalid;
|
||||
buffer.descriptor_swizzle = base.formatted ? descriptor.DstSelXYZW() : DstSel(4, 5, 6, 7);
|
||||
buffer.zero_stride_oob = descriptor.OutOfBounds() == 0u && stride == 0u;
|
||||
buffer.scalar_empty = base.scalar && descriptor.GetSize() == 0u;
|
||||
}
|
||||
snapshot.images.resize(program.info.images.size());
|
||||
specialization.images.resize(program.info.images.size());
|
||||
@@ -1543,6 +1553,17 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
}
|
||||
|
||||
auto& memory_info = program.memory_info;
|
||||
// Resolve empty scalar-table words before folding the indirect descriptors that use them.
|
||||
for (auto* block: program.blocks) {
|
||||
for (auto& inst: *block) {
|
||||
if (inst.GetOpcode() != ValueOpcode::ReadConstBuffer) continue;
|
||||
const auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
|
||||
if (memory.kind == ResourceKind::ScalarBuffer &&
|
||||
specialization.buffers[memory.resource].scalar_empty &&
|
||||
buffers[memory.resource].indirect_root == BufferResource::NoIndirectBuffer)
|
||||
inst.ReplaceUsesWith(Value(0u));
|
||||
}
|
||||
}
|
||||
const ImageRemap image_remap(specialization);
|
||||
for (auto* block: program.blocks) {
|
||||
for (auto it = block->begin(); it != block->end(); ++it) {
|
||||
@@ -1563,14 +1584,17 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
}
|
||||
if (BufferAccessOf(inst.GetOpcode()) == BufferAccess::Read) {
|
||||
const auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
|
||||
if (memory.kind == ResourceKind::Buffer &&
|
||||
const bool native_oob = memory.kind == ResourceKind::Buffer &&
|
||||
specialization.buffers[memory.resource].zero_stride_oob &&
|
||||
buffers[memory.resource].indirect_root == BufferResource::NoIndirectBuffer) {
|
||||
buffers[memory.resource].indirect_root == BufferResource::NoIndirectBuffer;
|
||||
const bool null_indirect = memory.kind == ResourceKind::IndirectBuffer &&
|
||||
NullBufferDescriptor(inst.Arg(0));
|
||||
if (native_oob || null_indirect) {
|
||||
// Bounds mode 0 checks offset >= stride, so zero stride
|
||||
// makes every vector read out of bounds regardless of its address.
|
||||
// makes every vector read out of bounds. An all-zero V# is unbound.
|
||||
const auto count = BufferComponentCount(inst.GetOpcode());
|
||||
std::array<Value, 4> values {Value(0u), Value(0u), Value(0u), Value(0u)};
|
||||
if (memory.formatted && !memory.typed) {
|
||||
if (native_oob && memory.formatted && !memory.typed) {
|
||||
const auto& buffer = buffers[memory.resource];
|
||||
const auto format = Format::GetFormatInfo(buffer.descriptor_format);
|
||||
const bool d16 = memory.data_bits == 16u;
|
||||
|
||||
@@ -13,6 +13,7 @@ struct ResourceSpecialization {
|
||||
Prospero::BufferFormat descriptor_format = Prospero::BufferFormat::kInvalid;
|
||||
uint32_t descriptor_swizzle = DstSel(4, 5, 6, 7);
|
||||
bool zero_stride_oob = false;
|
||||
bool scalar_empty = false;
|
||||
uint32_t indirect_root = BufferResource::NoIndirectBuffer;
|
||||
uint32_t indirect_mapping_offset = 0;
|
||||
uint32_t indirect_search_iterations = 0;
|
||||
|
||||
@@ -1024,6 +1024,115 @@ void TestCyclicControlPredicatesRemainUnknown() {
|
||||
"cyclic control lost a possible branch during reachability");
|
||||
}
|
||||
|
||||
void TestEmptyScalarBufferSpecialization() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
struct Fixture {
|
||||
Program program;
|
||||
std::vector<std::pair<Inst *, Type>> uses;
|
||||
Inst *nonnull = nullptr;
|
||||
};
|
||||
const auto make_fixture = [] {
|
||||
Fixture fixture;
|
||||
auto &program = fixture.program;
|
||||
program.stage = Libs::Graphics::ShaderType::Compute;
|
||||
program.srt_plan_complete = program.resource_tracking_complete = true;
|
||||
program.user_data_count = 4u;
|
||||
auto &block = AddValueBlock(program);
|
||||
DescriptorSource source;
|
||||
source.dword_count = 4u;
|
||||
for (uint32_t word = 0; word < 4u; ++word)
|
||||
source.dwords[word] = Value(&block.AppendNewInst(ValueOpcode::GetUserData,
|
||||
{Value(static_cast<ScalarReg>(word))}));
|
||||
program.descriptor_sources.push_back(source);
|
||||
program.info.buffers.push_back({.source = 0u, .read = true, .scalar = true});
|
||||
const auto table = Value(&block.AppendNewInst(ValueOpcode::GetBufferResource,
|
||||
{source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]}));
|
||||
std::array<Value, 4> words;
|
||||
for (uint32_t word = 0; word < 4u; ++word) {
|
||||
program.memory_info.push_back({.kind = ResourceKind::ScalarBuffer, .resource = 0u,
|
||||
.offset = word * 4u});
|
||||
auto &read = block.AppendNewInst(ValueOpcode::ReadConstBuffer, {table, Value(0u)});
|
||||
read.SetFlags(MemoryFlags{.index = word});
|
||||
words[word] = Value(&read);
|
||||
}
|
||||
const auto handle = Value(&block.AppendNewInst(ValueOpcode::GetBufferResource,
|
||||
{words[0], words[1], words[2], words[3]}));
|
||||
for (const auto opcode : {ValueOpcode::ReadConstBuffer, ValueOpcode::LoadBufferU8,
|
||||
ValueOpcode::LoadBufferU16, ValueOpcode::LoadBufferU32,
|
||||
ValueOpcode::LoadBufferU32x2, ValueOpcode::LoadBufferU32x3,
|
||||
ValueOpcode::LoadBufferU32x4}) {
|
||||
const auto index = static_cast<uint32_t>(program.memory_info.size());
|
||||
program.memory_info.push_back({.kind = ResourceKind::IndirectBuffer, .resource = UINT32_MAX,
|
||||
.data_dwords = BufferComponentCount(opcode),
|
||||
.data_bits = opcode == ValueOpcode::LoadBufferU8 ? 8u : opcode == ValueOpcode::LoadBufferU16 ? 16u : 32u,
|
||||
.formatted = opcode == ValueOpcode::LoadBufferU32});
|
||||
auto &read = opcode == ValueOpcode::ReadConstBuffer
|
||||
? block.AppendNewInst(opcode, {handle, Value(0u)})
|
||||
: block.AppendNewInst(opcode, {handle, Value(0u), Value(0u), Value(0u), Value(true)});
|
||||
read.SetFlags(MemoryFlags{.index = index});
|
||||
fixture.uses.emplace_back(&block.AppendNewInst(ValueOpcode::Identity, {Value(&read)}), read.GetType());
|
||||
}
|
||||
const auto nonnull = Value(&block.AppendNewInst(ValueOpcode::GetBufferResource,
|
||||
{words[0], words[1], words[2], Value(1u)}));
|
||||
auto &read = block.AppendNewInst(ValueOpcode::ReadConstBuffer, {nonnull, Value(0u)});
|
||||
read.SetFlags(MemoryFlags{.index = 4u});
|
||||
fixture.nonnull = &block.AppendNewInst(ValueOpcode::Identity, {Value(&read)});
|
||||
return fixture;
|
||||
};
|
||||
auto fixture = make_fixture();
|
||||
auto plan = ExtractResourcePlan(fixture.program);
|
||||
std::array<uint32_t, 4> descriptor{0x1000u, 8u << 16u, 0u, 3u << 28u};
|
||||
uint32_t reads = 0u;
|
||||
const SrtRuntime runtime{.user_data = descriptor, .read_memory = RejectSpecializationRead,
|
||||
.userdata = &reads, .read_specialization_memory = RejectSpecializationRead};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization, empty, populated;
|
||||
for (const uint32_t records : {0u, 1u, 0x20000000u, 0u}) {
|
||||
descriptor[2] = records;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) && reads == 0u &&
|
||||
specialization.buffers[0].scalar_empty == (records == 0u),
|
||||
"scalar emptiness read table contents or truncated its 64-bit byte size");
|
||||
if (records == 0u) {
|
||||
if (empty.buffers.empty()) empty = specialization;
|
||||
else Check(specialization == empty, "returning to an empty scalar table changed its specialization");
|
||||
}
|
||||
else if (records == 1u) populated = specialization;
|
||||
else Check(specialization == populated, "nonempty scalar size created a shader permutation");
|
||||
auto current = make_fixture();
|
||||
ApplyResourceSpecialization(current.program, specialization);
|
||||
for (const auto &[use, type] : current.uses) {
|
||||
const auto value = use->Arg(0).Resolve();
|
||||
Check(value.GetType() == type, "empty scalar-table folding changed the load result type");
|
||||
if (records != 0u) {
|
||||
Check(value.TryInstruction() != nullptr && BufferAccessOf(value.Instruction()->GetOpcode()) == BufferAccess::Read,
|
||||
"nonempty scalar table lost a runtime buffer load");
|
||||
} else if (value.IsImmediate()) {
|
||||
Check(value == Value(uint8_t{0}) || value == Value(uint16_t{0}) || value == Value(0u),
|
||||
"empty scalar table did not produce a typed zero");
|
||||
} else {
|
||||
for (size_t component = 0; component < value.Instruction()->NumArgs(); ++component)
|
||||
Check(value.Instruction()->Arg(component) == Value(0u),
|
||||
"empty scalar table retained a nonzero vector component");
|
||||
}
|
||||
}
|
||||
Check(current.nonnull->Arg(0).Resolve().TryInstruction() != nullptr,
|
||||
"a partly nonzero indirect descriptor was treated as unbound");
|
||||
}
|
||||
Check(empty != populated, "empty-to-populated scalar transition reused the folded specialization");
|
||||
plan.info.buffers[0].scalar = false;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
!specialization.buffers[0].scalar_empty,
|
||||
"vector-only resource acquired scalar-empty specialization");
|
||||
empty.buffers[0].indirect_root = 0u;
|
||||
empty.buffers.push_back(empty.buffers[0]);
|
||||
empty.buffers[1].scalar_empty = false;
|
||||
ApplyResourceSpecialization(fixture.program, empty);
|
||||
for (const auto &[use, type] : fixture.uses)
|
||||
Check(use->Arg(0).GetType() == type && use->Arg(0).Resolve().TryInstruction() != nullptr &&
|
||||
BufferAccessOf(use->Arg(0).ResolveInstruction()->GetOpcode()) == BufferAccess::Read,
|
||||
"empty root folded a scalar read with nonempty candidates");
|
||||
}
|
||||
|
||||
void TestMixedSamplerVariantsShareRuntimeDescriptor() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
auto program = MixedSamplerProgram();
|
||||
@@ -1145,6 +1254,7 @@ int main() {
|
||||
for (const bool independent_mask : {false, true})
|
||||
TestEmptyTraversalSkipsUnmappedDescriptor(independent_mask);
|
||||
TestCyclicControlPredicatesRemainUnknown();
|
||||
TestEmptyScalarBufferSpecialization();
|
||||
TestMixedSamplerVariantsShareRuntimeDescriptor();
|
||||
for (const bool mixed : {false, true}) TestIndirectImageSamplerClasses(mixed);
|
||||
std::puts("ResourceMaterializationTests: all cases passed");
|
||||
|
||||
@@ -36778,12 +36778,12 @@ void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) {
|
||||
for (const u32 table_bytes : {0u, 536u, 544u}) {
|
||||
TestCase test;
|
||||
test.name = wave == 32u ? "GpuSelectedRecordBufferAccess32" : "GpuSelectedRecordBufferAccess64";
|
||||
test.initial.assign((slot * 13u + keys.size() * wave * 8u) / 4u, 0xdeadbeefu);
|
||||
test.initial.assign((slot * 13u + keys.size() * wave * 16u) / 4u, 0xdeadbeefu);
|
||||
test.has_user_data = test.has_compute_info = true;
|
||||
const std::array<u32, 8> descriptors{selectors, 12u << 16u, keys.size(), 0x5204u,
|
||||
table, 0u, table_bytes, 0x5204u};
|
||||
std::copy(descriptors.begin(), descriptors.end(), test.user_data.begin());
|
||||
const std::array<u32, 4> output{slot * 13u, 8u << 16u, u32(keys.size()) * wave, 0x5204u};
|
||||
const std::array<u32, 4> output{slot * 13u, 16u << 16u, u32(keys.size()) * wave, 0x5204u};
|
||||
std::copy(output.begin(), output.end(), test.user_data.begin() + 16u);
|
||||
test.compute_info.wave_size = wave;
|
||||
test.compute_info.threads_num[0] = wave;
|
||||
@@ -36793,17 +36793,21 @@ void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) {
|
||||
test.compute_info.workgroup_register = 24u;
|
||||
test.compute_info.lds_size_dwords = 0;
|
||||
test.dispatch_x = keys.size();
|
||||
test.bda_mappings = {{0u, 0u}};
|
||||
auto &code = test.code;
|
||||
code.insert(code.end(), {EncodeSop2(0x26, 32, 24, InlineU32(12)),
|
||||
EncodeSmem0(0x08, 32, 0), EncodeSmem1(0u, 32), EncodeSopp(0x0c, 0),
|
||||
EncodeSop2(0x26, 33, 32, 255), 192u,
|
||||
EncodeSop2(0x02, 33, 33, 255), 96u,
|
||||
EncodeSmem0(0x0a, 36, 2), EncodeSmem1(144u, 33),
|
||||
EncodeSmem0(0x0a, 8, 2), EncodeSmem1(160u, 33),
|
||||
EncodeSmem0(0x0a, 12, 2), EncodeSmem1(176u, 33), EncodeSopp(0x0c, 0),
|
||||
EncodeMubuf0(0x83, 0, true, false), EncodeMubuf1(4, 2, 0),
|
||||
EncodeMubuf0(0x0a, 0, true, false), EncodeMubuf1(6, 9, 0),
|
||||
EncodeMubuf0(0x00, 0, true, false), EncodeMubuf1(7, 9, 0),
|
||||
EncodeSopp(0x0c, 0), EncodeSop2(0x1e, 34, 24, InlineU32(std::countr_zero(wave))),
|
||||
EncodeVop2(0x25, 20, 34, 0),
|
||||
EncodeMubuf0(0x1d, 0, true, false), EncodeMubuf1(4, 4, 20)});
|
||||
EncodeMubuf0(0x1e, 0, true, false), EncodeMubuf1(4, 4, 20)});
|
||||
AppendVMovLiteral(&code, 10, 0xa5c39b72u);
|
||||
AppendSMovLiteral(&code, 126, 0x55555555u);
|
||||
AppendSMovLiteral(&code, 127, wave == 64u ? 0x55555555u : 0u);
|
||||
@@ -36814,11 +36818,30 @@ void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) {
|
||||
static_cast<u32>(Prospero::BufferFormat::k16_16_16_16Float) << 12u | DstSel(4, 5, 6, 7),
|
||||
slot * (row + 7u), 1u << 16u, u32(keys.size()) * wave, 0x5204u};
|
||||
std::copy(record.begin(), record.end(), test.initial.begin() + (table + 192u + row * 64u) / 4u);
|
||||
// BDA loads use a separate tuple, as the captured shader's record members do.
|
||||
std::copy_n(record.begin(), 4u, test.initial.begin() + (table + 176u + row * 64u) / 4u);
|
||||
test.initial[slot * (row + 1u) / 4u] = ((0x3800u + row) << 16u) | (0x3c00u + row);
|
||||
test.initial[slot * (row + 1u) / 4u + 1u] = ((0xc000u + row) << 16u) | (0x4000u + row);
|
||||
}
|
||||
// Compile against poison selectors. A separate GPU dispatch writes the actual keys.
|
||||
auto compiled = CompileCase(test, vulkan.SubgroupSize());
|
||||
if (table_bytes == 0u)
|
||||
test.forbidden_spirv = {"convert_indirect_buffer_format"};
|
||||
else
|
||||
test.required_spirv = {"get_bda_pointer", "convert_indirect_buffer_format"};
|
||||
CheckSpirvEmission(test, compiled.spirv);
|
||||
u32 bda_function = 0, bda_calls = 0;
|
||||
for (size_t offset = 5; offset < compiled.spirv.size();) {
|
||||
const auto words = std::span(compiled.spirv).subspan(offset, compiled.spirv[offset] >> 16u);
|
||||
const auto opcode = static_cast<spv::Op>(words[0] & 0xffffu);
|
||||
if (opcode == spv::OpName &&
|
||||
std::string_view(reinterpret_cast<const char *>(words.data() + 2)) == "get_bda_pointer")
|
||||
bda_function = words[1];
|
||||
if (opcode == spv::OpFunctionCall && words[3] == bda_function) ++bda_calls;
|
||||
offset += words.size();
|
||||
}
|
||||
Require(test.name, "empty descriptor table specialization", (bda_calls == 0u) == (table_bytes == 0u),
|
||||
"empty table retained BDA reads or nonempty table lost GPU descriptor loads");
|
||||
for (const auto &descriptor : compiled.resources.buffers) {
|
||||
ShaderBufferResource resource{};
|
||||
std::copy_n(descriptor.dwords.begin(), 4u, resource.fields);
|
||||
@@ -36846,15 +36869,18 @@ void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) {
|
||||
const u32 dynamic = keys[group] * 192u + 96u;
|
||||
const u32 row = dynamic / 64u;
|
||||
for (u32 lane = 0; lane < wave; ++lane) {
|
||||
const u32 out = slot * 13u / 4u + (group * wave + lane) * 2u;
|
||||
const u32 out = slot * 13u / 4u + (group * wave + lane) * 4u;
|
||||
const bool source = uint64_t{dynamic} + 160u + 16u <= table_bytes && lane == 0u;
|
||||
test.expected[out] = source ? test.initial[slot * (row + 1u) / 4u] : 0u;
|
||||
test.expected[out + 1u] = source ? test.initial[slot * (row + 1u) / 4u + 1u] : 0u;
|
||||
const bool bda_source = uint64_t{dynamic} + 144u + 16u <= table_bytes && lane == 0u;
|
||||
test.expected[out + 2u] = bda_source ? 0x3c00u + row : 0u;
|
||||
test.expected[out + 3u] = bda_source ? 0x3f800000u + (row << 13u) : 0u;
|
||||
if (uint64_t{dynamic} + 176u + 16u <= table_bytes && (lane & 1u) == 0u)
|
||||
bytes[slot * (row + 7u) + group * wave + lane] = std::byte{0xc3};
|
||||
}
|
||||
}
|
||||
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size());
|
||||
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size(), true);
|
||||
vulkan.Dispatch(producer, producer_shader, buffer);
|
||||
vulkan.Dispatch(test, compiled, buffer);
|
||||
CompareWords(test, "GPU selectors, wrapped offsets, partial descriptors, OOB and masked byte stores",
|
||||
|
||||
Reference in New Issue
Block a user