Compare commits

...

12 Commits

46 changed files with 3184 additions and 696 deletions
+2 -2
View File
@@ -52,8 +52,8 @@ or graphical glitches, so please include the version you tested when reporting a
<img src="docs/screenshots/ps5-01.png" width="300" alt="Astro Bot running in KytyPS5">
</td>
<td align="center">
<strong>Dreaming Sarah</strong><br>
<img src="docs/screenshots/ps5-03.png" width="300" alt="Dreaming Sarah running in KytyPS5">
<strong>Saros</strong><br>
<img src="docs/screenshots/ps5-03.png" width="300" alt="Saros running in KytyPS5">
</td>
</tr>
<tr>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

+3 -2
View File
@@ -1003,7 +1003,7 @@ void CommandProcessor::DispatchDirect(uint32_t thread_group_x, uint32_t thread_g
}
m_renderer.GetRenderExecutor().DispatchDirect(m_submit_id, CurrentBuffer(), thread_group_x,
thread_group_y, thread_group_z, mode);
thread_group_y, thread_group_z, mode, IsAsyncComputeQueue());
}
}
@@ -1015,7 +1015,8 @@ void CommandProcessor::DispatchIndirect(uint64_t args_addr, uint32_t mode) {
return;
}
m_sh_ctx.SetCsWaveSize(Pm4::ComputeWaveSize(mode));
m_renderer.GetRenderExecutor().DispatchIndirect(m_submit_id, CurrentBuffer(), args_addr, mode);
m_renderer.GetRenderExecutor().DispatchIndirect(m_submit_id, CurrentBuffer(), args_addr, mode,
IsAsyncComputeQueue());
}
void CommandProcessor::DrawIndexAuto(DrawAutoArgs args) {
@@ -217,17 +217,53 @@ struct PipelineCache::ProgramCache {
};
struct Permutation {
std::vector<uint32_t> function_code;
ShaderRecompiler::IR::ResourceSpecialization specialization;
ShaderRecompiler::IR::CompiledShaderInfo program;
ShaderProgram handle;
};
struct SourceEntry {
explicit SourceEntry(ShaderRecompiler::IR::ResourcePlan plan)
: resource_plan(std::move(plan)) {
SourceEntry(ShaderRecompiler::IR::ResourcePlan plan,
std::unique_ptr<ShaderRecompiler::ShaderSource> call_source)
: call_source(std::move(call_source)), resource_plan(std::move(plan)) {
permutations.reserve(8);
}
std::span<const uint32_t> FunctionCode() const {
return call_source
? std::span<const uint32_t>(call_source->linked->code).subspan(call_source->code.size())
: std::span<const uint32_t>{};
}
bool RefreshCallSource(const ShaderRecompiler::IR::SrtRuntime& runtime) {
const auto revision = call_source->revision;
ShaderRecompiler::RefreshShaderSource(*call_source, runtime);
resource_plan.source_reads = call_source->reads;
return revision != call_source->revision;
}
template <bool HasCalls>
Permutation* FindPermutation(const ShaderRecompiler::IR::SrtRuntime& runtime,
uint32_t push_data_cursor) {
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
resource_plan, runtime, resources, specialization));
for (auto& candidate: permutations) {
const auto& layout = candidate.program.bindings;
if (layout.push_data_start_dword != ShaderRecompiler::IR::PushData::StartFor(
push_data_cursor, layout.ShaderDataDwords()) ||
candidate.specialization != specialization) {
continue;
}
if constexpr (HasCalls) {
if (!std::ranges::equal(candidate.function_code, FunctionCode())) continue;
}
return &candidate;
}
return nullptr;
}
std::unique_ptr<ShaderRecompiler::ShaderSource> call_source;
ShaderRecompiler::IR::ResourcePlan resource_plan;
ShaderRecompiler::IR::ResourceSnapshot resources;
ShaderRecompiler::IR::ResourceSpecialization specialization;
@@ -256,7 +292,8 @@ struct PipelineCache::ProgramCache {
const ShaderRecompiler::CompileOptions& options,
ShaderRecompiler::TranslateResult translated,
ShaderRecompiler::IR::ResourceSpecialization specialization,
uint32_t push_data_start_dword) {
uint32_t push_data_start_dword,
std::span<const uint32_t> function_code) {
auto result = ShaderRecompiler::CompileProgram(std::move(translated), options,
specialization, push_data_start_dword);
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv)) {
@@ -273,6 +310,7 @@ struct PipelineCache::ProgramCache {
static_cast<uint64_t>(result.spirv.size()), options.wave_size);
}
return {
.function_code = {function_code.begin(), function_code.end()},
.specialization = std::move(specialization),
.program = std::move(result.program).TakeCompiledInfo(),
.handle = {.id = ++next_shader_id, .module = module},
@@ -307,21 +345,18 @@ struct PipelineCache::ProgramCache {
if constexpr (std::is_same_v<InputInfo, ShaderComputeInputInfo>) {
runtime.workgroup_counts = input_info.workgroup_counts;
}
bool source_changed = false;
if (entry != programs.end()) {
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
entry->second.resource_plan, runtime, entry->second.resources,
entry->second.specialization));
if (const auto permutation = std::ranges::find_if(
entry->second.permutations, [&](const Permutation& candidate) {
const auto& layout = candidate.program.bindings;
return layout.push_data_start_dword ==
ShaderRecompiler::IR::PushData::StartFor(
push_data_cursor, layout.ShaderDataDwords()) &&
candidate.specialization == entry->second.specialization;
});
permutation != entry->second.permutations.end()) {
input_info.stage = {.program = &permutation->program,
.resources = &entry->second.resources};
auto& cached = entry->second;
Permutation* permutation = nullptr;
if (cached.call_source) {
source_changed = cached.RefreshCallSource(runtime);
if (!source_changed) permutation = cached.FindPermutation<true>(runtime, push_data_cursor);
} else {
permutation = cached.FindPermutation<false>(runtime, push_data_cursor);
}
if (permutation != nullptr) {
input_info.stage = {.program = &permutation->program, .resources = &entry->second.resources};
permutation->program.bindings.AdvancePushData(push_data_cursor);
return permutation->handle;
}
@@ -368,16 +403,39 @@ struct PipelineCache::ProgramCache {
options.wave_size = input_info.wave_size;
}
DumpShaderOriginal(stage_name, options.shader_hash, params.code);
auto translated = ShaderRecompiler::TranslateProgram(params.code, options);
ShaderRecompiler::ShaderSource source;
if (entry == programs.end()) source = ShaderRecompiler::PrepareShaderSource(params.code, options);
auto* current = entry == programs.end() ? &source : entry->second.call_source.get();
ShaderRecompiler::TranslateResult translated;
if (current != nullptr) {
if (current->call) {
if (entry == programs.end()) ShaderRecompiler::RefreshShaderSource(*current, runtime);
translated = ShaderRecompiler::TranslateProgram(current->linked->decoded, options);
translated.program.source_reads = current->reads;
} else {
translated = ShaderRecompiler::TranslateProgram(current->decoded, options);
}
} else {
translated = ShaderRecompiler::TranslateProgram(params.code, options);
}
if (entry == programs.end()) {
auto retained = source.call ? std::make_unique<ShaderRecompiler::ShaderSource>(std::move(source)) : nullptr;
entry = programs.try_emplace(lookup_key,
ShaderRecompiler::IR::ExtractResourcePlan(translated.program)).first;
ShaderRecompiler::IR::ExtractResourcePlan(translated.program), std::move(retained)).first;
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
entry->second.resource_plan, runtime, entry->second.resources,
entry->second.specialization));
} else if (source_changed) {
entry->second.resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program);
if (const auto* permutation = entry->second.FindPermutation<true>(runtime, push_data_cursor)) {
input_info.stage = {.program = &permutation->program, .resources = &entry->second.resources};
permutation->program.bindings.AdvancePushData(push_data_cursor);
return permutation->handle;
}
}
entry->second.permutations.push_back(CompilePermutation(
stage_name, options, std::move(translated), entry->second.specialization, push_data_cursor));
stage_name, options, std::move(translated), entry->second.specialization, push_data_cursor,
entry->second.FunctionCode()));
const auto& permutation = entry->second.permutations.back();
input_info.stage = {.program = &permutation.program, .resources = &entry->second.resources};
permutation.program.bindings.AdvancePushData(push_data_cursor);
+2 -2
View File
@@ -156,9 +156,9 @@ public:
KYTY_CLASS_NO_COPY(RenderExecutor);
void DispatchDirect(uint64_t submit_id, CommandBuffer& buffer, uint32_t thread_group_x,
uint32_t thread_group_y, uint32_t thread_group_z, uint32_t mode);
uint32_t thread_group_y, uint32_t thread_group_z, uint32_t mode, bool async_compute);
void DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer, uint64_t args_addr,
uint32_t mode);
uint32_t mode, bool async_compute);
void PrepareBindings(const ShaderStageRuntime& runtime, PreparedBindings& prepared);
void FindBuffers(std::span<PreparedBindings* const> stages);
@@ -226,7 +226,7 @@ static void BindSharedMemory(RenderContext& context, ShaderComputeInputInfo& inp
void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
uint32_t thread_group_x, uint32_t thread_group_y,
uint32_t thread_group_z, uint32_t mode) {
uint32_t thread_group_z, uint32_t mode, bool async_compute) {
EXIT_IF(buffer.IsInvalid());
m_context.GetCommandScheduler().PopPendingOperations();
auto& ctx = buffer.GetRegisters();
@@ -281,6 +281,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
const auto& sh_regs = ctx.GetShaderRegisters();
ShaderComputeInputInfo input_info {};
input_info.async_compute = async_compute;
const bool use_thread_dimensions = (mode & DISPATCH_INITIATOR_USE_THREAD_DIMENSIONS) != 0;
input_info.dispatch_thread_dimensions = use_thread_dimensions;
input_info.workgroup_counts[0] = thread_group_x;
@@ -437,7 +438,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
}
void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
uint64_t args_addr, uint32_t mode) {
uint64_t args_addr, uint32_t mode, bool async_compute) {
EXIT_IF(buffer.IsInvalid() || args_addr == 0 || (args_addr & 3u) != 0 ||
(mode & Pm4::COMPUTE_DISPATCH_INITIATOR_USE_THREAD_DIMENSIONS) != 0);
m_context.GetCommandScheduler().PopPendingOperations();
@@ -450,6 +451,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
return;
}
ShaderComputeInputInfo input_info {};
input_info.async_compute = async_compute;
const auto compute_program = m_context.GetPipelineCache().GetComputeProgram(
cs_regs, buffer.GetRegisters().GetShaderRegisters(), input_info);
buffer.EndRendering();
@@ -7,6 +7,7 @@
#include "graphics/shader/recompiler/frontend/cfg/ShaderCFG.h"
#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h"
#include "graphics/shader/recompiler/frontend/translate/Translate.h"
#include "graphics/shader/recompiler/frontend/translate/Translator.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/recompiler/ir/passes/ConstantPropagation.h"
#include "graphics/shader/recompiler/ir/passes/DeadCodeElimination.h"
@@ -20,6 +21,7 @@
#include <array>
#include <chrono>
#include <fmt/format.h>
#include <functional>
#include <map>
#include <span>
#include <utility>
@@ -466,6 +468,7 @@ Decoder::Program DecodeFusedProgram(std::span<const uint32_t> front, std::span<c
Decoder::DecodeInstruction(joined_code, front_words - 1u, result.instructions.back());
Decoder::Program back_program;
Decoder::DecodeProgram(back, back_program);
result.has_swap_pc |= back_program.has_swap_pc;
const auto back_pc = front_words * sizeof(uint32_t);
for (auto& inst: back_program.instructions) {
// A back-stage PC-relative data reference requires its guest code address.
@@ -478,11 +481,227 @@ Decoder::Program DecodeFusedProgram(std::span<const uint32_t> front, std::span<c
return result;
}
void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
using namespace IR;
for (uint32_t i = 0; i < source.decoded.instructions.size(); ++i) {
if (source.decoded.instructions[i].opcode != Decoder::Opcode::S_SWAPPC_B64) continue;
EXIT_NOT_IMPLEMENTED(source.call.has_value() || options.stage != ShaderType::Compute);
source.call = ShaderSource::Call {.instruction = i};
}
EXIT_IF(options.input_info.compute == nullptr);
const auto& call = source.decoded.instructions[source.call->instruction];
EXIT_NOT_IMPLEMENTED(call.src0.kind != Decoder::OperandKind::Sgpr ||
call.dst.kind != Decoder::OperandKind::Sgpr ||
call.src0.reg != call.dst.reg || call.dst.reg >= 105u);
const auto graph = CFG::BuildGraph(source.decoded);
Program query;
query.stage = options.stage;
query.user_data_base = options.user_data_base;
query.user_data_count = static_cast<uint32_t>(options.user_data.size());
query.wave_size = options.wave_size;
auto& block = *query.block_storage.emplace_back(std::make_unique<Block>());
query.blocks.push_back(&block);
struct Producer { bool ready = false; std::map<uint32_t, Value> values; };
std::map<uint32_t, Producer> producers;
std::function<Value(uint32_t, uint32_t)> Register;
std::function<Value(uint32_t, Value)> Resolve = [&](uint32_t before, Value value) -> Value {
value = value.Resolve();
auto* inst = value.TryInstruction();
if (inst == nullptr) return value;
uint32_t code = UINT32_MAX;
switch (inst->GetOpcode()) {
case ValueOpcode::GetScalarRegister: code = static_cast<uint32_t>(inst->Arg(0).ScalarRegister()); break;
case ValueOpcode::GetVccLo: code = 106u; break;
case ValueOpcode::GetVccHi: code = 107u; break;
default: break;
}
if (code != UINT32_MAX) {
const auto result = Register(before, code);
inst->ReplaceUsesWith(result);
return result;
}
for (size_t i = 0; i < inst->NumArgs(); ++i) inst->SetArg(i, Resolve(before, inst->Arg(i)));
return value;
};
Register = [&](uint32_t before, uint32_t code) -> Value {
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code);
if (index == UINT32_MAX) {
EXIT_IF(code < query.user_data_base || code - query.user_data_base >= query.user_data_count);
return IREmitter(&block).GetUserData(static_cast<ScalarReg>(code));
}
auto [entry, inserted] = producers.try_emplace(index);
auto& producer = entry->second;
if (inserted) {
Block translated;
Frontend::Translator translator(query, &translated, 1u,
(options.input_info.compute->float_mode & 0x10u) == 0u,
!options.input_info.compute->async_compute);
translator.TranslateInstruction(source.decoded.instructions[index]);
for (const auto& inst: translated) {
uint32_t destination = UINT32_MAX;
Value value;
switch (inst.GetOpcode()) {
case ValueOpcode::SetScalarRegister:
destination = static_cast<uint32_t>(inst.Arg(0).ScalarRegister()); value = inst.Arg(1); break;
case ValueOpcode::SetVccLo: destination = 106u; value = inst.Arg(0); break;
case ValueOpcode::SetVccHi: destination = 107u; value = inst.Arg(0); break;
default: break;
}
if (destination != UINT32_MAX) producer.values.insert_or_assign(destination, value);
}
for (auto& [destination, value]: producer.values) value = Resolve(index, value);
for (auto& inst: translated) inst.SetParent(&block);
block.Instructions().splice(block.end(), translated.Instructions());
producer.ready = true;
}
EXIT_IF(!producer.ready || !producer.values.contains(code));
return producer.values.at(code).Resolve();
};
for (uint32_t i = 0; i < 2u; ++i) {
const auto value = Register(source.call->instruction, call.src0.reg + i);
EXIT_IF(!ValidateRuntimeValue(query, value, RuntimeValueType::Integer));
auto& root = query.value_storage.emplace_back(ValueOpcode::ReferenceU32);
root.SetArg(0, value);
source.call->target[i] = Value(&root);
}
RewriteToSsa(query.blocks);
ConstantPropagationPass(query.blocks, query.wave_size);
RemoveIdentities(query.blocks);
EliminateDeadCode(query.blocks);
for (auto& value: source.call->target) value = value.Instruction()->Arg(0).Resolve();
query.value_storage.clear();
for (auto& inst: block) {
inst.SetParent(nullptr);
if (inst.GetOpcode() == ValueOpcode::LoadAddressU32 || inst.GetOpcode() == ValueOpcode::ReadConstBuffer)
inst.SetFlags(SrtReadFlags {.index = inst.Flags<MemoryFlags>().index});
}
query.value_storage.splice(query.value_storage.end(), block.Instructions());
source.call_targets = std::move(static_cast<ResourcePlan&>(query));
}
} // namespace
ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOptions& options) {
EXIT_IF(code.empty());
ShaderSource source;
if (!options.back_code.empty()) {
source.decoded = DecodeFusedProgram(code, options.back_code, source.code);
} else if (options.stage == ShaderType::Local) {
source.decoded = Decoder::DecodeFrontProgram(code);
auto& handoff = source.decoded.instructions.back();
handoff.opcode = Decoder::Opcode::S_ENDPGM;
handoff.src_count = 0;
} else {
Decoder::DecodeProgram(code, source.decoded);
}
if (source.decoded.has_swap_pc) {
PrepareCallTarget(source, options);
source.code.assign(code.begin(), code.end());
source.decoded.code = source.code;
}
return source;
}
const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtRuntime& runtime) {
auto& reads = source.reads;
reads.clear();
IR::SrtReadCapture capture(runtime, reads);
const auto clean = IR::CleanRuntime(capture.ObservedRuntime());
IR::SrtWalker walker(source.call_targets, clean);
uint32_t low = 0, high = 0;
const auto& call = source.decoded.instructions[source.call->instruction];
if (!walker.Evaluate(source.call->target[0], low) || !walker.Evaluate(source.call->target[1], high))
EXIT("shader call at pc 0x%08x has an unavailable scalar target", call.pc);
// SWAPPC reads the old aliased pair and ignores the target's low two bits.
const auto address = ((uint64_t {high} << 32u) | low) & ~uint64_t {3};
EXIT_IF(address == 0u);
const auto query_reads = reads.size();
if (source.linked) {
auto& linked = *source.linked;
const auto function = std::span(linked.code).subspan(source.code.size());
if (clean.read_memory(clean.userdata, address, linked.observed_function) &&
std::ranges::equal(function, linked.observed_function)) {
return linked.decoded;
}
reads.resize(query_reads);
}
ShaderSource::Linked linked {.code = source.code,
.decoded = source.decoded};
const auto& last = source.decoded.instructions.back();
EXIT_NOT_IMPLEMENTED(last.opcode != Decoder::Opcode::S_ENDPGM);
// Keep the caller's data/footer bytes at their native offsets. The leaf's virtual CFG
// PCs follow decoded instructions; its physical code is independently read and owned.
const auto function_pc = last.pc + last.word_count * sizeof(uint32_t);
for (uint32_t index = 0;;) {
// The observed queue helper is a scalar leaf. Scalar encodings need at most one literal.
EXIT_IF(index >= 64u || address > UINT64_MAX - (uint64_t {index} + 2u) * sizeof(uint32_t));
std::array<uint32_t, 2> words {};
if (!clean.read_memory(clean.userdata, address + uint64_t {index} * sizeof(uint32_t),
std::span(words).first(1)))
EXIT("shader call at pc 0x%08x cannot read leaf code", call.pc);
const auto family = Decoder::GetInstructionFamily(words[0]);
EXIT_NOT_IMPLEMENTED(family != Decoder::Family::SOP1 && family != Decoder::Family::SOP2 &&
family != Decoder::Family::SOPC && family != Decoder::Family::SOPK &&
family != Decoder::Family::SOPP);
Decoder::Instruction inst;
Decoder::DecodeInstruction(words, 0u, inst);
if (inst.word_count == 2u) {
if (!clean.read_memory(clean.userdata, address + uint64_t {index + 1u} * sizeof(uint32_t),
std::span(words).last(1)))
EXIT("shader call at pc 0x%08x cannot read leaf literal", call.pc);
inst = {};
Decoder::DecodeInstruction(words, 0u, inst);
}
inst.pc = index * sizeof(uint32_t);
if (Decoder::IsDirectBranch(inst.opcode)) inst.branch_target += inst.pc;
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
index += inst.word_count;
EXIT_NOT_IMPLEMENTED(inst.opcode == Decoder::Opcode::S_SWAPPC_B64 ||
inst.opcode == Decoder::Opcode::S_GETPC_B64 ||
inst.opcode == Decoder::Opcode::S_ENDPGM);
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg) || CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
const bool returns = inst.opcode == Decoder::Opcode::S_SETPC_B64;
if (returns) {
EXIT_NOT_IMPLEMENTED(inst.src0.kind != Decoder::OperandKind::Sgpr || inst.src0.reg != call.dst.reg);
inst.opcode = Decoder::Opcode::S_BRANCH;
inst.branch_target = call.pc + call.word_count * sizeof(uint32_t);
} else if (Decoder::IsDirectBranch(inst.opcode)) {
EXIT_IF(inst.branch_target > UINT32_MAX - function_pc);
inst.branch_target += function_pc;
}
inst.pc += function_pc;
linked.decoded.instructions.push_back(std::move(inst));
if (returns) break;
}
const auto function_end = linked.decoded.instructions.back().pc +
linked.decoded.instructions.back().word_count * sizeof(uint32_t);
for (size_t i = source.decoded.instructions.size(); i + 1u < linked.decoded.instructions.size(); ++i) {
const auto& inst = linked.decoded.instructions[i];
if (Decoder::IsDirectBranch(inst.opcode))
EXIT_IF(inst.branch_target < function_pc || inst.branch_target >= function_end);
}
reads.resize(query_reads);
reads.emplace_back(address, (linked.code.size() - source.code.size()) * sizeof(uint32_t));
linked.decoded.instructions[source.call->instruction].branch_target = function_pc;
linked.decoded.code = linked.code;
linked.observed_function.resize(linked.code.size() - source.code.size());
source.linked = std::move(linked);
++source.revision;
return source.linked->decoded;
}
TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOptions& options) {
if (code.empty()) {
EXIT("shader recompiler input is empty\n");
auto source = PrepareShaderSource(code, options);
return TranslateProgram(source.decoded, options);
}
TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileOptions& options) {
EXIT_IF(decoded.instructions.empty());
if (decoded.has_swap_pc) {
for (const auto& inst: decoded.instructions) {
if (inst.opcode == Decoder::Opcode::S_SWAPPC_B64 && inst.branch_target == UINT32_MAX)
EXIT("shader call at pc 0x%08x must be linked before translation", inst.pc);
}
}
if (options.stage != ShaderType::Compute && options.stage != ShaderType::Vertex &&
options.stage != ShaderType::Pixel && options.stage != ShaderType::Mesh &&
@@ -493,34 +712,11 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
}
const auto compile_begin = std::chrono::steady_clock::now();
const auto phase_ms = [&compile_begin]() {
const auto phase_ms = [&compile_begin]() {
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - compile_begin)
.count());
std::chrono::steady_clock::now() - compile_begin).count());
};
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " code_words=%" PRIu64 " decode\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
static_cast<uint64_t>(code.size()));
Decoder::Program decoded;
std::vector<uint32_t> joined_code;
if (!options.back_code.empty()) {
decoded = DecodeFusedProgram(code, options.back_code, joined_code);
} else if (options.stage == ShaderType::Local) {
decoded = Decoder::DecodeFrontProgram(code);
// The separately compiled hull half runs in the next Vulkan stage.
auto& handoff = decoded.instructions.back();
handoff.opcode = Decoder::Opcode::S_ENDPGM;
handoff.src_count = 0;
} else {
Decoder::DecodeProgram(code, decoded);
}
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " decode instructions=%" PRIu64
" elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
static_cast<uint64_t>(decoded.instructions.size()), phase_ms());
std::string decoded_dump;
if (options.dump_ir) {
decoded_dump = Decoder::ProgramToString(decoded);
@@ -606,6 +802,12 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
IR::RemoveIdentities(ir.blocks);
IR::EliminateDeadCode(ir.blocks);
}
if (decoded.has_swap_pc) {
// Leaf returns are resolved CFG edges; native PC values must not escape into GPU data.
for (const auto* block: ir.blocks)
for (const auto& inst: *block)
EXIT_NOT_IMPLEMENTED(inst.GetOpcode() == IR::ValueOpcode::GetShaderBase);
}
LowerTessellationMemory(ir, options);
std::string cfg_dump;
if (options.dump_ir) {
@@ -656,7 +858,11 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
const auto resource = inst.Flags<uint32_t>();
first = resource < ir.info.images.size() &&
ir.info.images[resource].indirect_root == resource ? 1u : 0u;
} else if (op != IR::ValueOpcode::GetSamplerResource) {
} else if (op == IR::ValueOpcode::GetSamplerResource) {
const auto resource = inst.Flags<uint32_t>();
first = resource < ir.info.samplers.size() &&
!ir.info.samplers[resource].indirect_resources.empty() ? 1u : 0u;
} else {
continue;
}
for (size_t index = first; index < inst.NumArgs(); index++) {
@@ -4,8 +4,11 @@
#include "common/common.h"
#include "common/stringUtils.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h"
#include "graphics/shader/shader.h"
#include <array>
#include <optional>
#include <span>
#include <vector>
@@ -37,6 +40,32 @@ struct CompileResult {
IR::Program program;
};
// Decoded source and typed scalar call queries are retained once per cache source.
struct ShaderSource {
struct Call {
uint32_t instruction;
std::array<IR::Value, 2> target;
};
struct Linked {
std::vector<uint32_t> code;
Decoder::Program decoded;
std::vector<uint32_t> observed_function;
};
std::vector<uint32_t> code;
Decoder::Program decoded;
IR::ResourcePlan call_targets;
std::optional<Call> call;
std::optional<Linked> linked;
uint64_t revision = 0;
std::vector<std::pair<uint64_t, uint64_t>> reads;
};
[[nodiscard]] ShaderSource PrepareShaderSource(std::span<const uint32_t> code,
const CompileOptions& options);
const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtRuntime& runtime);
[[nodiscard]] TranslateResult TranslateProgram(const Decoder::Program& decoded,
const CompileOptions& options);
[[nodiscard]] TranslateResult TranslateProgram(std::span<const uint32_t> code,
const CompileOptions& options);
[[nodiscard]] CompileResult CompileProgram(TranslateResult translated,
@@ -137,6 +137,15 @@ uint32_t ImageViewSizeType(EmitterState& state, ImageDimension dimension) {
}
}
namespace {
void DecorateNonUniformDescriptor(EmitterState& state, uint32_t id) {
state.builder.RequireExtension("SPV_EXT_descriptor_indexing");
state.builder.RequireCapability(spv::CapabilityShaderNonUniform);
state.builder.RequireCapability(spv::CapabilitySampledImageArrayNonUniformIndexing);
state.builder.AddAnnotation(spv::OpDecorate, id, spv::DecorationNonUniform);
}
} // namespace
uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mip,
uint32_t array_index) {
const auto pointer = ImageDescriptorPointer(state, resource, mip, array_index);
@@ -144,24 +153,29 @@ uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mi
const auto kind = *IR::DescriptorBindingForImage(state.program.info.images[resource]);
state.builder.AddFunction(spv::OpLoad, state.images[IR::ImageBindingIndex(kind)].type,
image, pointer);
if (array_index != 0u) {
DecorateNonUniformDescriptor(state, image);
}
return image;
}
uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler) {
uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler, uint32_t array_index) {
if (sampler >= state.program.info.samplers.size()) {
ExitDescriptorBindingFailure(state, IR::DescriptorBindingKind::Samplers, sampler,
"sampler resource index is out of range");
}
const auto pointer = DescriptorElementPointer(
state, state.sampler_pointer_type, state.sampler_variable, ConstantU32(state, sampler),
state, state.sampler_pointer_type, state.sampler_variable,
array_index == 0u ? ConstantU32(state, sampler) : array_index,
IR::DescriptorBindingKind::Samplers, sampler, "sampler descriptor array was not emitted");
const auto sampler_id = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, state.sampler_type, sampler_id, pointer);
if (array_index != 0u) DecorateNonUniformDescriptor(state, sampler_id);
return sampler_id;
}
uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler_id, uint32_t mip,
uint32_t array_index) {
uint32_t array_index, bool sampler_dynamic) {
const auto& image_resource = state.program.info.images.at(resource);
EXIT_IF(image_resource.resource_class != IR::ImageResourceClass::Sampled);
const auto image = LoadImageDescriptor(state, resource, mip, array_index);
@@ -173,11 +187,8 @@ uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampl
spv::OpTypeSampledImage, state.images[IR::ImageBindingIndex(kind)].type);
}
state.builder.AddFunction(spv::OpSampledImage, sampled_type, sampled_image, image, sampler_id);
if (array_index != 0u) {
state.builder.RequireExtension("SPV_EXT_descriptor_indexing");
state.builder.RequireCapability(spv::CapabilityShaderNonUniform);
state.builder.RequireCapability(spv::CapabilitySampledImageArrayNonUniformIndexing);
state.builder.AddAnnotation(spv::OpDecorate, sampled_image, spv::DecorationNonUniform);
if (array_index != 0u || sampler_dynamic) {
DecorateNonUniformDescriptor(state, sampled_image);
}
return sampled_image;
}
@@ -615,6 +615,90 @@ uint32_t StoreTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t d
return PackImageTexel(ctx, mem, texel);
}
template <typename Emit>
uint32_t EmitImageAccess(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t result_type,
const Emit& emit) {
auto& state = ctx.state;
const auto& mem = ctx.Memory(inst);
const auto& image = state.program.info.images.at(mem.resource);
if (image.indirect_root != mem.resource) return emit(mem.resource, 0u);
const auto* handle = inst.Arg(0).ResolveInstruction();
const auto* source = image.source < state.program.descriptor_sources.size()
? &state.program.descriptor_sources[image.source]
: nullptr;
if (handle == nullptr || source == nullptr || !source->indirect_descriptor.has_value() ||
handle->NumArgs() == 0u) {
ctx.Fail(inst, "has invalid indirect image key provenance");
}
const auto key = ctx.Def(handle->Arg(0));
if (state.flattened_srt_variable == 0 ||
image.indirect_resources.size() < 2u) {
ctx.Fail(inst, "has no indirect image runtime mapping");
}
const auto selected = EmitIndirectResourceIndex(
state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u);
struct ImageRun {
uint32_t first;
uint32_t count;
uint32_t resource;
uint32_t slot_bias;
};
std::vector<ImageRun> runs;
for (uint32_t ordinal = 0; ordinal < image.indirect_resources.size(); ++ordinal) {
const auto resource = image.indirect_resources[ordinal];
const auto& candidate = state.program.info.images[resource];
const auto kind = *IR::DescriptorBindingForImage(candidate);
if (!runs.empty()) {
auto& run = runs.back();
const auto& first = state.program.info.images[run.resource];
const auto next_slot = run.slot_bias + ordinal;
if (candidate.dimension == first.dimension && candidate.cube == first.cube &&
IR::DescriptorBindingForImage(first) == kind &&
candidate.mip_count == 1u && first.mip_count == 1u &&
(ordinal == 1u || candidate.descriptor_index == next_slot)) {
// Native roots precede appended children; only the root may have a slot gap.
if (ordinal == 1u)
run.slot_bias = candidate.descriptor_index - ordinal;
++run.count;
continue;
}
}
runs.push_back({ordinal, 1u, resource,
candidate.descriptor_index - ordinal});
}
const auto EmitRun = [&](uint32_t index) {
const auto& run = runs[index];
if (run.count == 1u) return emit(run.resource, 0u);
auto slot = Binary(state, spv::OpIAdd, TypeU32(state), selected,
ConstantU32(state, run.slot_bias));
if (run.first == 0u) {
const auto root_slot = image.descriptor_index;
if (root_slot != run.slot_bias) {
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
ConstantU32(state, 0u));
const auto root_or_child = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), root_or_child,
is_root, ConstantU32(state, root_slot), slot);
slot = root_or_child;
}
}
return emit(run.resource, slot);
};
auto run_index = ConstantU32(state, 0u);
for (uint32_t index = 1; index < runs.size(); ++index) {
const auto at_or_after = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state),
selected, ConstantU32(state, runs[index].first));
const auto next = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), next, at_or_after,
ConstantU32(state, index), run_index);
run_index = next;
}
return runs.size() == 1u
? EmitRun(0u)
: EmitIndexSwitch(state, run_index, static_cast<uint32_t>(runs.size()),
result_type, EmitRun);
}
} // namespace
void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
@@ -656,29 +740,35 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
return;
}
if (op == IR::ValueOpcode::ImageRead) {
const auto dimension = image.dimension;
const auto& dimension_info = ImageDimensionInfoFor(dimension);
const auto numeric_class = image.numeric_class;
const auto condition = ctx.Arg(inst, 2);
const auto numeric_class = image.numeric_class;
const auto condition = ctx.Arg(inst, 2);
ctx.Define(
inst,
EmitValueOrDefaultIfCondition(
state, condition, TypeU32Vector(state, 4), ConstantU32CompositeZero(state, 4),
[&]() {
const auto descriptor = LoadImageDescriptor(state, mem.resource);
const auto color = state.builder.AllocateId();
const auto coord = CoordU32(ctx, mem, *address, dimension);
if (dimension_info.multisampled != 0u) {
state.builder.AddFunction(
spv::OpImageFetch, ImageVectorType(state, numeric_class, 4), color,
descriptor, coord, spv::ImageOperandsSampleMask,
AddressU32(ctx, mem, *address, dimension_info.coordinate_components));
} else {
state.builder.AddFunction(spv::OpImageFetch,
ImageVectorType(state, numeric_class, 4), color,
descriptor, coord, spv::ImageOperandsLodMask,
LodU32(ctx, mem, *address, dimension));
}
const auto color = EmitImageAccess(
ctx, inst, ImageVectorType(state, numeric_class, 4),
[&](uint32_t resource, uint32_t array_index) {
const auto& candidate = state.program.info.images[resource];
const auto dimension = candidate.dimension;
const auto& dimension_info = ImageDimensionInfoFor(dimension);
const auto descriptor = LoadImageDescriptor(state, resource, 0u, array_index);
const auto fetched = state.builder.AllocateId();
const auto coord = CoordU32(ctx, mem, *address, dimension);
if (dimension_info.multisampled != 0u) {
state.builder.AddFunction(
spv::OpImageFetch, ImageVectorType(state, candidate.numeric_class, 4), fetched,
descriptor, coord, spv::ImageOperandsSampleMask,
AddressU32(ctx, mem, *address, dimension_info.coordinate_components));
} else {
state.builder.AddFunction(
spv::OpImageFetch, ImageVectorType(state, candidate.numeric_class, 4), fetched,
descriptor, coord, spv::ImageOperandsLodMask,
LodU32(ctx, mem, *address, mem.image_dimension));
}
return fetched;
});
return ResultVector(ctx, UnpackImageTexel(ctx, mem, color), numeric_class,
false, mem);
}));
@@ -808,106 +898,32 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
operands[1] = AddressF32(ctx, mem, *address, layout.bias);
operand_count = 2;
}
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler);
const auto EmitSample = [&](uint32_t resource, uint32_t array_index = 0u) {
const auto& samplers = state.program.info.samplers.at(mem.sampler).indirect_resources;
uint32_t sampler_index = 0;
if (!samplers.empty()) {
const auto* handle = inst.Arg(1).ResolveInstruction();
if (handle == nullptr || handle->GetOpcode() != IR::ValueOpcode::GetSamplerResource)
ctx.Fail(inst, "has invalid finite sampler key provenance");
sampler_index = EmitIndexSwitch(
state, ctx.Def(handle->Arg(0)), static_cast<uint32_t>(samplers.size()),
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, samplers[ordinal]); });
}
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler, sampler_index);
const auto EmitSample = [&](uint32_t resource, uint32_t array_index) {
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, sampler_id, 0u, array_index);
const auto sampled = MakeSampledImage(state, resource, sampler_id, 0u, array_index,
sampler_index != 0u);
const auto sample = state.builder.AllocateId();
state.builder.AddFunction(opcode, result_type, sample, sampled, coord,
std::span(&dref_value, dref ? 1u : 0u),
std::span<const uint32_t>(operands).first(operand_count));
return sample;
};
if (image.indirect_root != mem.resource) {
const auto sample = EmitSample(mem.resource);
auto result = sample;
if (!dref) {
result = UnpackImageTexel(ctx, mem, sample);
}
ctx.Define(inst, ResultVector(ctx, result, numeric_class, dref, mem));
return;
}
const auto* handle = image_arg.ResolveInstruction();
const auto* source = image.source < state.program.descriptor_sources.size()
? &state.program.descriptor_sources[image.source]
: nullptr;
if (handle == nullptr || source == nullptr || !source->indirect_descriptor.has_value() ||
handle->NumArgs() == 0u) {
ctx.Fail(inst, "has invalid indirect image key provenance");
return;
}
const auto key = ctx.Def(handle->Arg(0));
if (state.flattened_srt_variable == 0 ||
image.indirect_resources.size() < 2u) {
ctx.Fail(inst, "has no indirect image runtime mapping");
return;
}
const auto selected = EmitIndirectResourceIndex(
state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u);
struct SampleRun {
uint32_t first;
uint32_t count;
uint32_t resource;
uint32_t slot_bias;
};
std::vector<SampleRun> runs;
for (uint32_t ordinal = 0; ordinal < image.indirect_resources.size(); ++ordinal) {
const auto resource = image.indirect_resources[ordinal];
const auto& candidate = state.program.info.images[resource];
const auto kind = *IR::DescriptorBindingForImage(candidate);
if (!runs.empty()) {
auto& run = runs.back();
const auto& first = state.program.info.images[run.resource];
const auto next_slot = run.slot_bias + ordinal;
if (candidate.dimension == first.dimension && candidate.cube == first.cube &&
IR::DescriptorBindingForImage(first) == kind &&
candidate.mip_count == 1u && first.mip_count == 1u &&
(ordinal == 1u || candidate.descriptor_index == next_slot)) {
// Native roots precede appended children; only the root may have a slot gap.
if (ordinal == 1u)
run.slot_bias = candidate.descriptor_index - ordinal;
++run.count;
continue;
}
}
runs.push_back({ordinal, 1u, resource,
candidate.descriptor_index - ordinal});
}
const auto EmitRun = [&](uint32_t index) {
const auto& run = runs[index];
if (run.count == 1u) return EmitSample(run.resource);
auto slot = Binary(state, spv::OpIAdd, TypeU32(state), selected,
ConstantU32(state, run.slot_bias));
if (run.first == 0u) {
const auto root_slot = image.descriptor_index;
if (root_slot != run.slot_bias) {
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
ConstantU32(state, 0u));
const auto root_or_child = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), root_or_child,
is_root, ConstantU32(state, root_slot), slot);
slot = root_or_child;
}
}
return EmitSample(run.resource, slot);
};
auto run_index = ConstantU32(state, 0u);
for (uint32_t index = 1; index < runs.size(); ++index) {
const auto at_or_after = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state),
selected, ConstantU32(state, runs[index].first));
const auto next = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), next, at_or_after,
ConstantU32(state, index), run_index);
run_index = next;
}
auto result = runs.size() == 1u
? EmitRun(0u)
: EmitIndexSwitch(state, run_index, static_cast<uint32_t>(runs.size()),
result_type, EmitRun);
auto result = EmitImageAccess(ctx, inst, result_type, EmitSample);
if (!dref) {
result = UnpackImageTexel(ctx, mem, result);
}
@@ -322,10 +322,11 @@ uint32_t ImageDescriptorPointer(EmitterState& state, uint32_t resource, uint32_t
uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mip = 0,
uint32_t array_index = 0);
uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler);
uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler, uint32_t array_index = 0);
uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler_id,
uint32_t mip = 0, uint32_t array_index = 0);
uint32_t mip = 0, uint32_t array_index = 0,
bool sampler_dynamic = false);
void EmitStorageImageWrite(EmitterState& state, uint32_t resource, uint32_t mip_lod, uint32_t coord,
uint32_t texel);
@@ -1468,7 +1468,8 @@ Graph BuildGraph(const Decoder::Program& program) {
for (uint32_t i = 0; i < program.instructions.size(); i++) {
const auto& inst = program.instructions[i];
const auto next_pc = InstructionEndPc(inst);
if (Decoder::IsDirectBranch(inst.opcode)) {
if (Decoder::IsDirectBranch(inst.opcode) ||
(inst.opcode == Opcode::S_SWAPPC_B64 && inst.branch_target != UINT32_MAX)) {
if (!IsValidTarget(inst.branch_target, program.instructions, end_pc)) {
ExitBuildFailure(graph, FailureKind::InvalidBranchTarget, UINT32_MAX,
fmt::format("branch at pc 0x{:08x} targets invalid pc 0x{:08x}",
@@ -1501,7 +1502,7 @@ Graph BuildGraph(const Decoder::Program& program) {
if (next_pc <= end_pc) {
labels.push_back(next_pc);
}
} else if (inst.opcode == Opcode::S_ENDPGM) {
} else if (inst.opcode == Opcode::S_ENDPGM || inst.opcode == Opcode::S_SWAPPC_B64) {
labels.push_back(next_pc);
}
}
@@ -1550,6 +1551,9 @@ Graph BuildGraph(const Decoder::Program& program) {
const auto next_pc = InstructionEndPc(last);
if (last.opcode == Opcode::S_ENDPGM) {
block.terminator.kind = TerminatorKind::Return;
} else if (last.opcode == Opcode::S_SWAPPC_B64 && last.branch_target != UINT32_MAX) {
block.terminator.kind = TerminatorKind::Branch;
block.terminator.true_block = block_at_pc(last.branch_target);
} else if (last.opcode == Opcode::S_SETPC_B64) {
const auto& target_info = setpc_targets.at(last.pc);
if (target_info.indirect) {
@@ -1676,6 +1680,36 @@ Graph Structurize(const Graph& graph) {
return GotoStructurizer(graph).Run();
}
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code) {
uint32_t destination = UINT32_MAX;
// Scalar ALU pair widths are not all decoded. The typed producer must confirm this word.
return InstructionWritesScalarCode(inst, code) ||
(ScalarOperandCode(inst.dst, destination) && code == destination + 1u);
}
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code) {
EXIT_IF(before >= program.instructions.size());
const auto found = std::ranges::find_if(graph.blocks, [before](const auto& block) {
return block.inst_begin <= before && before < block.inst_end;
});
EXIT_IF(found == graph.blocks.end());
const auto* block = &*found;
for (size_t depth = 0; depth < graph.blocks.size(); ++depth) {
for (uint32_t i = before; i > block->inst_begin;) {
const auto& inst = program.instructions[--i];
EXIT_IF(inst.opcode == Opcode::S_SWAPPC_B64);
if (MayWriteScalarRegister(inst, code)) return i;
}
if (block->id == graph.entry_block) return UINT32_MAX;
EXIT_IF(block->predecessors.size() != 1u);
block = graph.FindBlock(block->predecessors[0]);
EXIT_IF(block == nullptr);
before = block->inst_end;
}
EXIT("scalar shader call source has cyclic reaching definitions");
}
std::string BranchConditionToString(BranchCondition condition) {
switch (condition) {
case BranchCondition::Always: return "always";
@@ -113,6 +113,9 @@ struct Graph {
};
Graph BuildGraph(const Decoder::Program& program);
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code);
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code);
// Returns structured control flow or failure diagnostics without changing the native graph.
// On failure, failure_block is an original block ID or UINT32_MAX.
Graph Structurize(const Graph& graph);
@@ -61,6 +61,7 @@ constexpr OpcodeMap SOP1_OPCODE_LIST[] = {
{0x1eu, Opcode::S_BITSET1_B64},
{0x1fu, Opcode::S_GETPC_B64},
{0x20u, Opcode::S_SETPC_B64},
{0x21u, Opcode::S_SWAPPC_B64},
{0x24u, Opcode::S_AND_SAVEEXEC_B64},
{0x28u, Opcode::S_ORN2_SAVEEXEC_B64},
{0x2du, Opcode::S_QUADMASK_B64},
@@ -88,7 +89,7 @@ constexpr OpcodeMap SOPK_OPCODE_LIST[] = {
{0x08u, Opcode::S_CMP_LE_I32}, {0x09u, Opcode::S_CMP_EQ_U32}, {0x0au, Opcode::S_CMP_LG_U32},
{0x0bu, Opcode::S_CMP_GT_U32}, {0x0cu, Opcode::S_CMP_GE_U32}, {0x0du, Opcode::S_CMP_LT_U32},
{0x0eu, Opcode::S_CMP_LE_U32}, {0x0fu, Opcode::S_ADD_I32}, {0x10u, Opcode::S_MULK_I32},
{0x13u, Opcode::S_SETREG_B32}, {0x17u, Opcode::S_WAITCNT_VSCNT}, {0x18u, Opcode::S_WAITCNT},
{0x12u, Opcode::S_GETREG_B32}, {0x13u, Opcode::S_SETREG_B32}, {0x17u, Opcode::S_WAITCNT_VSCNT}, {0x18u, Opcode::S_WAITCNT},
{0x19u, Opcode::S_WAITCNT}, {0x1au, Opcode::S_WAITCNT},
{0x1bu, Opcode::S_SUBVECTOR_LOOP_BEGIN}, {0x1cu, Opcode::S_SUBVECTOR_LOOP_END},
};
@@ -155,6 +156,10 @@ void DecodeSop1(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
}
switch (inst.opcode) {
case Opcode::S_SWAPPC_B64:
inst.branch_target = UINT32_MAX;
inst.data_dwords = 2;
break;
case Opcode::S_GETPC_B64:
inst.src_count = 0;
DecodeScalarDestination(sdst, pc, inst.dst);
@@ -202,7 +207,7 @@ void DecodeSopk(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
const uint32_t word = code[word_index];
const uint32_t opcode = (word >> 23u) & 0x1fu;
const uint32_t sdst = (word >> 16u) & 0x7fu;
const auto imm = opcode >= 0x09u && opcode <= 0x0eu
const auto imm = (opcode >= 0x09u && opcode <= 0x0eu) || opcode == 0x12u
? static_cast<int32_t>(word & 0xffffu)
: static_cast<int32_t>(static_cast<int16_t>(word & 0xffffu));
@@ -222,6 +227,7 @@ void DecodeSopk(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
}
switch (inst.opcode) {
case Opcode::S_GETREG_B32:
case Opcode::S_MOVK_I32: DecodeScalarDestination(sdst, pc, inst.dst); return;
case Opcode::S_SUBVECTOR_LOOP_BEGIN:
case Opcode::S_SUBVECTOR_LOOP_END:
@@ -389,6 +389,7 @@ Program DecodeFrontProgram(std::span<const uint32_t> front) {
while (front_words < front.size()) {
auto& inst = result.instructions.emplace_back();
DecodeInstruction(front, front_words, inst);
result.has_swap_pc |= inst.opcode == Opcode::S_SWAPPC_B64;
front_words += inst.word_count;
if (inst.opcode == Opcode::S_SETPC_B64) {
EXIT_NOT_IMPLEMENTED(inst.src0.kind != OperandKind::Sgpr || inst.src0.reg != 6u);
@@ -406,6 +407,7 @@ void DecodeProgram(std::span<const uint32_t> code, Program& program) {
program.instructions.clear();
program.instructions.reserve(code.size());
program.code = code;
program.has_swap_pc = false;
std::vector<bool> branch_targets;
for (uint32_t word_index = 0; word_index < code.size();) {
@@ -413,6 +415,7 @@ void DecodeProgram(std::span<const uint32_t> code, Program& program) {
DecodeInstruction(code, word_index, program.instructions.back());
const auto& inst = program.instructions.back();
program.has_swap_pc |= inst.opcode == Opcode::S_SWAPPC_B64;
word_index += inst.word_count;
if (IsDirectBranch(inst.opcode)) {
@@ -60,6 +60,7 @@ enum class Opcode {
S_QUADMASK_B64,
S_GETPC_B64,
S_SETPC_B64,
S_SWAPPC_B64,
S_SUBVECTOR_LOOP_BEGIN,
S_SUBVECTOR_LOOP_END,
S_AND_SAVEEXEC_B32,
@@ -678,6 +679,7 @@ enum class Opcode {
S_CBRANCH_CDBGSYS,
S_CBRANCH_CDBGSYS_OR_USER,
S_SENDMSG,
S_GETREG_B32,
S_SETREG_B32,
S_SLEEP,
S_TRAP,
@@ -816,6 +818,7 @@ struct Instruction {
struct Program {
std::span<const uint32_t> code;
std::vector<Instruction> instructions;
bool has_swap_pc = false;
};
// Code spans are trusted to contain complete instructions, valid branch targets, and 32-bit PCs.
@@ -10,13 +10,21 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_MOV_B64: S_MOV_B64(inst); return;
case O::S_WQM_B32: S_WQM(inst, false); return;
case O::S_WQM_B64: S_WQM(inst, true); return;
case O::S_GETPC_B64: S_GETPC_B64(inst); return;
case O::S_GETPC_B64:
case O::S_SWAPPC_B64: S_GETPC_B64(inst); return;
case O::S_SETPC_B64: return;
case O::S_SUBVECTOR_LOOP_BEGIN: S_SUBVECTOR_LOOP(inst, true); return;
case O::S_SUBVECTOR_LOOP_END: S_SUBVECTOR_LOOP(inst, false); return;
case O::S_CSELECT_B32: S_CSELECT_B32(inst); return;
case O::S_CSELECT_B64: ScalarSelect64(inst, inst.src1); return;
case O::S_CMOV_B64: ScalarSelect64(inst, inst.dst); return;
case O::S_GETREG_B32:
if (!graphics_compute || (inst.src0.value != 0x1818u && inst.src0.value != 0x0a18u))
EXIT("unsupported S_GETREG_B32 at pc 0x%08x: selector=0x%04x graphics_compute=%u",
inst.pc, inst.src0.value, static_cast<unsigned>(graphics_compute));
// HW_ID2 identifies graphics compute as QUEUE=6, ME=0.
WriteOperand(inst.dst, IR::Value(inst.src0.value == 0x1818u ? 6u : 0u));
return;
case O::S_SETREG_B32: EmitControlNop(); return;
case O::S_WAITCNT_VSCNT: S_WAITCNT_VSCNT(inst); return;
case O::S_WAITCNT: return;
@@ -1356,7 +1356,8 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
(options.input_info.compute->float_mode & 0x10u) == 0;
for (const auto& cfg_block: cfg.blocks) {
auto* block = blocks_by_id.at(cfg_block.id);
Translator translator(result, block, vector_limit, flush_f32_inputs);
Translator translator(result, block, vector_limit, flush_f32_inputs,
options.stage == ShaderType::Compute && !options.input_info.compute->async_compute);
for (uint32_t index = cfg_block.inst_begin; index < cfg_block.inst_end; index++) {
const auto& instruction = decoded.instructions[index];
if (IsCodeTableLoad(cfg, instruction.pc)) {
@@ -10,9 +10,9 @@ namespace Libs::Graphics::ShaderRecompiler::Frontend {
class Translator {
public:
Translator(IR::Program& program, IR::Block* block, uint32_t vector_limit,
bool flush_f32_inputs)
bool flush_f32_inputs, bool graphics_compute)
: program(program), ir(block), current_vector_limit(vector_limit),
flush_f32_inputs(flush_f32_inputs) {}
flush_f32_inputs(flush_f32_inputs), graphics_compute(graphics_compute) {}
void TranslateInstruction(const Decoder::Instruction& inst);
void TranslateEmbeddedFetch(const Decoder::Instruction& inst, uint32_t attribute,
@@ -264,6 +264,7 @@ private:
uint32_t current_pc = 0;
uint32_t current_vector_limit = 1;
bool flush_f32_inputs;
bool graphics_compute;
};
} // namespace Libs::Graphics::ShaderRecompiler::Frontend
@@ -161,6 +161,7 @@ struct SamplerResource {
uint32_t first_use_pc = 0;
// Native filtering/border variants share the original sampler's runtime descriptor.
uint32_t snapshot_index = 0;
std::vector<uint32_t> indirect_resources;
bool force_point_filtering = false;
bool depth_compare = false;
bool integer_border = false;
@@ -590,6 +591,7 @@ struct ResourcePlan {
std::vector<ResourceBlock> control_flow;
std::vector<SrtRead> srt_reads;
bool requires_specialization_memory = false;
std::vector<std::pair<uint64_t, uint64_t>> source_reads;
bool capture_specialization_reads = false;
bool srt_plan_complete = false;
bool resource_tracking_complete = false;
@@ -10,7 +10,6 @@
#include <array>
#include <bit>
#include <cstdio>
#include <cstring>
#include <fmt/format.h>
#include <functional>
#include <numeric>
@@ -141,31 +140,6 @@ bool DecodeBufferDescriptor(const DescriptorValue& descriptor, ShaderBufferResou
return true;
}
struct ReadCapture {
SrtRuntime source;
std::vector<std::pair<uint64_t, uint64_t>>& ranges;
};
bool CaptureStrictRead(void* userdata, uint64_t address, std::span<uint32_t> values) {
auto& capture = *static_cast<ReadCapture*>(userdata);
if (!capture.source.read_specialization_memory(capture.source.userdata, address, values)) {
return false;
}
capture.ranges.emplace_back(address, values.size_bytes());
return true;
}
bool CaptureOrdinaryRead(void* userdata, uint64_t address, std::span<uint32_t> values) {
auto& capture = *static_cast<ReadCapture*>(userdata);
if (capture.source.read_memory != nullptr) {
if (!capture.source.read_memory(capture.source.userdata, address, values)) return false;
} else {
std::memcpy(values.data(), reinterpret_cast<const void*>(address), values.size_bytes());
}
capture.ranges.emplace_back(address, values.size_bytes());
return true;
}
const DescriptorSource* Source(const ResourcePlan& program, uint32_t source) {
if (source >= program.descriptor_sources.size()) {
return nullptr;
@@ -366,20 +340,22 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
} else if (!indirect.selector_first.IsEmpty()) {
ShaderBufferResource material;
uint32_t first = 0, count = 0;
if (!DecodeBufferDescriptor(material_value, material) || material.Type() != 0u ||
material.SwizzleEnabled() ||
material.AddTid() || material.OutOfBounds() != 0u ||
uint64_t {selector->offset} + 4u > material.Stride() ||
if (!DecodeBufferDescriptor(material_value, material) ||
!clean.Evaluate(indirect.selector_first, first) ||
!clean.Evaluate(indirect.key_count, count) || count > MaxIndirectDescriptorProbes ||
uint64_t {first} + count > material.NumRecords())
!clean.Evaluate(indirect.key_count, count) || count > MaxIndirectDescriptorProbes)
return false;
if (count != 0u && (uint64_t {first} + count - 1u) * material.Stride() +
selector->offset + 4u >
uint64_t {UINT32_MAX} + 1u)
// SMEM uses its instruction's byte stride; V# stride only supplies its bounds.
const auto step = selector->stride != 0u ? selector->stride : material.Stride();
if (selector->stride == 0u &&
(material.Type() != 0u || material.SwizzleEnabled() || material.AddTid() ||
material.OutOfBounds() != 0u || uint64_t {selector->offset} + 4u > step ||
uint64_t {first} + count > material.NumRecords())) return false;
const auto end = count != 0u
? (uint64_t {first} + count - 1u) * step + selector->offset + 4u : 0u;
if (end > uint64_t {UINT32_MAX} + 1u || end > material.GetSize())
return false;
if (!read_keys(material, uint64_t {first} * step + selector->offset, step, count))
return false;
if (!read_keys(material, uint64_t {first} * material.Stride() + selector->offset,
material.Stride(), count)) return false;
} else if (!indirect.selector_mask.IsEmpty()) {
uint32_t mask = 0;
uint32_t count = 0;
@@ -659,6 +635,10 @@ bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
return false;
}
usage[pair.sampler] |= 1u << static_cast<uint32_t>(ClassifySampler(base.images[pair.image]));
for (const auto candidate: base.samplers[pair.sampler].indirect_resources) {
if (candidate >= base.samplers.size()) return false;
usage[candidate] |= usage[pair.sampler];
}
}
for (uint32_t index = 0; index < base.samplers.size(); index++) {
auto& mapping = plan.mapping[index];
@@ -741,7 +721,31 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, co
block.sources.erase(std::unique(block.sources.begin(), block.sources.end()),
block.sources.end());
}
for (auto& block: blocks) block.condition = predicate(block.condition);
// Only branches that can change future resource access belong in the host plan.
std::vector<uint8_t> resource_reachable(blocks.size());
std::vector<uint32_t> pending;
for (uint32_t i = 0; i < blocks.size(); ++i) {
if (!blocks[i].sources.empty() || !blocks[i].srt_reads.empty()) {
resource_reachable[i] = 1;
pending.push_back(i);
}
}
while (!pending.empty()) {
const auto index = pending.back();
pending.pop_back();
for (const auto* predecessor: program.blocks[index]->ImmPredecessors()) {
const auto found = indices.find(predecessor);
if (found == indices.end()) return {};
if (resource_reachable[found->second]) continue;
resource_reachable[found->second] = 1;
pending.push_back(found->second);
}
}
for (auto& block: blocks) {
block.condition = std::ranges::any_of(block.successors, [&](uint32_t successor) {
return resource_reachable[successor] != 0;
}) ? predicate(block.condition) : Value {};
}
if (std::ranges::none_of(
blocks, [](const ResourceBlock& block) { return !block.condition.IsEmpty(); })) {
return {};
@@ -927,6 +931,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
plan.user_data_count = program.user_data_count;
plan.info = program.info;
plan.memory_info = program.memory_info;
plan.source_reads = program.source_reads;
plan.srt_plan_complete = program.srt_plan_complete;
plan.resource_tracking_complete = program.resource_tracking_complete;
@@ -1007,10 +1012,15 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
// A proven uniform factor can decide a branch even when its other lanes are unknown.
// Keep only that Boolean structure, never the varying shader dependency graph.
Value unknown;
bool capture_control_reads = false;
std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value {
value = value.Resolve();
if (value.IsEmpty()) return {};
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) return Clone(value);
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) {
capture_control_reads |=
!ValidateRuntimeValue(program, value, RuntimeValueType::ImmutableInteger);
return Clone(value);
}
const auto* inst = value.TryInstruction();
if (inst == nullptr) return {};
const auto op = inst->GetOpcode();
@@ -1040,7 +1050,8 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
}
plan.capture_specialization_reads |= capture_indirect_reads || !plan.control_flow.empty();
plan.capture_specialization_reads |=
capture_indirect_reads || capture_control_reads || !plan.source_reads.empty();
if (plan.capture_specialization_reads) {
// Every former flat alias now targets its retained raw read. Mark those reads
// on the normalized graph, so discarded Phi/EXEC dependencies stay discarded.
@@ -1075,27 +1086,23 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
if (image.written) MarkCleanReads(planned_reads, Source(plan, image.source));
}
}
if (capture_indirect_reads) plan.resource_tracking_complete &= !program.has_address_writes;
if (capture_indirect_reads || !plan.source_reads.empty())
plan.resource_tracking_complete &= !program.has_address_writes;
return plan;
}
bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime,
ResourceSnapshot& snapshot, ResourceSpecialization& specialization) {
if (!program.resource_tracking_complete ||
(program.requires_specialization_memory && runtime.read_specialization_memory == nullptr)) {
((program.requires_specialization_memory || !program.source_reads.empty()) &&
runtime.read_specialization_memory == nullptr)) {
return false;
}
const bool capture_reads = program.capture_specialization_reads;
const bool capture_reads = program.capture_specialization_reads || !program.source_reads.empty();
auto& reads = snapshot.specialization_reads;
reads.clear();
ReadCapture capture {runtime, reads};
SrtRuntime observed = runtime;
if (capture_reads) {
observed.userdata = &capture;
observed.read_specialization_memory = runtime.read_specialization_memory != nullptr
? CaptureStrictRead : nullptr;
observed.read_memory = CaptureOrdinaryRead;
}
reads = program.source_reads;
SrtReadCapture capture {runtime, reads};
const auto observed = capture_reads ? capture.ObservedRuntime() : runtime;
SrtWalker clean(program, CleanRuntime(observed));
SrtWalker walker(program, observed,
capture_reads || program.requires_specialization_memory ? &clean : nullptr);
@@ -1221,7 +1228,14 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
}
snapshot.samplers.resize(program.info.samplers.size());
for (uint32_t i = 0; i < program.info.samplers.size(); ++i) {
if (!evaluate(program.info.samplers[i].source, snapshot.samplers[i])) {
auto source = program.info.samplers[i].source;
if (source >= program.descriptor_sources.size()) return false;
const auto& descriptor = program.descriptor_sources[source];
if (descriptor.indirect_descriptor) {
if (descriptor.indirect_descriptor->sources.empty()) return false;
source = descriptor.indirect_descriptor->sources[0];
}
if (!evaluate(source, snapshot.samplers[i])) {
return false;
}
if (program.info.samplers[i].gather_lod) {
@@ -1313,11 +1327,20 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
samplers[index].force_point_filtering = binding.type == SamplerClass::PointInteger;
samplers[index].integer_border = binding.type != SamplerClass::Float;
}
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
const auto& binding = sampler_plan.bindings[index];
for (auto& candidate: samplers[index].indirect_resources) {
candidate = sampler_plan.mapping[candidate][static_cast<uint32_t>(binding.type)];
EXIT_IF(candidate == UINT32_MAX);
}
}
for (auto& pair: sampled_pairs) {
const auto type = static_cast<uint32_t>(ClassifySampler(images[pair.image]));
pair.sampler = sampler_plan.mapping[pair.sampler][type];
EXIT_IF(pair.sampler == UINT32_MAX);
samplers[pair.sampler].depth_compare |= images[pair.image].depth_compare;
for (const auto candidate: samplers[pair.sampler].indirect_resources)
samplers[candidate].depth_compare |= images[pair.image].depth_compare;
}
auto& memory_info = program.memory_info;
@@ -1405,7 +1428,8 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
EXIT_IF(memory.sampler == UINT32_MAX);
}
EXIT_IF(image.indirect_root == memory.resource &&
inst.GetOpcode() != ValueOpcode::ImageSampleRaw);
inst.GetOpcode() != ValueOpcode::ImageSampleRaw &&
inst.GetOpcode() != ValueOpcode::ImageRead);
}
}
for (auto& memory: memory_info) {
@@ -430,7 +430,7 @@ public:
plan.handle->SetArg(word, Value(0u));
continue;
}
if (plan.handle->GetOpcode() == ValueOpcode::GetBufferResource) {
if (plan.handle->NumArgs() == 4u) {
plan.handle->SetArg(0, plan.key);
for (uint32_t word = 1; word < 4u; ++word) plan.handle->SetArg(word, Value(0u));
} else {
@@ -609,23 +609,37 @@ private:
return selected.IsEmpty() ? value : selected;
}
Value LowerDescriptorPhi(Value value) {
value = value.Resolve();
const auto* phi = value.TryInstruction();
if (m_shader_writes || phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi ||
phi->NumArgs() != 2u || phi->NumPhiBlocks() != 2u || phi->GetType() != Type::U32) {
return value;
}
const auto* merge = phi->Parent();
const auto* branch = phi->PhiBlock(0);
if (merge == nullptr || branch == nullptr || phi->PhiBlock(1) == nullptr ||
branch == phi->PhiBlock(1)) {
return value;
}
for (const auto& [original, selected]: m_descriptor_selections) {
if (original == phi) {
return selected;
Value LowerDescriptorValue(Value value) {
value = value.Resolve();
const auto* inst = value.TryInstruction();
if (inst == nullptr) return value;
// The scalar planner owns reads and lowers their address handles separately.
uint32_t memory_index = 0;
if (ScalarReadMemory(*inst, memory_index) != nullptr) return value;
const auto [cached, inserted] = m_descriptor_values.emplace(inst, value);
if (!inserted) return cached->second;
if (inst->GetOpcode() != ValueOpcode::Phi) {
Inst* projected = nullptr;
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
const auto lowered = LowerDescriptorValue(inst->Arg(arg));
if (lowered == inst->Arg(arg).Resolve()) continue;
if (projected == nullptr) {
projected = &m_program.value_storage.emplace_back(inst->GetOpcode(), inst->Flags<uint64_t>());
for (size_t i = 0; i < inst->NumArgs(); ++i) projected->SetArg(i, inst->Arg(i));
}
projected->SetArg(arg, lowered);
}
if (projected != nullptr) cached->second = Value(projected);
return cached->second;
}
if (inst->NumArgs() != 2u || inst->NumPhiBlocks() != 2u || inst->GetType() != Type::U32) {
return value;
}
const auto* merge = inst->Parent();
const auto* branch = inst->PhiBlock(0);
if (merge == nullptr || branch == nullptr || inst->PhiBlock(1) == nullptr ||
branch == inst->PhiBlock(1)) {
return value;
}
if (branch->ImmSuccessors().size() != 2u) {
if (branch->ImmPredecessors().size() != 1u) {
@@ -638,7 +652,7 @@ private:
}
std::array<const Block*, 2> targets;
for (uint32_t arm = 0; arm < 2; arm++) {
const auto* incoming = phi->PhiBlock(arm);
const auto* incoming = inst->PhiBlock(arm);
if (incoming == merge ||
(incoming != branch &&
(incoming->ImmPredecessors().size() != 1u ||
@@ -654,19 +668,20 @@ private:
if (term.kind != CFG::TerminatorKind::ConditionalBranch ||
!((term.true_block == targets[0] && term.false_block == targets[1]) ||
(term.false_block == targets[0] && term.true_block == targets[1])) ||
!ValidateRuntimeValue(m_program, info.condition, RuntimeValueType::Integer) ||
!ValidateRuntimeValue(m_program, phi->Arg(0)) ||
!ValidateRuntimeValue(m_program, phi->Arg(1))) {
!ValidateRuntimeValue(m_program, info.condition, m_shader_writes
? RuntimeValueType::ImmutableInteger : RuntimeValueType::Integer) ||
!ValidateRuntimeValue(m_program, inst->Arg(0)) ||
!ValidateRuntimeValue(m_program, inst->Arg(1))) {
return value;
}
// Retain a host expression; replacing the GPU Phi would break SSA dominance.
const auto true_arg = term.true_block == targets[0] ? 0u : 1u;
auto& selected = m_program.value_storage.emplace_back(ValueOpcode::SelectU32);
selected.SetArg(0, info.condition);
selected.SetArg(1, phi->Arg(true_arg));
selected.SetArg(2, phi->Arg(true_arg ^ 1u));
m_descriptor_selections.emplace_back(phi, Value(&selected));
return Value(&selected);
selected.SetArg(1, inst->Arg(true_arg));
selected.SetArg(2, inst->Arg(true_arg ^ 1u));
cached->second = Value(&selected);
return cached->second;
}
void MakeSource(const Inst& handle, uint32_t width, bool sampler, bool sample_adjust,
@@ -686,7 +701,7 @@ private:
for (uint32_t i = 0; i < width; i++) {
const auto value = base_reg != UINT32_MAX
? NativeDescriptorSource(handle.Arg(i), base_reg + i, pc) : handle.Arg(i);
descriptor.dwords[i] = LowerDescriptorPhi(value);
descriptor.dwords[i] = LowerDescriptorValue(value);
}
if (sample_adjust) {
descriptor.dwords[3] = CanonicalizeSampleAdjustDword3(descriptor.dwords[3]);
@@ -792,15 +807,6 @@ private:
}
}
}
for (auto* block: m_program.blocks) {
for (auto& inst: *block) {
uint32_t index = 0;
if (inst.GetOpcode() == ValueOpcode::LoadAddressU32 &&
ScalarReadMemory(inst, index) != nullptr && inst.Arg(1).Resolve().IsImmediate() &&
ValidateRuntimeValue(m_program, Value(&inst)))
CollectScalarRead(Value(&inst), inst.Flags<MemoryFlags>().pc);
}
}
for (auto* read: m_scalar_reads) {
const auto flags = read->Flags<MemoryFlags>();
auto& memory = m_program.memory_info[flags.index];
@@ -1463,7 +1469,18 @@ private:
const auto* step = phi->Arg(initial ^ 1u).Resolve().TryInstruction();
if (!zero.IsImmediate() || zero.GetType() != Type::U32 || zero.U32() != 0u ||
step == nullptr || step->GetOpcode() != ValueOpcode::IAdd32 ||
step->Parent() != phi->PhiBlock(initial ^ 1u)) continue;
step->Parent() == nullptr) continue;
// Structurization can forward the native increment through a synthetic continue block.
const auto* incoming = phi->PhiBlock(initial ^ 1u);
const auto* next = phi->Parent();
for (size_t edges = 0; incoming != step->Parent() && edges < m_program.blocks.size(); ++edges) {
if (incoming == nullptr || incoming->terminator.kind != CFG::TerminatorKind::Branch ||
incoming->terminator.true_block != next || incoming->ImmPredecessors().size() != 1u)
break;
next = incoming;
incoming = incoming->ImmPredecessors()[0];
}
if (incoming != step->Parent()) continue;
uint32_t increment = 0;
if ((step->Arg(0).Resolve() == key &&
ImmediateU32(step->Arg(1), increment) && increment == 1u) ||
@@ -1501,6 +1518,30 @@ private:
});
}
bool MatchScalarMaterialKey(Value key, DescriptorSource::IndirectDescriptor& indirect,
DescriptorSource& material_source) {
const auto* read = key.Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::ReadConstBuffer) return false;
uint32_t memory_index = 0;
const auto* memory = ScalarReadMemory(*read, memory_index);
Value index;
uint32_t offset = 0, stride = 0;
if (memory == nullptr || memory->kind != ResourceKind::ScalarBuffer ||
memory->offset > INT32_MAX || !MemoryIndexBelongsTo(memory_index, *read) ||
!MatchTableOffset(read->Arg(1), index, offset, stride) ||
((offset | stride | memory->offset) & 3u) != 0u ||
uint64_t {offset} + memory->offset > UINT32_MAX) return false;
const auto* bound = BoundedLoop(index, read->Parent());
if (bound == nullptr || bound->GetOpcode() != ValueOpcode::ULessThan32 ||
!MakeRuntimeTableSource(*read, material_source)) return false;
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead {
.source = InternSource(material_source), .stride = stride,
.offset = offset + memory->offset});
indirect.selector_first = Value(0u);
indirect.key_count = bound->Arg(1);
return true;
}
void FoldBoundedLoopSelectors() {
for (auto* header: m_program.blocks) {
for (auto& induction: *header) {
@@ -1736,6 +1777,10 @@ private:
}
if ((table_offset & 3u) != 0u ||
(bitscan && table_offset > UINT32_MAX - (32u * 32u - 1u))) return false;
} else if (!plan.table_indexed &&
MatchScalarMaterialKey(key, indirect, material_source)) {
// The host enumerates the guarded scalar loop; its key and T# payload stay on the GPU.
plan.retain_reads = true;
} else {
// A bounded V# supplies the complete image table. Leave every GPU selector
// and descriptor read in the shader; the host only translates table bytes.
@@ -1899,17 +1944,21 @@ private:
return found;
}
bool TryMakeFiniteImage(Inst& handle, IndirectDescriptorPlan& plan) {
if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u)
bool TryMakeFiniteDescriptor(Inst& handle, IndirectDescriptorPlan& plan) {
const bool sampler = handle.GetOpcode() == ValueOpcode::GetSamplerResource;
if ((!sampler && handle.GetOpcode() != ValueOpcode::GetImageResource) ||
handle.NumArgs() != (sampler ? 4u : 8u))
return false;
if (std::ranges::any_of(handle.Uses(), [](const Use& use) {
if (std::ranges::any_of(handle.Uses(), [&](const Use& use) {
const auto op = use.user->GetOpcode();
return op != ValueOpcode::ImageSampleRaw && op != ValueOpcode::ImageGatherRaw;
if (op != ValueOpcode::ImageSampleRaw) return sampler || op != ValueOpcode::ImageGatherRaw;
return sampler && (m_program.memory_info[use.user->Flags<MemoryFlags>().index]
.image_sample_flags & Decoder::ImageSampleFlagAdjust) != 0u;
})) return false;
bool has_phi = false;
for (size_t word = 0; word < handle.NumArgs(); ++word) has_phi |= handle.Arg(word).Resolve().IsPhi();
if (!has_phi) return false;
// Normalize the eight synchronized descriptor words to one GPU ordinal.
// Normalize synchronized descriptor words to one GPU ordinal.
// Build the complete graph before changing IR so an unsupported leaf is transactional.
struct Choice {
DescriptorSource descriptor;
@@ -1925,7 +1974,15 @@ private:
const auto index = static_cast<uint32_t>(choices.size());
choices.push_back({descriptor});
uint32_t bad = 0;
if (ValidateSource(descriptor, bad)) return index;
if (ValidateSource(descriptor, bad)) {
// Finite inline samplers have literal leaves; live descriptor tables use the
// existing direct sampler planning path.
if (sampler && !std::ranges::all_of(
std::span(descriptor.dwords).first(descriptor.dword_count),
[](Value word) { return word.IsImmediate() && word.GetType() == Type::U32; }))
return UINT32_MAX;
return index;
}
const auto* branch = descriptor.dwords[bad].TryInstruction();
if (branch == nullptr || branch->Parent() == nullptr ||
branch->GetOpcode() != ValueOpcode::Phi ||
@@ -1962,14 +2019,14 @@ private:
return index;
};
DescriptorSource root;
root.dword_count = 8u;
root.dword_count = handle.NumArgs();
for (uint32_t word = 0; word < root.dword_count; ++word) root.dwords[word] = handle.Arg(word);
if (visit(visit, root) == UINT32_MAX || choices.front().branch == nullptr) return false;
DescriptorSource image_source;
image_source.dword_count = 8u;
image_source.dwords.fill(Value(0u));
image_source.indirect_descriptor.emplace(DescriptorSource::IndirectDescriptor{});
auto& sources = image_source.indirect_descriptor->sources;
DescriptorSource finite_source;
finite_source.dword_count = root.dword_count;
finite_source.dwords.fill(Value(0u));
finite_source.indirect_descriptor.emplace(DescriptorSource::IndirectDescriptor{});
auto& sources = finite_source.indirect_descriptor->sources;
for (auto& choice: choices) {
if (choice.branch != nullptr) continue;
const auto source = InternSource(choice.descriptor);
@@ -1998,9 +2055,9 @@ private:
}
}
plan.handle = &handle;
plan.source = InternSource(image_source);
plan.source = InternSource(finite_source);
plan.key = choices.front().key;
plan.roots = image_source.dwords;
plan.roots = finite_source.dwords;
plan.reads.fill(nullptr);
return true;
}
@@ -2031,27 +2088,33 @@ private:
inst.NumArgs() == 0u) {
continue;
}
auto* handle = inst.Arg(0).Resolve().TryInstruction();
if (handle == nullptr || FindIndirectDescriptor(*handle) != nullptr) {
continue;
}
const auto flags = inst.Flags<MemoryFlags>();
const auto& memory = m_program.memory_info[flags.index];
DescriptorSource descriptor;
MakeSource(*handle, inst.GetOpcode() == ValueOpcode::StoreBufferU32 ? 4u : 8u,
false, false, memory.resource * 4u, descriptor, flags.pc);
uint32_t bad_dword = 0;
if (ValidateSource(descriptor, bad_dword)) continue;
IndirectDescriptorPlan plan;
if (TryMakeIndirectImage(*handle, descriptor, plan) || TryMakeFiniteImage(*handle, plan) ||
TryMakeIndirectBuffer(*handle, descriptor, plan)) {
for (const auto& previous: m_indirect_descriptors) {
if (plan.reads[0] != nullptr && previous.reads[0] == plan.reads[0] &&
!EquivalentValue(m_program, previous.key, plan.key))
Fail(flags.pc, "shared descriptor read has incompatible keys");
const auto plan_handle = [&](Value value, uint32_t width, uint32_t base, bool sampler) {
auto* handle = value.Resolve().TryInstruction();
if (handle == nullptr || FindIndirectDescriptor(*handle) != nullptr) return;
DescriptorSource descriptor;
MakeSource(*handle, width, sampler, false, base, descriptor, flags.pc);
uint32_t bad_dword = 0;
if (ValidateSource(descriptor, bad_dword)) return;
IndirectDescriptorPlan plan;
if (sampler ? TryMakeFiniteDescriptor(*handle, plan)
: (TryMakeIndirectImage(*handle, descriptor, plan) ||
TryMakeFiniteDescriptor(*handle, plan) ||
TryMakeIndirectBuffer(*handle, descriptor, plan))) {
for (const auto& previous: m_indirect_descriptors) {
if (plan.reads[0] != nullptr && previous.reads[0] == plan.reads[0] &&
!EquivalentValue(m_program, previous.key, plan.key))
Fail(flags.pc, "shared descriptor read has incompatible keys");
}
m_indirect_descriptors.push_back(std::move(plan));
}
m_indirect_descriptors.push_back(std::move(plan));
}
};
plan_handle(inst.Arg(0), inst.GetOpcode() == ValueOpcode::StoreBufferU32 ? 4u : 8u,
memory.resource * 4u, false);
if (inst.GetOpcode() == ValueOpcode::ImageSampleRaw &&
(memory.image_sample_flags & Decoder::ImageSampleFlagAdjust) == 0u)
plan_handle(inst.Arg(1), 4u, memory.sampler * 4u, true);
}
}
}
@@ -2180,7 +2243,20 @@ private:
return UINT32_MAX;
}
m_info.samplers.push_back({source, pc});
return static_cast<uint32_t>(m_info.samplers.size() - 1);
const auto root = static_cast<uint32_t>(m_info.samplers.size() - 1);
const auto* descriptor = Source(source);
if (descriptor != nullptr && descriptor->indirect_descriptor) {
const auto& sources = descriptor->indirect_descriptor->sources;
if (sources.empty()) Fail(pc, "finite sampler has no candidates");
std::vector<uint32_t> resources {root};
for (size_t i = 1; i < sources.size(); ++i) {
const auto candidate = AddSampler(sources[i], pc);
if (candidate == UINT32_MAX) return UINT32_MAX;
resources.push_back(candidate);
}
m_info.samplers[root].indirect_resources = std::move(resources);
}
return root;
}
void AddSampledPair(uint32_t image, uint32_t sampler, uint32_t pc) {
@@ -2340,8 +2416,15 @@ private:
uint32_t sampler_source = 0;
const bool sample_adjust =
(memory.image_sample_flags & Decoder::ImageSampleFlagAdjust) != 0;
GetHandle(inst.Arg(1), ValueOpcode::GetSamplerResource, 4, flags.pc,
memory.sampler * 4u, sampler_handle, sampler_source, true, sample_adjust);
sampler_handle = inst.Arg(1).Resolve().TryInstruction();
const auto* selected_sampler = sampler_handle != nullptr
? FindIndirectDescriptor(*sampler_handle) : nullptr;
if (selected_sampler != nullptr) {
sampler_source = selected_sampler->source;
} else {
GetHandle(inst.Arg(1), ValueOpcode::GetSamplerResource, 4, flags.pc,
memory.sampler * 4u, sampler_handle, sampler_source, true, sample_adjust);
}
sampler = AddSampler(sampler_source, flags.pc);
if (sampler == UINT32_MAX) {
Fail(flags.pc, "sampler resource limit exceeded");
@@ -2397,7 +2480,7 @@ private:
std::vector<HandlePatch> m_handle_patches;
std::vector<MemoryPatch> m_memory_patches;
std::vector<IndirectDescriptorPlan> m_indirect_descriptors;
std::vector<std::pair<const Inst*, Value>> m_descriptor_selections;
std::map<const Inst*, Value> m_descriptor_values;
bool m_shader_writes = false;
};
@@ -11,6 +11,35 @@
namespace Libs::Graphics::ShaderRecompiler::IR {
SrtRuntime SrtReadCapture::ObservedRuntime() {
auto runtime = m_source;
runtime.userdata = this;
runtime.read_specialization_memory = m_source.read_specialization_memory != nullptr
? ReadStrict : nullptr;
runtime.read_memory = ReadOrdinary;
return runtime;
}
bool SrtReadCapture::ReadStrict(void* userdata, uint64_t address, std::span<uint32_t> values) {
auto& capture = *static_cast<SrtReadCapture*>(userdata);
if (!capture.m_source.read_specialization_memory(capture.m_source.userdata, address, values)) {
return false;
}
capture.m_ranges.emplace_back(address, values.size_bytes());
return true;
}
bool SrtReadCapture::ReadOrdinary(void* userdata, uint64_t address, std::span<uint32_t> values) {
auto& capture = *static_cast<SrtReadCapture*>(userdata);
if (capture.m_source.read_memory != nullptr) {
if (!capture.m_source.read_memory(capture.m_source.userdata, address, values)) return false;
} else {
std::memcpy(values.data(), reinterpret_cast<const void*>(address), values.size_bytes());
}
capture.m_ranges.emplace_back(address, values.size_bytes());
return true;
}
SrtRuntime CleanRuntime(SrtRuntime runtime) {
runtime.read_memory = runtime.read_specialization_memory != nullptr
? runtime.read_specialization_memory
@@ -159,7 +188,7 @@ private:
bool Validate(Value value, bool require_uniform = true) {
value = value.Resolve();
// Host floating-point evaluation does not model shader rounding/denormal modes.
if (m_type == RuntimeValueType::Integer &&
if (m_type != RuntimeValueType::Any &&
TypesOverlap(value.GetType(), Type::F16 | Type::F32 | Type::F32x2)) {
return false;
}
@@ -188,6 +217,13 @@ private:
return valid;
};
const auto op = inst->GetOpcode();
if (m_type == RuntimeValueType::ImmutableInteger &&
(op == ValueOpcode::ReadConst || BufferAccessOf(op) != BufferAccess::None ||
AddressOpcodeInfoOf(op).access != AddressAccess::None ||
ImageOpcodeInfoOf(op).access != ImageAccess::None ||
SharedAccessOf(op) != SharedAccess::None)) {
return finish(false);
}
if (op == ValueOpcode::ReadConst) {
const auto slot = inst->NumArgs() == 2 ? inst->Arg(1).Resolve() : Value {};
if (inst->NumArgs() != 2 || inst->Arg(0).Resolve().TryInstruction() == nullptr ||
@@ -197,7 +233,7 @@ private:
slot.U32() >= m_program.srt_reads.size()) {
return finish(false);
}
if (m_type == RuntimeValueType::Integer) {
if (m_type != RuntimeValueType::Any) {
const auto active_mask = m_active_mask;
m_active_mask = {};
const bool valid = Validate(m_program.srt_reads[slot.U32()].value);
@@ -209,7 +245,7 @@ private:
if (!m_active_mask.IsEmpty() && IsRuntimeSelect(op) && inst->NumArgs() == 3 &&
inst->Arg(0).Resolve() == m_active_mask) {
// Empty EXEC reads lane zero, so ignored operands still require integer types.
if (m_type == RuntimeValueType::Integer && !Validate(inst->Arg(2), false)) {
if (m_type != RuntimeValueType::Any && !Validate(inst->Arg(2), false)) {
return finish(false);
}
return finish(Validate(inst->Arg(1)));
@@ -237,7 +273,7 @@ private:
return finish(true);
}
if (op == ValueOpcode::Phi) {
if (m_type == RuntimeValueType::Integer && !ValidateArguments(*inst, false)) {
if (m_type != RuntimeValueType::Any && !ValidateArguments(*inst, false)) {
return finish(false);
}
const auto invariant = ResolveInvariantPhi(m_program, value);
@@ -251,7 +287,7 @@ private:
inst->Arg(1).GetType() != Type::U1) {
return finish(false);
}
if (m_type == RuntimeValueType::Integer && !Validate(inst->Arg(1), false)) {
if (m_type != RuntimeValueType::Any && !Validate(inst->Arg(1), false)) {
return finish(false);
}
const auto active_mask = m_active_mask;
@@ -959,7 +995,8 @@ bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
}
uint32_t condition = 0;
auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this;
if (!block.condition.IsEmpty() && m_runtime.read_specialization_memory != nullptr &&
if (!block.condition.IsEmpty() &&
(m_runtime.read_specialization_memory != nullptr || !m_program.capture_specialization_reads) &&
predicate.Evaluate(block.condition, condition)) {
pending.push_back(block.successors[condition != 0u ? 0u : 1u]);
} else {
@@ -20,6 +20,20 @@ struct SrtRuntime {
std::span<const uint32_t> workgroup_counts;
};
class SrtReadCapture {
public:
SrtReadCapture(SrtRuntime source, std::vector<std::pair<uint64_t, uint64_t>>& ranges):
m_source(source), m_ranges(ranges) {}
SrtRuntime ObservedRuntime();
private:
static bool ReadStrict(void* userdata, uint64_t address, std::span<uint32_t> values);
static bool ReadOrdinary(void* userdata, uint64_t address, std::span<uint32_t> values);
SrtRuntime m_source;
std::vector<std::pair<uint64_t, uint64_t>>& m_ranges;
};
// Retained reads no longer need the guest instruction PC. A clean read evaluates
// its address and bounds through the strict reader as well as the final DWORD.
struct SrtReadFlags {
@@ -27,7 +41,7 @@ struct SrtReadFlags {
uint32_t clean = 0;
};
enum class RuntimeValueType { Any, Integer };
enum class RuntimeValueType { Any, Integer, ImmutableInteger };
bool ValidateRuntimeValue(const ResourcePlan& program, Value value,
RuntimeValueType type = RuntimeValueType::Any);
+4 -1
View File
@@ -574,10 +574,12 @@ static void ShaderGetStaticInputInfoPS(
static void ShaderGetStaticInputInfoCS(const HW::ComputeShaderInfo& regs,
const HW::ShaderRegisters& /*sh*/,
const ShaderMappedData& data, ShaderComputeInputInfo& info) {
const bool async_compute = info.async_compute;
const bool dispatch_thread_dimensions = info.dispatch_thread_dimensions;
const auto host_subgroup_size = info.host_subgroup_size;
const auto workgroup_counts = std::to_array(info.workgroup_counts);
info = {};
info.async_compute = async_compute;
info.dispatch_thread_dimensions = dispatch_thread_dimensions;
info.host_subgroup_size = host_subgroup_size;
std::ranges::copy(workgroup_counts, info.workgroup_counts);
@@ -693,7 +695,8 @@ void BuildStageStaticKey(const ShaderComputeInputInfo& info, std::vector<uint32_
key.push_back(info.thread_ids_num);
key.push_back(info.lds_size_dwords);
key.push_back(info.scratch_size_dwords);
key.push_back(static_cast<uint32_t>(info.dispatch_thread_dimensions));
key.push_back(static_cast<uint32_t>(info.dispatch_thread_dimensions) |
(static_cast<uint32_t>(info.async_compute) << 1u));
for (int i = 0; i < 3; i++) {
key.push_back(info.threads_num[i]);
key.push_back(static_cast<uint32_t>(info.group_id[i]));
+1
View File
@@ -146,6 +146,7 @@ struct ShaderVertexInputInfo {
};
struct ShaderComputeInputInfo: ShaderWorkgroupInputInfo {
bool async_compute = false;
uint8_t float_mode = 0xc0;
uint32_t dispatch_threads_num[3] = {0, 0, 0};
uint32_t workgroup_counts[3] = {0, 0, 0};
+112 -43
View File
@@ -204,6 +204,72 @@
</property>
</spacer>
</item>
<item>
<widget class="QSlider" name="grid_size_slider">
<property name="maximumSize">
<size><width>120</width><height>16777215</height></size>
</property>
<property name="toolTip">
<string>Game image size</string>
</property>
<property name="accessibleName">
<string>Game image size</string>
</property>
<property name="minimum">
<number>128</number>
</property>
<property name="maximum">
<number>320</number>
</property>
<property name="singleStep">
<number>16</number>
</property>
<property name="pageStep">
<number>32</number>
</property>
<property name="value">
<number>224</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<spacer name="gridSizeSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size><width>8</width><height>0</height></size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="grid_view_button">
<property name="minimumSize">
<size><width>36</width><height>36</height></size>
</property>
<property name="text">
<string>Grid view</string>
</property>
<property name="toolTip">
<string>Toggle grid view</string>
</property>
<property name="iconSize">
<size><width>32</width><height>32</height></size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="search_line_edit">
<property name="sizePolicy">
@@ -238,50 +304,53 @@
</layout>
</item>
<item>
<widget class="GameListTreeWidget" name="cfgs_list">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>Game</string>
<widget class="QStackedWidget" name="game_views">
<widget class="GameListTreeWidget" name="cfgs_list">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
</column>
<column>
<property name="text">
<string>Serial</string>
</property>
</column>
<column>
<property name="text">
<string>Game Version</string>
</property>
</column>
<column>
<property name="text">
<string>Firmware Version</string>
</property>
</column>
<column>
<property name="text">
<string>Size</string>
</property>
</column>
<column>
<property name="text">
<string>Path</string>
</property>
</column>
<column>
<property name="text">
<string>Status</string>
</property>
</column>
<column>
<property name="text">
<string>Comments</string>
</property>
</column>
<column>
<property name="text">
<string>Game</string>
</property>
</column>
<column>
<property name="text">
<string>Serial</string>
</property>
</column>
<column>
<property name="text">
<string>Game Version</string>
</property>
</column>
<column>
<property name="text">
<string>Firmware Version</string>
</property>
</column>
<column>
<property name="text">
<string>Size</string>
</property>
</column>
<column>
<property name="text">
<string>Path</string>
</property>
</column>
<column>
<property name="text">
<string>Status</string>
</property>
</column>
<column>
<property name="text">
<string>Comments</string>
</property>
</column>
</widget>
<widget class="QListView" name="cfgs_grid"/>
</widget>
</item>
</layout>
+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#000000" stroke-width="2" stroke-linejoin="round">
<rect x="5" y="5" width="8" height="8" rx="1"/>
<rect x="19" y="5" width="8" height="8" rx="1"/>
<rect x="5" y="19" width="8" height="8" rx="1"/>
<rect x="19" y="19" width="8" height="8" rx="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 363 B

@@ -12,9 +12,9 @@
class ConfigurationItem;
class CompatibilityDatabase;
class ControllerPreview;
class QEvent;
class QTreeWidgetItem;
class QPoint;
namespace Ui {
class ConfigurationListWidget;
@@ -29,6 +29,7 @@ public:
void SetRunEnabled(bool flag) { m_run_enabled = flag; }
void SetRuntimeDirectory(const QString& directory) { m_runtime_directory = directory; }
void SetControllerPreview(ControllerPreview* preview) { m_controller_preview = preview; }
[[nodiscard]] const ConfigurationItem* GetSelectedItem() const { return m_selected_item; }
ConfigurationItem* GetSelectedItem() { return m_selected_item; }
@@ -65,13 +66,13 @@ protected slots:
void edit_global_settings();
void edit_input_mapping();
void list_itemDoubleClicked(QTreeWidgetItem* witem, int column);
void show_context_menu(const QPoint& pos);
void open_game_folder();
void remove_save_data();
void filter_configurations(const QString& text);
private:
void SelectItem(QTreeWidgetItem* witem);
void ShowContextMenu(QTreeWidgetItem* witem);
void ApplyCompatibility();
void UpdateToolbarIcons();
void ImportGameSettings(QWidget* parent);
@@ -87,6 +88,7 @@ private:
Configuration m_global_info;
QMap<QString, Configuration*> m_custom_infos;
CompatibilityDatabase* m_compatibility = nullptr;
ControllerPreview* m_controller_preview = nullptr;
};
#endif // CONFIGURATION_LIST_WIDGET_H
-29
View File
@@ -1,29 +0,0 @@
#ifndef CONTROLLER_LIGHTBAR_H
#define CONTROLLER_LIGHTBAR_H
#include <QColor>
#include <QHash>
#include <QObject>
#include <QString>
#include <QTimer>
#include <SDL3/SDL_gamepad.h>
class ControllerLightbar final: public QObject {
public:
explicit ControllerLightbar(QObject* parent = nullptr);
~ControllerLightbar() override;
void SetColor(const QString& color);
void Stop();
private:
void Refresh();
void ApplyColor(SDL_Gamepad* gamepad) const;
QTimer m_timer;
QHash<SDL_JoystickID, SDL_Gamepad*> m_gamepads;
QColor m_color;
};
#endif // CONTROLLER_LIGHTBAR_H
+53
View File
@@ -0,0 +1,53 @@
#ifndef CONTROLLER_PREVIEW_H
#define CONTROLLER_PREVIEW_H
#include <QColor>
#include <QMap>
#include <QObject>
#include <QPointF>
#include <QSet>
#include <QString>
#include <QTimer>
#include <SDL3/SDL_gamepad.h>
class ControllerPreview final: public QObject {
Q_OBJECT
public:
explicit ControllerPreview(QObject* parent = nullptr);
~ControllerPreview() override;
void SetColor(const QString& color);
void SetPreviewEnabled(bool enabled);
void Stop();
void SetSuspended(bool suspended);
[[nodiscard]] const QSet<QString>& Pressed() const { return m_pressed; }
[[nodiscard]] QPointF LeftStick() const { return m_left_stick; }
[[nodiscard]] QPointF RightStick() const { return m_right_stick; }
[[nodiscard]] const QString& DeviceName() const { return m_device_name; }
[[nodiscard]] const QString& Error() const { return m_error; }
signals:
void StateChanged();
private:
void UpdateRunning();
void OpenGamepad(SDL_JoystickID id);
void Refresh();
void ApplyColor(SDL_Gamepad* gamepad) const;
QTimer m_timer;
QMap<SDL_JoystickID, SDL_Gamepad*> m_gamepads;
QColor m_color;
bool m_preview_enabled = false;
bool m_suspended = false;
QSet<QString> m_pressed;
QPointF m_left_stick;
QPointF m_right_stick;
QString m_device_name;
QString m_error;
};
#endif // CONTROLLER_PREVIEW_H
+40
View File
@@ -0,0 +1,40 @@
#ifndef LAUNCHER_INCLUDE_DUALSENSE_WIDGET_H_
#define LAUNCHER_INCLUDE_DUALSENSE_WIDGET_H_
#include <QPointF>
#include <QSet>
#include <QString>
#include <QTransform>
#include <QWidget>
class DualSenseWidget final: public QWidget {
Q_OBJECT
public:
explicit DualSenseWidget(QWidget* parent = nullptr);
[[nodiscard]] QSize sizeHint() const override;
void SetPressed(const QSet<QString>& pressed);
void SetSelected(const QString& selected);
void SetSticks(const QPointF& left, const QPointF& right);
signals:
void ControlClicked(const QString& id);
protected:
void paintEvent(QPaintEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void leaveEvent(QEvent* event) override;
private:
[[nodiscard]] QTransform ViewTransform() const;
[[nodiscard]] QString HitTest(const QPointF& position) const;
QSet<QString> m_pressed;
QString m_selected;
QString m_hovered;
QPointF m_left_stick;
QPointF m_right_stick;
};
#endif /* LAUNCHER_INCLUDE_DUALSENSE_WIDGET_H_ */
+18 -12
View File
@@ -2,32 +2,38 @@
#define LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
#include <QDialog>
#include <QHash>
#include <QStringList>
class ControllerPreview;
class DualSenseWidget;
class QLabel;
class QPushButton;
class QDoubleSpinBox;
class QTreeWidget;
class QTreeWidgetItem;
class QWidget;
class InputMappingDialog final: public QDialog {
public:
explicit InputMappingDialog(const QStringList& mapping, QWidget* parent = nullptr);
explicit InputMappingDialog(const QStringList& mapping, ControllerPreview* preview,
QWidget* parent = nullptr);
~InputMappingDialog() override;
[[nodiscard]] QStringList Mapping() const;
private:
void ChangeBinding();
void ClearBinding();
void ChangeBinding(const QString& id);
void RestoreDefaults();
void SetBinding(QTreeWidgetItem* item, const QString& binding);
void UpdateButtons();
void UpdatePreview();
void UpdateHighlights();
QTreeWidget* m_bindings = nullptr;
QPushButton* m_change_button = nullptr;
QPushButton* m_clear_button = nullptr;
QDoubleSpinBox* m_sensitivity = nullptr;
bool m_custom_bindings = false;
ControllerPreview* m_preview;
DualSenseWidget* m_controller = nullptr;
QHash<QString, QString> m_bindings;
QHash<QString, QPushButton*> m_buttons;
QString m_selected;
QLabel* m_status = nullptr;
QDoubleSpinBox* m_sensitivity = nullptr;
bool m_custom_bindings = false;
};
#endif /* LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ */
+1
View File
@@ -4,6 +4,7 @@
<file alias="global-settings.svg">icons/global-settings.svg</file>
<file alias="input-mapping.svg">icons/input-mapping.svg</file>
<file alias="refresh.svg">icons/refresh.svg</file>
<file alias="grid-view.svg">icons/grid-view.svg</file>
<file alias="remove-configuration.svg">icons/remove-configuration.svg</file>
<file alias="trophy.svg">icons/trophy.svg</file>
<file alias="hidden-trophy.png">../../assets/images/hidden-trophy.png</file>
+82 -4
View File
@@ -34,6 +34,7 @@
#include <QJsonParseError>
#include <QKeySequence>
#include <QLineEdit>
#include <QListView>
#include <QMenu>
#include <QMessageBox>
#include <QPainter>
@@ -46,7 +47,9 @@
#include <QSet>
#include <QSettings>
#include <QSignalBlocker>
#include <QSlider>
#include <QStyle>
#include <QStyledItemDelegate>
#include <QToolButton>
#include <QTreeWidget>
#include <QUrl>
@@ -141,6 +144,26 @@ static QStringList SettingsStringList(const QVariant& value) {
return text.isEmpty() ? QStringList() : QStringList({text});
}
class GameGridDelegate: public QStyledItemDelegate {
public:
using QStyledItemDelegate::QStyledItemDelegate;
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex&) const override {
return option.decorationSize + QSize(16, 24 + option.fontMetrics.lineSpacing() * 2);
}
protected:
void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override {
const auto image_size = option->decorationSize;
QStyledItemDelegate::initStyleOption(option, index);
option->decorationSize = image_size;
option->decorationPosition = QStyleOptionViewItem::Top;
option->decorationAlignment = Qt::AlignCenter;
option->displayAlignment = Qt::AlignHCenter | Qt::AlignTop;
option->features |= QStyleOptionViewItem::WrapText;
}
};
static void ConfigureGameList(Ui::ConfigurationListWidget* ui) {
ui->cfgs_list->setAlternatingRowColors(false);
ui->cfgs_list->setAllColumnsShowFocus(true);
@@ -152,6 +175,21 @@ static void ConfigureGameList(Ui::ConfigurationListWidget* ui) {
ui->cfgs_list->header()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
ui->cfgs_list->header()->setHighlightSections(false);
ui->cfgs_list->header()->setStretchLastSection(true);
// Both presentations use the same items, icons and current game.
ui->cfgs_grid->setModel(ui->cfgs_list->model());
ui->cfgs_grid->setSelectionModel(ui->cfgs_list->selectionModel());
ui->cfgs_grid->setViewMode(QListView::IconMode);
ui->cfgs_grid->setMovement(QListView::Static);
ui->cfgs_grid->setResizeMode(QListView::Adjust);
ui->cfgs_grid->setSelectionMode(QAbstractItemView::SingleSelection);
ui->cfgs_grid->setSelectionBehavior(QAbstractItemView::SelectRows);
ui->cfgs_grid->setEditTriggers(QAbstractItemView::NoEditTriggers);
ui->cfgs_grid->setSpacing(8);
ui->cfgs_grid->setUniformItemSizes(true);
ui->cfgs_grid->setWordWrap(true);
ui->cfgs_grid->setItemDelegate(new GameGridDelegate(ui->cfgs_grid));
ui->cfgs_grid->setContextMenuPolicy(Qt::CustomContextMenu);
}
static void AddSaveDataDir(QStringList* dirs, QSet<QString>* seen, const QString& root,
@@ -252,7 +290,27 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
connect(m_ui->cfgs_list, &QTreeWidget::itemDoubleClicked, this,
&ConfigurationListWidget::list_itemDoubleClicked);
connect(m_ui->cfgs_list, &QTreeWidget::customContextMenuRequested, this,
&ConfigurationListWidget::show_context_menu);
[this](const QPoint& pos) { ShowContextMenu(m_ui->cfgs_list->itemAt(pos)); });
connect(m_ui->cfgs_grid, &QListView::doubleClicked, this, [this](const QModelIndex& index) {
list_itemDoubleClicked(m_ui->cfgs_list->itemFromIndex(index), GAME_NAME_COLUMN);
});
connect(m_ui->cfgs_grid, &QListView::customContextMenuRequested, this,
[this](const QPoint& pos) {
ShowContextMenu(m_ui->cfgs_list->itemFromIndex(m_ui->cfgs_grid->indexAt(pos)));
});
connect(m_ui->grid_view_button, &QToolButton::toggled, this, [this](bool grid) {
m_ui->grid_size_slider->setVisible(grid);
auto* view = grid ? static_cast<QAbstractItemView*>(m_ui->cfgs_grid) : m_ui->cfgs_list;
m_ui->game_views->setCurrentWidget(view);
view->setCurrentIndex(view->currentIndex().siblingAtColumn(GAME_NAME_COLUMN));
view->scrollTo(view->currentIndex());
view->setFocus();
});
connect(m_ui->grid_view_button, &QToolButton::clicked, this,
&ConfigurationListWidget::WriteSettings);
connect(m_ui->cfgs_list->model(), &QAbstractItemModel::layoutChanged, this, [this]() {
filter_configurations(m_ui->search_line_edit->text());
});
connect(m_ui->search_line_edit, &QLineEdit::textChanged, this,
&ConfigurationListWidget::filter_configurations);
connect(m_compatibility, &CompatibilityDatabase::Updated, this,
@@ -261,6 +319,19 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
m_ui->cfgs_list->setDragDropMode(QAbstractItemView::NoDragDrop);
ReadSettings();
m_ui->grid_size_slider->setVisible(m_ui->grid_view_button->isChecked());
const auto resize_grid = [this](int width) {
m_ui->cfgs_grid->setIconSize(QSize(width, width * 9 / 16));
};
resize_grid(m_ui->grid_size_slider->value());
connect(m_ui->grid_size_slider, &QSlider::valueChanged, this, [this, resize_grid](int width) {
resize_grid(width);
if (!m_ui->grid_size_slider->isSliderDown()) {
WriteSettings();
}
});
connect(m_ui->grid_size_slider, &QSlider::sliderReleased, this,
&ConfigurationListWidget::WriteSettings);
if (m_compatibility->IsLocal()) {
m_compatibility->Load();
}
@@ -300,6 +371,7 @@ void ConfigurationListWidget::UpdateToolbarIcons() {
set_icon(m_ui->edit_button, QStringLiteral(":/icons/edit-configuration.svg"));
set_icon(m_ui->delete_button, QStringLiteral(":/icons/remove-configuration.svg"));
set_icon(m_ui->trophy_overview_button, QStringLiteral(":/icons/trophy.svg"));
set_icon(m_ui->grid_view_button, QStringLiteral(":/icons/grid-view.svg"));
}
void ConfigurationListWidget::WriteSettings() {
@@ -323,6 +395,8 @@ void ConfigurationListWidget::WriteSettings() {
s->beginGroup(CONF_LAUNCHER);
m_game_dirs = NormalizeGameDirectories(m_game_dirs);
s->setValue(CONF_GAME_DIRS, m_game_dirs);
s->setValue("grid_view", m_ui->grid_view_button->isChecked());
s->setValue("grid_icon_width", m_ui->grid_size_slider->value());
s->remove(CONF_GAME_DIR);
s->endGroup();
@@ -363,6 +437,9 @@ void ConfigurationListWidget::ReadSettings() {
ConfigurationEditDialog::ReadSettings(*s);
s->beginGroup(CONF_LAUNCHER);
m_ui->grid_view_button->setChecked(s->value("grid_view", false).toBool());
m_ui->grid_size_slider->setValue(
s->value("grid_icon_width", m_ui->grid_size_slider->value()).toInt());
m_game_dirs = NormalizeGameDirectories(SettingsStringList(s->value(CONF_GAME_DIRS)));
if (m_game_dirs.isEmpty()) {
m_game_dirs = NormalizeGameDirectories(SettingsStringList(s->value(CONF_GAME_DIR)));
@@ -959,7 +1036,7 @@ void ConfigurationListWidget::ExportGameSettings(QWidget* parent) const {
}
void ConfigurationListWidget::edit_input_mapping() {
InputMappingDialog dialog(m_global_info.host_input_mapping, this);
InputMappingDialog dialog(m_global_info.host_input_mapping, m_controller_preview, this);
if (dialog.exec() == QDialog::Accepted) {
m_global_info.host_input_mapping = dialog.Mapping();
WriteSettings();
@@ -1097,6 +1174,7 @@ void ConfigurationListWidget::filter_configurations(const QString& text) {
item->text(GAME_NAME_COLUMN).contains(query, Qt::CaseInsensitive) ||
item->text(GAME_SERIAL_COLUMN).contains(query, Qt::CaseInsensitive);
item->setHidden(!match);
m_ui->cfgs_grid->setRowHidden(item_index, !match);
if (match && item == m_selected_item) {
selection_is_shown = true;
@@ -1137,8 +1215,8 @@ void ConfigurationListWidget::list_itemDoubleClicked(QTreeWidgetItem* witem, int
}
}
void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->itemAt(pos));
void ConfigurationListWidget::ShowContextMenu(QTreeWidgetItem* witem) {
auto* item = static_cast<ConfigurationItem*>(witem);
if (item != nullptr) {
m_ui->cfgs_list->setCurrentItem(item);
-84
View File
@@ -1,84 +0,0 @@
#include "controllerLightbar.h"
#include <QDebug>
#include <SDL3/SDL.h>
ControllerLightbar::ControllerLightbar(QObject* parent): QObject(parent) {
m_timer.setInterval(100);
connect(&m_timer, &QTimer::timeout, this, &ControllerLightbar::Refresh);
}
ControllerLightbar::~ControllerLightbar() {
Stop();
}
void ControllerLightbar::SetColor(const QString& color) {
const QColor parsed(color);
if (!parsed.isValid()) {
Stop();
return;
}
if (m_color == parsed) {
return;
}
if (!m_timer.isActive()) {
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
qWarning() << "Cannot initialize controller color preview:" << SDL_GetError();
return;
}
m_timer.start();
}
m_color = parsed;
for (auto* gamepad: m_gamepads) {
ApplyColor(gamepad);
}
Refresh();
}
void ControllerLightbar::Stop() {
if (!m_timer.isActive()) {
return;
}
m_timer.stop();
for (auto* gamepad: m_gamepads) {
SDL_CloseGamepad(gamepad);
}
m_gamepads.clear();
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
m_color = {};
}
void ControllerLightbar::Refresh() {
// Bluetooth DualSense may defer an accepted LED command until its connection
// animation ends. Polling events also delivers those deferred commands.
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_GAMEPAD_ADDED && !m_gamepads.contains(event.gdevice.which)) {
auto* gamepad = SDL_OpenGamepad(event.gdevice.which);
if (gamepad == nullptr) {
qWarning() << "Cannot open controller color preview:" << SDL_GetError();
continue;
}
if (SDL_GetBooleanProperty(SDL_GetGamepadProperties(gamepad),
SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false)) {
m_gamepads.insert(event.gdevice.which, gamepad);
ApplyColor(gamepad);
} else {
SDL_CloseGamepad(gamepad);
}
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED) {
if (auto* gamepad = m_gamepads.take(event.gdevice.which); gamepad != nullptr) {
SDL_CloseGamepad(gamepad);
}
}
}
}
void ControllerLightbar::ApplyColor(SDL_Gamepad* gamepad) const {
if (!SDL_SetGamepadLED(gamepad, static_cast<Uint8>(m_color.red()),
static_cast<Uint8>(m_color.green()),
static_cast<Uint8>(m_color.blue()))) {
qWarning() << "Cannot set controller LED:" << SDL_GetError();
}
}
+202
View File
@@ -0,0 +1,202 @@
#include "controllerPreview.h"
#include <QDebug>
#include <SDL3/SDL.h>
#include <cmath>
namespace {
constexpr struct {
SDL_GamepadButton button;
const char* control;
} BUTTONS[] = {
{SDL_GAMEPAD_BUTTON_SOUTH, "Cross"}, {SDL_GAMEPAD_BUTTON_EAST, "Circle"},
{SDL_GAMEPAD_BUTTON_WEST, "Square"}, {SDL_GAMEPAD_BUTTON_NORTH, "Triangle"},
{SDL_GAMEPAD_BUTTON_DPAD_UP, "Up"}, {SDL_GAMEPAD_BUTTON_DPAD_DOWN, "Down"},
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, "Left"}, {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "Right"},
{SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "L1"}, {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "R1"},
{SDL_GAMEPAD_BUTTON_LEFT_STICK, "L3"}, {SDL_GAMEPAD_BUTTON_RIGHT_STICK, "R3"},
{SDL_GAMEPAD_BUTTON_START, "Options"},
};
double Axis(SDL_Gamepad* gamepad, SDL_GamepadAxis axis) {
const int value = SDL_GetGamepadAxis(gamepad, axis);
return value / (value < 0 ? 32768.0 : 32767.0);
}
} // namespace
ControllerPreview::ControllerPreview(QObject* parent): QObject(parent) {
m_timer.setInterval(100);
connect(&m_timer, &QTimer::timeout, this, &ControllerPreview::Refresh);
}
ControllerPreview::~ControllerPreview() {
Stop();
}
void ControllerPreview::SetColor(const QString& color) {
const QColor parsed(color);
if (m_color == parsed && m_timer.isActive()) {
return;
}
m_color = parsed;
UpdateRunning();
for (auto* gamepad: m_gamepads) {
ApplyColor(gamepad);
}
}
void ControllerPreview::SetPreviewEnabled(bool enabled) {
m_preview_enabled = enabled;
m_timer.setInterval(enabled ? 16 : 100);
UpdateRunning();
}
void ControllerPreview::Stop() {
m_preview_enabled = false;
m_color = {};
UpdateRunning();
}
void ControllerPreview::SetSuspended(bool suspended) {
if (m_suspended != suspended) {
m_suspended = suspended;
UpdateRunning();
}
}
void ControllerPreview::UpdateRunning() {
if (!m_suspended && (m_preview_enabled || m_color.isValid())) {
if (m_timer.isActive()) {
return;
}
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
m_error = QString::fromUtf8(SDL_GetError());
qWarning() << "Cannot initialize controller preview:" << m_error;
emit StateChanged();
return;
}
m_error.clear();
int count = 0;
auto* ids = SDL_GetGamepads(&count);
for (int index = 0; index < count; ++index) {
OpenGamepad(ids[index]);
}
SDL_free(ids);
m_timer.start();
Refresh();
emit StateChanged();
return;
}
if (m_timer.isActive()) {
m_timer.stop();
for (auto* gamepad: m_gamepads) {
SDL_CloseGamepad(gamepad);
}
m_gamepads.clear();
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
}
m_pressed.clear();
m_left_stick = {};
m_right_stick = {};
m_device_name.clear();
m_error = m_suspended ? tr("Preview is paused while a game is running.") : QString();
emit StateChanged();
}
void ControllerPreview::OpenGamepad(SDL_JoystickID id) {
if (!m_gamepads.contains(id)) {
if (auto* gamepad = SDL_OpenGamepad(id); gamepad != nullptr) {
m_gamepads.insert(id, gamepad);
ApplyColor(gamepad);
}
}
}
void ControllerPreview::Refresh() {
// One SDL event pump serves both the live diagram and lightbar preview.
// It also delivers deferred Bluetooth DualSense LED commands.
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_GAMEPAD_ADDED) {
OpenGamepad(event.gdevice.which);
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED) {
if (auto* gamepad = m_gamepads.take(event.gdevice.which); gamepad != nullptr) {
SDL_CloseGamepad(gamepad);
}
}
}
QSet<QString> pressed;
QPointF left;
QPointF right;
QString name;
if (!m_gamepads.isEmpty()) {
auto* gamepad = m_gamepads.first();
name = QString::fromUtf8(SDL_GetGamepadName(gamepad));
for (const auto& binding: BUTTONS) {
if (SDL_GetGamepadButton(gamepad, binding.button)) {
pressed.insert(QString::fromLatin1(binding.control));
}
}
const auto stick = [&](SDL_GamepadAxis x, SDL_GamepadAxis y, const char* prefix) {
QPointF position(Axis(gamepad, x), Axis(gamepad, y));
if (std::hypot(position.x(), position.y()) < 0.15) {
position = {};
}
const auto id = QString::fromLatin1(prefix);
if (position.x() < -0.25) pressed.insert(id + QStringLiteral("Left"));
if (position.x() > 0.25) pressed.insert(id + QStringLiteral("Right"));
if (position.y() < -0.25) pressed.insert(id + QStringLiteral("Up"));
if (position.y() > 0.25) pressed.insert(id + QStringLiteral("Down"));
return position;
};
left = stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, "LeftStick");
right = stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, "RightStick");
if (Axis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0.05) {
pressed.insert(QStringLiteral("L2"));
}
if (Axis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0.05) {
pressed.insert(QStringLiteral("R2"));
}
bool touching = false;
if (SDL_GetNumGamepadTouchpads(gamepad) > 0) {
for (int finger = 0; finger < SDL_GetNumGamepadTouchpadFingers(gamepad, 0); ++finger) {
bool down = false;
float x = 0;
if (SDL_GetGamepadTouchpadFinger(gamepad, 0, finger, &down, &x, nullptr, nullptr) &&
down) {
pressed.insert(x < 0.5f ? QStringLiteral("TouchPad")
: QStringLiteral("TouchPadRight"));
touching = true;
}
}
}
if (!touching && SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_TOUCHPAD)) {
pressed.insert(QStringLiteral("TouchPad"));
pressed.insert(QStringLiteral("TouchPadRight"));
}
}
if (pressed != m_pressed || left != m_left_stick || right != m_right_stick ||
name != m_device_name) {
m_pressed = pressed;
m_left_stick = left;
m_right_stick = right;
m_device_name = name;
emit StateChanged();
}
}
void ControllerPreview::ApplyColor(SDL_Gamepad* gamepad) const {
if (m_color.isValid() &&
SDL_GetBooleanProperty(SDL_GetGamepadProperties(gamepad),
SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false) &&
!SDL_SetGamepadLED(gamepad, static_cast<Uint8>(m_color.red()),
static_cast<Uint8>(m_color.green()),
static_cast<Uint8>(m_color.blue()))) {
qWarning() << "Cannot set controller LED:" << SDL_GetError();
}
}
+300
View File
@@ -0,0 +1,300 @@
#include "dualsenseWidget.h"
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <algorithm>
#include <cmath>
namespace {
enum class Shape { Button, Shoulder, Touchpad, Face, Dpad, Stick, Direction };
struct Control {
const char* id;
QRectF bounds;
Shape shape = Shape::Button;
int angle = 0;
};
// Painting and hit testing share a canvas with room above the shell for the shoulders.
const QRectF CANVAS(0, -20, 480, 330);
const Control CONTROLS[] = {
{"L2", {90, -12, 58, 18}, Shape::Shoulder},
{"R2", {332, -12, 58, 18}, Shape::Shoulder},
{"L1", {84, 12, 64, 12}, Shape::Shoulder},
{"R1", {332, 12, 64, 12}, Shape::Shoulder},
{"TouchPad", {150, 25, 90, 100}, Shape::Touchpad},
{"TouchPadRight", {240, 25, 90, 100}, Shape::Touchpad},
{"Options", {330, 60, 9, 20}},
{"Up", {105, 72, 22, 25}, Shape::Dpad, 0},
{"Right", {124, 94, 25, 22}, Shape::Dpad, 90},
{"Down", {105, 113, 22, 25}, Shape::Dpad, 180},
{"Left", {83, 94, 25, 22}, Shape::Dpad, 270},
{"Triangle", {353, 67, 26, 26}, Shape::Face},
{"Circle", {382, 96, 26, 26}, Shape::Face},
{"Cross", {353, 125, 26, 26}, Shape::Face},
{"Square", {324, 96, 26, 26}, Shape::Face},
{"L3", {143, 131, 66, 66}, Shape::Stick},
{"R3", {271, 131, 66, 66}, Shape::Stick},
{"LeftStickUp", {143, 131, 66, 66}, Shape::Direction, 0},
{"LeftStickRight", {143, 131, 66, 66}, Shape::Direction, 90},
{"LeftStickDown", {143, 131, 66, 66}, Shape::Direction, 180},
{"LeftStickLeft", {143, 131, 66, 66}, Shape::Direction, 270},
{"RightStickUp", {271, 131, 66, 66}, Shape::Direction, 0},
{"RightStickRight", {271, 131, 66, 66}, Shape::Direction, 90},
{"RightStickDown", {271, 131, 66, 66}, Shape::Direction, 180},
{"RightStickLeft", {271, 131, 66, 66}, Shape::Direction, 270},
};
QPainterPath TouchpadPath() {
QPainterPath path;
path.moveTo(158, 42);
path.cubicTo(156, 33, 168, 32, 178, 31);
path.cubicTo(219, 28, 261, 28, 302, 31);
path.cubicTo(312, 32, 324, 33, 322, 42);
path.cubicTo(318, 64, 315, 87, 309, 105);
path.cubicTo(306, 117, 299, 122, 286, 122);
path.lineTo(194, 122);
path.cubicTo(181, 122, 174, 117, 171, 105);
path.cubicTo(165, 87, 162, 64, 158, 42);
path.closeSubpath();
return path;
}
QPainterPath ControlPath(const Control& control) {
QPainterPath path;
if (control.shape == Shape::Direction) {
const auto inner = control.bounds.adjusted(7, 7, -7, -7);
const int start = 49 - control.angle;
path.arcMoveTo(control.bounds, start);
path.arcTo(control.bounds, start, 82);
path.arcTo(inner, start + 82, -82);
path.closeSubpath();
} else if (control.shape == Shape::Touchpad) {
path.addRect(control.bounds);
return TouchpadPath().intersected(path);
} else if (control.shape == Shape::Face || control.shape == Shape::Stick) {
path.addEllipse(control.shape == Shape::Stick ? control.bounds.adjusted(7, 7, -7, -7)
: control.bounds);
} else if (control.shape == Shape::Dpad) {
path.moveTo(-8, -12);
path.quadTo(-11, -12, -11, -8);
path.lineTo(-10, 3);
path.quadTo(-10, 6, -7, 8);
path.lineTo(0, 12);
path.lineTo(7, 8);
path.quadTo(10, 6, 10, 3);
path.lineTo(11, -8);
path.quadTo(11, -12, 8, -12);
path.closeSubpath();
QTransform transform;
transform.translate(control.bounds.center().x(), control.bounds.center().y());
transform.rotate(control.angle);
return transform.map(path);
} else if (control.shape == Shape::Shoulder) {
const auto& bounds = control.bounds;
path.moveTo(bounds.bottomLeft());
path.lineTo(bounds.left() + 2, bounds.top() + bounds.height() / 2);
path.cubicTo(bounds.left() + 10, bounds.top(), bounds.right() - 10, bounds.top(),
bounds.right() - 2, bounds.top() + bounds.height() / 2);
path.lineTo(bounds.bottomRight());
path.closeSubpath();
} else {
path.addRoundedRect(control.bounds, 4, 4);
}
return path;
}
void DrawSymbol(QPainter& painter, const Control& control) {
const auto id = QLatin1String(control.id);
painter.save();
painter.translate(control.bounds.center());
painter.setBrush(Qt::NoBrush);
if (control.shape == Shape::Dpad) {
painter.rotate(control.angle);
painter.drawPolyline(QPolygonF({{-4, 2}, {0, -2}, {4, 2}}));
} else if (id == QLatin1String("Triangle")) {
painter.drawPolygon(QPolygonF({{0, -7}, {7, 5}, {-7, 5}}));
} else if (id == QLatin1String("Circle")) {
painter.drawEllipse(QPointF(), 7, 7);
} else if (id == QLatin1String("Square")) {
painter.drawRect(QRectF(-6, -6, 12, 12));
} else if (id == QLatin1String("Cross")) {
painter.drawLine(QPointF(-5, -5), QPointF(5, 5));
painter.drawLine(QPointF(-5, 5), QPointF(5, -5));
}
painter.restore();
}
} // namespace
DualSenseWidget::DualSenseWidget(QWidget* parent): QWidget(parent) {
setMinimumSize(330, 230);
setMouseTracking(true);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setAccessibleName(tr("DualSense controller"));
setAccessibleDescription(tr("Click a control to select its binding. Connected controller input "
"lights up the controls."));
}
QSize DualSenseWidget::sizeHint() const {
return CANVAS.size().toSize();
}
void DualSenseWidget::SetPressed(const QSet<QString>& pressed) {
if (m_pressed != pressed) {
m_pressed = pressed;
update();
}
}
void DualSenseWidget::SetSelected(const QString& selected) {
if (m_selected != selected) {
m_selected = selected;
update();
}
}
void DualSenseWidget::SetSticks(const QPointF& left, const QPointF& right) {
if (m_left_stick != left || m_right_stick != right) {
m_left_stick = left;
m_right_stick = right;
update();
}
}
QTransform DualSenseWidget::ViewTransform() const {
const qreal scale = std::min(width() / CANVAS.width(), height() / CANVAS.height());
QTransform transform;
transform.translate(width() / 2.0, height() / 2.0);
transform.scale(scale, scale);
transform.translate(-CANVAS.center().x(), -CANVAS.center().y());
return transform;
}
QString DualSenseWidget::HitTest(const QPointF& position) const {
const auto point = ViewTransform().inverted().map(position);
for (const auto& control: CONTROLS) {
if (ControlPath(control).contains(point)) {
return QString::fromLatin1(control.id);
}
}
return {};
}
void DualSenseWidget::paintEvent(QPaintEvent*) {
const QColor black("#111111");
const QColor white("#eeeeee");
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setTransform(ViewTransform());
QPainterPath shell;
shell.moveTo(81, 48);
shell.cubicTo(151, 21, 329, 21, 399, 48);
shell.cubicTo(420, 94, 445, 166, 445, 215);
shell.cubicTo(447, 253, 437, 291, 419, 296);
shell.cubicTo(403, 302, 395, 297, 389, 283);
shell.cubicTo(381, 268, 373, 236, 354, 213);
shell.cubicTo(347, 205, 341, 205, 325, 205);
shell.lineTo(155, 205);
shell.cubicTo(139, 205, 133, 205, 126, 213);
shell.cubicTo(107, 236, 99, 268, 91, 283);
shell.cubicTo(85, 297, 77, 302, 61, 296);
shell.cubicTo(43, 291, 33, 253, 35, 215);
shell.cubicTo(35, 166, 60, 94, 81, 48);
shell.closeSubpath();
painter.setBrush(black);
painter.setPen(QPen(white, 1.5));
painter.drawPath(shell);
// Panel seams flow into the inner grips without a second outline across the belly.
QPainterPath seams;
seams.moveTo(153, 35);
seams.cubicTo(153, 69, 176, 105, 158, 127);
seams.cubicTo(123, 166, 108, 226, 76, 297);
seams.moveTo(327, 35);
seams.cubicTo(327, 69, 304, 105, 322, 127);
seams.cubicTo(357, 166, 372, 226, 404, 297);
painter.setBrush(Qt::NoBrush);
painter.drawPath(seams);
painter.setBrush(black);
painter.drawPath(TouchpadPath());
painter.drawRoundedRect(QRectF(141, 60, 9, 20), 4, 4); // Create button, currently unmapped.
painter.drawEllipse(QPointF(240, 166), 8, 8);
QFont font = painter.font();
font.setPixelSize(9);
font.setBold(true);
painter.setFont(font);
for (const auto& control: CONTROLS) {
const auto id = QString::fromLatin1(control.id);
const bool pressed = m_pressed.contains(id);
const bool selected = m_selected == id;
const bool hovered = m_hovered == id;
const bool active = pressed || selected || hovered;
if ((control.shape == Shape::Direction || control.shape == Shape::Touchpad) && !active) {
continue;
}
const QColor fill = pressed ? QColor("#2eb6df") : active ? QColor("#3a6179") : black;
painter.setBrush(fill);
painter.setPen(QPen(active ? QColor("#79e3ff") : white, selected ? 2 : 1.3));
if (control.shape == Shape::Touchpad) {
// Clip one shared surface so independent bindings never add a permanent center seam.
painter.save();
painter.setClipRect(control.bounds);
painter.drawPath(TouchpadPath());
painter.restore();
} else if (control.shape == Shape::Stick) {
painter.setBrush(black);
painter.drawEllipse(control.bounds);
const auto position = id == QLatin1String("L3") ? m_left_stick : m_right_stick;
QPointF offset(std::clamp(position.x(), -1.0, 1.0),
std::clamp(position.y(), -1.0, 1.0));
offset *= 5 / std::max(1.0, std::hypot(offset.x(), offset.y()));
painter.setBrush(fill);
painter.drawEllipse(control.bounds.adjusted(12, 12, -12, -12).translated(offset));
} else {
painter.drawPath(ControlPath(control));
painter.setPen(QPen(white, 1.3));
if (control.shape == Shape::Face || control.shape == Shape::Dpad) {
DrawSymbol(painter, control);
} else if (control.shape == Shape::Shoulder) {
painter.drawText(control.bounds.adjusted(0, 2, 0, 0), Qt::AlignCenter, id);
} else if (id == QLatin1String("Options")) {
painter.drawLine(control.bounds.center() - QPointF(0, 4),
control.bounds.center() + QPointF(0, 4));
}
}
}
}
void DualSenseWidget::mouseMoveEvent(QMouseEvent* event) {
const auto hovered = HitTest(event->position());
if (hovered != m_hovered) {
m_hovered = hovered;
setCursor(hovered.isEmpty() ? Qt::ArrowCursor : Qt::PointingHandCursor);
setToolTip(hovered);
update();
}
}
void DualSenseWidget::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
const auto id = HitTest(event->position());
if (!id.isEmpty()) {
emit ControlClicked(id);
return;
}
}
QWidget::mousePressEvent(event);
}
void DualSenseWidget::leaveEvent(QEvent*) {
m_hovered.clear();
unsetCursor();
update();
}
+221 -131
View File
@@ -1,61 +1,69 @@
#include "inputMappingDialog.h"
#include <QAbstractItemView>
#include "controllerPreview.h"
#include "dualsenseWidget.h"
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHash>
#include <QHeaderView>
#include <QKeyEvent>
#include <QKeySequence>
#include <QLabel>
#include <QMouseEvent>
#include <QPushButton>
#include <QScreen>
#include <QScrollArea>
#include <QStringList>
#include <QTreeWidget>
#include <QVBoxLayout>
namespace {
constexpr int BINDING_COLUMN = 1;
constexpr auto DEFAULT_MOUSE_SENSITIVITY = 1.0;
constexpr char MOUSE_SENSITIVITY[] = "MouseSensitivity=";
enum Group { Dpad, LeftStick, RightStick, Face, Shoulders, Center, Shortcuts, GroupCount };
struct PadControl {
const char* id;
const char* label;
const char* default_binding;
Group group;
int row;
int column;
};
constexpr PadControl PAD_CONTROLS[] = {
{"Up", "D-pad Up", "Up"},
{"Down", "D-pad Down", "Down"},
{"Left", "D-pad Left", "Left"},
{"Right", "D-pad Right", "Right"},
{"LeftStickUp", "Left stick Up", "W"},
{"LeftStickDown", "Left stick Down", "S"},
{"LeftStickLeft", "Left stick Left", "A"},
{"LeftStickRight", "Left stick Right", "D"},
{"RightStickUp", "Right stick Up", "T"},
{"RightStickDown", "Right stick Down", "G"},
{"RightStickLeft", "Right stick Left", "F"},
{"RightStickRight", "Right stick Right", "H"},
{"Triangle", "Triangle", "I"},
{"Circle", "Circle", "L"},
{"Cross", "Cross", "J"},
{"Square", "Square", "K"},
{"L1", "L1", "Q"},
{"R1", "R1", "E"},
{"L2", "L2", "Z"},
{"R2", "R2", "C"},
{"L3", "L3", "Left Shift"},
{"R3", "R3", "Left Ctrl"},
{"Options", "Options", "Return"},
{"TouchPad", "Touch pad left (SELECT)", "Backspace"},
{"TouchPadRight", "Touch pad right (START)", "Tab"},
{"SpeakerVolume", "Speaker volume (cycle)", "1"},
{"VibrationIntensity", "Vibration intensity (cycle)", "2"},
{"TriggerEffectIntensity", "Trigger effect intensity (cycle)", "3"},
{"Up", "Up", "Up", Dpad, 0, 1},
{"Down", "Down", "Down", Dpad, 2, 1},
{"Left", "Left", "Left", Dpad, 1, 0},
{"Right", "Right", "Right", Dpad, 1, 2},
{"LeftStickUp", "Up", "W", LeftStick, 0, 1},
{"LeftStickDown", "Down", "S", LeftStick, 2, 1},
{"LeftStickLeft", "Left", "A", LeftStick, 1, 0},
{"LeftStickRight", "Right", "D", LeftStick, 1, 2},
{"RightStickUp", "Up", "T", RightStick, 0, 1},
{"RightStickDown", "Down", "G", RightStick, 2, 1},
{"RightStickLeft", "Left", "F", RightStick, 1, 0},
{"RightStickRight", "Right", "H", RightStick, 1, 2},
{"Triangle", "Triangle", "I", Face, 0, 1},
{"Circle", "Circle", "L", Face, 1, 2},
{"Cross", "Cross", "J", Face, 2, 1},
{"Square", "Square", "K", Face, 1, 0},
{"L1", "L1", "Q", Shoulders, 0, 1},
{"R1", "R1", "E", Shoulders, 0, 2},
{"L2", "L2", "Z", Shoulders, 0, 0},
{"R2", "R2", "C", Shoulders, 0, 3},
{"L3", "Press / L3", "Left Shift", LeftStick, 1, 1},
{"R3", "Press / R3", "Left Ctrl", RightStick, 1, 1},
{"Options", "Options", "Return", Center, 0, 1},
{"TouchPad", "Touch pad left", "Backspace", Center, 0, 0},
{"TouchPadRight", "Touch pad right", "Tab", Center, 0, 2},
{"SpeakerVolume", "Speaker volume", "1", Shortcuts, 0, 0},
{"VibrationIntensity", "Vibration intensity", "2", Shortcuts, 0, 1},
{"TriggerEffectIntensity", "Trigger intensity", "3", Shortcuts, 0, 2},
};
QString KeypadName(int key) {
@@ -183,6 +191,17 @@ private:
QString m_binding;
};
void AssignBinding(QHash<QString, QString>& bindings, const QString& id, const QString& binding) {
for (auto item = bindings.begin(); item != bindings.end();) {
if (item.value().compare(binding, Qt::CaseInsensitive) == 0) {
item = bindings.erase(item);
} else {
++item;
}
}
bindings.insert(id, binding);
}
QHash<QString, QString> ParseMapping(const QStringList& mapping) {
QHash<QString, QString> result;
for (const auto& entry: mapping) {
@@ -191,15 +210,7 @@ QHash<QString, QString> ParseMapping(const QStringList& mapping) {
}
const auto separator = entry.indexOf(QLatin1Char('='));
if (separator > 0 && separator + 1 < entry.size()) {
const auto binding = entry.mid(separator + 1);
for (auto item = result.begin(); item != result.end();) {
if (item.value().compare(binding, Qt::CaseInsensitive) == 0) {
item = result.erase(item);
} else {
++item;
}
}
result.insert(entry.left(separator), binding);
AssignBinding(result, entry.left(separator), entry.mid(separator + 1));
}
}
return result;
@@ -216,82 +227,146 @@ double ParseMouseSensitivity(const QStringList& mapping) {
} // namespace
InputMappingDialog::InputMappingDialog(const QStringList& mapping, QWidget* parent)
: QDialog(parent) {
setWindowTitle(tr("Input Mapping"));
resize(460, 650);
InputMappingDialog::InputMappingDialog(const QStringList& mapping, ControllerPreview* preview,
QWidget* parent)
: QDialog(parent), m_preview(preview) {
setWindowTitle(tr("Controls"));
resize(QSize(1100, 820).boundedTo(screen()->availableGeometry().size()));
auto* layout = new QVBoxLayout(this);
layout->addWidget(
new QLabel(tr("Map keyboard or mouse buttons to DualSense controls.\n"
"Press F7 in-game to toggle mouse movement on the right stick."),
this));
layout->setSpacing(10);
auto* title = new QLabel(tr("Controller setup"), this);
auto font = title->font();
font.setPointSize(font.pointSize() + 7);
font.setBold(true);
title->setFont(font);
layout->addWidget(title);
auto* hint = new QLabel(
tr("Select a button to remap. Press controller buttons to test."), this);
hint->setWordWrap(true);
layout->addWidget(hint);
m_status = new QLabel(this);
m_status->setObjectName(QStringLiteral("controllerStatus"));
m_status->setWordWrap(true);
layout->addWidget(m_status);
const auto parsed = ParseMapping(mapping);
m_custom_bindings = !parsed.isEmpty();
auto* scroll = new QScrollArea(this);
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
auto* content = new QWidget(scroll);
auto* grid = new QGridLayout(content);
grid->setContentsMargins(0, 0, 0, 0);
grid->setSpacing(12);
QGroupBox* groups[GroupCount];
QGridLayout* cells[GroupCount];
const char* titles[] = {"D-Pad",
"Left stick",
"Right stick",
"Face buttons",
"Shoulders and triggers",
"Touch pad and options",
"In-game shortcuts (cycle)"};
for (int index = 0; index < GroupCount; ++index) {
groups[index] = new QGroupBox(tr(titles[index]), content);
cells[index] = new QGridLayout(groups[index]);
cells[index]->setSpacing(5);
}
m_bindings = ParseMapping(mapping);
m_custom_bindings = !m_bindings.isEmpty();
for (const auto& control: PAD_CONTROLS) {
const auto id = QString::fromLatin1(control.id);
auto* cell = new QGroupBox(tr(control.label), groups[control.group]);
auto* cell_layout = new QVBoxLayout(cell);
cell_layout->setContentsMargins(3, 5, 3, 3);
auto* button = new QPushButton(cell);
button->setObjectName(QStringLiteral("binding_") + id);
button->setAutoDefault(false);
button->setMinimumWidth(64);
button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
button->setAccessibleName(tr(titles[control.group]) + QStringLiteral(" / ") +
tr(control.label));
cell_layout->addWidget(button);
cells[control.group]->addWidget(cell, control.row, control.column);
m_buttons.insert(id, button);
if (!m_custom_bindings) {
m_bindings.insert(id, QString::fromLatin1(control.default_binding));
}
connect(button, &QPushButton::clicked, this, [this, id] { ChangeBinding(id); });
}
auto* left = new QVBoxLayout;
left->addWidget(groups[Dpad]);
left->addWidget(groups[LeftStick]);
left->addStretch();
grid->addLayout(left, 0, 0);
auto* center = new QVBoxLayout;
center->addWidget(groups[Shoulders]);
m_controller = new DualSenseWidget(content);
m_controller->setObjectName(QStringLiteral("controllerDiagram"));
center->addWidget(m_controller, 1);
center->addWidget(groups[Center]);
grid->addLayout(center, 0, 1);
auto* right = new QVBoxLayout;
right->addWidget(groups[Face]);
right->addWidget(groups[RightStick]);
right->addStretch();
grid->addLayout(right, 0, 2);
grid->setColumnStretch(1, 1);
grid->addWidget(groups[Shortcuts], 1, 0, 1, 3);
scroll->setWidget(content);
layout->addWidget(scroll, 1);
auto* sensitivity_layout = new QHBoxLayout;
sensitivity_layout->addWidget(new QLabel(tr("Mouse sensitivity"), this));
m_sensitivity = new QDoubleSpinBox(this);
m_sensitivity->setObjectName(QStringLiteral("mouseSensitivity"));
m_sensitivity->setRange(0.1, 5.0);
m_sensitivity->setSingleStep(0.1);
m_sensitivity->setDecimals(1);
m_sensitivity->setSuffix(QStringLiteral("x"));
m_sensitivity->setSuffix(QStringLiteral("×"));
m_sensitivity->setValue(ParseMouseSensitivity(mapping));
sensitivity_layout->addWidget(m_sensitivity);
sensitivity_layout->addWidget(
new QLabel(tr("F7 in-game toggles mouse movement on the right stick."), this));
sensitivity_layout->addStretch();
layout->addLayout(sensitivity_layout);
m_bindings = new QTreeWidget(this);
m_bindings->setColumnCount(2);
m_bindings->setHeaderLabels({tr("DualSense control"), tr("Host input")});
m_bindings->setRootIsDecorated(false);
m_bindings->setSelectionMode(QAbstractItemView::SingleSelection);
m_bindings->header()->setSectionResizeMode(0, QHeaderView::Stretch);
m_bindings->header()->setSectionResizeMode(1, QHeaderView::Stretch);
layout->addWidget(m_bindings);
for (const auto& control: PAD_CONTROLS) {
auto* item = new QTreeWidgetItem(m_bindings);
item->setText(0, tr(control.label));
item->setData(0, Qt::UserRole, QString::fromLatin1(control.id));
SetBinding(item, m_custom_bindings ? parsed.value(QString::fromLatin1(control.id))
: QString::fromLatin1(control.default_binding));
}
m_bindings->setCurrentItem(m_bindings->topLevelItem(0));
auto* controls = new QHBoxLayout;
m_change_button = new QPushButton(tr("Change..."), this);
m_clear_button = new QPushButton(tr("Clear"), this);
auto* defaults = new QPushButton(tr("Defaults"), this);
controls->addWidget(m_change_button);
controls->addWidget(m_clear_button);
controls->addWidget(defaults);
controls->addStretch();
layout->addLayout(controls);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults |
QDialogButtonBox::Save | QDialogButtonBox::Cancel,
this);
auto* defaults = buttons->button(QDialogButtonBox::RestoreDefaults);
defaults->setObjectName(QStringLiteral("defaults"));
defaults->setAutoDefault(false);
layout->addWidget(buttons);
connect(m_bindings, &QTreeWidget::itemDoubleClicked, this,
[this](QTreeWidgetItem*, int) { ChangeBinding(); });
connect(m_bindings, &QTreeWidget::itemSelectionChanged, this, [this]() { UpdateButtons(); });
connect(m_change_button, &QPushButton::clicked, this, [this]() { ChangeBinding(); });
connect(m_clear_button, &QPushButton::clicked, this, [this]() { ClearBinding(); });
connect(defaults, &QPushButton::clicked, this, [this]() { RestoreDefaults(); });
connect(m_controller, &DualSenseWidget::ControlClicked, this,
&InputMappingDialog::ChangeBinding);
connect(defaults, &QPushButton::clicked, this, &InputMappingDialog::RestoreDefaults);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
if (m_preview != nullptr) {
connect(m_preview, &ControllerPreview::StateChanged, this,
&InputMappingDialog::UpdatePreview);
m_preview->SetPreviewEnabled(true);
}
UpdateButtons();
UpdatePreview();
}
InputMappingDialog::~InputMappingDialog() {
if (m_preview != nullptr) {
m_preview->SetPreviewEnabled(false);
}
}
QStringList InputMappingDialog::Mapping() const {
QStringList result;
if (m_custom_bindings) {
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
const auto* item = m_bindings->topLevelItem(index);
const auto binding = item->data(BINDING_COLUMN, Qt::UserRole).toString();
for (const auto& control: PAD_CONTROLS) {
const auto id = QString::fromLatin1(control.id);
const auto binding = m_bindings.value(id);
if (!binding.isEmpty()) {
result.append(item->data(0, Qt::UserRole).toString() + QLatin1Char('=') + binding);
result.append(id + QLatin1Char('=') + binding);
}
}
}
@@ -302,58 +377,73 @@ QStringList InputMappingDialog::Mapping() const {
return result;
}
void InputMappingDialog::ChangeBinding() {
auto* item = m_bindings->currentItem();
if (item == nullptr) {
void InputMappingDialog::ChangeBinding(const QString& id) {
if (!m_buttons.contains(id)) {
return;
}
m_selected = id;
UpdateHighlights();
InputCaptureDialog dialog(this);
if (dialog.exec() != QDialog::Accepted) {
return;
dialog.setWindowTitle(tr("Set binding — %1").arg(m_buttons.value(id)->accessibleName()));
if (dialog.exec() == QDialog::Accepted) {
AssignBinding(m_bindings, id, dialog.Binding());
m_custom_bindings = true;
}
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
auto* other = m_bindings->topLevelItem(index);
if (other != item && other->data(BINDING_COLUMN, Qt::UserRole)
.toString()
.compare(dialog.Binding(), Qt::CaseInsensitive) == 0) {
SetBinding(other, {});
}
}
SetBinding(item, dialog.Binding());
m_custom_bindings = true;
}
void InputMappingDialog::ClearBinding() {
SetBinding(m_bindings->currentItem(), {});
m_custom_bindings = true;
m_selected.clear();
UpdateButtons();
}
void InputMappingDialog::RestoreDefaults() {
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
SetBinding(m_bindings->topLevelItem(index),
QString::fromLatin1(PAD_CONTROLS[index].default_binding));
m_bindings.clear();
for (const auto& control: PAD_CONTROLS) {
m_bindings.insert(QString::fromLatin1(control.id),
QString::fromLatin1(control.default_binding));
}
m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY);
m_custom_bindings = false;
}
void InputMappingDialog::SetBinding(QTreeWidgetItem* item, const QString& binding) {
if (item == nullptr) {
return;
}
item->setData(BINDING_COLUMN, Qt::UserRole, binding);
item->setText(BINDING_COLUMN, binding.isEmpty() ? tr("None") : binding);
UpdateButtons();
}
void InputMappingDialog::UpdateButtons() {
if (m_change_button == nullptr) {
return;
for (auto item = m_buttons.begin(); item != m_buttons.end(); ++item) {
auto* button = item.value();
const auto binding = m_bindings.value(item.key());
const auto label = binding.isEmpty() ? tr("None") : binding;
button->setText(QString(label).replace(QLatin1Char('&'), QLatin1String("&&")));
button->setToolTip(tr("%1: %2. Click to change.").arg(button->accessibleName(), label));
}
UpdateHighlights();
}
void InputMappingDialog::UpdatePreview() {
const auto pressed = m_preview != nullptr ? m_preview->Pressed() : QSet<QString>();
m_controller->SetPressed(pressed);
m_controller->SetSticks(m_preview != nullptr ? m_preview->LeftStick() : QPointF(),
m_preview != nullptr ? m_preview->RightStick() : QPointF());
UpdateHighlights();
if (m_preview != nullptr && !m_preview->Error().isEmpty()) {
m_status->setText(tr("Controller preview unavailable: %1").arg(m_preview->Error()));
} else if (m_preview != nullptr && !m_preview->DeviceName().isEmpty()) {
m_status->setText(tr("Connected: %1").arg(m_preview->DeviceName()));
} else {
m_status->setText(tr("No controller connected"));
}
}
void InputMappingDialog::UpdateHighlights() {
m_controller->SetSelected(m_selected);
for (auto item = m_buttons.begin(); item != m_buttons.end(); ++item) {
const bool down = m_preview != nullptr && m_preview->Pressed().contains(item.key());
auto colors = palette();
if (down) {
colors.setColor(QPalette::Button, colors.color(QPalette::Highlight));
colors.setColor(QPalette::ButtonText, colors.color(QPalette::HighlightedText));
} else if (item.key() == m_selected) {
colors.setColor(QPalette::ButtonText, colors.color(QPalette::Highlight));
}
auto label_font = font();
label_font.setBold(down);
item.value()->setPalette(colors);
item.value()->setFont(label_font);
}
const auto* item = m_bindings->currentItem();
m_change_button->setEnabled(item != nullptr);
m_clear_button->setEnabled(item != nullptr &&
!item->data(BINDING_COLUMN, Qt::UserRole).toString().isEmpty());
}
+8 -6
View File
@@ -4,7 +4,7 @@
#include "configuration.h"
#include "configurationItem.h"
#include "configurationListWidget.h"
#include "controllerLightbar.h"
#include "controllerPreview.h"
#include "gameContent.h"
#include "updateChecker.h"
@@ -89,7 +89,7 @@ private:
Ui::MainDialog* m_ui = {nullptr};
MainDialog* m_main_dialog = nullptr;
UpdateChecker* m_update_checker = nullptr;
ControllerLightbar m_lightbar;
ControllerPreview m_controller_preview;
QString m_interpreter;
QProcess m_process;
@@ -112,6 +112,7 @@ MainDialogPrivate::~MainDialogPrivate() {
void MainDialogPrivate::Setup(MainDialog* main_dialog) {
m_ui = new Ui::MainDialog;
m_ui->setupUi(main_dialog);
m_ui->widget->SetControllerPreview(&m_controller_preview);
m_main_dialog = main_dialog;
m_update_checker = new UpdateChecker(main_dialog);
@@ -127,7 +128,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
connect(m_ui->widget, &ConfigurationListWidget::PreviewControllerColor, this,
[this](const QString& color) {
if (m_process.state() == QProcess::NotRunning) {
m_lightbar.SetColor(color);
m_controller_preview.SetColor(color);
}
});
connect(m_ui->widget, &ConfigurationListWidget::Run, this, &MainDialogPrivate::Run);
@@ -514,7 +515,7 @@ void MainDialogPrivate::Run() {
}
m_running_item->SetRunning(true);
m_lightbar.Stop();
m_controller_preview.SetSuspended(true);
auto info = m_ui->widget->CreateConfiguration(*m_running_item);
m_main_dialog->RunInterpreter(&m_process, *info);
@@ -535,10 +536,11 @@ void MainDialogPrivate::Update() {
m_ui->widget->SetRunEnabled(run_enabled);
if (m_process.state() != QProcess::NotRunning) {
m_lightbar.Stop();
m_controller_preview.SetSuspended(true);
return;
}
m_lightbar.SetColor(m_ui->widget->GetGlobalControllerColor());
m_controller_preview.SetSuspended(false);
m_controller_preview.SetColor(m_ui->widget->GetGlobalControllerColor());
}
#include "mainDialog.moc"
+14
View File
@@ -1,3 +1,13 @@
QListView#cfgs_grid {
background: palette(base);
border: 1px solid palette(mid);
color: palette(text);
}
QListView#cfgs_grid::item {
padding: 8px;
}
GameListTreeWidget#cfgs_list {
background: transparent;
border: 1px solid palette(mid);
@@ -39,6 +49,7 @@ QLineEdit#search_line_edit:focus {
border-color: palette(highlight);
}
QToolButton#grid_view_button,
QToolButton#refresh_button,
QToolButton#global_settings_button,
QToolButton#input_mapping_button,
@@ -51,6 +62,7 @@ QToolButton#delete_button {
padding: 3px;
}
QToolButton#grid_view_button:hover,
QToolButton#refresh_button:hover,
QToolButton#global_settings_button:hover,
QToolButton#input_mapping_button:hover,
@@ -61,6 +73,8 @@ QToolButton#delete_button:hover {
border-color: palette(mid);
}
QToolButton#grid_view_button:checked,
QToolButton#grid_view_button:pressed,
QToolButton#refresh_button:pressed,
QToolButton#global_settings_button:pressed,
QToolButton#input_mapping_button:pressed,
+81
View File
@@ -20,6 +20,7 @@
#include "graphics/presentation/videoOut.h"
#include "graphics/presentation/window.h"
#include "graphics/shader/shader.h"
#include "kernel/memory.h"
#include "kernel/pthread.h"
#include "libs/errno.h"
#include "libs/libs.h"
@@ -44,6 +45,23 @@ namespace Libs::Graphics {
static RenderContext* g_renderer = nullptr;
namespace Gen5Driver {
static constexpr uint64_t DriverDmemBase = 0x0fe0000000ull;
static constexpr uint32_t DriverDmemSize = 0x200000;
static constexpr uint32_t AgcDmemOffset = 0x40000;
struct DriverDmem {
std::mutex mutex;
int64_t physical_offset = -1;
};
static DriverDmem g_driver_dmem;
static int InitializeDmem();
static void ReleaseDmem();
} // namespace Gen5Driver
template <typename... Args>
static void AgcTrace(const char* format, const Args&... args) {
if (Config::GraphicsDebugDumpEnabled()) {
@@ -70,6 +88,7 @@ void Initialize() {
void Shutdown() {
EXIT_IF(g_renderer == nullptr);
g_renderer->ShutdownGpu();
Gen5Driver::ReleaseDmem();
VideoOut::VideoOutShutdown();
WindowShutdown();
g_renderer = nullptr;
@@ -337,6 +356,38 @@ int KYTY_SYSV_ABI AgcInit(uint32_t* state, uint32_t ver) {
}
printf("version = %u\n", ver);
std::scoped_lock lock {Gen5Driver::g_driver_dmem.mutex};
if (Gen5Driver::g_driver_dmem.physical_offset >= 0) {
return OK;
}
if (const auto result = Gen5Driver::InitializeDmem(); result != OK) {
return result;
}
struct FunctionShaderBindings {
uint64_t code;
uint32_t user_data[2];
};
static_assert(sizeof(FunctionShaderBindings) == 16);
// Ordered-count queue validator, including its return to the caller.
static constexpr std::array<uint32_t, 16> validator {
0xbeeb03ff, 0x00000021, 0xb96a1818, 0xbf06106a,
0xbf800000, 0x8590807e, 0xb96a0a18, 0xbf066a80,
0xbf800000, 0x85ea807e, 0x88ea106a, 0xbf870003,
0xbefc03ff, 0x8a6ca000, 0xbf920009, 0xbefd210e,
};
const auto base = Gen5Driver::DriverDmemBase + Gen5Driver::AgcDmemOffset;
const auto function_table = base + 0x60;
const auto code_address = base + 0x100; // Shader code requires 256-byte alignment.
std::array<FunctionShaderBindings, 8> functions {};
functions[2].code = code_address;
const std::array<uint32_t, 4> descriptor {
static_cast<uint32_t>(function_table),
static_cast<uint32_t>(function_table >> 32u) | 0x00100000u,
static_cast<uint32_t>(functions.size()), 0x5204u,
};
std::memcpy(reinterpret_cast<void*>(function_table), functions.data(), sizeof(functions));
std::memcpy(reinterpret_cast<void*>(code_address), validator.data(), sizeof(validator));
std::memcpy(reinterpret_cast<void*>(base), descriptor.data(), sizeof(descriptor));
return OK;
}
@@ -4224,6 +4275,36 @@ namespace Gen5Driver {
LIB_NAME("Graphics5Driver", "Graphics5Driver");
static int InitializeDmem() {
int64_t physical_offset = -1;
auto result = LibKernel::Memory::KernelAllocateDirectMemory(
0, LibKernel::Memory::KernelGetDirectMemorySize(), DriverDmemSize, DriverDmemSize,
12, &physical_offset);
if (result != OK) {
return result;
}
void* address = reinterpret_cast<void*>(DriverDmemBase);
result = LibKernel::Memory::KernelMapNamedDirectMemory(
&address, DriverDmemSize, 0x33, 0, physical_offset, DriverDmemSize, "SceAgcDriver");
if (result != OK) {
EXIT_IF(LibKernel::Memory::KernelCheckedReleaseDirectMemory(physical_offset, DriverDmemSize) != OK);
return result;
}
EXIT_IF(reinterpret_cast<uint64_t>(address) != DriverDmemBase);
std::memset(address, 0, DriverDmemSize);
g_driver_dmem.physical_offset = physical_offset;
return OK;
}
static void ReleaseDmem() {
std::scoped_lock lock {g_driver_dmem.mutex};
if (g_driver_dmem.physical_offset >= 0) {
EXIT_IF(LibKernel::Memory::KernelCheckedReleaseDirectMemory(
g_driver_dmem.physical_offset, DriverDmemSize) != OK);
g_driver_dmem.physical_offset = -1;
}
}
struct TessellationDriverState {
uint64_t tf_ring_base = 0;
uint32_t tf_ring_size = 0;
+38 -17
View File
@@ -435,7 +435,7 @@ void TestUnbasedFlatCacheHitMaterializes() {
"unbased FLAT plan produced unexpected descriptors");
}
void TestWrittenDescriptorUsesStrictReaderOnce() {
void TestWrittenDescriptorPredicateReads(bool memory_condition, bool loop) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
@@ -456,10 +456,10 @@ void TestWrittenDescriptorUsesStrictReaderOnce() {
source.dword_count = 4;
program.descriptor_sources.push_back(source);
program.info.buffers.push_back({.source = 0, .written = true});
// A host-evaluable branch captures resource reads and needs the writable
// descriptor's clean provenance for the renderer's disjointness proof.
// Only memory-derived host decisions need clean, disjoint descriptor reads.
auto &condition = block.AppendNewInst(ValueOpcode::IEqual32,
{Value(&offset), Value(4u)});
{memory_condition ? Value(&read) : Value(&offset),
Value(memory_condition ? 0x8000u : 4u)});
auto &store_block = AddValueBlock(program);
AddValueBlock(program);
program.blocks[0]->condition = Value(&condition);
@@ -467,12 +467,26 @@ void TestWrittenDescriptorUsesStrictReaderOnce() {
Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::ConditionalBranch;
program.blocks[0]->terminator.true_block = program.blocks[1];
program.blocks[0]->terminator.false_block = program.blocks[2];
program.blocks[1]->id = 1;
program.blocks[1]->terminator.kind =
Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::Return;
program.blocks[2]->id = 2;
program.blocks[2]->terminator.kind =
Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::Return;
if (loop) {
// The predicate reaches the store through a continue block, then loops back.
auto &continued = AddValueBlock(program);
program.blocks[0]->terminator.true_block = &continued;
continued.terminator.kind = Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::Branch;
continued.terminator.true_block = &store_block;
continued.AddBranch(&store_block);
store_block.terminator.kind = Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::Branch;
store_block.terminator.true_block = &block;
store_block.AddBranch(&block);
}
block.AddBranch(program.blocks[0]->terminator.true_block);
block.AddBranch(program.blocks[2]);
program.memory_info.push_back({.kind = ResourceKind::Buffer, .resource = 0});
auto &output = store_block.AppendNewInst(ValueOpcode::GetBufferResource,
{source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]});
@@ -480,8 +494,8 @@ void TestWrittenDescriptorUsesStrictReaderOnce() {
{Value(&output), Value(0u), Value(0u), Value(0u), Value(1u), Value(true)})
.SetFlags(MemoryFlags{.index = 1});
auto plan = ExtractResourcePlan(program);
Check(plan.capture_specialization_reads,
"conditional writable descriptor lost its alias proof");
Check(plan.capture_specialization_reads == memory_condition && !plan.control_flow.empty(),
"conditional writable descriptor used the wrong memory dependency policy");
struct Reads { uint32_t ordinary = 0; uint32_t strict = 0; bool clean = false; } reads;
const std::array<uint32_t, 1> user_data{4u};
const SrtRuntime runtime{
@@ -501,16 +515,21 @@ void TestWrittenDescriptorUsesStrictReaderOnce() {
}};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(!MaterializeResources(plan, runtime, snapshot, specialization) &&
reads.ordinary == 0 && reads.strict == 1,
"GPU-dirty dynamic writable descriptor bypassed strict provenance");
reads.clean = true;
reads.strict = 0;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
reads.ordinary == 0 && reads.strict == 1 && snapshot.buffers[0].dwords[0] == 0x8000u &&
snapshot.specialization_reads ==
std::vector<std::pair<uint64_t, uint64_t>>{{0x1004u, 4u}},
"writable descriptor was evaluated twice or scalar EXEC suppressed its read");
for (const bool clean : {false, true}) {
reads = {.clean = clean};
// A failed predicate explores both edges and the writable descriptor retries its clean read.
const bool expected = !memory_condition || clean;
Check(MaterializeResources(plan, runtime, snapshot, specialization) == expected &&
reads.ordinary == (memory_condition ? 0u : 1u) &&
reads.strict == (memory_condition ? (clean ? 1u : 2u) : 0u),
"writable descriptor changed reader policy or bypassed a dirty memory predicate");
if (!expected) continue;
Check(snapshot.buffers[0].dwords[0] == 0x8000u &&
snapshot.specialization_reads == (memory_condition
? std::vector<std::pair<uint64_t, uint64_t>>{{0x1004u, 4u}}
: std::vector<std::pair<uint64_t, uint64_t>>{}),
"writable descriptor lost its address or captured an immutable predicate");
}
}
void TestFailedMaterializationRejectsStage() {
@@ -671,7 +690,9 @@ int main() {
TestUniformVectorDescriptorRead();
TestExactReciprocalDescriptorArithmetic();
TestUnbasedFlatCacheHitMaterializes();
TestWrittenDescriptorUsesStrictReaderOnce();
for (const bool memory_condition : {false, true})
for (const bool loop : {false, true})
TestWrittenDescriptorPredicateReads(memory_condition, loop);
TestFailedMaterializationRejectsStage();
TestFiniteImageRefreshReusesScalarReads();
TestMixedSamplerVariantsShareRuntimeDescriptor();
+167 -7
View File
@@ -742,7 +742,9 @@ void TestBoundedComputeImageLoop() {
enum class Variant {
Bounded, Plain, Nonzero, TrueEdge, WrongGuard, EntryBypass, ExitBypass, GuardBlock,
WrongStep, DivergentBound, DivergentKey, Disjunction, WrongPolarity,
IncrementBypass, PreviousBound, Masked, MaskedWrongGuard, MaskResurrection, StatusOverwrite,
IncrementBypass, PreviousBound, Forwarded, ForwardedChain, ForwardedJoin,
ForwardedBranch, ForwardedIncrementBypass,
Masked, MaskedWrongGuard, MaskResurrection, StatusOverwrite,
GuardedDiamond, GuardedDiamondBypass
};
const auto make_plan = [](Variant variant) {
@@ -759,16 +761,21 @@ void TestBoundedComputeImageLoop() {
auto *guard = masked ? fixture.AddBlock() : header;
auto *increment = masked ? fixture.AddBlock() : latch;
auto *final_exit = variant == Variant::PreviousBound ? fixture.AddBlock() : exit;
const bool forwarded = variant >= Variant::Forwarded && variant <= Variant::ForwardedIncrementBypass;
auto *forward = forwarded ? fixture.AddBlock() : increment;
auto *incoming = variant == Variant::ForwardedChain ? fixture.AddBlock() : forward;
auto *detour = variant == Variant::ForwardedJoin ? fixture.AddBlock() : nullptr;
entry->AddBranch(header);
if (!masked) {
header->AddBranch(exit);
header->AddBranch(body);
body->AddBranch(latch);
latch->AddBranch(header);
latch->AddBranch(forwarded ? forward : header);
if (variant == Variant::PreviousBound) latch->AddBranch(final_exit);
if (variant == Variant::EntryBypass) entry->AddBranch(body);
if (variant == Variant::ExitBypass) exit->AddBranch(body);
if (variant == Variant::IncrementBypass || variant == Variant::PreviousBound)
if (variant == Variant::IncrementBypass || variant == Variant::PreviousBound ||
variant == Variant::ForwardedIncrementBypass)
exit->AddBranch(latch);
fixture.program.blocks[0]->terminator = {
.kind = variant == Variant::EntryBypass
@@ -789,16 +796,16 @@ void TestBoundedComputeImageLoop() {
.kind = variant == Variant::PreviousBound
? CFG::TerminatorKind::ConditionalBranch
: CFG::TerminatorKind::Branch,
.true_block = fixture.program.blocks[1u],
.true_block = forwarded ? forward : fixture.program.blocks[1u],
.false_block = final_exit};
fixture.program.blocks[4]->terminator = {
.kind = final_exit != exit || variant == Variant::ExitBypass ||
variant == Variant::IncrementBypass
variant == Variant::IncrementBypass || variant == Variant::ForwardedIncrementBypass
? CFG::TerminatorKind::Branch
: CFG::TerminatorKind::Return,
.true_block =
fixture.program.blocks[final_exit != exit ||
variant == Variant::IncrementBypass
variant == Variant::IncrementBypass || variant == Variant::ForwardedIncrementBypass
? 3u
: 2u]};
if (final_exit != exit)
@@ -822,6 +829,29 @@ void TestBoundedComputeImageLoop() {
count}, 0, compare);
const auto initial_active = fixture.Emit(ValueOpcode::INotEqual32,
{local, Value(0u)}, 0, entry);
if (forwarded) {
forward->AddBranch(incoming == forward ? header : incoming);
forward->terminator = {.kind = CFG::TerminatorKind::Branch,
.true_block = incoming == forward ? header : incoming};
if (incoming != forward) {
incoming->AddBranch(header);
incoming->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = header};
}
if (variant == Variant::ForwardedBranch) {
forward->AddBranch(exit);
forward->condition = initial_active;
forward->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = header, .false_block = exit};
}
if (detour != nullptr) {
latch->AddBranch(detour);
latch->condition = initial_active;
latch->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = forward, .false_block = detour};
detour->AddBranch(forward);
detour->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = forward};
}
}
if (masked) {
auto *active_phi = diamond ? nullptr : &header->AppendNewInst(
ValueOpcode::Phi, {}, static_cast<uint64_t>(Type::U1));
@@ -934,7 +964,9 @@ void TestBoundedComputeImageLoop() {
{key, Value(variant == Variant::WrongStep ? 2u : 1u)},
0, increment);
phi.AddPhiOperand(entry, variant == Variant::DivergentKey ? local : Value(0u));
phi.AddPhiOperand(increment, step);
phi.AddPhiOperand(forwarded ? incoming : increment, step);
Check(!forwarded || step.ResolveInstruction()->Parent() != phi.PhiBlock(1),
"forwarded loop fixture did not separate the increment from its Phi incoming edge");
fixture.block = variant == Variant::GuardBlock ? header : body;
const auto table = fixture.Address(fixture.UserData(0), fixture.UserData(1));
@@ -975,7 +1007,11 @@ void TestBoundedComputeImageLoop() {
make_plan(Variant::TrueEdge);
make_plan(Variant::Masked);
make_plan(Variant::GuardedDiamond);
make_plan(Variant::Forwarded);
make_plan(Variant::ForwardedChain);
for (const auto variant : {Variant::IncrementBypass, Variant::PreviousBound,
Variant::ForwardedJoin, Variant::ForwardedBranch,
Variant::ForwardedIncrementBypass,
Variant::MaskedWrongGuard,
Variant::MaskResurrection, Variant::StatusOverwrite,
Variant::GuardedDiamondBypass}) {
@@ -2612,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}) {
@@ -3572,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);
+413 -45
View File
@@ -5055,7 +5055,7 @@ public:
for (const auto mode : {0x41u, 0x61u}) {
for (const auto &groups : empty_dispatches) {
context.GetRenderExecutor().DispatchDirect(
0, scheduler.Current(), groups[0], groups[1], groups[2], mode);
0, scheduler.Current(), groups[0], groups[1], groups[2], mode, false);
}
}
Require(name, "HTile fixture",
@@ -9950,6 +9950,113 @@ public:
return result;
}
void CheckScalarLeafCall() {
constexpr const char* name = "ScalarLeafCall";
constexpr uintptr_t base = 0x0000000204800000ull;
constexpr uint64_t size = 0x10000;
constexpr auto table = base + 0x100;
constexpr auto first = base + 0x1000;
constexpr auto second = base + 0x2000;
constexpr auto output = base + 0x4000;
std::vector<u32> code{
EncodeSMovB32(20, InlineU32(0)),
0xbeea03ffu, static_cast<u32>(base), 0xbeeb03ffu, static_cast<u32>(base >> 32u),
0xf4080135u, 0xfa000000u, 0xbf8cc07fu, 0xf4240382u, 0xfa000020u,
0xbf8cc07fu, 0xbf130e80u, 0xbf840004u, 0xf4240402u, 0xfa000028u,
0xbf8cc07fu, 0xbe8e210eu, EncodeVop1(0x01, 1, 20),
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(1, 0, 0)};
AppendEnd(&code);
ShaderMapUserData(reinterpret_cast<uint64_t>(code.data()),
{.type = Prospero::ShaderBinaryType::kCs,
.code_size_bytes = static_cast<u32>(code.size() * sizeof(u32))});
EnsureRuntimeContext();
int64_t physical = -1;
Require(name, "allocate", LibKernel::Memory::KernelAllocateDirectMemory(
0, LibKernel::Memory::KernelGetDirectMemorySize(), size, size, 0, &physical) == 0,
"call fixture allocation failed");
void* mapped = reinterpret_cast<void*>(base);
Require(name, "map", LibKernel::Memory::KernelMapDirectMemory(
&mapped, size, 0x3, 0x10, physical, size) == 0 && mapped == reinterpret_cast<void*>(base),
"call fixture mapping failed");
std::memset(mapped, 0, size);
const ShaderBufferResource table_desc{{static_cast<u32>(table),
static_cast<u32>(table >> 32u) | (16u << 16u), 8, 0x5204u}};
LibKernel::Memory::WriteBacking(base, table_desc.fields, sizeof(table_desc.fields));
const std::array leaf{EncodeSMovB32(20, 255), 41u, EncodeSop1(0x21, 125, 14)};
for (const auto address : {first, second})
LibKernel::Memory::WriteBacking(address, leaf.data(), sizeof(leaf));
RenderContext context(m_runtime_context);
context.InitializeGpu(nullptr);
LibKernel::Memory::InstallGpuResources(&context);
context.GetGpu().SendCommandSync([&] {
CommandProcessor processor(context, 0);
processor.BufferInit();
context.MapMemory(base, size);
auto& cache = context.GetBufferCache();
auto& shaders = processor.GetShCtx();
shaders.SetCsShader({.data_addr = reinterpret_cast<uint64_t>(code.data()),
.num_thread_x = 1, .num_thread_y = 1, .num_thread_z = 1,
.wave_size = 64, .user_sgpr = 4});
ShaderBufferResource out{};
out.UpdateAddress48(output);
out.fields[2] = 4;
out.fields[3] = DstSel(4, 5, 6, 7) |
(static_cast<u32>(Prospero::BufferFormat::k32UInt) << 12u);
for (u32 i = 0; i < 4; ++i)
shaders.SetCsUserSgpr(i, out.fields[i], HW::UserSgprType::Unknown);
uint64_t original_id = 0;
struct Case { uint64_t address; u32 value; bool update; u32 field = 0; };
for (const auto test : {Case{first, 41, false}, Case{first, 41, false},
Case{second, 41, false}, Case{second, 73, true},
Case{first, 41, false}, Case{first, 6, false, 0x1818},
Case{first, 0, false, 0x0a18}}) {
Require(name, "refresh function pointer", context.InvalidateMemory(table + 0x20, 8),
"function table escaped mapped memory");
const uint64_t target = test.address | 3u;
LibKernel::Memory::WriteBacking(table + 0x20, &target, sizeof(target));
if (test.field != 0) {
Require(name, "refresh hardware-field leaf", context.InvalidateMemory(first, sizeof(leaf)),
"hardware-field leaf escaped mapped memory");
const std::array field_leaf{0xb96a0000u | test.field,
EncodeSMovB32(20, 106), EncodeSop1(0x21, 125, 14)};
LibKernel::Memory::WriteBacking(first, field_leaf.data(), sizeof(field_leaf));
}
if (test.update) {
auto [buffer, offset] = cache.ObtainBuffer(test.address + 4, 4, true);
buffer->Fill(offset, 4, test.value);
Require(name, "GPU-written function code", cache.HasGpuDirtyBytes(test.address + 4, 4),
"code mutation did not remain GPU-owned before refresh");
}
ShaderComputeInputInfo input{};
const auto program = context.GetPipelineCache().GetComputeProgram(
shaders.GetCs(), processor.GetCtx().GetShaderRegisters(), input);
if (original_id == 0) original_id = program.id;
Require(name, "exact function identity",
(program.id == original_id) == (test.value == 41),
"helper bytes were stale or identical relocated/restored code recompiled");
Require(name, "current code dependency",
std::ranges::find(input.stage.resources->specialization_reads,
std::pair<uint64_t, uint64_t>{test.address, sizeof(leaf)}) !=
input.stage.resources->specialization_reads.end(),
"function relocation retained an old code dependency");
processor.DispatchDirect(1, 1, 1, 0x41u);
cache.ReadMemory(output, sizeof(u32));
u32 actual = 0;
Require(name, "scalar leaf execution and return",
LibKernel::Memory::TryReadBacking(output, &actual, sizeof(actual)) && actual == test.value,
"caller did not observe the leaf's scalar result after return");
}
context.UnmapMemory(base, size);
context.GetCommandScheduler().Finish();
});
LibKernel::Memory::InstallGpuResources(nullptr);
context.ShutdownGpu();
Require(name, "release", LibKernel::Memory::KernelCheckedReleaseDirectMemory(physical, size) == 0,
"call fixture release failed");
std::printf("[gpu] %-32s ok\n", name);
}
void CheckNativeIndirectDispatch() {
constexpr const char *name = "NativeIndirectDispatch";
constexpr uintptr_t base = 0x0000000204600000ull;
@@ -10604,7 +10711,7 @@ public:
for (uint32_t i = 0; i < user_data.size(); i++) {
shaders.SetCsUserSgpr(i, user_data[i], HW::UserSgprType::Unknown);
}
executor.DispatchDirect(0, scheduler.Current(), (count + 63) / 64, 1, 1, 0x41u);
executor.DispatchDirect(0, scheduler.Current(), (count + 63) / 64, 1, 1, 0x41u, false);
};
const auto metadata_words = static_cast<uint32_t>(metadata_size / 4);
fill_metadata(metadata_words);
@@ -15249,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;
@@ -15564,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;
@@ -28387,7 +28496,7 @@ TestCase ScalarMemoryLoadVariants() {
std::vector<u32> expected = initial;
expected.insert(expected.end(), initial.begin(), initial.end());
return {"ScalarMemoryLoadVariants",
TestCase test{"ScalarMemoryLoadVariants",
code,
initial,
expected,
@@ -28396,6 +28505,9 @@ TestCase ScalarMemoryLoadVariants() {
O::S_BUFFER_LOAD_DWORDX2, O::S_BUFFER_LOAD_DWORDX4,
O::S_BUFFER_LOAD_DWORDX8, O::S_BUFFER_LOAD_DWORDX16, O::V_MOV_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
test.bda_mappings = {{0, 0}};
test.required_spirv = {"get_bda_pointer"};
return test;
}
TestCase ScalarBufferOffsetAlignmentAndCarry() {
@@ -28439,11 +28551,43 @@ TestCase ScalarLoadSignedImmediateOffsetAddsSoffset() {
AppendStoreSgpr(&code, 1, 0);
AppendEnd(&code);
return {"ScalarLoadSignedImmediateOffsetAddsSoffset",
TestCase test{"ScalarLoadSignedImmediateOffsetAddsSoffset",
code,
{0x11111111u, 0x22222222u},
{0x22222222u, 0x22222222u},
{O::S_MOV_B32, O::S_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
test.bda_mappings = {{0, 0}};
test.required_spirv = {"get_bda_pointer"};
return test;
}
TestCase ScalarPayloadBeforeAliasedAtomic() {
using O = ShaderOpcode;
constexpr u32 GuestBase = 0x10000u;
std::vector<u32> code;
AppendSMovLiteral(&code, 8, GuestBase);
AppendSMovLiteral(&code, 9, 0u);
code.insert(code.end(), {EncodeSmem0(0x00, 12, 4), EncodeSmem1(24, 125),
EncodeSopp(0x0c, 0xc07f)});
AppendVMovU32(&code, 0, 1u << 20u);
AppendVMovU32(&code, 1, 0u);
// Same 64-bit OR and byte offset as SAROS b62b494cb2567011.
code.insert(code.end(), {0xe1680018u, 0x80000000u});
AppendStoreSgpr(&code, 12, 0);
AppendEnd(&code);
TestCase test{"ScalarPayloadBeforeAliasedAtomic", code,
{0u, 0u, 0u, 0u, 0u, 0u, 0x10u, 0x11223344u},
{0x10u, 0u, 0u, 0u, 0u, 0u, 0x100010u, 0x11223344u},
{O::S_MOV_B32, O::S_LOAD_DWORD, O::S_WAITCNT, O::V_MOV_B32,
O::BUFFER_ATOMIC_OR_X2, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
test.bda_mappings = {{GuestBase, 0}};
test.required_spirv = {"get_bda_pointer"};
test.forbidden_spirv = {"flattened_srt"};
test.compute_info.threads_num[0] = 1;
test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1;
test.compute_info.workgroup_register = 4;
test.has_compute_info = true;
return test;
}
TestCase BufferLoadStore() {
@@ -31034,23 +31178,26 @@ TestCase BranchVccnzUsesCarryProducedWaveMask() {
return test;
}
TestCase ScalarLoadAlignsComponentsAndMasksAddress() {
TestCase ScalarLoadAlignsComponents() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendSMovLiteral(&code, 0, 1u);
AppendSMovLiteral(&code, 2, 3u);
AppendSMovLiteral(&code, 3, 0xffff0000u);
AppendSMovLiteral(&code, 3, 0u); // S_LOAD uses the full base; only buffer descriptors mask it.
code.push_back(EncodeSmem0(0x00, 1, 1));
code.push_back(EncodeSmem1(3, 0));
AppendStoreSgpr(&code, 1, 0);
AppendEnd(&code);
return {"ScalarLoadAlignsComponentsAndMasksAddress",
TestCase test{"ScalarLoadAlignsComponents",
code,
{0x11111111u, 0x22222222u},
{0x11111111u, 0x22222222u},
{O::S_MOV_B32, O::S_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
test.bda_mappings = {{0, 0}};
test.required_spirv = {"get_bda_pointer"};
return test;
}
TestCase ScalarLoadAlignsDynamicBase() {
@@ -34802,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;
@@ -35047,42 +35310,87 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
std::vector<VulkanHarness::Image> textures;
for (u32 resource = 0; resource < 7u; ++resource) {
const auto layers = resource == 4u ? 6u : 1u;
std::vector<u32> pixels(layers * 4u, std::bit_cast<u32>(float(resource)));
std::vector<u32> pixels(layers * 16u, std::bit_cast<u32>(float(resource)));
std::vector<u32> mip(layers * 4u,
std::bit_cast<u32>(resource == 0u ? 0.0f : float(resource + 10u)));
textures.push_back(vulkan.CreateImageMips(
test.name, 1, 1, vk::Format::eR32G32B32A32Sfloat,
vk::ImageUsageFlagBits::eSampled, {pixels}, 4u,
test.name, 2, 2, vk::Format::eR32G32B32A32Sfloat,
vk::ImageUsageFlagBits::eSampled, {pixels, mip}, 4u,
vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageType::e2D,
resource == 4u ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, layers));
}
const auto native_sampler = vulkan.CreateSampler(test.name);
for (const u32 wave_size : {32u, 64u}) {
compute.wave_size = wave_size;
compute.host_subgroup_size = vulkan.SubgroupSize();
compute.threads_num[0] = wave_size;
compute.threads_num[1] = compute.threads_num[2] = 1u;
compiled.program.wave_size = wave_size;
compiled.spirv = ShaderRecompiler::Spirv::EmitProgram(compiled.program, {.compute = &compute});
compiled.packed_user_data.resize(compiled.program.bindings.ShaderDataDwords());
ValidateSpirv(test.name, compiled.spirv);
const auto halves = wave_size > compute.host_subgroup_size ? 2u : 1u;
Require(test.name, "native array run samples", tools.Disassemble(compiled.spirv, &text) &&
CountText(text, "OpImageSampleExplicitLod") == 3u * halves,
"mixed runs expanded into per-image samples");
test.initial.assign(wave_size, 0xdeadbeefu);
test.expected.assign(wave_size, 0u);
for (u32 lane = 0; lane < ordinals.size(); ++lane) {
test.expected[lane] = std::bit_cast<u32>(float(root.indirect_resources[ordinals[lane]]));
for (const bool fetch : {false, true}) {
if (fetch) {
test.name = "IndirectImageFetchContiguousRuns";
const auto position = std::ranges::find_if(*block, [&](const Inst &inst) {
return &inst == &sample;
});
auto &read = *block->PrependNewInst(
position, ValueOpcode::ImageRead,
{Value(&image), Value(&address), Value(true)}, memory_flag_bits);
sample.ReplaceUsesWith(Value(&read), false);
sampler.Invalidate();
compiled.program.info.samplers.clear();
compiled.program.info.sampled_pairs.clear();
compiled.program.info.images[4].cube = false;
compiled.program.memory_info[0].image_sample_flags = 0;
compiled.program.memory_info[0].image_has_mip = true;
for (u32 component = 0; component < 4u; ++component)
address.SetArg(component, Value(0u));
// Native DIM=2DArray places LOD after the layer, even for a 2D candidate.
address.SetArg(3, Value(1u));
compiled.program.shader_info_complete = false;
CollectShaderInfo(compiled.program, {.compute = &compute});
}
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size());
vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, nullptr, nullptr,
native_sampler, textures);
const auto actual = vulkan.ReadBuffer(test.name, buffer, test.expected.size());
vulkan.DestroyBuffer(&buffer);
CompareWords(test, "root, native slots, cube and mapping bounds", test.expected, actual);
for (const u32 wave_size : {32u, 64u}) {
compute.wave_size = wave_size;
compute.host_subgroup_size = vulkan.SubgroupSize();
compute.threads_num[0] = wave_size;
compute.threads_num[1] = compute.threads_num[2] = 1u;
compiled.program.wave_size = wave_size;
compiled.spirv = ShaderRecompiler::Spirv::EmitProgram(compiled.program, {.compute = &compute});
compiled.packed_user_data.resize(compiled.program.bindings.ShaderDataDwords());
ValidateSpirv(test.name, compiled.spirv);
const auto halves = wave_size > compute.host_subgroup_size ? 2u : 1u;
Require(test.name, "native array image runs", tools.Disassemble(compiled.spirv, &text) &&
CountText(text, fetch ? "OpImageFetch" : "OpImageSampleExplicitLod") == 3u * halves,
"mixed runs expanded into per-image accesses");
if (fetch) {
std::vector<u32> decorated;
std::vector<u32> fetched_images;
for (size_t offset = 5; offset < compiled.spirv.size();) {
const auto words = std::span<const u32>(compiled.spirv).subspan(
offset, compiled.spirv[offset] >> 16u);
const auto opcode = static_cast<spv::Op>(words[0] & 0xffffu);
if (opcode == spv::OpDecorate && words[2] == spv::DecorationNonUniform)
decorated.push_back(words[1]);
if (opcode == spv::OpImageFetch) fetched_images.push_back(words[3]);
offset += words.size();
}
Require(test.name, "nonuniform fetch operands",
std::ranges::count_if(fetched_images, [&](u32 operand) {
return std::ranges::find(decorated, operand) != decorated.end();
}) == 2u * halves,
"a dynamically selected fetch image lacks its nonuniform decoration");
}
test.initial.assign(wave_size, 0xdeadbeefu);
test.expected.assign(wave_size, 0u);
for (u32 lane = 0; lane < ordinals.size(); ++lane) {
const auto resource = root.indirect_resources[ordinals[lane]];
test.expected[lane] = std::bit_cast<u32>(float(resource + (fetch && 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);
const auto actual = vulkan.ReadBuffer(test.name, buffer, test.expected.size());
vulkan.DestroyBuffer(&buffer);
CompareWords(test, "root, native slots, cube and mapping bounds", test.expected, actual);
}
std::printf("[compute] %-32s ok\n", test.name);
}
vulkan.Device().destroySampler(native_sampler);
for (auto &texture : textures) vulkan.DestroyImage(&texture);
std::printf("[compute] %-32s ok\n", test.name);
}
TestCase ImageStoreMipSelectsPpsa01340Descriptor() {
@@ -36691,7 +36999,8 @@ std::vector<TestCase> MakeCases() {
AddCase(ScalarMemoryLoadVariants);
AddCase(ScalarBufferOffsetAlignmentAndCarry);
AddCase(ScalarLoadSignedImmediateOffsetAddsSoffset);
AddCase(ScalarLoadAlignsComponentsAndMasksAddress);
AddCase(ScalarPayloadBeforeAliasedAtomic);
AddCase(ScalarLoadAlignsComponents);
AddCase(ScalarLoadAlignsDynamicBase);
cases.push_back(ScalarBufferFromLoopReadlane(32));
cases.push_back(ScalarBufferFromLoopReadlane(64));
@@ -39615,17 +39924,19 @@ void CheckResourcePlanHandoff() {
ValidateSpirv(name, compiled.spirv);
const auto &bindings = compiled.program.bindings;
Require(name, "GPU data requirements",
compiled.program.info.user_data_registers.empty() && bindings.descriptor_counts[0] == 1 &&
(bindings.descriptor_counts[static_cast<size_t>(DescriptorBindingKind::FlattenedSrt)] != 0) ==
compiled.program.info.user_data_registers ==
(numeric_read ? std::vector<u32>{0, 1} : std::vector<u32>{}) &&
bindings.descriptor_counts[0] == 1 &&
bindings.descriptor_counts[static_cast<size_t>(DescriptorBindingKind::FlattenedSrt)] == 0 &&
(bindings.descriptor_counts[static_cast<size_t>(DescriptorBindingKind::BdaPagetable)] != 0) ==
numeric_read,
"descriptor-only shader retained SRT uploads or live scalar data was removed");
"descriptor-only shader retained SRT uploads or native scalar payload lost its address");
memory[0] = 0x2000u;
memory[4] = 13u;
Require(name, "independent host plan",
MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers[0].dwords[0] == 0x2000u &&
(!numeric_read || snapshot.flattened_srt.back() == 13u),
snapshot.flattened_srt.size() == 4u,
"compiled shader cleanup invalidated or froze the host resource plan");
}
std::printf("[host] %-32s ok\n", name);
@@ -41240,6 +41551,53 @@ void CheckShaderFusion() {
std::printf("[host] %-32s ok\n", "ShaderFusion");
}
void CheckAgcSystemTable(RenderContext &renderer) {
constexpr const char *name = "AgcSystemTable";
constexpr uint64_t driver_base = 0x0fe0000000ull;
constexpr uint64_t table_address = driver_base + 0x40000;
constexpr std::array<uint32_t, 16> native_validator{
0xbeeb03ff, 0x00000021, 0xb96a1818, 0xbf06106a,
0xbf800000, 0x8590807e, 0xb96a0a18, 0xbf066a80,
0xbf800000, 0x85ea807e, 0x88ea106a, 0xbf870003,
0xbefc03ff, 0x8a6ca000, 0xbf920009, 0xbefd210e};
LibKernel::Memory::InstallGpuResources(&renderer);
uint32_t state = 0xabcdef01u;
Require(name, "driver startup", Gen5::AgcInit(&state, 13) == 0 && state == 0xabcdef01u,
"Agc startup failed or changed its ignored public state pointer");
LibKernel::Memory::VirtualQueryInfo mapping{};
Require(name, "shared driver backing",
LibKernel::Memory::KernelVirtualQuery(reinterpret_cast<void *>(driver_base), 0,
&mapping, sizeof(mapping)) == 0 &&
mapping.start == driver_base && mapping.end == driver_base + 0x200000 &&
mapping.memory_type == 12 && mapping.protection == 0x33 && mapping.is_direct &&
std::strcmp(mapping.name, "SceAgcDriver") == 0 &&
renderer.IsMapped(driver_base, 0x200000),
"driver startup bypassed shared DMEM metadata or GPU range registration");
ShaderBufferResource descriptor{};
std::array<uint64_t, 16> functions{};
std::array<uint32_t, 16> code{};
Require(name, "ordered-count function backing",
LibKernel::Memory::TryReadBacking(table_address, &descriptor, sizeof(descriptor)) &&
descriptor.Base48() == table_address + 0x60 && descriptor.Stride() == 16 &&
descriptor.NumRecords() == 8 && descriptor.fields[3] == 0x5204 &&
LibKernel::Memory::TryReadBacking(descriptor.Base48(), functions.data(),
sizeof(functions)) &&
functions[4] != 0 && (functions[4] & 0xffu) == 0 && functions[5] == 0 &&
LibKernel::Memory::TryReadBacking(functions[4], code.data(), sizeof(code)) &&
code == native_validator,
"system descriptor did not reach the queue validator and its user data");
auto *instrumentation = reinterpret_cast<uint64_t *>(table_address + 0x18);
Require(name, "default instrumentation", *instrumentation == 0,
"unused vertex validation was enabled at startup");
*instrumentation = 0x12340000u;
Require(name, "repeat startup", Gen5::AgcInit(nullptr, 13) == 0 &&
*instrumentation == 0x12340000u,
"repeat Agc initialization reset existing system state");
*instrumentation = 0;
LibKernel::Memory::InstallGpuResources(nullptr);
std::printf("[host] %-32s ok\n", name);
}
struct CommandBufferLayout {
using Callback = KYTY_SYSV_ABI bool (*)(Gen5::CommandBuffer *, uint32_t,
void *);
@@ -42591,6 +42949,10 @@ int main(int argc, char **argv) {
VulkanHarness vulkan;
RunCase(&vulkan, BufferOffsetsUsePackedWords(false));
RunCase(&vulkan, BufferOffsetsUsePackedWords(true));
RunCase(&vulkan, ScalarMemoryLoadVariants());
RunCase(&vulkan, ScalarLoadSignedImmediateOffsetAddsSoffset());
RunCase(&vulkan, ScalarLoadAlignsComponents());
RunCase(&vulkan, ScalarPayloadBeforeAliasedAtomic());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--buffer-publication-only") == 0) {
@@ -42659,6 +43021,8 @@ int main(int argc, char **argv) {
if (argc == 2 && std::strcmp(argv[1], "--gpu-command-lane-only") == 0) {
VulkanHarness vulkan;
vulkan.CheckGpuCommandLane();
vulkan.CheckScalarLeafCall();
CheckAgcSystemTable(vulkan.RuntimeRenderer());
CheckPm4IndirectControlFlow(vulkan.RuntimeRenderer());
CheckPm4WaitResume(vulkan.RuntimeRenderer());
CheckPm4RewindResume(vulkan.RuntimeRenderer());
@@ -42945,6 +43309,7 @@ int main(int argc, char **argv) {
CheckImageSamplerSpecialization();
VulkanHarness vulkan;
CheckIndirectImageKeySwitch(vulkan);
CheckFiniteInlineSamplerPhi(vulkan);
RunCase(&vulkan, ImageCubeGradientsPreserveDerivatives());
return 0;
}
@@ -43091,6 +43456,7 @@ int main(int argc, char **argv) {
CheckPm4DepthControlHighBits(vulkan.RuntimeRenderer());
CheckPm4DepthRenderOverride(vulkan.RuntimeRenderer());
CheckShaderFusion();
CheckAgcSystemTable(vulkan.RuntimeRenderer());
CheckPm4WaitPackets(vulkan.RuntimeRenderer());
CheckPm4DrawIndirectMultiPacket(vulkan.RuntimeRenderer());
CheckPm4ContextStateOperations(vulkan.RuntimeRenderer());
@@ -43107,6 +43473,7 @@ int main(int argc, char **argv) {
CheckRuntimeBufferRecords(vulkan);
CheckComputeThreadDimensions(vulkan);
CheckIndirectImageKeySwitch(vulkan);
CheckFiniteInlineSamplerPhi(vulkan);
CheckWave64WholeWaveResults();
CheckPs5GameExampleImageClearRuntimeShape();
vulkan.CheckSchedulerTimeline();
@@ -43151,6 +43518,7 @@ int main(int argc, char **argv) {
}
vulkan.CheckGpuSuspendPoint();
vulkan.CheckGpuCommandLane();
vulkan.CheckScalarLeafCall();
if (skipped_device_checks) {
std::printf(
"ShaderRecompilerComputeTests: device rasterization checks skipped, this "
+560 -1
View File
@@ -15,6 +15,7 @@
#include "graphics/shader/recompiler/frontend/cfg/ShaderCFG.h"
#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h"
#include "graphics/shader/recompiler/frontend/translate/Translate.h"
#include "graphics/shader/recompiler/frontend/translate/Translator.h"
#include "graphics/shader/recompiler/ir/IREmitter.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/recompiler/ir/passes/ConstantPropagation.h"
@@ -10308,6 +10309,218 @@ void TestNewShaderRecompilerBranchConditionForms() {
}
}
void TestGraphicsComputeGetreg() {
using namespace ShaderRecompiler;
for (const auto [word, expected] : {std::pair{0xb96a1818u, 6u}, std::pair{0xb96a0a18u, 0u}}) {
Decoder::Instruction decoded;
Decoder::DecodeInstruction(std::span(&word, 1), 0, decoded);
Check(decoded.opcode == Decoder::Opcode::S_GETREG_B32 &&
decoded.dst.kind == Decoder::OperandKind::VccLo &&
decoded.src0.value == (word & 0xffffu), "GETREG field or VCC destination decoded incorrectly");
for (const auto wave : {32u, 64u}) {
IR::Program program;
program.wave_size = wave;
IR::Block block;
Frontend::Translator translator(program, &block, 1, false, true);
translator.TranslateInstruction(decoded);
bool wrote_vcc = false;
for (const auto& inst : block) {
Check(inst.GetOpcode() != IR::ValueOpcode::SetScc, "GETREG changed SCC");
if (inst.GetOpcode() == IR::ValueOpcode::SetVccLo) {
Check(inst.Arg(0).IsImmediate() && inst.Arg(0).U32() == expected,
"graphics compute hardware field has the wrong value");
wrote_vcc = true;
}
}
Check(wrote_vcc, "GETREG did not write VCC_LO");
}
}
const uint32_t shader[] = {0xb96a1818u, EncodeSopp(0x01)};
HW::ComputeShaderInfo regs{};
regs.cs_regs.data_addr = reinterpret_cast<uint64_t>(shader);
ShaderMappedData mapped{};
mapped.code_size_bytes = sizeof(shader);
ShaderMapUserData(regs.cs_regs.data_addr, mapped);
HW::ShaderRegisters sh{};
ShaderComputeInputInfo graphics{}, async{};
async.async_compute = true;
(void)PrepareProgram(regs, sh, graphics);
(void)PrepareProgram(regs, sh, async);
Check(!graphics.async_compute && async.async_compute &&
MakeStageStaticKey(graphics) != MakeStageStaticKey(async) &&
MakeStageStaticKey(graphics).size() == MakeStageStaticKey(async).size(),
"compute input reset or cache identity lost queue provenance");
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
auto options = MakeCompileOptions(ShaderType::Compute);
options.input_info.compute = &async;
ExpectFatal([&] { (void)TranslateProgram(shader, options); }, "async GETREG used graphics constants");
auto vertex = MakeCompileOptions(ShaderType::Vertex);
ExpectFatal([&] { (void)TranslateProgram(shader, vertex); }, "vertex GETREG used compute constants");
options.input_info.compute = &graphics;
const uint32_t unsupported[] = {0xb96af818u, EncodeSopp(0x01)};
Decoder::Program decoded;
Decoder::DecodeProgram(unsupported, decoded);
Check(decoded.instructions[0].src0.value == 0xf818u, "GETREG selector was sign extended");
ExpectFatal([&] { (void)TranslateProgram(unsupported, options); }, "unsupported GETREG field was accepted");
#endif
}
void TestMemoryFedScalarLeafCall() {
using namespace ShaderRecompiler;
constexpr uint64_t base = 0xfe0040000ull;
const uint32_t shader[] = {
0xbeea03ffu, 0xe0040000u, 0xbeeb03ffu, 0x0000000fu,
0xf4080135u, 0xfa000000u, 0xbf8cc07fu, 0xf4240382u, 0xfa000020u,
0xbf8cc07fu, 0xbf130e80u, 0xbf840004u, 0xf4240402u, 0xfa000028u,
0xbf8cc07fu, 0xbe8e210eu, 0xbf810000u};
Decoder::Program decoded;
Decoder::DecodeProgram(shader, decoded);
Check(decoded.has_swap_pc, "decoded scalar call was not recorded");
const uint32_t literal[] = {EncodeSMovB32(0, 255), 0xbe8e210eu, EncodeSopp(0x01)};
Decoder::DecodeProgram(literal, decoded);
Check(!decoded.has_swap_pc && decoded.instructions.size() == 2,
"reused decoder retained a call or treated a literal as SWAPPC");
const uint32_t null_call[] = {EncodeSop1(0x21, 125, 14), EncodeSopp(0x01)};
Decoder::DecodeProgram(null_call, decoded);
Check(!decoded.has_swap_pc && decoded.instructions.front().opcode == Decoder::Opcode::S_SETPC_B64,
"NULL-destination SWAPPC was recorded as a call");
struct Memory {
std::array<uint32_t, 128> words{};
uint64_t end = base + 0x10c;
bool clean = true;
} memory;
std::copy_n(std::array{0xe0040060u, 0x0010000fu, 8u, 0x5204u}.begin(),
4, memory.words.begin());
memory.words[32] = static_cast<uint32_t>(base + 0x103); // SWAPPC aligns the old pair.
memory.words[33] = static_cast<uint32_t>(base >> 32u);
memory.words[64] = EncodeSMovB32(3, 255);
memory.words[65] = 0x12345678;
memory.words[66] = EncodeSop1(0x21, 125, 14);
const auto read = [](void *data, uint64_t address, std::span<uint32_t> out) {
const auto& m = *static_cast<Memory*>(data);
if (!m.clean || address < base || address > m.end || out.size_bytes() > m.end - address)
return false;
std::memcpy(out.data(), reinterpret_cast<const uint8_t*>(m.words.data()) + address - base,
out.size_bytes());
return true;
};
IR::SrtRuntime runtime{.read_memory = read, .userdata = &memory, .read_specialization_memory = read};
const auto options = MakeCompileOptions(ShaderType::Compute);
auto source = PrepareShaderSource(shader, options);
Check(source.call && source.decoded.has_swap_pc &&
source.decoded.instructions[source.call->instruction].branch_target == UINT32_MAX,
"memory-fed SWAPPC did not retain an unresolved target");
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
ExpectFatal([&] { (void)TranslateProgram(source.decoded, options); },
"unresolved SWAPPC silently fell through");
#endif
const auto& linked = RefreshShaderSource(source, runtime);
Check(linked.has_swap_pc && source.revision == 1 && linked.instructions.back().branch_target == 0x40 &&
linked.instructions[source.call->instruction].branch_target == 0x44 &&
source.reads.back() == std::pair<uint64_t, uint64_t>{base + 0x100, 12},
"aliased call target, exact leaf range, or continuation was lost");
const auto* storage = linked.instructions.data();
const auto* scratch = source.linked->observed_function.data();
(void)RefreshShaderSource(source, runtime);
Check(source.revision == 1 && source.linked->decoded.instructions.data() == storage &&
source.linked->observed_function.data() == scratch,
"unchanged leaf rebuilt decoded instructions or refresh storage");
auto translated = TranslateProgram(linked, options);
translated.program.source_reads = source.reads;
const auto plan = IR::ExtractResourcePlan(translated.program);
Check(plan.source_reads == source.reads && plan.capture_specialization_reads,
"linked code dependencies were omitted from resource alias validation");
std::vector<uint32_t> writer(std::begin(shader), std::end(shader) - 1);
writer.insert(writer.end(), {EncodeVop1(0x01, 1, 3), EncodeMubuf0(0x1c, 0, false),
EncodeMubuf1(1, 6, 0), EncodeSopp(0x01)});
std::array<uint32_t, 64> user_data{};
user_data[25] = static_cast<uint32_t>(base >> 32u);
user_data[26] = 4;
user_data[27] = 3u << 28u;
auto write_options = options;
write_options.user_data = user_data;
auto write_runtime = runtime;
write_runtime.user_data = user_data;
auto write_source = PrepareShaderSource(writer, write_options);
auto write_program = TranslateProgram(RefreshShaderSource(write_source, write_runtime), write_options);
write_program.program.source_reads = write_source.reads;
auto write_plan = IR::ExtractResourcePlan(write_program.program);
IR::ResourceSnapshot snapshot;
IR::ResourceSpecialization specialization;
const auto written = std::ranges::find_if(write_plan.info.buffers,
[](const auto& buffer) { return buffer.written; });
Check(written != write_plan.info.buffers.end(), "caller store lost its writable binding");
const auto output_index = static_cast<size_t>(written - write_plan.info.buffers.begin());
for (const auto offset : {0x80u, 0x100u, 0x1f0u}) {
user_data[24] = static_cast<uint32_t>(base + offset);
Check(IR::MaterializeResources(write_plan, write_runtime, snapshot, specialization) &&
snapshot.buffers[output_index].dwords[0] == user_data[24] &&
snapshot.buffers[output_index].dwords[2] == 4 &&
std::ranges::find(snapshot.specialization_reads,
std::pair<uint64_t, uint64_t>{base + 0x80, 4}) != snapshot.specialization_reads.end() &&
std::ranges::find(snapshot.specialization_reads,
std::pair<uint64_t, uint64_t>{base + 0x100, 12}) != snapshot.specialization_reads.end(),
"call dependencies or writable ranges were lost before binding validation");
}
// Relocating identical code updates dependencies without rebuilding the source.
std::copy_n(memory.words.begin() + 64, 3, memory.words.begin() + 96);
memory.words[32] = static_cast<uint32_t>(base + 0x180);
memory.end = base + 0x18c;
(void)RefreshShaderSource(source, runtime);
Check(source.revision == 1 && source.reads.back().first == base + 0x180,
"identical relocated leaf recompiled or retained its old memory dependency");
memory.words[97] = 0x87654321;
(void)RefreshShaderSource(source, runtime);
Check(source.revision == 2 && source.linked->code.back() == EncodeSop1(0x21, 125, 14),
"changed leaf bytes retained stale source");
// A newly selected shorter function may end before the old bulk-read length.
memory.words[96] = EncodeSMovB32(3, 129);
memory.words[97] = EncodeSop1(0x21, 125, 14);
memory.end = base + 0x188;
(void)RefreshShaderSource(source, runtime);
Check(source.revision == 3 && source.reads.back().second == 8,
"shorter exact-boundary leaf was not decoded after a failed old-span read");
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
memory.clean = false;
ExpectFatal([&] { (void)RefreshShaderSource(source, runtime); },
"unreadable call dependencies reused stale linked code");
memory.clean = true;
auto ordinary = runtime;
ordinary.read_specialization_memory = nullptr;
ExpectFatal([&] { (void)RefreshShaderSource(source, ordinary); },
"call target query accepted an ordinary non-strict memory reader");
std::vector<uint32_t> escaping(std::begin(shader), std::end(shader) - 1);
escaping.insert(escaping.end(), {EncodeVop1(0x01, 1, 14),
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(1, 0, 0), EncodeSopp(0x01)});
ExpectFatal([&] {
auto escaping_source = PrepareShaderSource(escaping, options);
(void)TranslateProgram(RefreshShaderSource(escaping_source, runtime), options);
}, "saved native return PC escaped into GPU data");
memory.words[96] = EncodeSMovB32(14, 129);
ExpectFatal([&] { (void)RefreshShaderSource(source, runtime); },
"leaf clobbering its return pair was linked");
// AND_B64 writes the high half despite the decoder's generic scalar width.
const uint32_t high_writer[] = {
EncodeSMovB32(15, 129), EncodeSop2(0x0f, 14, 128, 128),
EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
auto pair_source = PrepareShaderSource(high_writer, options);
IR::SrtWalker pair_walker(pair_source.call_targets, runtime);
uint32_t high = 1;
Check(pair_walker.Evaluate(pair_source.call->target[1], high) && high == 0,
"scalar pair producer used a stale preceding high-half definition");
const uint32_t joined[] = {
EncodeSopp(0x04, 2), EncodeSMovB32(14, 129), EncodeSopp(0x02, 1),
EncodeSMovB32(14, 130), EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
ExpectFatal([&] { (void)PrepareShaderSource(joined, options); },
"ambiguous reaching definitions were accepted for a call target");
const uint32_t multiple[] = {EncodeSop1(0x21, 14, 14), EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
ExpectFatal([&] { (void)PrepareShaderSource(multiple, options); },
"call metadata bypassed the multiple-call rejection");
#endif
}
void TestNewShaderRecompilerSetpcBranch() {
const uint32_t shader[] = {
EncodeSop1(0x1f, 4, 0), // s_getpc_b64 s[4:5]
@@ -10350,7 +10563,10 @@ void TestFusedShaderHandoffPreservesRegisters() {
options.back_code = back;
for (const auto handoff: {EncodeSop1(0x20, 0, 6), 0xbefd2106u}) {
front[2] = handoff; // SETPC or captured SWAPPC with NULL destination
auto translated = TranslateProgram(front, options);
auto source = PrepareShaderSource(front, options);
Check(!source.decoded.has_swap_pc && !source.call,
"fused SETPC handoff entered scalar call preparation");
auto translated = TranslateProgram(source.decoded, options);
uint32_t allocations = 0;
for (const auto* block: translated.program.blocks) {
for (const auto& inst: *block) {
@@ -10365,6 +10581,17 @@ void TestFusedShaderHandoffPreservesRegisters() {
}
Check(allocations == 1u, "fused shader omitted the back shader allocation");
}
const uint32_t call_front[] = {EncodeSop1(0x21, 14, 14), EncodeSop1(0x20, 0, 6)};
Check(Decoder::DecodeFrontProgram(call_front).has_swap_pc,
"front decoder omitted its scalar call");
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
ExpectFatal([&] { (void)PrepareShaderSource(call_front, options); },
"fused front call bypassed the non-compute rejection");
const uint32_t call_back[] = {EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
options.back_code = call_back;
ExpectFatal([&] { (void)PrepareShaderSource(front, options); },
"fused back call metadata was not propagated");
#endif
}
void TestMeshExportStorage() {
@@ -13682,6 +13909,331 @@ void TestGpuProducedWritableDescriptor() {
"GPU-updated address or record count created a shader permutation");
}
void TestImmutableDescriptorPredicate() {
using namespace ShaderRecompiler::IR;
ResourcePlan plan;
const auto emit = [&](ValueOpcode op, std::initializer_list<Value> args) {
auto &inst = plan.value_storage.emplace_back(op);
uint32_t index = 0;
for (const auto arg : args) inst.SetArg(index++, arg);
return Value(&inst);
};
const auto handle = emit(ValueOpcode::GetBufferResource,
{Value(0x1000u), Value(4u << 16u), Value(16u), Value(0u)});
for (const bool coherent : {false, true}) {
plan.memory_info.push_back({.kind = ResourceKind::Buffer,
.idxen = !coherent, .coherent = coherent});
const auto loaded = emit(ValueOpcode::LoadBufferU32,
{handle, Value(0u), Value(0u), Value(0u), Value(true)});
loaded.ResolveInstruction()->SetFlags(MemoryFlags{
.index = static_cast<uint32_t>(plan.memory_info.size() - 1u)});
const auto mask = emit(ValueOpcode::INotEqual32, {loaded, Value(0u)});
const auto selected = emit(ValueOpcode::SelectU32, {mask, Value(1u), Value(0u)});
const auto reduced = emit(ValueOpcode::ReadFirstLane, {selected, mask});
Check(ValidateRuntimeValue(plan, reduced, RuntimeValueType::Integer),
"integer EXEC reduction lost its active-lane proof");
Check(!ValidateRuntimeValue(plan, reduced, RuntimeValueType::ImmutableInteger),
"memory-dependent EXEC reduction was classified as immutable");
}
}
void TestUniformSelectedWritableDescriptor() {
using namespace ShaderRecompiler::IR;
// SAROS b9da5e64f4c5b10a builds s[8:11] from two scalar address expressions,
// then sets the stride in the high address word after the branch merge.
const uint32_t shader[] = {
0xbfa00002u, 0xbf070280u, 0xbf84000du, 0xf4001a82u,
0xfa00009cu, 0xbf8cc07fu, 0x8f6b856au, 0xf4001a82u,
0xfa000080u, 0xbf8cc07fu, 0x98eb6b6au, 0xb7eb0080u,
0x80086b03u, 0xbf800000u, 0x82098000u, 0xbf820006u,
0xf4041a82u, 0xfa000040u, 0xbf8cc07fu, 0x8008066au,
0xbf800000u, 0x8209016bu, 0xd7460001u, 0x04010c07u,
0x7e000280u, 0xf4001a82u, 0xfa000084u, 0xbf8cc07fu,
0x810a846au, 0xbe8b03ffu, 0x00016204u, 0x4a020284u,
0xbe891d92u, 0xe0702000u, 0x80020001u, 0xbf810000u,
};
std::array<uint32_t, 8> user_data{0x30u, 0x1cu, 1u, 0xfffffff0u,
0x1000u, 0u, 0xfffffffcu, 0u};
auto options = MakeCompileOptions(ShaderType::Compute);
options.user_data = user_data;
auto translated = ShaderRecompiler::TranslateProgram(shader, options);
auto plan = ExtractResourcePlan(translated.program);
Check(plan.info.buffers.size() == 1 && plan.info.buffers[0].written,
"uniform address selection lost its writable buffer");
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
const bool alternate = *static_cast<uint32_t *>(data) == 0;
if (words.size() != 1) return false;
switch (address) {
case 0x1040: words[0] = 8u; return alternate;
case 0x1044: words[0] = 1u; return alternate;
case 0x1080: words[0] = 3u; return !alternate;
case 0x109c: words[0] = 2u; return !alternate;
case 0x1084: words[0] = 10u; return true;
default: return false;
}
};
const SrtRuntime runtime{.user_data = user_data, .read_memory = read,
.userdata = &user_data[2], .read_specialization_memory = read};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const uint32_t flag : {1u, 0u, 1u}) {
user_data[2] = flag;
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"uniform selection evaluated an unavailable untaken scalar source");
const auto &descriptor = snapshot.buffers[0].dwords;
Check(descriptor[0] == (flag ? 0xe0u : 4u) &&
descriptor[1] == (flag ? 0x40031u : 0x4001eu) &&
descriptor[2] == 14u && descriptor[3] == 0x16204u,
"uniform writable descriptor lost its branch, carry, or post-merge stride");
}
}
void TestUniformSelectedDescriptorLoadAddress() {
using namespace ShaderRecompiler::IR;
const uint32_t shader[] = {
EncodeSopc(0x06, 0, 128), // s_cmp_eq_u32 s0, 0
EncodeSopp(0x04, 2), // s_cbranch_scc0 alternate
EncodeSop1(0x04, 6, 2), // s_mov_b64 s[6:7], s[2:3]
EncodeSopp(0x02, 1), // s_branch load
EncodeSop1(0x04, 6, 4), // s_mov_b64 s[6:7], s[4:5]
EncodeSmem0(0x02, 8, 3), 125u << 25u, // s_load_dwordx4 s[8:11], s[6:7], 0
EncodeMubuf0(0x1c), EncodeMubuf1(0, 2, 0),
EncodeSopp(0x01),
};
std::array<uint32_t, 6> user_data{0u, 0u, 0x1000u, 0u, 0x2000u, 0u};
auto options = MakeCompileOptions(ShaderType::Compute);
options.user_data = user_data;
auto translated = ShaderRecompiler::TranslateProgram(shader, options);
auto plan = ExtractResourcePlan(translated.program);
Check(plan.srt_reads.size() == 4u && plan.info.buffers.size() == 1u &&
plan.info.buffers[0].written && !plan.capture_specialization_reads,
"conditional scalar address duplicated or detached descriptor reads");
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
const auto flag = *static_cast<uint32_t *>(data);
const uint64_t base = flag ? 0x2000u : 0x1000u;
const std::array<uint32_t, 4> descriptor{0x3000u + flag * 0x1000u,
4u << 16u, 16u, 0x16204u};
if (words.size() != 1u || address < base || address >= base + 16u ||
(address & 3u) != 0u) return false;
words[0] = descriptor[(address - base) / 4u];
return true;
};
const SrtRuntime runtime{.user_data = user_data, .read_memory = read,
.userdata = &user_data[0]};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const uint32_t flag : {0u, 1u, 0u}) {
user_data[0] = flag;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers[0].dwords[0] == 0x3000u + flag * 0x1000u &&
snapshot.specialization_reads.empty(),
"conditional descriptor load read the untaken pointer");
}
}
void TestNativeScalarAtomicPayloadStaysOnGpu() {
using namespace ShaderRecompiler::IR;
// Exact final block from SAROS b62b494cb2567011: read a mutable flag,
// atomically update that address, then test the loaded flag before S_TRAP.
const uint32_t shader[] = {
0x7e0002ffu, 0x00100000u, 0x7e020280u, 0xf4040100u,
0xfa000018u, 0xbf8cc07fu, 0x8801ff05u, 0x01000000u,
0xbe800304u, 0xbe820381u, 0xbe8303ffu, 0x00016204u,
0xf4001a82u, 0xfa000018u, 0xe1680018u, 0x80000000u,
0xbf8cc07fu, 0xbf0d846au, 0xbf840001u, 0xbf920001u,
0xbf810000u,
};
const std::array<uint32_t, 2> user_data{0x1000u, 0u};
auto options = MakeCompileOptions(ShaderType::Compute);
options.user_data = user_data;
auto translated = ShaderRecompiler::TranslateProgram(shader, options);
const auto &program = translated.program;
const Inst *payload = nullptr;
for (const auto *block : program.blocks) {
for (const auto &inst : *block) {
if (inst.GetOpcode() == ValueOpcode::LoadAddressU32 &&
program.memory_info[inst.Flags<MemoryFlags>().index].kind == ResourceKind::ScalarAddress)
payload = &inst;
}
}
Check(payload != nullptr && payload->Parent() != nullptr &&
!program.memory_info[payload->Flags<MemoryFlags>().index].planning_only &&
program.info.uses_dma && program.info.buffers.size() == 1 &&
program.info.buffers[0].atomic,
"native scalar payload was replaced by a host snapshot before its atomic write");
auto plan = ExtractResourcePlan(program);
Check(plan.srt_reads.size() == 2 && plan.control_flow.empty() &&
!plan.capture_specialization_reads,
"terminal native shader assertion entered resource planning");
struct Reads { uint32_t descriptors = 0; uint32_t payload = 0; } reads;
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
if (address == 0x2018u) { ++reads.payload; return false; }
if (words.size() != 1 || (address != 0x1018u && address != 0x101cu)) return false;
++reads.descriptors;
words[0] = address == 0x1018u ? 0x2000u : 0u;
return true;
};
const SrtRuntime runtime{.user_data = user_data, .read_memory = read,
.userdata = &reads, .read_specialization_memory = read};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
reads.descriptors == 2 && reads.payload == 0 &&
snapshot.specialization_reads.empty() && snapshot.buffers[0].dwords[0] == 0x2000u,
"native resource materialization read the mutable flag or lost its atomic descriptor");
}
void TestBoundedScalarMaterialImageKeys() {
using namespace ShaderRecompiler::IR;
// SAROS 9fba2edffc549531: min(header count,64), scalar rows of 160 bytes,
// and separate float/uint image fields at 0x7c/0x94 in the same image heap.
std::vector<uint32_t> shader {
0xf4080106u, 0xfa0000a0u, 0xf4080806u, 0xfa0000b0u,
0xbf8cc07fu, 0xf4201550u, 0xfa000024u, 0xbf8cc07fu,
EncodeSop2(0x07, 85, 85, 0xc0), EncodeSMovB32(86, 128),
EncodeSMovB32(87, 126),
};
const auto loop = shader.size();
shader.insert(shader.end(), {0x7da2aaf9u, 0x86860056u, EncodeSopp(0x08),
0x931cff56u, 0x000000a0u, 0xf42006c2u, 0x38000094u,
0xf4200682u, 0x3800007cu, 0xbf8cc07fu, 0x8f58851bu, 0x8f3c851au,
0xf42c1304u, 0xb0000000u, 0xf42c0d04u, 0x78000000u, 0xbf8cc07fu,
0xf0000110u, 0x00130106u, 0xf0000110u, 0x000d0206u,
EncodeVop1(0x01, 3, 78), // Preserve a T# dimension word as native scalar payload.
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(1, 0, 0),
EncodeMubuf0(0x1c, 4, false), EncodeMubuf1(2, 0, 0),
EncodeMubuf0(0x1c, 8, false), EncodeMubuf1(3, 0, 0),
EncodeSop2(0x02, 86, 86, 129), EncodeSMovB32(126, 87),
EncodeSopp(0x02)});
shader.back() = EncodeSopp(0x02, static_cast<uint16_t>(loop - shader.size()));
shader[loop + 2u] = EncodeSopp(0x08, shader.size() - loop - 3u);
shader.push_back(EncodeSopp(0x01));
std::array<uint32_t, 14> user_data{};
user_data[8] = 0x100000u;
user_data[9] = 32u << 16u;
user_data[10] = 198656u;
user_data[11] = 0x5204u;
user_data[12] = 0x1000u;
auto options = MakeCompileOptions(ShaderType::Compute);
options.user_data = user_data;
auto translated = ShaderRecompiler::TranslateProgram(shader, options);
auto plan = ExtractResourcePlan(translated.program);
Check(plan.info.images.size() == 2 && plan.capture_specialization_reads,
"scalar material fields lost their distinct image roots or clean read capture");
for (const auto &image : plan.info.images) {
const auto &source = plan.descriptor_sources[image.source].indirect_descriptor;
Check(source && source->selector && source->selector->stride == 160u &&
(source->selector->offset == 0x7cu || source->selector->offset == 0x94u) &&
source->table_stride == 32u && !source->table_scalar &&
source->key_count.ResolveInstruction()->GetOpcode() == ValueOpcode::UMin32,
"scalar material image selection fell back to the whole heap");
}
Check(std::ranges::any_of(translated.program.blocks, [&](const auto *block) {
return std::ranges::any_of(*block, [&](const auto &inst) {
if (inst.GetOpcode() != ValueOpcode::ReadConstBuffer) return false;
const auto &memory = translated.program.memory_info[inst.template Flags<MemoryFlags>().index];
return memory.component_count == 8u && memory.component_index == 2u &&
!memory.planning_only && inst.HasUses();
});
}), "scalar material projection removed a live T# dimension payload");
struct Memory {
uint32_t count = 2;
uint32_t stride = 160;
uint32_t records = 409;
uint64_t unavailable = 0;
bool clean = true;
bool mixed = false;
bool same_key = false;
uint32_t keys = 0;
uint32_t images = 0;
} memory;
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &m = *static_cast<Memory *>(data);
if (!m.clean || address == m.unavailable) return false;
if (words.size() == 1 && address >= 0x10a0u && address < 0x10c0u) {
const uint32_t descriptors[] = {0x2000u, m.stride << 16u, m.records, 0x5204u,
0x4000u, 16u << 16u, 8u, 0x4dfacu};
words[0] = descriptors[(address - 0x10a0u) / 4u];
return true;
}
if (address == 0x4024u && words.size() == 1) { words[0] = m.count; return true; }
for (const uint32_t row : {0u, 1u}) {
for (const uint32_t field : {0x7cu, 0x94u}) {
if (address != 0x2000u + row * 160u + field || words.size() != 1) continue;
++m.keys;
words[0] = (row == 0 || m.same_key ? 5582u : 5567u) + (field == 0x94u ? 6u : 0u);
return true;
}
}
for (const uint32_t key : {5567u, 5582u, 5573u, 5588u}) {
if (address != 0x100000u + key * 32u || words.size() != 8) continue;
++m.images;
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[3] = DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Prospero::ImageType::kColor3D) << 28u);
return true;
}
return false;
};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = read};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const uint32_t stride : {160u, 16u}) {
memory = {.stride = stride, .records = stride == 160u ? 409u : 20u};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.keys == 4 && memory.images == 4 && snapshot.images.size() == 4,
"scalar material selection used V# stride as its instruction stride or scanned unused keys");
for (uint32_t root = 0; root < plan.info.images.size(); ++root) {
const auto &selector = *plan.descriptor_sources[plan.info.images[root].source].indirect_descriptor->selector;
const uint32_t first = selector.offset == 0x94u ? 5573u : 5567u;
const auto offset = specialization.images[root].indirect_mapping_offset;
Check(snapshot.flattened_srt[offset] == 2u &&
snapshot.flattened_srt[offset + 1u] == first &&
snapshot.flattened_srt[offset + 3u] == first + 15u,
"scalar material image mapping changed descriptor-index keys into byte offsets");
}
}
for (const bool same_key : {false, true}) {
memory = {.count = same_key ? 2u : 1u, .same_key = same_key};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.keys == (same_key ? 4u : 2u) && memory.images == 2 &&
snapshot.images.size() == 2 &&
std::ranges::all_of(specialization.images, [](const auto &image) {
return image.indirect_root == ImageResource::NoIndirectImage;
}), "single-candidate scalar material loop retained an indirect image mapping");
}
memory = {.count = 0};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.keys == 0 && memory.images == 0 && snapshot.images.size() == 2,
"empty scalar material loop retained image probes");
memory = {.clean = false};
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"dirty scalar material proof was accepted");
memory = {.unavailable = 0x2094u};
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"unavailable selected material key was accepted");
memory = {.stride = 1, .records = 311};
Check(!MaterializeResources(plan, runtime, snapshot, specialization) && memory.keys == 0,
"scalar material byte range escaped V# bounds");
memory = {.mixed = true};
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"selected material fields bypassed image compatibility");
memory = {};
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"native scalar material image selection did not recover after rejected candidates");
ApplyResourceSpecialization(translated.program, specialization);
Check(translated.program.info.images.size() == 4 &&
translated.program.info.images[0].indirect_resources.size() == 2 &&
translated.program.info.images[1].indirect_resources.size() == 2,
"native image reads lost their distinct material field dispatches");
}
void TestTypedDescriptorRealCarryAndScalarLoads() {
const uint32_t carry_shader[] = {
EncodeSop1(0x1f, 0, 0), // s_getpc_b64 s[0:1]
@@ -15120,6 +15672,8 @@ int main() {
TestNewShaderRecompilerPixelImageSampleLodSelection();
TestNewShaderRecompilerBranchConditionForms();
TestNewShaderRecompilerSetpcBranch();
TestGraphicsComputeGetreg();
TestMemoryFedScalarLeafCall();
TestFusedShaderHandoffPreservesRegisters();
TestMeshExportStorage();
TestMergedShaderUserDataSnapshot();
@@ -15153,6 +15707,11 @@ int main() {
TestTypedDescriptorRealWideMoveTranslation();
TestComputeImageFill();
TestGpuProducedWritableDescriptor();
TestUniformSelectedWritableDescriptor();
TestUniformSelectedDescriptorLoadAddress();
TestNativeScalarAtomicPayloadStaysOnGpu();
TestBoundedScalarMaterialImageKeys();
TestImmutableDescriptorPredicate();
TestTypedDescriptorRealCarryAndScalarLoads();
TestSrtWalkerRealSmemTranslation();
TestSrtWalkerVccBaseTranslation();