mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-20 02:47:02 +02:00
shader: detect full buffer fills from uniform stores
Co-authored-by: Techx3 <56268269+Techx3@users.noreply.github.com>
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
#include "libs/errno.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
@@ -82,46 +83,54 @@ bool ResolveComputeImageClear(const ShaderComputeInputInfo& input, uint32_t grou
|
||||
uint64_t& resolved_size) {
|
||||
const auto& program = *input.stage.program;
|
||||
const auto& resources = input.stage.resources;
|
||||
if (program.info.buffers.size() != 1 || resources.buffers.size() != 1 ||
|
||||
!program.info.images.empty() || !program.info.samplers.empty() || program.info.uses_dma ||
|
||||
!resources.images.empty() || !resources.samplers.empty()) {
|
||||
const auto& fill = resources.buffer_fill;
|
||||
if (fill.element_size == 0 || fill.element_size > 16 || fill.element_size % 4 != 0 ||
|
||||
fill.buffer >= resources.buffers.size() ||
|
||||
resources.buffers.size() != program.info.buffers.size()) {
|
||||
return false;
|
||||
}
|
||||
const auto& resource = program.info.buffers.front();
|
||||
const auto& raw = resources.buffers.front();
|
||||
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(raw);
|
||||
const auto& resource = program.info.buffers[fill.buffer];
|
||||
const auto& raw = resources.buffers[fill.buffer];
|
||||
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(raw);
|
||||
constexpr std::array formats {
|
||||
Prospero::BufferFormat::k32UInt, Prospero::BufferFormat::k32_32UInt,
|
||||
Prospero::BufferFormat::k32_32_32UInt, Prospero::BufferFormat::k32_32_32_32UInt};
|
||||
if (!resource.formatted || !resource.written || resource.read || resource.atomic ||
|
||||
resource.scalar || resource.max_byte_extent != 16 || descriptor.Stride() != 16 ||
|
||||
descriptor.Format() != Prospero::BufferFormat::k32_32_32_32UInt ||
|
||||
descriptor.SwizzleEnabled() || descriptor.IndexStride() != 0 || descriptor.AddTid() ||
|
||||
resource.scalar || descriptor.Stride() != fill.element_size ||
|
||||
descriptor.Format() != formats[fill.element_size / 4 - 1] || descriptor.SwizzleEnabled() ||
|
||||
descriptor.IndexStride() != 0 || descriptor.AddTid() ||
|
||||
resource.packed_stride != descriptor.PackedStride() || raw.dword_count != 4 ||
|
||||
program.user_data_base != 0 || resources.user_data.size() != 8) {
|
||||
descriptor.Base48() == 0) {
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 0; i < raw.dword_count; i++) {
|
||||
if (raw.dwords[i] != resources.user_data[i]) {
|
||||
if (input.threads_num[0] == 0 || input.threads_num[0] != fill.group_stride ||
|
||||
input.threads_num[1] != 1 || input.threads_num[2] != 1 || group_x == 0 || group_y != 1 ||
|
||||
group_z != 1 || mode != (input.dispatch_thread_dimensions ? 0x61u : 0x41u)) {
|
||||
return false;
|
||||
}
|
||||
const uint64_t invocations = input.dispatch_thread_dimensions
|
||||
? group_x
|
||||
: static_cast<uint64_t>(group_x) * input.threads_num[0];
|
||||
const auto size = BufferDescriptorSize(descriptor);
|
||||
if (invocations != descriptor.NumRecords() || size == 0 || size > UINT32_MAX ||
|
||||
descriptor.Base48() > UINT64_MAX - size ||
|
||||
(input.dispatch_thread_dimensions &&
|
||||
(group_x % input.threads_num[0] != 0 || input.dispatch_threads_num[0] != group_x ||
|
||||
input.dispatch_threads_num[1] != 1 || input.dispatch_threads_num[2] != 1))) {
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 0; i < resources.buffers.size(); ++i) {
|
||||
if (i == fill.buffer) continue;
|
||||
const auto source = DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
|
||||
const auto bytes = BufferDescriptorSize(source);
|
||||
if (source.Base48() > UINT64_MAX - bytes ||
|
||||
(source.Base48() < descriptor.Base48() + size &&
|
||||
descriptor.Base48() < source.Base48() + bytes)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const uint32_t clear = resources.user_data[4];
|
||||
if (resources.user_data[5] != clear || resources.user_data[6] != clear ||
|
||||
resources.user_data[7] != clear) {
|
||||
return false;
|
||||
}
|
||||
const bool full_dispatch =
|
||||
input.dispatch_thread_dimensions && input.threads_num[0] == 64 &&
|
||||
input.threads_num[1] == 1 && input.threads_num[2] == 1 && group_x != 0 && group_y == 1 &&
|
||||
group_z == 1 && input.dispatch_threads_num[0] == group_x &&
|
||||
input.dispatch_threads_num[1] == 1 && input.dispatch_threads_num[2] == 1 &&
|
||||
input.group_id[0] && !input.group_id[1] && !input.group_id[2] &&
|
||||
input.thread_ids_num == 1 && input.wave_size == 64 && !input.tg_size_en && mode == 0x61u &&
|
||||
group_x % input.threads_num[0] == 0 && descriptor.NumRecords() == group_x;
|
||||
const auto size = BufferDescriptorSize(descriptor);
|
||||
if (!full_dispatch || size == 0) {
|
||||
return false;
|
||||
}
|
||||
resolved_descriptor = descriptor;
|
||||
resolved_clear = clear;
|
||||
resolved_clear = fill.value;
|
||||
resolved_size = size;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -16,12 +16,22 @@ struct DescriptorValue {
|
||||
}
|
||||
};
|
||||
|
||||
struct BufferFill {
|
||||
uint32_t buffer = 0;
|
||||
uint32_t group_stride = 0;
|
||||
uint32_t element_size = 0;
|
||||
uint32_t value = 0;
|
||||
|
||||
bool operator==(const BufferFill&) const = default;
|
||||
};
|
||||
|
||||
struct ResourceSnapshot {
|
||||
std::vector<DescriptorValue> buffers;
|
||||
std::vector<DescriptorValue> images;
|
||||
std::vector<DescriptorValue> samplers;
|
||||
std::vector<uint32_t> flattened_srt;
|
||||
std::vector<uint32_t> user_data;
|
||||
BufferFill buffer_fill;
|
||||
};
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
||||
@@ -490,8 +490,13 @@ struct CompiledShaderInfo {
|
||||
BindingLayout bindings;
|
||||
};
|
||||
|
||||
// Immutable runtime resource analysis retained by the shader cache. It owns the descriptor/SRT
|
||||
// and uniform condition values, plus resource-use edges, without retaining translated blocks.
|
||||
struct BufferFillPlan {
|
||||
BufferFill fill;
|
||||
std::array<Value, 4> values;
|
||||
};
|
||||
|
||||
// Immutable runtime resource analysis retained by the shader cache. It owns descriptor/SRT,
|
||||
// uniform condition and fill values without retaining translated blocks.
|
||||
struct ResourcePlan {
|
||||
ResourcePlan() = default;
|
||||
~ResourcePlan();
|
||||
@@ -516,6 +521,7 @@ struct ResourcePlan {
|
||||
bool srt_plan_complete = false;
|
||||
bool resource_tracking_complete = false;
|
||||
ShaderInfo info;
|
||||
BufferFillPlan buffer_fill;
|
||||
};
|
||||
|
||||
struct Program: ResourcePlan {
|
||||
|
||||
@@ -299,8 +299,19 @@ static bool MaterializeSnapshot(const ResourcePlan& program, const SrtRuntime& r
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& next = snapshot.resources;
|
||||
auto cursor = values.begin();
|
||||
auto& next = snapshot.resources;
|
||||
const auto& fill = program.buffer_fill;
|
||||
const auto words = fill.fill.element_size / sizeof(uint32_t);
|
||||
std::array<uint32_t, 4> stored {};
|
||||
if (words != 0 &&
|
||||
EvaluateUniformValues(program, std::span(fill.values).first(words), runtime,
|
||||
std::span(stored).first(words)) &&
|
||||
std::all_of(stored.begin(), stored.begin() + words,
|
||||
[&](uint32_t value) { return value == stored[0]; })) {
|
||||
next.buffer_fill = fill.fill;
|
||||
next.buffer_fill.value = stored[0];
|
||||
}
|
||||
auto cursor = values.begin();
|
||||
next.buffers.assign(cursor, cursor + program.info.buffers.size());
|
||||
cursor += program.info.buffers.size();
|
||||
next.flattened_srt = std::move(flattened_srt);
|
||||
@@ -609,11 +620,11 @@ bool BuildSamplerPlan(const ShaderInfo& base, const Images& images, SamplerPlan&
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool IntegerCondition(const ResourcePlan& program, Value condition) {
|
||||
if (!ValidateRuntimeValue(program, condition)) {
|
||||
static bool UniformIntegerValue(const ResourcePlan& program, Value root) {
|
||||
if (!ValidateRuntimeValue(program, root)) {
|
||||
return false;
|
||||
}
|
||||
std::vector<Value> pending {condition};
|
||||
std::vector<Value> pending {root};
|
||||
std::unordered_set<const Inst*> visited;
|
||||
while (!pending.empty()) {
|
||||
const auto value = pending.back().Resolve();
|
||||
@@ -659,7 +670,7 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
|
||||
case CFG::TerminatorKind::Branch: successors.push_back(terminator.true_block); break;
|
||||
case CFG::TerminatorKind::ConditionalBranch:
|
||||
successors = {terminator.true_block, terminator.false_block};
|
||||
if (IntegerCondition(program, info.condition)) {
|
||||
if (UniformIntegerValue(program, info.condition)) {
|
||||
block.condition = info.condition;
|
||||
}
|
||||
break;
|
||||
@@ -713,6 +724,118 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// Nonnegative affine coefficients for constant, local X and workgroup X. Reject modular
|
||||
// arithmetic that could wrap; runtime coverage also bounds the largest invocation index.
|
||||
static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t depth = 0) {
|
||||
value = value.Resolve();
|
||||
if (depth > 32 || value.GetType() != Type::U32) {
|
||||
return {};
|
||||
}
|
||||
if (value.IsImmediate()) {
|
||||
return std::array<uint64_t, 3> {value.U32(), 0, 0};
|
||||
}
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) {
|
||||
return {};
|
||||
}
|
||||
const auto op = inst->GetOpcode();
|
||||
if (op == ValueOpcode::GetBuiltin && inst->Arg(1) == Value(0u)) {
|
||||
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId))) {
|
||||
return std::array<uint64_t, 3> {0, 1, 0};
|
||||
}
|
||||
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::WorkgroupId))) {
|
||||
return std::array<uint64_t, 3> {0, 0, 1};
|
||||
}
|
||||
}
|
||||
if (op != ValueOpcode::IAdd32 && op != ValueOpcode::IMul32 &&
|
||||
op != ValueOpcode::ShiftLeftLogical32) {
|
||||
return {};
|
||||
}
|
||||
auto left = FillIndex(inst->Arg(0), depth + 1);
|
||||
auto right = FillIndex(inst->Arg(1), depth + 1);
|
||||
if (!left || !right) {
|
||||
return {};
|
||||
}
|
||||
if (op == ValueOpcode::IMul32 && ((*right)[1] != 0 || (*right)[2] != 0)) {
|
||||
std::swap(left, right);
|
||||
}
|
||||
if (op != ValueOpcode::IAdd32 && ((*right)[1] != 0 || (*right)[2] != 0)) {
|
||||
return {};
|
||||
}
|
||||
if (op == ValueOpcode::ShiftLeftLogical32) {
|
||||
if ((*right)[0] >= 32) return {};
|
||||
(*right)[0] = uint64_t {1} << (*right)[0];
|
||||
}
|
||||
for (uint32_t i = 0; i < left->size(); ++i) {
|
||||
(*left)[i] =
|
||||
op == ValueOpcode::IAdd32 ? (*left)[i] + (*right)[i] : (*left)[i] * (*right)[0];
|
||||
if ((*left)[i] > UINT32_MAX) return {};
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
static BufferFillPlan AnalyzeBufferFill(const Program& program) {
|
||||
if (program.stage != ShaderType::Compute || program.blocks.empty() ||
|
||||
program.blocks.size() != program.block_info.size() || program.info.uses_dma ||
|
||||
!program.info.images.empty() || !program.info.samplers.empty()) {
|
||||
return {};
|
||||
}
|
||||
std::unordered_set<uint32_t> visited;
|
||||
uint32_t index = 0;
|
||||
const Inst* store = nullptr;
|
||||
for (;;) {
|
||||
if (!visited.insert(index).second) return {};
|
||||
for (const auto& inst: *program.blocks[index]) {
|
||||
if (AddressOpcodeInfoOf(inst.GetOpcode()).access != AddressAccess::None) return {};
|
||||
if (!inst.MayHaveSideEffects()) continue;
|
||||
if (store != nullptr || BufferAccessOf(inst.GetOpcode()) != BufferAccess::Write)
|
||||
return {};
|
||||
store = &inst;
|
||||
}
|
||||
const auto& term = program.block_info[index].terminator;
|
||||
if (term.kind == CFG::TerminatorKind::Return) break;
|
||||
if (term.kind != CFG::TerminatorKind::Branch) return {};
|
||||
const auto next = std::ranges::find(program.block_info, term.true_block, &BlockInfo::id);
|
||||
if (next == program.block_info.end()) return {};
|
||||
index = static_cast<uint32_t>(next - program.block_info.begin());
|
||||
}
|
||||
if (store == nullptr || visited.size() != program.blocks.size()) return {};
|
||||
const auto op = store->GetOpcode();
|
||||
constexpr std::array stores {ValueOpcode::StoreBufferU32, ValueOpcode::StoreBufferU32x2,
|
||||
ValueOpcode::StoreBufferU32x3, ValueOpcode::StoreBufferU32x4};
|
||||
const auto store_op = std::ranges::find(stores, op);
|
||||
if (store_op == stores.end() || store->Arg(2).Resolve() != Value(0u) ||
|
||||
store->Arg(3).Resolve() != Value(0u) || store->Arg(5).Resolve() != Value(true))
|
||||
return {};
|
||||
const auto& memory = program.memory_info.at(store->Flags<MemoryFlags>().index);
|
||||
if (!memory.formatted || memory.typed || !memory.idxen || memory.offen || memory.offset != 0 ||
|
||||
memory.data_bits != 32 ||
|
||||
memory.data_dwords != static_cast<uint32_t>(store_op - stores.begin() + 1))
|
||||
return {};
|
||||
const auto address = FillIndex(store->Arg(1));
|
||||
if (!address || (*address)[0] != 0 || (*address)[1] != 1 || (*address)[2] == 0) return {};
|
||||
for (const auto& buffer: program.info.buffers) {
|
||||
if (buffer.read && (!buffer.scalar || buffer.written)) return {};
|
||||
}
|
||||
BufferFillPlan result;
|
||||
result.fill = {memory.resource, static_cast<uint32_t>((*address)[2]), memory.data_dwords * 4,
|
||||
0};
|
||||
const auto data = store->Arg(4).Resolve();
|
||||
const auto* vector = data.TryInstruction();
|
||||
constexpr std::array composites {ValueOpcode::CompositeConstructU32x2,
|
||||
ValueOpcode::CompositeConstructU32x3,
|
||||
ValueOpcode::CompositeConstructU32x4};
|
||||
for (uint32_t i = 0; i < memory.data_dwords; ++i) {
|
||||
if (memory.data_dwords > 1 &&
|
||||
(vector == nullptr || vector->GetOpcode() != composites[memory.data_dwords - 2]))
|
||||
return {};
|
||||
const auto word = memory.data_dwords == 1 ? data : vector->Arg(i);
|
||||
if (word.GetType() != Type::U32 || !UniformIntegerValue(program, word)) return {};
|
||||
result.values[i] = word;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
ResourcePlan plan;
|
||||
plan.stage = program.stage;
|
||||
@@ -772,6 +895,10 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
for (auto& block: plan.control_flow) {
|
||||
block.condition = Clone(block.condition);
|
||||
}
|
||||
plan.buffer_fill = AnalyzeBufferFill(program);
|
||||
for (uint32_t i = 0; i < plan.buffer_fill.fill.element_size / sizeof(uint32_t); ++i) {
|
||||
plan.buffer_fill.values[i] = Clone(plan.buffer_fill.values[i]);
|
||||
}
|
||||
plan.materialization_sources.reserve(plan.info.buffers.size() + plan.info.images.size() +
|
||||
plan.info.samplers.size());
|
||||
for (const auto& buffer: plan.info.buffers) {
|
||||
|
||||
@@ -1034,6 +1034,24 @@ void BuildSrtPlan(Program& program) {
|
||||
program.srt_plan_complete = true;
|
||||
}
|
||||
|
||||
bool EvaluateUniformValues(const ResourcePlan& program, std::span<const Value> values,
|
||||
const SrtRuntime& runtime, std::span<uint32_t> results) {
|
||||
if (values.size() != results.size()) {
|
||||
return false;
|
||||
}
|
||||
auto clean = runtime;
|
||||
clean.read_memory = runtime.read_specialization_memory != nullptr
|
||||
? runtime.read_specialization_memory
|
||||
: +[](void*, uint64_t, uint32_t*) { return false; };
|
||||
Evaluator evaluator(program, clean);
|
||||
for (size_t i = 0; i < values.size(); ++i) {
|
||||
if (!evaluator.Evaluate(values[i], results[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EvaluateDescriptorSource(const ResourcePlan& program, uint32_t source,
|
||||
const SrtRuntime& runtime, DescriptorValue& result) {
|
||||
std::vector<DescriptorValue> results;
|
||||
|
||||
@@ -23,6 +23,8 @@ struct SrtRuntime {
|
||||
// dynamic offsets remain explicit and are never assigned a fake slot.
|
||||
void BuildSrtPlan(Program& program);
|
||||
bool ValidateRuntimeValue(const ResourcePlan& program, Value value);
|
||||
bool EvaluateUniformValues(const ResourcePlan& program, std::span<const Value> values,
|
||||
const SrtRuntime& runtime, std::span<uint32_t> results);
|
||||
|
||||
bool EvaluateDescriptorSource(const ResourcePlan& program, uint32_t source,
|
||||
const SrtRuntime& runtime, DescriptorValue& result);
|
||||
|
||||
@@ -35,7 +35,7 @@ bool SameResourceSnapshot(const ResourceSnapshot &lhs,
|
||||
return lhs.buffers == rhs.buffers && lhs.images == rhs.images &&
|
||||
lhs.samplers == rhs.samplers &&
|
||||
lhs.flattened_srt == rhs.flattened_srt &&
|
||||
lhs.user_data == rhs.user_data;
|
||||
lhs.user_data == rhs.user_data && lhs.buffer_fill == rhs.buffer_fill;
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
@@ -457,6 +457,111 @@ void TestInvariantIndirectImageMaterialization() {
|
||||
"wrapped scalar immediate entered the invariant image proof");
|
||||
}
|
||||
|
||||
void TestComputeBufferFill() {
|
||||
struct Options {
|
||||
bool scalar = false;
|
||||
bool conditional = false;
|
||||
bool shifted = false;
|
||||
bool extra_store = false;
|
||||
bool clean = false;
|
||||
bool branch = false;
|
||||
};
|
||||
const auto Run = [](Options options) {
|
||||
Fixture fixture;
|
||||
fixture.program.block_info[0].terminator.kind =
|
||||
Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::Return;
|
||||
if (options.branch) {
|
||||
fixture.program.block_info[0].terminator.kind = Libs::Graphics::
|
||||
ShaderRecompiler::CFG::TerminatorKind::ConditionalBranch;
|
||||
}
|
||||
const auto buffer =
|
||||
fixture.Buffer({fixture.UserData(0), fixture.UserData(1),
|
||||
fixture.UserData(2), fixture.UserData(3)});
|
||||
const auto local = fixture.Emit(
|
||||
ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)),
|
||||
Value(0u)});
|
||||
const auto group = fixture.Emit(
|
||||
ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::WorkgroupId)), Value(0u)});
|
||||
auto index =
|
||||
fixture.Emit(ValueOpcode::IAdd32,
|
||||
{local, fixture.Emit(ValueOpcode::ShiftLeftLogical32,
|
||||
{group, Value(6u)})});
|
||||
if (options.shifted)
|
||||
index = fixture.Emit(ValueOpcode::IAdd32, {index, Value(1u)});
|
||||
Value value(0u);
|
||||
TestMemory memory;
|
||||
memory.words[0] = 0x40404040u;
|
||||
if (options.scalar) {
|
||||
const auto input =
|
||||
fixture.Buffer({Value(static_cast<uint32_t>(memory.base)),
|
||||
Value(4u << 16), Value(1u), Value(0x14204u)});
|
||||
MemoryInfo load;
|
||||
load.kind = ResourceKind::ScalarBuffer;
|
||||
value = fixture.Emit(ValueOpcode::ReadConstBuffer, {input, Value(0u)},
|
||||
fixture.AddMemory(load, 8));
|
||||
}
|
||||
MemoryInfo store;
|
||||
store.kind = ResourceKind::Buffer;
|
||||
store.formatted = true;
|
||||
store.idxen = true;
|
||||
const auto flags = fixture.AddMemory(store, 16);
|
||||
const auto predicate =
|
||||
options.conditional
|
||||
? fixture.Emit(ValueOpcode::ULessThan32, {local, Value(32u)})
|
||||
: Value(true);
|
||||
const auto EmitStore = [&] {
|
||||
fixture.Emit(ValueOpcode::StoreBufferU32,
|
||||
{buffer, index, Value(0u), Value(0u), value, predicate},
|
||||
flags);
|
||||
};
|
||||
EmitStore();
|
||||
if (options.extra_store)
|
||||
EmitStore();
|
||||
fixture.PlanAndTrack();
|
||||
auto plan = ExtractResourcePlan(fixture.program);
|
||||
std::array<uint32_t, 4> userdata{0x200000u, 4u << 16, 0x4000u, 0x14204u};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
const auto Read = +[](void *data, uint64_t address, uint32_t *word) {
|
||||
auto &memory = *static_cast<TestMemory *>(data);
|
||||
if (address != memory.base)
|
||||
return false;
|
||||
++memory.reads;
|
||||
*word = memory.words[0];
|
||||
return true;
|
||||
};
|
||||
Check(MaterializeResources(
|
||||
plan,
|
||||
{.user_data = userdata,
|
||||
.read_memory = Read,
|
||||
.userdata = &memory,
|
||||
.read_specialization_memory = options.clean ? Read : nullptr},
|
||||
snapshot, specialization),
|
||||
"fill fixture did not materialize");
|
||||
const bool expected = !options.conditional && !options.shifted &&
|
||||
!options.extra_store && !options.branch &&
|
||||
(!options.scalar || options.clean);
|
||||
Check((snapshot.buffer_fill.element_size != 0) == expected,
|
||||
"fill proof accepted an unsafe store or missed the real GTA3 clear");
|
||||
if (expected) {
|
||||
Check(snapshot.buffer_fill.element_size == 4 &&
|
||||
snapshot.buffer_fill.group_stride == 64 &&
|
||||
snapshot.buffer_fill.value ==
|
||||
(options.scalar ? 0x40404040u : 0u),
|
||||
"fill proof lost address coverage or the actual stored scalar");
|
||||
}
|
||||
};
|
||||
Run({});
|
||||
Run({.scalar = true, .clean = true});
|
||||
Run({.scalar = true});
|
||||
Run({.conditional = true, .clean = true});
|
||||
Run({.shifted = true, .clean = true});
|
||||
Run({.extra_store = true, .clean = true});
|
||||
Run({.clean = true, .branch = true});
|
||||
}
|
||||
|
||||
void TestDenseBufferTracking() {
|
||||
Fixture fixture;
|
||||
std::array<Value, 8> userdata;
|
||||
@@ -1650,6 +1755,7 @@ int main() {
|
||||
}
|
||||
};
|
||||
Run("dense buffers", TestDenseBufferTracking);
|
||||
Run("compute buffer fill", TestComputeBufferFill);
|
||||
Run("scalar/vector alias", TestScalarAndVectorBufferAlias);
|
||||
Run("runtime unsigned min", TestRuntimeUnsignedMinDescriptor);
|
||||
Run("images and samplers", TestImagesSamplersAndAliases);
|
||||
|
||||
@@ -22271,7 +22271,7 @@ std::vector<GraphicsCase> MakeGraphicsCases() {
|
||||
void CheckPs5GameExampleImageClearRuntimeShape() {
|
||||
const auto MakeCode = [] {
|
||||
std::vector<u32> code;
|
||||
AppendVop3(&code, 0x347u, 4, 8, InlineU32(6), Vgpr(0));
|
||||
AppendVop3(&code, 0x346u, 4, 8, InlineU32(6), Vgpr(0));
|
||||
for (u32 i = 0; i < 4; i++) {
|
||||
code.push_back(EncodeVop1(0x01u, i, i + 4u));
|
||||
}
|
||||
@@ -22300,6 +22300,8 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
|
||||
compute.wave_size = 64;
|
||||
compute.thread_ids_num = 1;
|
||||
compute.workgroup_register = 8;
|
||||
u32 scalar_clear = 0x40404040u;
|
||||
bool clean_scalar = false;
|
||||
|
||||
const auto Compile = [&](const std::vector<u32> &code) {
|
||||
ShaderRecompiler::CompileOptions options;
|
||||
@@ -22317,6 +22319,13 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
|
||||
const ShaderRecompiler::IR::SrtRuntime runtime{
|
||||
.user_data = options.user_data,
|
||||
.shader_base = reinterpret_cast<uint64_t>(code.data()),
|
||||
.userdata = &scalar_clear,
|
||||
.read_specialization_memory = clean_scalar
|
||||
? +[](void *data, uint64_t address, uint32_t *word) {
|
||||
if (address != reinterpret_cast<uint64_t>(data)) return false;
|
||||
*word = *static_cast<uint32_t *>(data);
|
||||
return true;
|
||||
} : nullptr,
|
||||
};
|
||||
Require("Ps5GameExampleImageClear", "resource materialization",
|
||||
ShaderRecompiler::IR::MaterializeResources(
|
||||
@@ -22353,9 +22362,16 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
|
||||
packed_clear == 0xff000000u,
|
||||
"exact Prospero runtime binding did not resolve to a complete clear");
|
||||
|
||||
auto non_repeated = positive.resources;
|
||||
non_repeated.user_data[7] ^= 1u;
|
||||
compute.stage.resources = non_repeated;
|
||||
auto wrong_index = code;
|
||||
wrong_index[0] = (wrong_index[0] & ~(0x3ffu << 16u)) | (0x347u << 16u);
|
||||
compute.stage.resources = Compile(wrong_index).resources;
|
||||
Require("Ps5GameExampleImageClear", "add-before-shift address",
|
||||
!ResolveComputeImageClear(compute, 64, 1, 1, 0x61u, descriptor,
|
||||
packed_clear, size),
|
||||
"V_ADD_LSHL was mistaken for V_LSHL_ADD contiguous coverage");
|
||||
|
||||
user_data[7] ^= 1u;
|
||||
compute.stage.resources = Compile(code).resources;
|
||||
Require("Ps5GameExampleImageClear", "non-repeated clear",
|
||||
!ResolveComputeImageClear(compute, 64, 1, 1, 0x61u, descriptor,
|
||||
packed_clear, size),
|
||||
@@ -22366,6 +22382,91 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
|
||||
!ResolveComputeImageClear(compute, 32, 1, 1, 0x61u, descriptor,
|
||||
packed_clear, size),
|
||||
"partial buffer coverage was classified as a complete clear");
|
||||
|
||||
// Same descriptor/dispatch as GTA3, with the stored value encoded in the
|
||||
// shader.
|
||||
user_data = {0x00200000u, 4u << 16u, 64u, 0x14204u};
|
||||
compute.workgroup_register = 4;
|
||||
compute.dispatch_thread_dimensions = false;
|
||||
std::vector<u32> scalar_code;
|
||||
AppendVop3(&scalar_code, 0x346u, 0, 4, InlineU32(6), Vgpr(0));
|
||||
scalar_code.push_back(EncodeVop1(0x01u, 1, InlineU32(0)));
|
||||
scalar_code.push_back(EncodeMubuf0(0x04u, 0, true, false));
|
||||
scalar_code.push_back(EncodeMubuf1(1, 0, 0));
|
||||
AppendEnd(&scalar_code);
|
||||
auto scalar = Compile(scalar_code);
|
||||
program.info = scalar.program.info;
|
||||
compute.stage.resources = scalar.resources;
|
||||
Require("Ps5GameExampleImageClear", "inline scalar fill",
|
||||
ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor,
|
||||
packed_clear, size) &&
|
||||
descriptor.Base48() == 0x200000u && size == 256u &&
|
||||
packed_clear == 0u,
|
||||
"GTA3's actual scalar store was not recognized from IR");
|
||||
Require("Ps5GameExampleImageClear", "excess dispatch",
|
||||
!ResolveComputeImageClear(compute, 2, 1, 1, 0x41u, descriptor,
|
||||
packed_clear, size),
|
||||
"excess invocation coverage was accepted as a fill");
|
||||
auto alias = scalar.resources.buffers.front();
|
||||
compute.stage.resources.buffers.push_back(alias);
|
||||
program.info.buffers.push_back({.read = true, .scalar = true});
|
||||
Require("Ps5GameExampleImageClear", "aliased scalar input",
|
||||
!ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor,
|
||||
packed_clear, size),
|
||||
"a scalar read aliasing the destination was accepted as uniform");
|
||||
program.info = scalar.program.info;
|
||||
compute.stage.resources = scalar.resources;
|
||||
compute.stage.resources.buffers[0].dwords[3] =
|
||||
(static_cast<u32>(Prospero::BufferFormat::k32Float) << 12u) | 0x204u;
|
||||
Require("Ps5GameExampleImageClear", "wrong store format",
|
||||
!ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor,
|
||||
packed_clear, size),
|
||||
"a format that changes the stored value was accepted as a uint fill");
|
||||
// A body change preserving every descriptor and dispatch field must change
|
||||
// the result.
|
||||
scalar_code[2] = EncodeVop1(0x01u, 1, InlineU32(7));
|
||||
compute.stage.resources = Compile(scalar_code).resources;
|
||||
Require("Ps5GameExampleImageClear", "changed store value",
|
||||
ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor,
|
||||
packed_clear, size) &&
|
||||
packed_clear == 7u,
|
||||
"clear recognition ignored the shader's actual stored value");
|
||||
scalar_code[2] = EncodeVop1(0x01u, 1, Vgpr(0));
|
||||
compute.stage.resources = Compile(scalar_code).resources;
|
||||
Require("Ps5GameExampleImageClear", "varying store value",
|
||||
!ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor,
|
||||
packed_clear, size),
|
||||
"a varying store with identical resources was treated as a fill");
|
||||
|
||||
const auto scalar_address = reinterpret_cast<uint64_t>(&scalar_clear);
|
||||
user_data[4] = static_cast<u32>(scalar_address);
|
||||
user_data[5] = static_cast<u32>(scalar_address >> 32u) | (16u << 16u);
|
||||
user_data[6] = 1;
|
||||
user_data[7] = 0x4dfacu;
|
||||
compute.workgroup_register = 8;
|
||||
scalar_code.clear();
|
||||
AppendVop3(&scalar_code, 0x346u, 0, 8, InlineU32(6), Vgpr(0));
|
||||
scalar_code.push_back(EncodeSmem0(0x08u, 106, 2));
|
||||
scalar_code.push_back(EncodeSmem1(0, 125));
|
||||
scalar_code.push_back(EncodeVop1(0x01u, 1, 106));
|
||||
scalar_code.push_back(EncodeMubuf0(0x04u, 0, true, false));
|
||||
scalar_code.push_back(EncodeMubuf1(1, 0, 0));
|
||||
AppendEnd(&scalar_code);
|
||||
clean_scalar = true;
|
||||
auto loaded = Compile(scalar_code);
|
||||
program.info = loaded.program.info;
|
||||
compute.stage.resources = loaded.resources;
|
||||
Require("Ps5GameExampleImageClear", "scalar-loaded fill",
|
||||
ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor,
|
||||
packed_clear, size) &&
|
||||
packed_clear == scalar_clear && descriptor.Base48() == 0x200000u,
|
||||
"GTA3's scalar input was confused with its destination");
|
||||
clean_scalar = false;
|
||||
compute.stage.resources = Compile(scalar_code).resources;
|
||||
Require("Ps5GameExampleImageClear", "unavailable clean scalar",
|
||||
!ResolveComputeImageClear(compute, 1, 1, 1, 0x41u, descriptor,
|
||||
packed_clear, size),
|
||||
"a scalar without a clean reader was replaced by a fill");
|
||||
std::printf("[host] %-32s ok\n", "Ps5GameExampleImageClear");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user