Skip to content
Open
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ Main fields in `config.toml`:
* `general.patch_cache`: apply configured patches to CACHE instructions.
* `general.stubs`: names to force as stubs. Also accepts `handler@0xADDRESS` to bind a stripped function address directly to a runtime syscall/stub handler. Includes generic handlers `ret0`, `ret1`, `reta0`.
* `general.skip`: names to force as skipped wrappers.
* `general.external_call_target_manifests`: optional array of `external_call_targets.txt` paths emitted by other recompile invocations (e.g. a separately recompiled overlay); their call targets that land inside this invocation's functions are registered as additional entry points. Each invocation writes its own `external_call_targets.txt` into `general.output`, and the emitted candidates include targets outside the emitting unit's own sections entirely (cross-ELF/overlay calls) - the emitting unit cannot know the callee unit's layout, so the ingesting unit is authoritative about which candidates actually resolve.

For a multi-unit build (each unit listing one or more of the others in `external_call_target_manifests`), run every unit twice: first with `ps2recomp <config.toml> --emit-manifest-only` for every unit (any order - manifest emission depends only on that unit's own decoded functions, not on any sibling), then run every unit normally (`ps2recomp <config.toml>`) so every configured sibling manifest already exists. A configured manifest that is still missing at generate time is a hard error.
* `patches.instructions`: raw instruction replacements by address.

Address binding for stripped ELFs:
Expand Down
1 change: 1 addition & 0 deletions ps2xRecomp/include/ps2recomp/elf_parser.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef PS2RECOMP_ELF_PARSER_H
#define PS2RECOMP_ELF_PARSER_H

#include <cstdint>
#include <elfio/elfio.hpp>
#include <string>
#include <vector>
Expand Down
41 changes: 40 additions & 1 deletion ps2xRecomp/include/ps2recomp/ps2_recompiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include <filesystem>
#include <memory>
#include <map>
#include <istream>
#include <functional>

namespace ps2recomp
{
Expand All @@ -31,7 +33,13 @@ namespace ps2recomp
~PS2Recompiler();

bool initialize();
bool recompile();
// emitManifestOnly: run only the input-independent analysis phase (decode +
// emit this unit's external_call_targets.txt) and return before ingesting any
// sibling manifest or discovering entry points. Used to drive a two-phase
// multi-unit build: run every unit with emitManifestOnly=true first (any
// order), then run every unit normally so every configured sibling manifest
// is guaranteed to already exist.
bool recompile(bool emitManifestOnly = false);
void generateOutput();
void printReport() const;

Expand All @@ -46,6 +54,31 @@ namespace ps2recomp

static std::string ClampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength);

// Parses a call-target manifest (one "0x%08x" address per line, blank lines and
// '#' comments ignored) into a sorted, de-duplicated list of addresses. Shared by
// the production manifest loader and unit tests.
static std::vector<uint32_t> ParseCallTargetManifest(std::istream &input);

// Discovers thread entry function pointers embedded in static ThreadParam
// structs passed to the CreateThread syscall (0x20). Returns the entry-pointer
// values read from data memory, sorted and de-duplicated.
static std::vector<uint32_t> DiscoverDataEmbeddedThreadEntries(
const std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
const std::function<bool(uint32_t)> &isValidAddress,
const std::function<uint32_t(uint32_t)> &readWord);

// Collects jal/j targets that fall outside every recompiled local function
// range and outside this unit's own data/bss sections - candidate cross-unit
// call targets (including cross-ELF/overlay targets outside every one of this
// unit's sections) to publish in the external call-target manifest. The
// caller cannot know a callee unit's section layout, so this is a permissive
// collector: the ingesting unit's findContainingFunction is the authoritative
// filter. Sorted and de-duplicated.
static std::vector<uint32_t> CollectExternalCallTargets(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method currently requires the target to be inside an executable section of the caller's ELF. This appears to exclude the main use case described by the PR: a direct jal from a main ELF to a separately compiled overlay whose target range is not represented by any executable section in the main ELF.

The current unit test explicitly expects targets outside the current ELF's executable sections to be discarded, while the end-to-end test starts from a manually pre-created manifest and therefore never exercises cross-ELF manifest emission.

Could we add a real two-ELF regression test where ELF A contains a jal to code that exists only in ELF B, then verify that A emits the target and B ingests it? Unless both ELFs happen to have overlapping executable section ranges, the current filter seems to drop exactly the cross-unit target this feature is intended to preserve.

const std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
const std::vector<Function> &functions,
const std::vector<Section> &sections);

private:
ConfigManager m_configManager;
std::unique_ptr<ElfParser> m_elfParser;
Expand All @@ -68,10 +101,16 @@ namespace ps2recomp
std::map<uint32_t, std::string> m_generatedStubs;
std::unordered_map<uint32_t, std::string> m_functionRenames;
std::unordered_map<uint32_t, std::vector<uint32_t>> m_resumeEntryTargetsByOwner;
std::vector<uint32_t> m_ingestedExternalCallTargets;
CodeGenerator::BootstrapInfo m_bootstrapInfo;

bool decodeFunction(Function &function);
void discoverAdditionalEntryPoints();
// Returns false (after reporting an error) when a configured manifest path
// cannot be opened - there is no legitimate reason for a configured manifest
// to be missing once every unit's analysis phase has run.
bool loadExternalCallTargetManifests();
void emitExternalCallTargetManifest();
bool shouldSkipFunction(const Function &function) const;
bool isStubFunction(const Function &function) const;
bool generateFunctionHeader();
Expand Down
1 change: 1 addition & 0 deletions ps2xRecomp/include/ps2recomp/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ namespace ps2recomp
std::vector<std::string> stubImplementations;
std::unordered_map<uint32_t, uint32_t> mmioByInstructionAddress;
std::vector<JumpTable> jumpTables;
std::vector<std::string> externalCallTargetManifests;
};

} // namespace ps2recomp
Expand Down
15 changes: 15 additions & 0 deletions ps2xRecomp/src/lib/config_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ namespace ps2recomp
config.skipFunctions = toml::find<std::vector<std::string>>(data, "skip");
}

if (general.contains("external_call_target_manifests") && general.at("external_call_target_manifests").is_array())
{
config.externalCallTargetManifests =
toml::find<std::vector<std::string>>(general, "external_call_target_manifests");
}
else if (data.contains("external_call_target_manifests") && data.at("external_call_target_manifests").is_array())
{
config.externalCallTargetManifests =
toml::find<std::vector<std::string>>(data, "external_call_target_manifests");
}

if (data.contains("patches") && data.at("patches").is_table())
{
const auto &patches = toml::find(data, "patches");
Expand Down Expand Up @@ -276,6 +287,10 @@ namespace ps2recomp
general["patch_cache"] = config.patchCache;
general["skip"] = config.skipFunctions;
general["stubs"] = config.stubImplementations;
if (!config.externalCallTargetManifests.empty())
{
general["external_call_target_manifests"] = config.externalCallTargetManifests;
}
data["general"] = general;

if (!config.mmioByInstructionAddress.empty())
Expand Down
Loading