mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-27 21:36:54 +01:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59183dbfa8 | |||
| c1926f915d | |||
| 7da3c4ed21 | |||
| 3e9f88bf79 | |||
| 8c81386568 | |||
| bb5edbd490 | |||
| 1818aae562 | |||
| 165cd884ca | |||
| 973809cde0 | |||
| 71c7261740 | |||
| 5b68a26cce | |||
| 3b1a94707a | |||
| 488c0acae9 | |||
| 4c8fc53a85 | |||
| 3ae5a9b70a | |||
| a31e2cd361 | |||
| 1bd282ec0a | |||
| 1fc520cd62 |
@@ -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 |
@@ -689,7 +689,7 @@ void CommandProcessor::ProcessPm4(Pm4Execution& execution) {
|
||||
const auto opcode = (packet_header >> 8u) & 0xffu;
|
||||
EXIT_NOT_IMPLEMENTED(remaining_dw > total_dw);
|
||||
|
||||
if (packet_header == 0x80000000u) {
|
||||
if (packet_header == 0x80000000u || Pm4::PacketSizeDw(packet_header) == 1u) {
|
||||
cursor.offset_dw++;
|
||||
execution.m_made_progress = true;
|
||||
continue;
|
||||
@@ -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) {
|
||||
|
||||
@@ -120,15 +120,15 @@ void DumpPm4PacketStream(Common::File* file, const uint32_t* cmd_buffer, uint32_
|
||||
uint32_t len = 0;
|
||||
|
||||
const auto packet_type = static_cast<PacketType>(cmd_id >> 30u);
|
||||
// Type-2 packets are header-only padding; every other packet type requires a body.
|
||||
EXIT_NOT_IMPLEMENTED(dw < 2 && packet_type != PacketType::Type2);
|
||||
EXIT_NOT_IMPLEMENTED(dw < 2 && packet_type != PacketType::Type2 &&
|
||||
PacketSizeDw(cmd_id) != 1u);
|
||||
|
||||
switch (packet_type) {
|
||||
case PacketType::Type3: {
|
||||
const bool sh_gx = (cmd_id & 0x2u) == 0;
|
||||
const uint8_t op = ((cmd_id >> 8u) & 0xffu);
|
||||
const auto r = KYTY_PM4_R(cmd_id);
|
||||
len = ((cmd_id >> 16u) & 0x3fffu) + 1;
|
||||
len = PacketSizeDw(cmd_id) - 1u;
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(len >= dw);
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ class File;
|
||||
|
||||
namespace Libs::Graphics::Pm4 {
|
||||
|
||||
constexpr uint32_t PacketSizeDw(uint32_t header) {
|
||||
return (header & 0x3fffff00u) == 0x3fff1000u ? 1u : KYTY_PM4_LEN(header);
|
||||
}
|
||||
|
||||
constexpr uint32_t IT_NOP = 0x10;
|
||||
constexpr uint32_t IT_SET_BASE = 0x11;
|
||||
constexpr uint32_t IT_CLEAR_STATE = 0x12;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -146,6 +146,15 @@ inline constexpr auto EmitFPOrdEqual32 = EmitFloatCompare32<spv::OpFOrdEqual>;
|
||||
EMIT_NATIVE(FPOrdEqual64, OpFOrdEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPOrdLessThanEqual64, OpFOrdLessThanEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPOrdGreaterThanEqual64, OpFOrdGreaterThanEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPOrdNotEqual64, OpFOrdNotEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPOrdLessThan64, OpFOrdLessThan, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPOrdGreaterThan64, OpFOrdGreaterThan, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPUnordEqual64, OpFUnordEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPUnordNotEqual64, OpFUnordNotEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPUnordLessThan64, OpFUnordLessThan, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPUnordGreaterThan64, OpFUnordGreaterThan, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPUnordLessThanEqual64, OpFUnordLessThanEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPUnordGreaterThanEqual64, OpFUnordGreaterThanEqual, U1, uint32_t, uint32_t)
|
||||
inline constexpr auto EmitFPUnordEqual32 = EmitFloatCompare32<spv::OpFUnordEqual>;
|
||||
inline constexpr auto EmitFPOrdNotEqual32 = EmitFloatCompare32<spv::OpFOrdNotEqual>;
|
||||
inline constexpr auto EmitFPUnordNotEqual32 = EmitFloatCompare32<spv::OpFUnordNotEqual>;
|
||||
@@ -158,6 +167,7 @@ inline constexpr auto EmitFPUnordLessThanEqual32 = EmitFloatCompare32<spv::OpFUn
|
||||
inline constexpr auto EmitFPOrdGreaterThanEqual32 = EmitFloatCompare32<spv::OpFOrdGreaterThanEqual>;
|
||||
inline constexpr auto EmitFPUnordGreaterThanEqual32 = EmitFloatCompare32<spv::OpFUnordGreaterThanEqual>;
|
||||
EMIT_NATIVE(FPIsNan32, OpIsNan, U1, uint32_t)
|
||||
EMIT_NATIVE(FPIsNan64, OpIsNan, U1, uint32_t)
|
||||
inline constexpr auto EmitFPCmpClass32 = EmitClassMaskF32;
|
||||
inline constexpr auto EmitFPCmpClass16 = EmitClassMaskF16;
|
||||
EMIT_NATIVE(FPAdd32, OpFAdd, F32, uint32_t, uint32_t)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -47,6 +47,8 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
|
||||
{0x1du, Opcode::BUFFER_STORE_DWORDX2, 2, 32},
|
||||
{0x1eu, Opcode::BUFFER_STORE_DWORDX4, 4, 32},
|
||||
{0x1fu, Opcode::BUFFER_STORE_DWORDX3, 3, 32},
|
||||
{0x24u, Opcode::BUFFER_LOAD_SHORT_D16, 1, 16},
|
||||
{0x25u, Opcode::BUFFER_LOAD_SHORT_D16_HI, 1, 16},
|
||||
{0x30u, Opcode::BUFFER_ATOMIC_SWAP, 1, 32},
|
||||
{0x31u, Opcode::BUFFER_ATOMIC_CMPSWAP, 1, 32},
|
||||
{0x32u, Opcode::BUFFER_ATOMIC_ADD, 1, 32},
|
||||
@@ -298,8 +300,10 @@ void DecodeMubuf(uint32_t pc, std::span<const uint32_t> code, uint32_t word_inde
|
||||
}
|
||||
|
||||
DecodeVectorGpr(vdata, inst.dst);
|
||||
if (inst.opcode == Opcode::BUFFER_LOAD_FORMAT_D16_X) {
|
||||
inst.dst.sdwa_sel = 4u;
|
||||
if (inst.opcode == Opcode::BUFFER_LOAD_FORMAT_D16_X ||
|
||||
inst.opcode == Opcode::BUFFER_LOAD_SHORT_D16 ||
|
||||
inst.opcode == Opcode::BUFFER_LOAD_SHORT_D16_HI) {
|
||||
inst.dst.sdwa_sel = inst.opcode == Opcode::BUFFER_LOAD_SHORT_D16_HI ? 5u : 4u;
|
||||
}
|
||||
DecodeVectorGpr(vaddr, inst.src0);
|
||||
DecodeScalarSource(srsrc * 4u, pc, inst.src1);
|
||||
@@ -371,7 +375,7 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
ApplyMemoryInfo(inst, info);
|
||||
SetRawWords(inst, code, word_index, 2);
|
||||
|
||||
if (lds != 0 || inst.glc || inst.slc || seg == 3u) {
|
||||
if (lds != 0 || inst.glc || (inst.slc && !IsFlatStoreOpcode(inst.opcode)) || seg == 3u) {
|
||||
SetUnsupported(inst, Family::FLAT, opcode, "FLAT modifiers or segment are not implemented");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
@@ -627,6 +630,8 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::BUFFER_STORE_FORMAT_D16_X:
|
||||
case Opcode::BUFFER_LOAD_UBYTE:
|
||||
case Opcode::BUFFER_LOAD_USHORT:
|
||||
case Opcode::BUFFER_LOAD_SHORT_D16:
|
||||
case Opcode::BUFFER_LOAD_SHORT_D16_HI:
|
||||
case Opcode::BUFFER_LOAD_DWORD:
|
||||
case Opcode::BUFFER_LOAD_DWORDX2:
|
||||
case Opcode::BUFFER_LOAD_DWORDX3:
|
||||
|
||||
@@ -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,
|
||||
@@ -373,10 +374,38 @@ enum class Opcode {
|
||||
V_CMP_NEQ_F32,
|
||||
V_CMP_NLT_F32,
|
||||
V_CMP_TRU_F32,
|
||||
V_CMP_F_F64,
|
||||
V_CMP_LT_F64,
|
||||
V_CMP_EQ_F64,
|
||||
V_CMP_LE_F64,
|
||||
V_CMP_GT_F64,
|
||||
V_CMP_LG_F64,
|
||||
V_CMP_GE_F64,
|
||||
V_CMP_O_F64,
|
||||
V_CMP_U_F64,
|
||||
V_CMP_NGE_F64,
|
||||
V_CMP_NLG_F64,
|
||||
V_CMP_NGT_F64,
|
||||
V_CMP_NLE_F64,
|
||||
V_CMP_NEQ_F64,
|
||||
V_CMP_NLT_F64,
|
||||
V_CMP_TRU_F64,
|
||||
V_CMPX_F_F64,
|
||||
V_CMPX_LT_F64,
|
||||
V_CMPX_EQ_F64,
|
||||
V_CMPX_LE_F64,
|
||||
V_CMPX_GT_F64,
|
||||
V_CMPX_LG_F64,
|
||||
V_CMPX_GE_F64,
|
||||
V_CMPX_O_F64,
|
||||
V_CMPX_U_F64,
|
||||
V_CMPX_NGE_F64,
|
||||
V_CMPX_NLG_F64,
|
||||
V_CMPX_NGT_F64,
|
||||
V_CMPX_NLE_F64,
|
||||
V_CMPX_NEQ_F64,
|
||||
V_CMPX_NLT_F64,
|
||||
V_CMPX_TRU_F64,
|
||||
V_CMPX_LT_F32,
|
||||
V_CMPX_EQ_F32,
|
||||
V_CMPX_LE_F32,
|
||||
@@ -518,6 +547,8 @@ enum class Opcode {
|
||||
BUFFER_LOAD_SBYTE,
|
||||
BUFFER_LOAD_USHORT,
|
||||
BUFFER_LOAD_SSHORT,
|
||||
BUFFER_LOAD_SHORT_D16,
|
||||
BUFFER_LOAD_SHORT_D16_HI,
|
||||
BUFFER_LOAD_DWORD,
|
||||
BUFFER_LOAD_DWORDX2,
|
||||
BUFFER_LOAD_DWORDX3,
|
||||
@@ -676,6 +707,7 @@ enum class Opcode {
|
||||
S_CBRANCH_CDBGSYS,
|
||||
S_CBRANCH_CDBGSYS_OR_USER,
|
||||
S_SENDMSG,
|
||||
S_GETREG_B32,
|
||||
S_SETREG_B32,
|
||||
S_SLEEP,
|
||||
S_TRAP,
|
||||
@@ -814,6 +846,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.
|
||||
|
||||
@@ -236,8 +236,23 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
|
||||
{0x19u, Opcode::V_CMPX_NGE_F32}, {0x1au, Opcode::V_CMPX_NLG_F32},
|
||||
{0x1bu, Opcode::V_CMPX_NGT_F32}, {0x1cu, Opcode::V_CMPX_NLE_F32},
|
||||
{0x1du, Opcode::V_CMPX_NEQ_F32}, {0x1eu, Opcode::V_CMPX_NLT_F32},
|
||||
{0x22u, Opcode::V_CMP_EQ_F64, false}, {0x23u, Opcode::V_CMP_LE_F64, false},
|
||||
{0x33u, Opcode::V_CMPX_LE_F64, false}, {0x36u, Opcode::V_CMPX_GE_F64, false},
|
||||
{0x20u, Opcode::V_CMP_F_F64, false}, {0x21u, Opcode::V_CMP_LT_F64, false},
|
||||
{0x22u, Opcode::V_CMP_EQ_F64, false}, {0x23u, Opcode::V_CMP_LE_F64, false},
|
||||
{0x24u, Opcode::V_CMP_GT_F64, false}, {0x25u, Opcode::V_CMP_LG_F64, false},
|
||||
{0x26u, Opcode::V_CMP_GE_F64, false}, {0x27u, Opcode::V_CMP_O_F64, false},
|
||||
{0x28u, Opcode::V_CMP_U_F64, false}, {0x29u, Opcode::V_CMP_NGE_F64, false},
|
||||
{0x2au, Opcode::V_CMP_NLG_F64, false}, {0x2bu, Opcode::V_CMP_NGT_F64, false},
|
||||
{0x2cu, Opcode::V_CMP_NLE_F64, false}, {0x2du, Opcode::V_CMP_NEQ_F64, false},
|
||||
{0x2eu, Opcode::V_CMP_NLT_F64, false}, {0x2fu, Opcode::V_CMP_TRU_F64, false},
|
||||
{0x30u, Opcode::V_CMPX_F_F64, false},
|
||||
{0x31u, Opcode::V_CMPX_LT_F64, false}, {0x32u, Opcode::V_CMPX_EQ_F64, false},
|
||||
{0x33u, Opcode::V_CMPX_LE_F64, false}, {0x34u, Opcode::V_CMPX_GT_F64, false},
|
||||
{0x35u, Opcode::V_CMPX_LG_F64, false}, {0x36u, Opcode::V_CMPX_GE_F64, false},
|
||||
{0x37u, Opcode::V_CMPX_O_F64, false}, {0x38u, Opcode::V_CMPX_U_F64, false},
|
||||
{0x39u, Opcode::V_CMPX_NGE_F64, false}, {0x3au, Opcode::V_CMPX_NLG_F64, false},
|
||||
{0x3bu, Opcode::V_CMPX_NGT_F64, false}, {0x3cu, Opcode::V_CMPX_NLE_F64, false},
|
||||
{0x3du, Opcode::V_CMPX_NEQ_F64, false}, {0x3eu, Opcode::V_CMPX_NLT_F64, false},
|
||||
{0x3fu, Opcode::V_CMPX_TRU_F64, false},
|
||||
{0x80u, Opcode::V_CMP_F_I32}, {0x81u, Opcode::V_CMP_LT_I32},
|
||||
{0x82u, Opcode::V_CMP_EQ_I32}, {0x83u, Opcode::V_CMP_LE_I32},
|
||||
{0x84u, Opcode::V_CMP_GT_I32}, {0x85u, Opcode::V_CMP_NE_I32},
|
||||
@@ -879,10 +894,38 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
|
||||
case Opcode::V_CMP_F_F32:
|
||||
case Opcode::V_CMP_LT_F32:
|
||||
case Opcode::V_CMP_EQ_F32:
|
||||
case Opcode::V_CMP_F_F64:
|
||||
case Opcode::V_CMP_LT_F64:
|
||||
case Opcode::V_CMP_EQ_F64:
|
||||
case Opcode::V_CMP_LE_F64:
|
||||
case Opcode::V_CMP_GT_F64:
|
||||
case Opcode::V_CMP_LG_F64:
|
||||
case Opcode::V_CMP_GE_F64:
|
||||
case Opcode::V_CMP_O_F64:
|
||||
case Opcode::V_CMP_U_F64:
|
||||
case Opcode::V_CMP_NGE_F64:
|
||||
case Opcode::V_CMP_NLG_F64:
|
||||
case Opcode::V_CMP_NGT_F64:
|
||||
case Opcode::V_CMP_NLE_F64:
|
||||
case Opcode::V_CMP_NEQ_F64:
|
||||
case Opcode::V_CMP_NLT_F64:
|
||||
case Opcode::V_CMP_TRU_F64:
|
||||
case Opcode::V_CMPX_F_F64:
|
||||
case Opcode::V_CMPX_LT_F64:
|
||||
case Opcode::V_CMPX_EQ_F64:
|
||||
case Opcode::V_CMPX_LE_F64:
|
||||
case Opcode::V_CMPX_GT_F64:
|
||||
case Opcode::V_CMPX_LG_F64:
|
||||
case Opcode::V_CMPX_GE_F64:
|
||||
case Opcode::V_CMPX_O_F64:
|
||||
case Opcode::V_CMPX_U_F64:
|
||||
case Opcode::V_CMPX_NGE_F64:
|
||||
case Opcode::V_CMPX_NLG_F64:
|
||||
case Opcode::V_CMPX_NGT_F64:
|
||||
case Opcode::V_CMPX_NLE_F64:
|
||||
case Opcode::V_CMPX_NEQ_F64:
|
||||
case Opcode::V_CMPX_NLT_F64:
|
||||
case Opcode::V_CMPX_TRU_F64:
|
||||
case Opcode::V_CMP_LE_F32:
|
||||
case Opcode::V_CMP_GT_F32:
|
||||
case Opcode::V_CMP_LG_F32:
|
||||
@@ -1202,10 +1245,9 @@ VopcSdwaFields DecodeVopcSdwaFields(uint32_t modifier) {
|
||||
}
|
||||
|
||||
bool SupportsVopcSdwa(Opcode opcode) {
|
||||
// F64 SDWA operand handling is not implemented.
|
||||
return opcode != Opcode::UNSUPPORTED && opcode != Opcode::V_CMP_NE_I64 &&
|
||||
opcode != Opcode::V_CMP_EQ_F64 &&
|
||||
opcode != Opcode::V_CMP_LE_F64 && opcode != Opcode::V_CMPX_LE_F64 &&
|
||||
opcode != Opcode::V_CMPX_GE_F64;
|
||||
(opcode < Opcode::V_CMP_F_F64 || opcode > Opcode::V_CMPX_TRU_F64);
|
||||
}
|
||||
|
||||
void DecodeVopcSdwa(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
|
||||
@@ -1538,8 +1580,22 @@ void ApplyNativeVop3SourceModifiers(Instruction& inst, uint32_t abs, uint32_t ne
|
||||
|
||||
bool IsVopcCompareExec(Opcode opcode) {
|
||||
switch (opcode) {
|
||||
case Opcode::V_CMPX_F_F64:
|
||||
case Opcode::V_CMPX_LT_F64:
|
||||
case Opcode::V_CMPX_EQ_F64:
|
||||
case Opcode::V_CMPX_LE_F64:
|
||||
case Opcode::V_CMPX_GT_F64:
|
||||
case Opcode::V_CMPX_LG_F64:
|
||||
case Opcode::V_CMPX_GE_F64:
|
||||
case Opcode::V_CMPX_O_F64:
|
||||
case Opcode::V_CMPX_U_F64:
|
||||
case Opcode::V_CMPX_NGE_F64:
|
||||
case Opcode::V_CMPX_NLG_F64:
|
||||
case Opcode::V_CMPX_NGT_F64:
|
||||
case Opcode::V_CMPX_NLE_F64:
|
||||
case Opcode::V_CMPX_NEQ_F64:
|
||||
case Opcode::V_CMPX_NLT_F64:
|
||||
case Opcode::V_CMPX_TRU_F64:
|
||||
case Opcode::V_CMPX_LT_F32:
|
||||
case Opcode::V_CMPX_EQ_F32:
|
||||
case Opcode::V_CMPX_LE_F32:
|
||||
|
||||
@@ -65,11 +65,14 @@ void Translator::EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpc
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs}, flags)), false, cmpx);
|
||||
}
|
||||
|
||||
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx) {
|
||||
const auto lhs = IR::F32(ReadOperand(inst.src0, IR::Type::F32));
|
||||
const auto rhs = IR::F32(ReadOperand(inst.src1, IR::Type::F32));
|
||||
const auto unordered = ir.LogicalOr(IR::U1(ir.Emit(IR::ValueOpcode::FPIsNan32, {lhs})),
|
||||
IR::U1(ir.Emit(IR::ValueOpcode::FPIsNan32, {rhs})));
|
||||
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
|
||||
bool is64) {
|
||||
const auto type = is64 ? IR::Type::F64 : IR::Type::F32;
|
||||
const auto is_nan_op = is64 ? IR::ValueOpcode::FPIsNan64 : IR::ValueOpcode::FPIsNan32;
|
||||
const auto lhs = ReadOperand(inst.src0, type);
|
||||
const auto rhs = ReadOperand(inst.src1, type);
|
||||
const auto unordered = ir.LogicalOr(IR::U1(ir.Emit(is_nan_op, {lhs})),
|
||||
IR::U1(ir.Emit(is_nan_op, {rhs})));
|
||||
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false, cmpx);
|
||||
}
|
||||
|
||||
|
||||
@@ -973,6 +973,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
case Decoder::Opcode::BUFFER_LOAD_SBYTE:
|
||||
case Decoder::Opcode::BUFFER_LOAD_USHORT:
|
||||
case Decoder::Opcode::BUFFER_LOAD_SSHORT:
|
||||
case Decoder::Opcode::BUFFER_LOAD_SHORT_D16:
|
||||
case Decoder::Opcode::BUFFER_LOAD_SHORT_D16_HI:
|
||||
case Decoder::Opcode::BUFFER_LOAD_DWORD:
|
||||
case Decoder::Opcode::BUFFER_LOAD_DWORDX2:
|
||||
case Decoder::Opcode::BUFFER_LOAD_DWORDX3:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -875,10 +875,38 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
|
||||
case Decoder::Opcode::V_FLOOR_F64:
|
||||
case Decoder::Opcode::V_FRACT_F64: include_vector(inst.dst, 2u); [[fallthrough]];
|
||||
case Decoder::Opcode::V_CVT_F32_F64: include_vector(inst.src0, 2u); break;
|
||||
case Decoder::Opcode::V_CMP_F_F64:
|
||||
case Decoder::Opcode::V_CMP_LT_F64:
|
||||
case Decoder::Opcode::V_CMP_EQ_F64:
|
||||
case Decoder::Opcode::V_CMP_LE_F64:
|
||||
case Decoder::Opcode::V_CMP_GT_F64:
|
||||
case Decoder::Opcode::V_CMP_LG_F64:
|
||||
case Decoder::Opcode::V_CMP_GE_F64:
|
||||
case Decoder::Opcode::V_CMP_O_F64:
|
||||
case Decoder::Opcode::V_CMP_U_F64:
|
||||
case Decoder::Opcode::V_CMP_NGE_F64:
|
||||
case Decoder::Opcode::V_CMP_NLG_F64:
|
||||
case Decoder::Opcode::V_CMP_NGT_F64:
|
||||
case Decoder::Opcode::V_CMP_NLE_F64:
|
||||
case Decoder::Opcode::V_CMP_NEQ_F64:
|
||||
case Decoder::Opcode::V_CMP_NLT_F64:
|
||||
case Decoder::Opcode::V_CMP_TRU_F64:
|
||||
case Decoder::Opcode::V_CMPX_F_F64:
|
||||
case Decoder::Opcode::V_CMPX_LT_F64:
|
||||
case Decoder::Opcode::V_CMPX_EQ_F64:
|
||||
case Decoder::Opcode::V_CMPX_LE_F64:
|
||||
case Decoder::Opcode::V_CMPX_GT_F64:
|
||||
case Decoder::Opcode::V_CMPX_LG_F64:
|
||||
case Decoder::Opcode::V_CMPX_GE_F64:
|
||||
case Decoder::Opcode::V_CMPX_O_F64:
|
||||
case Decoder::Opcode::V_CMPX_U_F64:
|
||||
case Decoder::Opcode::V_CMPX_NGE_F64:
|
||||
case Decoder::Opcode::V_CMPX_NLG_F64:
|
||||
case Decoder::Opcode::V_CMPX_NGT_F64:
|
||||
case Decoder::Opcode::V_CMPX_NLE_F64:
|
||||
case Decoder::Opcode::V_CMPX_NEQ_F64:
|
||||
case Decoder::Opcode::V_CMPX_NLT_F64:
|
||||
case Decoder::Opcode::V_CMPX_TRU_F64:
|
||||
case Decoder::Opcode::V_CMP_EQ_I64:
|
||||
case Decoder::Opcode::V_CMP_LT_I64:
|
||||
case Decoder::Opcode::V_CMP_LE_I64:
|
||||
@@ -1356,7 +1384,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,
|
||||
@@ -127,7 +127,8 @@ private:
|
||||
bool signed_value, bool cmpx);
|
||||
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half,
|
||||
bool cmpx);
|
||||
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx);
|
||||
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
|
||||
bool is64 = false);
|
||||
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half = false);
|
||||
void V_CVT_F32_UBYTE(const Decoder::Instruction& inst, uint32_t byte_index);
|
||||
void V_CVT_F32_U32(const Decoder::Instruction& inst);
|
||||
@@ -264,6 +265,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
|
||||
|
||||
@@ -242,6 +242,74 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_CMPX_GE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, true);
|
||||
return;
|
||||
case O::V_CMP_F_F64: EmitCompareConstant(inst, false, false, false); return;
|
||||
case O::V_CMP_TRU_F64: EmitCompareConstant(inst, true, false, false); return;
|
||||
case O::V_CMPX_F_F64: EmitCompareConstant(inst, false, false, true); return;
|
||||
case O::V_CMPX_TRU_F64: EmitCompareConstant(inst, true, false, true); return;
|
||||
case O::V_CMP_LT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, true);
|
||||
return;
|
||||
case O::V_CMPX_EQ_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false, true);
|
||||
return;
|
||||
case O::V_CMP_GT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_GT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, true);
|
||||
return;
|
||||
case O::V_CMP_LG_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LG_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, true);
|
||||
return;
|
||||
case O::V_CMP_GE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMP_NLG_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NLG_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, true);
|
||||
return;
|
||||
case O::V_CMP_NEQ_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NEQ_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, true);
|
||||
return;
|
||||
case O::V_CMP_NLE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NLE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, true);
|
||||
return;
|
||||
case O::V_CMP_NLT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NLT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, true);
|
||||
return;
|
||||
case O::V_CMP_NGE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NGE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, true);
|
||||
return;
|
||||
case O::V_CMP_NGT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NGT_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, true);
|
||||
return;
|
||||
case O::V_CMP_O_F64: EmitFloatOrderedCompare(inst, true, false, true); return;
|
||||
case O::V_CMP_U_F64: EmitFloatOrderedCompare(inst, false, false, true); return;
|
||||
case O::V_CMPX_O_F64: EmitFloatOrderedCompare(inst, true, true, true); return;
|
||||
case O::V_CMPX_U_F64: EmitFloatOrderedCompare(inst, false, true, true); return;
|
||||
case O::V_CMPX_EQ_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false, true);
|
||||
return;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -162,6 +162,15 @@ VALUE_OPCODE(FPOrdEqual32, U1, F32, F32)
|
||||
VALUE_OPCODE(FPOrdEqual64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPOrdLessThanEqual64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPOrdGreaterThanEqual64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPOrdNotEqual64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPOrdLessThan64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPOrdGreaterThan64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPUnordEqual64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPUnordNotEqual64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPUnordLessThan64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPUnordGreaterThan64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPUnordLessThanEqual64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPUnordGreaterThanEqual64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPUnordEqual32, U1, F32, F32)
|
||||
VALUE_OPCODE(FPOrdNotEqual32, U1, F32, F32)
|
||||
VALUE_OPCODE(FPUnordNotEqual32, U1, F32, F32)
|
||||
@@ -174,6 +183,7 @@ VALUE_OPCODE(FPUnordLessThanEqual32, U1, F32, F32)
|
||||
VALUE_OPCODE(FPOrdGreaterThanEqual32, U1, F32, F32)
|
||||
VALUE_OPCODE(FPUnordGreaterThanEqual32, U1, F32, F32)
|
||||
VALUE_OPCODE(FPIsNan32, U1, F32)
|
||||
VALUE_OPCODE(FPIsNan64, U1, F64)
|
||||
VALUE_OPCODE(FPCmpClass32, U1, F32, U32)
|
||||
VALUE_OPCODE(FPCmpClass16, U1, U32, U32)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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_ */
|
||||
@@ -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_ */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
+128
-29
@@ -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;
|
||||
}
|
||||
@@ -830,6 +881,8 @@ enum class RegIndirectPacket : uint32_t {
|
||||
Uc,
|
||||
};
|
||||
|
||||
static constexpr uint32_t RegIndirectPacketSizeDw = 5;
|
||||
|
||||
static uint32_t reg_indirect_native_op(RegIndirectPacket type) {
|
||||
switch (type) {
|
||||
case RegIndirectPacket::Cx: return Pm4::IT_SET_CONTEXT_REG_INDIRECT;
|
||||
@@ -853,7 +906,7 @@ static uint32_t reg_indirect_pm4_r(RegIndirectPacket type) {
|
||||
|
||||
static void reg_indirect_write_packet(uint32_t* cmd, uint64_t vaddr, uint32_t num_regs,
|
||||
RegIndirectPacket type) {
|
||||
cmd[0] = KYTY_PM4(5, reg_indirect_native_op(type), reg_indirect_pm4_r(type));
|
||||
cmd[0] = KYTY_PM4(RegIndirectPacketSizeDw, reg_indirect_native_op(type), reg_indirect_pm4_r(type));
|
||||
cmd[1] = static_cast<uint32_t>(vaddr) & 0xfffffffcu;
|
||||
cmd[2] = static_cast<uint32_t>(vaddr >> 32u);
|
||||
cmd[3] = 0x80000000u;
|
||||
@@ -1728,7 +1781,7 @@ int KYTY_SYSV_ABI AgcDriverRegisterWorkloadStream(uint32_t stream_id, const void
|
||||
}
|
||||
|
||||
uint32_t* KYTY_SYSV_ABI AgcCbNop(CommandBuffer* buf, uint32_t size_in_dwords) {
|
||||
if (buf == nullptr || size_in_dwords < 2) {
|
||||
if (buf == nullptr || size_in_dwords == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -1739,9 +1792,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbNop(CommandBuffer* buf, uint32_t size_in_dwords) {
|
||||
}
|
||||
|
||||
cmd[0] = KYTY_PM4(size_in_dwords, Pm4::IT_NOP, Pm4::R_ZERO);
|
||||
if (size_in_dwords > 1) {
|
||||
memset(cmd + 1, 0, static_cast<size_t>(size_in_dwords - 1) * 4);
|
||||
}
|
||||
|
||||
return cmd;
|
||||
}
|
||||
@@ -1789,6 +1839,33 @@ uint32_t KYTY_SYSV_ABI AgcCbDispatchGetSize() {
|
||||
return 20;
|
||||
}
|
||||
|
||||
static constexpr uint32_t BranchPacketSizeDw = 14;
|
||||
|
||||
static void write_branch_target(uint32_t* words, uint64_t address, uint8_t cache_policy,
|
||||
uint32_t size_in_dwords) {
|
||||
words[0] = (words[0] & 0x3u) | (static_cast<uint32_t>(address) & ~0x3u);
|
||||
words[1] = static_cast<uint32_t>(address >> 32u);
|
||||
words[2] = (words[2] & 0xcff00000u) | (size_in_dwords & 0xfffffu) |
|
||||
((static_cast<uint32_t>(cache_policy) & 0x3u) << 28u);
|
||||
}
|
||||
|
||||
uint64_t KYTY_SYSV_ABI AgcCbBranchGetSize() {
|
||||
PRINT_NAME();
|
||||
return BranchPacketSizeDw * sizeof(uint32_t);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI AgcBranchPatchSetThenTarget(uint32_t* cmd, uint8_t cache_policy,
|
||||
const volatile uint32_t* target,
|
||||
uint32_t size_in_dwords) {
|
||||
PRINT_NAME();
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
if (((cmd[0] >> 8u) & 0xffu) != Pm4::IT_INDIRECT_BUFFER) {
|
||||
return GRAPHICS5_ERROR_INVALID_PACKET;
|
||||
}
|
||||
write_branch_target(cmd + 8, reinterpret_cast<uint64_t>(target), cache_policy, size_in_dwords);
|
||||
return OK;
|
||||
}
|
||||
|
||||
uint32_t* KYTY_SYSV_ABI AgcCbBranch(CommandBuffer* buf, uint8_t mode, uint8_t compare_function,
|
||||
const volatile uint64_t* compare_addr, uint64_t mask,
|
||||
uint64_t reference, uint8_t cache_policy1,
|
||||
@@ -1814,7 +1891,7 @@ uint32_t* KYTY_SYSV_ABI AgcCbBranch(CommandBuffer* buf, uint8_t mode, uint8_t co
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
auto* cmd = buf->AllocateDW(14);
|
||||
auto* cmd = buf->AllocateDW(BranchPacketSizeDw);
|
||||
|
||||
if (cmd == nullptr) {
|
||||
LOGF_COLOR(Log::Color::Red, "\t failed to allocate branch packet\n");
|
||||
@@ -1825,7 +1902,7 @@ uint32_t* KYTY_SYSV_ABI AgcCbBranch(CommandBuffer* buf, uint8_t mode, uint8_t co
|
||||
const auto then_vaddr = reinterpret_cast<uint64_t>(buffer1);
|
||||
const auto else_vaddr = reinterpret_cast<uint64_t>(buffer2);
|
||||
|
||||
cmd[0] = KYTY_PM4(14, Pm4::IT_INDIRECT_BUFFER, 0u);
|
||||
cmd[0] = KYTY_PM4(BranchPacketSizeDw, Pm4::IT_INDIRECT_BUFFER, 0u);
|
||||
cmd[1] = (mode & 0x3u) | ((static_cast<uint32_t>(compare_function) & 0x7u) << 8u);
|
||||
cmd[2] = static_cast<uint32_t>(compare_vaddr & 0xfffffff8u);
|
||||
cmd[3] = static_cast<uint32_t>((compare_vaddr >> 32u) & 0xffffffffu);
|
||||
@@ -1833,12 +1910,9 @@ uint32_t* KYTY_SYSV_ABI AgcCbBranch(CommandBuffer* buf, uint8_t mode, uint8_t co
|
||||
cmd[5] = static_cast<uint32_t>((mask >> 32u) & 0xffffffffu);
|
||||
cmd[6] = static_cast<uint32_t>(reference & 0xffffffffu);
|
||||
cmd[7] = static_cast<uint32_t>((reference >> 32u) & 0xffffffffu);
|
||||
cmd[8] = static_cast<uint32_t>(then_vaddr & 0xfffffffcu);
|
||||
cmd[9] = static_cast<uint32_t>((then_vaddr >> 32u) & 0xffffffffu);
|
||||
cmd[10] = (size_in_dwords1 & 0xfffffu) | ((static_cast<uint32_t>(cache_policy1) & 0x3u) << 28u);
|
||||
cmd[11] = static_cast<uint32_t>(else_vaddr & 0xfffffffcu);
|
||||
cmd[12] = static_cast<uint32_t>((else_vaddr >> 32u) & 0xffffffffu);
|
||||
cmd[13] = (size_in_dwords2 & 0xfffffu) | ((static_cast<uint32_t>(cache_policy2) & 0x3u) << 28u);
|
||||
std::fill(cmd + 8, cmd + BranchPacketSizeDw, 0u);
|
||||
write_branch_target(cmd + 8, then_vaddr, cache_policy1, size_in_dwords1);
|
||||
write_branch_target(cmd + 11, else_vaddr, cache_policy2, size_in_dwords2);
|
||||
|
||||
return cmd;
|
||||
}
|
||||
@@ -2353,7 +2427,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirect(CommandBuffer*
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(5);
|
||||
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
||||
@@ -2364,6 +2438,11 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirect(CommandBuffer*
|
||||
return cmd;
|
||||
}
|
||||
|
||||
uint64_t KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirectGetSize() {
|
||||
PRINT_NAME();
|
||||
return RegIndirectPacketSizeDw * sizeof(uint32_t);
|
||||
}
|
||||
|
||||
uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer* buf,
|
||||
const volatile ShaderRegister* regs,
|
||||
uint32_t num_regs) {
|
||||
@@ -2377,7 +2456,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer*
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(5);
|
||||
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
||||
@@ -2391,7 +2470,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer*
|
||||
uint64_t KYTY_SYSV_ABI AgcDcbSetShRegistersIndirectGetSize() {
|
||||
PRINT_NAME();
|
||||
|
||||
return 5u * sizeof(uint32_t);
|
||||
return RegIndirectPacketSizeDw * sizeof(uint32_t);
|
||||
}
|
||||
|
||||
uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegistersIndirect(CommandBuffer* buf,
|
||||
@@ -2407,7 +2486,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegistersIndirect(CommandBuffer*
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(5);
|
||||
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
||||
@@ -3647,12 +3726,7 @@ int KYTY_SYSV_ABI AgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate(
|
||||
}
|
||||
|
||||
uint32_t KYTY_SYSV_ABI AgcGetPacketSize(uint32_t* packet) {
|
||||
const auto cmd_id = packet[0];
|
||||
if ((cmd_id & 0x3fffff00u) == 0x3fff1000u) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return KYTY_PM4_LEN(cmd_id);
|
||||
return Pm4::PacketSizeDw(packet[0]);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI AgcSetPacketPredication(uint32_t* packet, uint32_t predication) {
|
||||
@@ -3694,12 +3768,7 @@ int KYTY_SYSV_ABI AgcSetRangePredication(uint32_t* start, const volatile uint32_
|
||||
const auto cmd_id = packet[0];
|
||||
packet[0] = (cmd_id & ~1u) | predication_bit;
|
||||
|
||||
auto size = KYTY_PM4_LEN(cmd_id);
|
||||
if ((cmd_id & 0x3fffff00u) == 0x3fff1000u) {
|
||||
size = 1;
|
||||
}
|
||||
|
||||
packet_va += size * sizeof(uint32_t);
|
||||
packet_va += Pm4::PacketSizeDw(cmd_id) * sizeof(uint32_t);
|
||||
packet = reinterpret_cast<uint32_t*>(packet_va);
|
||||
}
|
||||
|
||||
@@ -4206,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;
|
||||
|
||||
@@ -101,6 +101,10 @@ uint32_t* KYTY_SYSV_ABI AgcCbBranch(CommandBuffer* buf, uint8_t mode, uint8_t co
|
||||
const volatile uint32_t* buffer1, uint32_t size_in_dwords1,
|
||||
uint8_t cache_policy2, const volatile uint32_t* buffer2,
|
||||
uint32_t size_in_dwords2);
|
||||
uint64_t KYTY_SYSV_ABI AgcCbBranchGetSize();
|
||||
int KYTY_SYSV_ABI AgcBranchPatchSetThenTarget(uint32_t* cmd, uint8_t cache_policy,
|
||||
const volatile uint32_t* target,
|
||||
uint32_t size_in_dwords);
|
||||
uint32_t* KYTY_SYSV_ABI AgcCbSetShRegisterRangeDirect(CommandBuffer* buf, uint32_t offset,
|
||||
const uint32_t* values, uint32_t num_values);
|
||||
uint32_t KYTY_SYSV_ABI AgcCbSetShRegisterRangeDirectGetSize(uint32_t num_values);
|
||||
@@ -134,6 +138,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegisterDirect(CommandBuffer* buf, ShaderRegi
|
||||
uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirect(CommandBuffer* buf,
|
||||
const volatile ShaderRegister* regs,
|
||||
uint32_t num_regs);
|
||||
uint64_t KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirectGetSize();
|
||||
uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer* buf,
|
||||
const volatile ShaderRegister* regs,
|
||||
uint32_t num_regs);
|
||||
|
||||
@@ -59,6 +59,8 @@ LIB_DEFINE(InitAgcDriver_1) {
|
||||
LIB_FUNC("k3GhuSNmBLU", Gen5::AgcCbDispatch);
|
||||
LIB_FUNC("Abendgtz+3o", Gen5::AgcCbDispatchGetSize);
|
||||
LIB_FUNC("w1KFAHVqpaU", Gen5::AgcCbBranch);
|
||||
LIB_FUNC("uZW-mqsxkrM", Gen5::AgcCbBranchGetSize);
|
||||
LIB_FUNC("7Wa3aeJgeVU", Gen5::AgcBranchPatchSetThenTarget);
|
||||
LIB_FUNC("n2fD4A+pb+g", Gen5::AgcCbSetShRegisterRangeDirect);
|
||||
LIB_FUNC("bxGoVxpdSPQ", Gen5::AgcCbSetShRegisterRangeDirectGetSize);
|
||||
LIB_FUNC("UZbQjYAwwXM", Gen5::AgcCbSetShRegistersDirect);
|
||||
@@ -95,6 +97,7 @@ LIB_DEFINE(InitAgcDriver_1) {
|
||||
LIB_FUNC("1DeUNpRIDDA", Gen5::AgcDcbSetCxRegisterDirectGetSize);
|
||||
LIB_FUNC("w4-d0n60hdo", Gen5::AgcDcbSetUcRegisterDirect);
|
||||
LIB_FUNC("ZvwO9euwYzc", Gen5::AgcDcbSetCxRegistersIndirect);
|
||||
LIB_FUNC("GBCh3zCihoU", Gen5::AgcDcbSetCxRegistersIndirectGetSize);
|
||||
LIB_FUNC("-HOOCn0JY48", Gen5::AgcDcbSetShRegistersIndirect);
|
||||
LIB_FUNC("nNlUtdDDvZ0", Gen5::AgcDcbSetShRegistersIndirectGetSize);
|
||||
LIB_FUNC("hvUfkUIQcOE", Gen5::AgcDcbSetUcRegistersIndirect);
|
||||
|
||||
@@ -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 = █
|
||||
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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+560
-1
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user