mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-25 12:26:51 +01:00
shader: resolve descriptors from control flow
This commit is contained in:
@@ -548,30 +548,40 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
|
||||
|
||||
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " CFG BuildGraph\n", GetDumpLabel(options),
|
||||
StageName(options.stage), options.shader_hash);
|
||||
auto cfg = CFG::BuildGraph(decoded);
|
||||
auto native_cfg = CFG::BuildGraph(decoded);
|
||||
CFG::Graph structured_cfg;
|
||||
auto* selected_cfg = &native_cfg;
|
||||
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " CFG BuildGraph blocks=%" PRIu64
|
||||
" loops=%" PRIu64 " back_edges=%" PRIu64 " elapsed_ms=%" PRIu64 "\n",
|
||||
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
|
||||
static_cast<uint64_t>(cfg.blocks.size()), static_cast<uint64_t>(cfg.natural_loops.size()),
|
||||
static_cast<uint64_t>(cfg.back_edges.size()), phase_ms());
|
||||
if (cfg.irreducible) {
|
||||
LogDispatcherFallback(options, cfg, "build");
|
||||
static_cast<uint64_t>(native_cfg.blocks.size()),
|
||||
static_cast<uint64_t>(native_cfg.natural_loops.size()),
|
||||
static_cast<uint64_t>(native_cfg.back_edges.size()), phase_ms());
|
||||
if (native_cfg.irreducible) {
|
||||
LogDispatcherFallback(options, native_cfg, "build");
|
||||
} else {
|
||||
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " CFG Structurize\n",
|
||||
GetDumpLabel(options), StageName(options.stage), options.shader_hash);
|
||||
if (!CFG::Structurize(cfg)) {
|
||||
LogDispatcherFallback(options, cfg, "structurize");
|
||||
structured_cfg = CFG::Structurize(native_cfg);
|
||||
if (structured_cfg.unsupported) {
|
||||
native_cfg.unsupported = true;
|
||||
native_cfg.failure_kind = structured_cfg.failure_kind;
|
||||
native_cfg.failure_block = structured_cfg.failure_block;
|
||||
native_cfg.unsupported_reason = structured_cfg.unsupported_reason;
|
||||
LogDispatcherFallback(options, native_cfg, "structurize");
|
||||
} else {
|
||||
selected_cfg = &structured_cfg;
|
||||
LOGF("%s structured CFG success: blocks=%" PRIu64 "\n", GetDumpLabel(options),
|
||||
static_cast<uint64_t>(cfg.blocks.size()));
|
||||
static_cast<uint64_t>(selected_cfg->blocks.size()));
|
||||
}
|
||||
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " CFG Structurize blocks=%" PRIu64
|
||||
" loops=%" PRIu64 " elapsed_ms=%" PRIu64 "\n",
|
||||
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
|
||||
static_cast<uint64_t>(cfg.blocks.size()),
|
||||
static_cast<uint64_t>(cfg.natural_loops.size()), phase_ms());
|
||||
static_cast<uint64_t>(selected_cfg->blocks.size()),
|
||||
static_cast<uint64_t>(selected_cfg->natural_loops.size()), phase_ms());
|
||||
}
|
||||
|
||||
const auto& cfg = *selected_cfg;
|
||||
Frontend::EmbeddedFetchPlan embedded_fetch;
|
||||
if ((options.stage == ShaderType::Vertex || options.stage == ShaderType::Local) &&
|
||||
options.input_info.vertex != nullptr && options.input_info.vertex->fetch_embedded) {
|
||||
@@ -616,7 +626,7 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
|
||||
LowerTessellationMemory(ir, options);
|
||||
IR::BuildSrtPlan(ir);
|
||||
IR::EliminateDeadCode(ir.blocks);
|
||||
IR::TrackResources(ir);
|
||||
IR::TrackResources(ir, decoded, native_cfg);
|
||||
IR::EliminateDeadCode(ir.blocks);
|
||||
TranslateResult result;
|
||||
result.program = std::move(ir);
|
||||
|
||||
@@ -1758,16 +1758,13 @@ Graph BuildGraph(const Decoder::Program& program) {
|
||||
return graph;
|
||||
}
|
||||
|
||||
bool Structurize(Graph& graph) {
|
||||
if (graph.unsupported || graph.irreducible) return false;
|
||||
auto structured = GotoStructurizer(graph).Run();
|
||||
if (structured.unsupported) {
|
||||
SetFailure(graph, structured.failure_kind, structured.failure_block,
|
||||
structured.unsupported_reason);
|
||||
return false;
|
||||
Graph Structurize(const Graph& graph) {
|
||||
if (graph.unsupported || graph.irreducible) {
|
||||
Graph failed;
|
||||
SetFailure(failed, graph.failure_kind, graph.failure_block, graph.unsupported_reason);
|
||||
return failed;
|
||||
}
|
||||
graph = std::move(structured);
|
||||
return true;
|
||||
return GotoStructurizer(graph).Run();
|
||||
}
|
||||
|
||||
std::string BranchConditionToString(BranchCondition condition) {
|
||||
|
||||
@@ -119,9 +119,9 @@ struct Graph {
|
||||
};
|
||||
|
||||
Graph BuildGraph(const Decoder::Program& program);
|
||||
// Commits structured control flow on success; preserves the original graph with
|
||||
// failure diagnostics on failure. failure_block is an original block ID or UINT32_MAX.
|
||||
bool Structurize(Graph& graph);
|
||||
// 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);
|
||||
std::string BranchConditionToString(BranchCondition condition);
|
||||
std::string FailureKindToString(FailureKind kind);
|
||||
std::string GraphToString(const Graph& graph);
|
||||
|
||||
@@ -317,6 +317,7 @@ void Translator::WriteRawU32(const Decoder::Operand& operand, IR::U32 value) {
|
||||
case Decoder::OperandKind::Sgpr: {
|
||||
const auto reg = static_cast<IR::ScalarReg>(operand.reg);
|
||||
ir.SetScalarReg(reg, value);
|
||||
program.scalar_writes.push_back({current_pc, reg});
|
||||
ir.SetScalarMaskTag(reg, IR::U1(IR::Value(false)));
|
||||
if (IR::RegIndex(reg) > 0u) {
|
||||
ir.SetScalarMaskTag(static_cast<IR::ScalarReg>(IR::RegIndex(reg) - 1u),
|
||||
@@ -699,11 +700,13 @@ std::array<IR::U32, 2> Translator::WriteMask(const Decoder::Operand& operand, IR
|
||||
IR::U1(IR::Value(false)));
|
||||
}
|
||||
ir.SetScalarReg(reg, mask[0]);
|
||||
program.scalar_writes.push_back({current_pc, reg});
|
||||
// A wave32 VALU mask destination must not overwrite the neighboring SGPR.
|
||||
if ((write_64 || program.wave_size == 64u) &&
|
||||
IR::RegIndex(reg) + 1u < IR::NumScalarRegs) {
|
||||
const auto high = static_cast<IR::ScalarReg>(IR::RegIndex(reg) + 1u);
|
||||
ir.SetScalarReg(high, mask[1]);
|
||||
program.scalar_writes.push_back({current_pc, high});
|
||||
ir.SetThreadBitScalarReg(high, IR::U1(IR::Value(false)));
|
||||
ir.SetScalarMaskTag(high, IR::U1(IR::Value(false)));
|
||||
}
|
||||
|
||||
@@ -81,7 +81,8 @@ bool EquivalentValue(const ResourcePlan& program, Value left, Value right,
|
||||
program.memory_info[li] != program.memory_info[ri]) {
|
||||
return false;
|
||||
}
|
||||
} else if (lhs->Flags<uint64_t>() != rhs->Flags<uint64_t>()) {
|
||||
} else if (lhs->GetOpcode() != ValueOpcode::ReadConst &&
|
||||
lhs->Flags<uint64_t>() != rhs->Flags<uint64_t>()) {
|
||||
return false;
|
||||
}
|
||||
for (size_t index = 0; index < lhs->NumArgs(); index++) {
|
||||
|
||||
@@ -583,6 +583,8 @@ struct Program: ResourcePlan {
|
||||
CFG::FailureKind cfg_failure_kind = CFG::FailureKind::None;
|
||||
std::string fallback_reason;
|
||||
std::vector<BlockInfo> block_info;
|
||||
struct ScalarWrite { uint32_t pc; ScalarReg reg; };
|
||||
std::vector<ScalarWrite> scalar_writes;
|
||||
// Typed memory and export instructions reference shader-local metadata by dense index.
|
||||
// Decoder-only details (such as NSA register numbers) have already become IR operands.
|
||||
std::vector<ExportInfo> export_info;
|
||||
|
||||
@@ -82,7 +82,10 @@ uint32_t ByteExtent(const MemoryInfo& memory) {
|
||||
|
||||
class Tracker {
|
||||
public:
|
||||
explicit Tracker(Program& program): m_program(program), m_info(program.info) {
|
||||
Tracker(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg)
|
||||
: m_program(program), m_decoded(decoded), m_native_cfg(native_cfg),
|
||||
m_scalar_writes(std::move(program.scalar_writes)), m_info(program.info) {
|
||||
std::ranges::sort(m_scalar_writes, {}, &Program::ScalarWrite::pc);
|
||||
m_info.buffers.clear();
|
||||
m_info.images.clear();
|
||||
m_info.samplers.clear();
|
||||
@@ -169,7 +172,98 @@ private:
|
||||
std::abort();
|
||||
}
|
||||
|
||||
Value LowerDescriptorPhi(Value value, const Block* use) {
|
||||
Value NativeDescriptorSource(Value value, uint32_t reg, uint32_t use_pc) const {
|
||||
value = value.Resolve();
|
||||
const auto* phi = value.TryInstruction();
|
||||
if (phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi || m_native_cfg.blocks.empty())
|
||||
return value;
|
||||
std::vector<const Inst*> candidates;
|
||||
std::vector<const Inst*> visited;
|
||||
std::vector<const Inst*> pending {phi};
|
||||
while (!pending.empty()) {
|
||||
const auto* inst = pending.back();
|
||||
pending.pop_back();
|
||||
if (std::ranges::find(visited, inst) != visited.end()) continue;
|
||||
visited.push_back(inst);
|
||||
if (inst->GetOpcode() == ValueOpcode::Phi) {
|
||||
for (size_t i = 0; i < inst->NumArgs(); ++i) {
|
||||
const auto* arg = inst->Arg(i).Resolve().TryInstruction();
|
||||
if (arg != nullptr) pending.push_back(arg);
|
||||
}
|
||||
} else if (inst->GetOpcode() == ValueOpcode::ReadConst ||
|
||||
inst->GetOpcode() == ValueOpcode::GetUserData) {
|
||||
candidates.push_back(inst);
|
||||
}
|
||||
}
|
||||
if (candidates.empty()) return value;
|
||||
const auto source_at = [&](uint32_t pc) {
|
||||
Value source;
|
||||
const auto native = std::ranges::lower_bound(m_decoded.instructions, pc, {},
|
||||
&Decoder::Instruction::pc);
|
||||
for (const auto* candidate: candidates) {
|
||||
if (pc == UINT32_MAX) {
|
||||
if (candidate->GetOpcode() != ValueOpcode::GetUserData ||
|
||||
RegIndex(candidate->Arg(0).ScalarRegister()) != reg)
|
||||
continue;
|
||||
} else {
|
||||
if (candidate->GetOpcode() != ValueOpcode::ReadConst) continue;
|
||||
const auto flags = candidate->Flags<MemoryFlags>();
|
||||
if (flags.pc != pc || flags.index >= m_program.memory_info.size()) continue;
|
||||
if (native == m_decoded.instructions.end() || native->pc != pc ||
|
||||
native->dst.kind != Decoder::OperandKind::Sgpr ||
|
||||
native->dst.reg + m_program.memory_info[flags.index].component_index != reg)
|
||||
continue;
|
||||
}
|
||||
const Value current(const_cast<Inst*>(candidate));
|
||||
if (!source.IsEmpty() && !EquivalentValue(m_program, source, current)) return Value {};
|
||||
source = current;
|
||||
}
|
||||
return source;
|
||||
};
|
||||
const auto use = std::ranges::find_if(m_native_cfg.blocks, [&](const auto& block) {
|
||||
return block.start_pc <= use_pc && use_pc < block.end_pc;
|
||||
});
|
||||
if (use == m_native_cfg.blocks.end()) return value;
|
||||
struct Position { uint32_t block; uint32_t before; };
|
||||
std::vector<Position> positions {{use->id, use_pc}};
|
||||
std::vector<bool> reached(m_native_cfg.blocks.size());
|
||||
Value selected;
|
||||
const auto select = [&](uint32_t pc) {
|
||||
const auto source = source_at(pc);
|
||||
if (source.IsEmpty() || (!selected.IsEmpty() && !EquivalentValue(m_program, selected, source)))
|
||||
return false;
|
||||
selected = source;
|
||||
return true;
|
||||
};
|
||||
while (!positions.empty()) {
|
||||
const auto position = positions.back();
|
||||
positions.pop_back();
|
||||
const auto& block = m_native_cfg.blocks[position.block];
|
||||
// A backedge may revisit the use block after a write later than the original use.
|
||||
if (position.before == block.end_pc) {
|
||||
if (reached[block.id]) continue;
|
||||
reached[block.id] = true;
|
||||
}
|
||||
auto write = std::ranges::lower_bound(m_scalar_writes, position.before, {},
|
||||
&Program::ScalarWrite::pc);
|
||||
bool found = false;
|
||||
while (write != m_scalar_writes.begin()) {
|
||||
--write;
|
||||
if (write->pc < block.start_pc) break;
|
||||
if (RegIndex(write->reg) != reg) continue;
|
||||
if (!select(write->pc)) return value;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (found) continue;
|
||||
if (block.id == m_native_cfg.entry_block && !select(UINT32_MAX)) return value;
|
||||
for (const auto pred: block.predecessors)
|
||||
positions.push_back({pred, m_native_cfg.blocks[pred].end_pc});
|
||||
}
|
||||
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 ||
|
||||
@@ -183,42 +277,6 @@ private:
|
||||
branch == phi->PhiBlock(1)) {
|
||||
return value;
|
||||
}
|
||||
// Structurization can merge the descriptor and its use predicate in parallel Phis.
|
||||
// Match their incoming blocks to exclude only edges that cannot reach this use.
|
||||
if (use != nullptr && use->ImmPredecessors().size() == 1u &&
|
||||
use->ImmPredecessors()[0] == merge) {
|
||||
const auto merge_it = std::ranges::find(m_program.blocks, merge);
|
||||
const auto use_it = std::ranges::find(m_program.blocks, use);
|
||||
if (merge_it != m_program.blocks.end() && use_it != m_program.blocks.end()) {
|
||||
const auto& info = m_program.block_info[merge_it - m_program.blocks.begin()];
|
||||
const auto& term = info.terminator;
|
||||
const auto id = m_program.block_info[use_it - m_program.blocks.begin()].id;
|
||||
const auto* condition = info.condition.Resolve().TryInstruction();
|
||||
if (term.kind == CFG::TerminatorKind::ConditionalBranch &&
|
||||
term.true_block != term.false_block &&
|
||||
(id == term.true_block || id == term.false_block) && condition != nullptr &&
|
||||
condition->GetOpcode() == ValueOpcode::Phi && condition->GetType() == Type::U1 &&
|
||||
condition->Parent() == merge && condition->NumArgs() == 2u &&
|
||||
condition->NumPhiBlocks() == 2u) {
|
||||
const bool taken = id == term.true_block;
|
||||
for (uint32_t skipped = 0; skipped < 2u; skipped++) {
|
||||
const auto excluded = condition->Arg(skipped).Resolve();
|
||||
const auto included = condition->Arg(skipped ^ 1u).Resolve();
|
||||
if (!excluded.IsImmediate() || excluded.GetType() != Type::U1 ||
|
||||
excluded.U1() == taken ||
|
||||
(included.IsImmediate() && included.U1() != taken)) {
|
||||
continue;
|
||||
}
|
||||
for (uint32_t selected = 0; selected < 2u; selected++) {
|
||||
if (phi->PhiBlock(selected) == condition->PhiBlock(skipped ^ 1u) &&
|
||||
phi->PhiBlock(selected ^ 1u) == condition->PhiBlock(skipped)) {
|
||||
return phi->Arg(selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& [original, selected]: m_descriptor_selections) {
|
||||
if (original == phi) {
|
||||
return selected;
|
||||
@@ -276,14 +334,16 @@ private:
|
||||
}
|
||||
|
||||
void MakeSource(const Inst& handle, uint32_t width, bool sampler, bool sample_adjust,
|
||||
DescriptorSource& descriptor, uint32_t pc) {
|
||||
uint32_t base_reg, DescriptorSource& descriptor, uint32_t pc) {
|
||||
if (handle.NumArgs() != width) {
|
||||
Fail(pc, fmt::format("{} has {} descriptor dwords, expected {}",
|
||||
ValueOpcodeName(handle.GetOpcode()), handle.NumArgs(), width));
|
||||
}
|
||||
descriptor.dword_count = width;
|
||||
for (uint32_t i = 0; i < width; i++) {
|
||||
descriptor.dwords[i] = LowerDescriptorPhi(handle.Arg(i), handle.Parent());
|
||||
const auto value = base_reg != UINT32_MAX
|
||||
? NativeDescriptorSource(handle.Arg(i), base_reg + i, pc) : handle.Arg(i);
|
||||
descriptor.dwords[i] = LowerDescriptorPhi(value);
|
||||
}
|
||||
if (sample_adjust) {
|
||||
descriptor.dwords[3] = CanonicalizeSampleAdjustDword3(descriptor.dwords[3]);
|
||||
@@ -404,7 +464,7 @@ private:
|
||||
if (width == 0u) {
|
||||
return false;
|
||||
}
|
||||
MakeSource(handle, width, false, false, descriptor, pc);
|
||||
MakeSource(handle, width, false, false, UINT32_MAX, descriptor, pc);
|
||||
uint32_t bad_dword = 0;
|
||||
return ValidateSource(descriptor, bad_dword);
|
||||
}
|
||||
@@ -1014,14 +1074,15 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
bool GetHandle(Value value, ValueOpcode expected, uint32_t width, uint32_t pc, Inst*& handle,
|
||||
uint32_t& source, bool sampler = false, bool sample_adjust = false) {
|
||||
bool GetHandle(Value value, ValueOpcode expected, uint32_t width, uint32_t pc,
|
||||
uint32_t base_reg, Inst*& handle, uint32_t& source, bool sampler = false,
|
||||
bool sample_adjust = false) {
|
||||
handle = value.Resolve().TryInstruction();
|
||||
if (handle == nullptr || handle->GetOpcode() != expected) {
|
||||
Fail(pc, fmt::format("memory operation requires {}", ValueOpcodeName(expected)));
|
||||
}
|
||||
DescriptorSource descriptor;
|
||||
MakeSource(*handle, width, sampler, sample_adjust, descriptor, pc);
|
||||
MakeSource(*handle, width, sampler, sample_adjust, base_reg, descriptor, pc);
|
||||
uint32_t bad_dword = 0;
|
||||
if (!ValidateSource(descriptor, bad_dword)) {
|
||||
if (expected == ValueOpcode::GetBufferResource &&
|
||||
@@ -1204,8 +1265,8 @@ private:
|
||||
uint32_t resource = 0;
|
||||
|
||||
if (buffer != BufferAccess::None) {
|
||||
if (!GetHandle(inst.Arg(0), ValueOpcode::GetBufferResource, 4, flags.pc, handle,
|
||||
source)) {
|
||||
if (!GetHandle(inst.Arg(0), ValueOpcode::GetBufferResource, 4, flags.pc,
|
||||
memory.resource * 4u, handle, source)) {
|
||||
if (memory.kind != ResourceKind::Buffer || !memory.SupportsIndirectBufferLoad(op)) {
|
||||
Fail(flags.pc,
|
||||
"buffer descriptor is not a valid runtime value; GPU-selected access "
|
||||
@@ -1255,7 +1316,8 @@ private:
|
||||
if (indirect != nullptr) {
|
||||
source = indirect->source;
|
||||
} else {
|
||||
GetHandle(inst.Arg(0), ValueOpcode::GetImageResource, 8, flags.pc, handle, source);
|
||||
GetHandle(inst.Arg(0), ValueOpcode::GetImageResource, 8, flags.pc,
|
||||
memory.resource * 4u, handle, source);
|
||||
}
|
||||
resource = AddImage(source, memory, op, flags.pc);
|
||||
if (resource == UINT32_MAX) {
|
||||
@@ -1271,8 +1333,8 @@ 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, sampler_handle,
|
||||
sampler_source, true, sample_adjust);
|
||||
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");
|
||||
@@ -1313,6 +1375,9 @@ private:
|
||||
}
|
||||
|
||||
Program& m_program;
|
||||
const Decoder::Program& m_decoded;
|
||||
const CFG::Graph& m_native_cfg;
|
||||
std::vector<Program::ScalarWrite> m_scalar_writes;
|
||||
ShaderInfo m_info;
|
||||
std::vector<DescriptorSource> m_sources;
|
||||
std::vector<HandlePatch> m_handle_patches;
|
||||
@@ -1324,8 +1389,8 @@ private:
|
||||
|
||||
} // namespace
|
||||
|
||||
void TrackResources(Program& program) {
|
||||
Tracker(program).Run();
|
||||
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg) {
|
||||
Tracker(program, decoded, native_cfg).Run();
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
||||
// Collects immutable resource topology from typed SSA handles, interns their resolved dwords in
|
||||
// descriptor_sources, then writes dense indices to handle flags and MemoryInfo.
|
||||
void TrackResources(Program& program);
|
||||
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg);
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
||||
|
||||
@@ -430,7 +430,8 @@ private:
|
||||
const auto resource =
|
||||
Value(&*block->PrependNewInst(where, ValueOpcode::GetSrtResource));
|
||||
const auto flat = Value(&*block->PrependNewInst(where, ValueOpcode::ReadConst,
|
||||
{resource, Value(patch.slot)}));
|
||||
{resource, Value(patch.slot)},
|
||||
patch.inst->Flags<uint64_t>()));
|
||||
const auto uses = patch.inst->Uses();
|
||||
for (const auto& use: uses) {
|
||||
use.user->SetArg(use.operand, flat);
|
||||
|
||||
Reference in New Issue
Block a user