mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-10-04 11:24:58 -04:00
Feature/runtime ecosystem refactor (#107)
* feat: remove memory and pad from stub section * feat: add support for resume entry targets in CodeGenerator (this allow jumps in address outside function) feat: refactor entry point discovery one more try to reduce big generated file * feat: optmizations for release build * feat: remove unused file * feat: added some test cases for code gen * feat: added log macro and remove win specific code * feat: refactor runtime folder structure feat: added reset sound driver RPC state and compatibility layout feat: rename and added new test feat: update RPC calls to use defined constants feat: added more PSMC(16, 32) feat: change cd read to try find the asset ignoring case sensitive fix: fix some render problems feat: add logs on pad feat: added more RPC handles * feat: added game override for code veronica * feat: apply vita patch * feat: flags to disable build * feat: fix merges feat: break a lot of tests * feat: better throw error on empty cd path feat: remove recompiler unusde function feat: apply missing patch * feat: gamedp is now part of lib feat: missing file * feat: small cleanup * feat: missing vita changes * feat: fix more merge * feat: fix tests * feat: last missing feature * feat: added missing import * feat: rename test local functions * feat: init syscall on ps2 list * feat: added DMA helpers * feat: faster builds feat: more implement for darkcloud * feat: back missing file * feat: added missing includes * feat: remove test * feat: missing include * feat: read register funtion * feat: build fix * feat: force exit on detach thread
This commit is contained in:
@@ -153,6 +153,17 @@ namespace ps2recomp
|
||||
}
|
||||
}
|
||||
|
||||
void CodeGenerator::setResumeEntryTargets(const std::unordered_map<uint32_t, std::vector<uint32_t>> &resumeTargetsByOwner)
|
||||
{
|
||||
m_resumeEntryTargetsByOwner = resumeTargetsByOwner;
|
||||
for (auto &[owner, targets] : m_resumeEntryTargetsByOwner)
|
||||
{
|
||||
(void)owner;
|
||||
std::sort(targets.begin(), targets.end());
|
||||
targets.erase(std::unique(targets.begin(), targets.end()), targets.end());
|
||||
}
|
||||
}
|
||||
|
||||
std::string CodeGenerator::getFunctionName(uint32_t address) const
|
||||
{
|
||||
auto it = m_renamedFunctions.find(address);
|
||||
@@ -230,9 +241,14 @@ namespace ps2recomp
|
||||
auto emitInternalTarget = [&](uint32_t target, uint32_t sourcePc, std::string_view indent)
|
||||
{
|
||||
ss << fmt::format("{}ctx->pc = 0x{:X}u;\n", indent, target);
|
||||
if (target <= sourcePc)
|
||||
const bool isCallLikeEdge =
|
||||
(branchInst.opcode == OPCODE_JAL) ||
|
||||
(branchInst.opcode == OPCODE_SPECIAL && branchInst.function == SPECIAL_JALR);
|
||||
if (target <= sourcePc && !isCallLikeEdge)
|
||||
{
|
||||
ss << fmt::format("{}runtime->cooperativeGuestYield();\n", indent);
|
||||
ss << fmt::format("{}if (runtime->shouldPreemptGuestExecution()) {{\n", indent);
|
||||
ss << fmt::format("{} return;\n", indent);
|
||||
ss << fmt::format("{}}}\n", indent);
|
||||
ss << fmt::format("{}goto label_{:x};\n", indent, target);
|
||||
}
|
||||
else
|
||||
@@ -652,7 +668,7 @@ namespace ps2recomp
|
||||
CodeGenerator::~CodeGenerator() = default;
|
||||
|
||||
CodeGenerator::AnalysisResult CodeGenerator::collectInternalBranchTargets(
|
||||
const Function &function, const std::vector<Instruction> &instructions)
|
||||
const Function &function, const std::vector<Instruction> &instructions, const std::vector<Function> *allFunctions)
|
||||
{
|
||||
AnalysisResult result;
|
||||
std::unordered_set<uint32_t> instructionAddresses;
|
||||
@@ -660,6 +676,97 @@ namespace ps2recomp
|
||||
bool hasIndirectRegisterJump = false;
|
||||
std::vector<const Instruction*> indirectJumps;
|
||||
|
||||
auto isExecutableAddress = [&](uint32_t address) -> bool
|
||||
{
|
||||
for (const auto §ion : m_sections)
|
||||
{
|
||||
if (!section.isCode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (address >= section.address && address < (section.address + section.size))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
auto findContainingExternalFunction = [&](uint32_t address) -> const Function *
|
||||
{
|
||||
if (!allFunctions || !isExecutableAddress(address))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const Function *best = nullptr;
|
||||
for (const auto &candidateFn : *allFunctions)
|
||||
{
|
||||
if (!candidateFn.isRecompiled || candidateFn.isStub || candidateFn.isSkipped)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (candidateFn.name.rfind("entry_", 0) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address < candidateFn.start || address >= candidateFn.end)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!best || candidateFn.start > best->start)
|
||||
{
|
||||
best = &candidateFn;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
};
|
||||
|
||||
auto queueExternalEntryTarget = [&](uint32_t target)
|
||||
{
|
||||
const Function *containingFn = findContainingExternalFunction(target);
|
||||
if (!containingFn)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (containingFn->start == function.start)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (target == containingFn->start)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
result.externalEntryPoints.insert(target);
|
||||
};
|
||||
|
||||
auto queueResumeEntryTarget = [&](uint32_t resumeAddr)
|
||||
{
|
||||
if (resumeAddr >= function.start && resumeAddr < function.end &&
|
||||
instructionAddresses.contains(resumeAddr))
|
||||
{
|
||||
result.entryPoints.insert(resumeAddr);
|
||||
result.resumeEntryPoints.insert(resumeAddr);
|
||||
}
|
||||
};
|
||||
|
||||
auto queueLoopResumeEntryTarget = [&](uint32_t target, uint32_t sourcePc)
|
||||
{
|
||||
if (target > sourcePc || target == function.start)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
queueResumeEntryTarget(target);
|
||||
};
|
||||
|
||||
for (const auto &inst : instructions)
|
||||
{
|
||||
instructionAddresses.insert(inst.address);
|
||||
@@ -685,6 +792,11 @@ namespace ps2recomp
|
||||
instructionAddresses.contains(target))
|
||||
{
|
||||
result.entryPoints.insert(target);
|
||||
queueLoopResumeEntryTarget(target, inst.address);
|
||||
}
|
||||
else
|
||||
{
|
||||
queueExternalEntryTarget(target);
|
||||
}
|
||||
}
|
||||
else if (isStaticJump)
|
||||
@@ -694,15 +806,20 @@ namespace ps2recomp
|
||||
instructionAddresses.contains(target))
|
||||
{
|
||||
result.entryPoints.insert(target);
|
||||
queueLoopResumeEntryTarget(target, inst.address);
|
||||
|
||||
if (inst.opcode == OPCODE_JAL)
|
||||
{
|
||||
uint32_t returnAddr = inst.address + 8;
|
||||
if (returnAddr >= function.start && returnAddr < function.end &&
|
||||
instructionAddresses.contains(returnAddr))
|
||||
{
|
||||
result.entryPoints.insert(returnAddr);
|
||||
}
|
||||
queueResumeEntryTarget(inst.address + 8u);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
queueExternalEntryTarget(target);
|
||||
|
||||
if (inst.opcode == OPCODE_JAL)
|
||||
{
|
||||
queueResumeEntryTarget(inst.address + 8u);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -712,6 +829,11 @@ namespace ps2recomp
|
||||
{
|
||||
bool needsJrFallback = false;
|
||||
for (const Instruction* jrInst : indirectJumps) {
|
||||
if (jrInst->function == SPECIAL_JALR)
|
||||
{
|
||||
queueResumeEntryTarget(jrInst->address + 8u);
|
||||
}
|
||||
|
||||
bool foundTable = false;
|
||||
|
||||
uint32_t jrReg = jrInst->rs;
|
||||
@@ -790,6 +912,10 @@ namespace ps2recomp
|
||||
{
|
||||
jrTargets.push_back(target);
|
||||
}
|
||||
else
|
||||
{
|
||||
queueExternalEntryTarget(target);
|
||||
}
|
||||
}
|
||||
|
||||
if (!jrTargets.empty())
|
||||
@@ -849,6 +975,10 @@ namespace ps2recomp
|
||||
uniqueTargets.insert(target);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
queueExternalEntryTarget(target);
|
||||
}
|
||||
} else {
|
||||
validJumpTable = false;
|
||||
break;
|
||||
@@ -909,6 +1039,20 @@ namespace ps2recomp
|
||||
}
|
||||
|
||||
AnalysisResult analysisResult = collectInternalBranchTargets(function, instructions);
|
||||
std::vector<uint32_t> resumeTargets(analysisResult.resumeEntryPoints.begin(),
|
||||
analysisResult.resumeEntryPoints.end());
|
||||
auto resumeIt = m_resumeEntryTargetsByOwner.find(function.start);
|
||||
if (resumeIt != m_resumeEntryTargetsByOwner.end())
|
||||
{
|
||||
resumeTargets.insert(resumeTargets.end(), resumeIt->second.begin(), resumeIt->second.end());
|
||||
}
|
||||
std::sort(resumeTargets.begin(), resumeTargets.end());
|
||||
resumeTargets.erase(std::unique(resumeTargets.begin(), resumeTargets.end()), resumeTargets.end());
|
||||
for (uint32_t target : resumeTargets)
|
||||
{
|
||||
analysisResult.entryPoints.insert(target);
|
||||
}
|
||||
|
||||
const std::unordered_set<uint32_t>& internalTargets = analysisResult.entryPoints;
|
||||
ss << "// Function: " << function.name << "\n";
|
||||
ss << "// Address: 0x" << std::hex << function.start << " - 0x" << function.end << std::dec << "\n";
|
||||
@@ -926,6 +1070,16 @@ namespace ps2recomp
|
||||
ss << " PS_LOG_ENTRY(\"" << sanitizedName << "\");\n";
|
||||
ss << "#endif\n";
|
||||
ss << "\n";
|
||||
if (!resumeTargets.empty())
|
||||
{
|
||||
ss << " switch (ctx->pc) {\n";
|
||||
for (uint32_t target : resumeTargets)
|
||||
{
|
||||
ss << " case 0x" << std::hex << target << "u: goto label_" << target << ";\n" << std::dec;
|
||||
}
|
||||
ss << " default: break;\n";
|
||||
ss << " }\n\n";
|
||||
}
|
||||
ss << " ctx->pc = 0x" << std::hex << function.start << "u;\n"
|
||||
<< std::dec;
|
||||
ss << "\n";
|
||||
@@ -3782,6 +3936,21 @@ namespace ps2recomp
|
||||
emitRegistration(first, second);
|
||||
}
|
||||
|
||||
ss << "\n // Register resumable entry points\n";
|
||||
for (const auto &[ownerStart, targets] : m_resumeEntryTargetsByOwner)
|
||||
{
|
||||
const std::string ownerName = getFunctionName(ownerStart);
|
||||
if (ownerName.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (uint32_t target : targets)
|
||||
{
|
||||
emitRegistration(target, ownerName);
|
||||
}
|
||||
}
|
||||
|
||||
ss << "\n // Register stub functions\n";
|
||||
for (const auto &[first, second] : stubFunctions)
|
||||
{
|
||||
|
||||
@@ -241,16 +241,16 @@ namespace ps2recomp
|
||||
size_t passCount = 0;
|
||||
};
|
||||
|
||||
struct StaticEntryTarget
|
||||
bool isEntryFunctionName(const std::string &name)
|
||||
{
|
||||
uint32_t target = 0u;
|
||||
bool isCall = false;
|
||||
};
|
||||
return name.rfind("entry_", 0) == 0;
|
||||
}
|
||||
|
||||
EntryDiscoveryStats discoverAdditionalEntryPointsImpl(
|
||||
std::vector<Function> &functions,
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||
const std::vector<Section> §ions,
|
||||
CodeGenerator *codeGenerator,
|
||||
const std::function<bool(Function &)> &decodeExternalFunction)
|
||||
{
|
||||
std::unordered_set<uint32_t> existingStarts;
|
||||
@@ -275,25 +275,6 @@ namespace ps2recomp
|
||||
return false;
|
||||
};
|
||||
|
||||
auto getStaticEntryTarget = [](const Instruction &inst) -> std::optional<StaticEntryTarget>
|
||||
{
|
||||
if (inst.opcode == OPCODE_J || inst.opcode == OPCODE_JAL)
|
||||
{
|
||||
StaticEntryTarget target{};
|
||||
target.target = decodeAbsoluteJumpTarget(inst.address, inst.target);
|
||||
target.isCall = (inst.opcode == OPCODE_JAL);
|
||||
return target;
|
||||
}
|
||||
|
||||
if (inst.opcode == OPCODE_SPECIAL &&
|
||||
(inst.function == SPECIAL_JR || inst.function == SPECIAL_JALR))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
auto isSimpleReturnThunkStart = [](const Instruction &inst) -> bool
|
||||
{
|
||||
return inst.opcode == OPCODE_SPECIAL &&
|
||||
@@ -355,6 +336,30 @@ namespace ps2recomp
|
||||
std::vector<Function> newEntries;
|
||||
std::unordered_set<uint32_t> pendingStarts;
|
||||
|
||||
auto queuePendingEntry = [&](uint32_t target)
|
||||
{
|
||||
if (!isExecutableAddress(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingStarts.contains(target) || pendingStarts.contains(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PendingEntry pending{};
|
||||
pending.target = target;
|
||||
if (const Function *containingFunction = findContainingFunction(target))
|
||||
{
|
||||
pending.containingStart = containingFunction->start;
|
||||
pending.containingEnd = containingFunction->end;
|
||||
}
|
||||
|
||||
pendingEntries.push_back(pending);
|
||||
pendingStarts.insert(target);
|
||||
};
|
||||
|
||||
for (const auto &function : functions)
|
||||
{
|
||||
if (!function.isRecompiled || function.isStub || function.isSkipped)
|
||||
@@ -362,6 +367,11 @@ namespace ps2recomp
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isEntryFunctionName(function.name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto decodedIt = decodedFunctions.find(function.start);
|
||||
if (decodedIt == decodedFunctions.end())
|
||||
{
|
||||
@@ -369,60 +379,26 @@ namespace ps2recomp
|
||||
}
|
||||
|
||||
const auto &instructions = decodedIt->second;
|
||||
|
||||
for (const auto &inst : instructions)
|
||||
CodeGenerator::AnalysisResult analysisResult{};
|
||||
if (codeGenerator)
|
||||
{
|
||||
auto targetOpt = getStaticEntryTarget(inst);
|
||||
if (!targetOpt.has_value())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
analysisResult = codeGenerator->collectInternalBranchTargets(
|
||||
function, instructions, &functions);
|
||||
}
|
||||
else
|
||||
{
|
||||
analysisResult.resumeEntryPoints.clear();
|
||||
analysisResult.externalEntryPoints.clear();
|
||||
}
|
||||
|
||||
const StaticEntryTarget staticTarget = targetOpt.value();
|
||||
const uint32_t target = staticTarget.target;
|
||||
for (uint32_t target : analysisResult.externalEntryPoints)
|
||||
{
|
||||
queuePendingEntry(target);
|
||||
}
|
||||
|
||||
if ((target & 0x3) != 0 || !isExecutableAddress(target))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existingStarts.contains(target) || pendingStarts.contains(target))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool targetInCurrentFunction = std::any_of(
|
||||
instructions.begin(), instructions.end(),
|
||||
[&](const Instruction &candidate)
|
||||
{ return candidate.address == target; });
|
||||
if (targetInCurrentFunction && !staticTarget.isCall)
|
||||
{
|
||||
// jumps with the current decoded function remain labels/gotos.
|
||||
continue;
|
||||
}
|
||||
|
||||
const Function *containingFunction = findContainingFunction(target);
|
||||
if (targetInCurrentFunction)
|
||||
{
|
||||
PendingEntry pending{};
|
||||
pending.target = target;
|
||||
pending.containingStart = function.start;
|
||||
pending.containingEnd = function.end;
|
||||
pendingEntries.push_back(pending);
|
||||
pendingStarts.insert(target);
|
||||
continue;
|
||||
}
|
||||
|
||||
PendingEntry pending{};
|
||||
pending.target = target;
|
||||
if (containingFunction)
|
||||
{
|
||||
pending.containingStart = containingFunction->start;
|
||||
pending.containingEnd = containingFunction->end;
|
||||
}
|
||||
|
||||
pendingEntries.push_back(pending);
|
||||
pendingStarts.insert(target);
|
||||
for (uint32_t target : analysisResult.resumeEntryPoints)
|
||||
{
|
||||
queuePendingEntry(target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,12 +529,6 @@ namespace ps2recomp
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
bool isEntryFunctionName(const std::string &name)
|
||||
{
|
||||
return name.rfind("entry_", 0) == 0;
|
||||
}
|
||||
|
||||
size_t resliceEntryFunctionsImpl(
|
||||
std::vector<Function> &functions,
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions)
|
||||
@@ -1293,25 +1263,124 @@ namespace ps2recomp
|
||||
|
||||
void PS2Recompiler::discoverAdditionalEntryPoints()
|
||||
{
|
||||
const EntryDiscoveryStats stats = discoverAdditionalEntryPointsImpl(
|
||||
m_functions,
|
||||
m_decodedFunctions,
|
||||
m_sections,
|
||||
[&](Function &entryFunction)
|
||||
{ return decodeFunction(entryFunction); });
|
||||
|
||||
if (stats.discoveredCount > 0)
|
||||
m_resumeEntryTargetsByOwner.clear();
|
||||
if (!m_codeGenerator)
|
||||
{
|
||||
std::cout << "Discovered " << stats.discoveredCount
|
||||
<< " additional entry point(s) inside existing functions across "
|
||||
<< stats.passCount << " pass(es)." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t reslicedCount = resliceEntryFunctionsImpl(m_functions, m_decodedFunctions);
|
||||
if (reslicedCount > 0)
|
||||
auto findContainingFunction = [&](uint32_t address) -> const Function *
|
||||
{
|
||||
std::cout << "Resliced " << reslicedCount
|
||||
<< " entry function(s) after discovery." << std::endl;
|
||||
const Function *best = nullptr;
|
||||
for (const auto &function : m_functions)
|
||||
{
|
||||
if (!function.isRecompiled || function.isStub || function.isSkipped)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isEntryFunctionName(function.name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address < function.start || address >= function.end)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto decodedIt = m_decodedFunctions.find(function.start);
|
||||
if (decodedIt == m_decodedFunctions.end())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto &decoded = decodedIt->second;
|
||||
const bool hasAddress = std::any_of(decoded.begin(), decoded.end(),
|
||||
[&](const Instruction &candidate)
|
||||
{ return candidate.address == address; });
|
||||
if (!hasAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!best || function.start > best->start)
|
||||
{
|
||||
best = &function;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
};
|
||||
|
||||
for (const auto &function : m_functions)
|
||||
{
|
||||
if (!function.isRecompiled || function.isStub || function.isSkipped)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isEntryFunctionName(function.name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto decodedIt = m_decodedFunctions.find(function.start);
|
||||
if (decodedIt == m_decodedFunctions.end())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto &instructions = decodedIt->second;
|
||||
CodeGenerator::AnalysisResult analysisResult =
|
||||
m_codeGenerator->collectInternalBranchTargets(function, instructions, &m_functions);
|
||||
|
||||
auto &ownerTargets = m_resumeEntryTargetsByOwner[function.start];
|
||||
ownerTargets.insert(ownerTargets.end(),
|
||||
analysisResult.resumeEntryPoints.begin(),
|
||||
analysisResult.resumeEntryPoints.end());
|
||||
|
||||
for (uint32_t target : analysisResult.externalEntryPoints)
|
||||
{
|
||||
const Function *owner = findContainingFunction(target);
|
||||
if (!owner)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (owner->start == target)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto &targets = m_resumeEntryTargetsByOwner[owner->start];
|
||||
targets.push_back(target);
|
||||
}
|
||||
}
|
||||
|
||||
size_t totalTargets = 0u;
|
||||
for (auto it = m_resumeEntryTargetsByOwner.begin(); it != m_resumeEntryTargetsByOwner.end();)
|
||||
{
|
||||
auto &targets = it->second;
|
||||
std::sort(targets.begin(), targets.end());
|
||||
targets.erase(std::unique(targets.begin(), targets.end()), targets.end());
|
||||
if (targets.empty())
|
||||
{
|
||||
it = m_resumeEntryTargetsByOwner.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
totalTargets += targets.size();
|
||||
++it;
|
||||
}
|
||||
|
||||
m_codeGenerator->setResumeEntryTargets(m_resumeEntryTargetsByOwner);
|
||||
|
||||
if (totalTargets > 0u)
|
||||
{
|
||||
std::cout << "Collected " << totalTargets
|
||||
<< " resumable entry point(s) across "
|
||||
<< m_resumeEntryTargetsByOwner.size()
|
||||
<< " owner function(s)." << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1542,10 +1611,12 @@ namespace ps2recomp
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||
const std::vector<Section> §ions)
|
||||
{
|
||||
CodeGenerator codeGenerator({}, sections);
|
||||
const EntryDiscoveryStats stats = discoverAdditionalEntryPointsImpl(
|
||||
functions,
|
||||
decodedFunctions,
|
||||
sections,
|
||||
&codeGenerator,
|
||||
[](Function &)
|
||||
{ return false; });
|
||||
return stats.discoveredCount;
|
||||
|
||||
Reference in New Issue
Block a user