Implement nrm filename toml input, renaming list, trace mode, and context dumping flag (#111)

* implement nrm filename toml input

* change name of mod toml setting to 'mod_filename'

* add renaming and re mode

* fix --dump-context arg, fix entrypoint detection

* refactor re_mode to function_trace_mode

* adjust trace mode to use a general TRACE_ENTRY() macro

* fix some renaming and trace mode comments, revert no toml entrypoint code, add TODO to broken block

* fix arg2 check and usage string
This commit is contained in:
LittleCube 2024-12-24 02:10:26 -05:00 committed by GitHub
parent d33d381617
commit 17438755a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 95 additions and 12 deletions

View file

@ -29,7 +29,7 @@ For relocatable overlays, the tool will modify supported instructions possessing
Support for relocations for TLB mapping is coming in the future, which will add the ability to provide a list of MIPS32 relocations so that the runtime can relocate them on load. Combining this with the functionality used for relocatable overlays should allow running most TLB mapped code without incurring a performance penalty on every RAM access.
## How to Use
The recompiler is configured by providing a toml file in order to configure the recompiler behavior, which is the only argument provided to the recompiler. The toml is where you specify input and output file paths, as well as optionally stub out specific functions, skip recompilation of specific functions, and patch single instructions in the target binary. There is also planned functionality to be able to emit hooks in the recompiler output by adding them to the toml (the `[[patches.func]]` and `[[patches.hook]]` sections of the linked toml below), but this is currently unimplemented. Documentation on every option that the recompiler provides is not currently available, but an example toml can be found in the Zelda 64: Recompiled project [here](https://github.com/Mr-Wiseguy/Zelda64Recomp/blob/dev/us.rev1.toml).
The recompiler is configured by providing a toml file in order to configure the recompiler behavior, which is the first argument provided to the recompiler. The toml is where you specify input and output file paths, as well as optionally stub out specific functions, skip recompilation of specific functions, and patch single instructions in the target binary. There is also planned functionality to be able to emit hooks in the recompiler output by adding them to the toml (the `[[patches.func]]` and `[[patches.hook]]` sections of the linked toml below), but this is currently unimplemented. Documentation on every option that the recompiler provides is not currently available, but an example toml can be found in the Zelda 64: Recompiled project [here](https://github.com/Mr-Wiseguy/Zelda64Recomp/blob/dev/us.rev1.toml).
Currently, the only way to provide the required metadata is by passing an elf file to this tool. The easiest way to get such an elf is to set up a disassembly or decompilation of the target binary, but there will be support for providing the metadata via a custom format to bypass the need to do so in the future.

View file

@ -35,6 +35,7 @@ struct ModManifest {
struct ModInputs {
std::filesystem::path elf_path;
std::string mod_filename;
std::filesystem::path func_reference_syms_file_path;
std::vector<std::filesystem::path> data_reference_syms_file_paths;
std::vector<std::filesystem::path> additional_files;
@ -316,6 +317,15 @@ ModInputs parse_mod_config_inputs(const std::filesystem::path& basedir, const to
throw toml::parse_error("Mod toml input section is missing elf file", inputs_table.source());
}
// Output NRM file
std::optional<std::string> mod_filename_opt = inputs_table["mod_filename"].value<std::string>();
if (mod_filename_opt.has_value()) {
ret.mod_filename = std::move(mod_filename_opt.value());
}
else {
throw toml::parse_error("Mod toml input section is missing the output mod filename", inputs_table.source());
}
// Function reference symbols file
std::optional<std::string> func_reference_syms_file_opt = inputs_table["func_reference_syms_file"].value<std::string>();
if (func_reference_syms_file_opt.has_value()) {
@ -879,7 +889,7 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo
}
bool create_mod_zip(const std::filesystem::path& output_dir, const ModConfig& config) {
std::filesystem::path output_path = output_dir / (config.manifest.mod_id + "-" + config.manifest.version_string + ".nrm");
std::filesystem::path output_path = output_dir / (config.inputs.mod_filename + ".nrm");
#ifdef _WIN32
std::filesystem::path temp_zip_path = output_path;

View file

@ -216,6 +216,9 @@ namespace N64Recomp {
// List of symbols from events, which contains the names of events that this context provides.
std::vector<EventSymbol> event_symbols;
// Causes functions to print their name to the console the first time they're called.
bool trace_mode;
// Imports sections and function symbols from a provided context into this context's reference sections and reference functions.
bool import_reference_context(const Context& reference_context);
// Reads a data symbol file and adds its contents into this context's reference data symbols.

View file

@ -93,7 +93,7 @@ std::vector<std::string> get_ignored_funcs(const toml::table* patches_data) {
// Make room for all the ignored funcs in the array.
ignored_funcs.reserve(ignored_funcs_array->size());
// Gather the stubs and place them into the array.
// Gather the ignored and place them into the array.
ignored_funcs_array->for_each([&ignored_funcs](auto&& el) {
if constexpr (toml::is_string<decltype(el)>) {
ignored_funcs.push_back(*el);
@ -104,6 +104,29 @@ std::vector<std::string> get_ignored_funcs(const toml::table* patches_data) {
return ignored_funcs;
}
std::vector<std::string> get_renamed_funcs(const toml::table* patches_data) {
std::vector<std::string> renamed_funcs{};
// Check if the renamed funcs array exists.
const toml::node_view renamed_funcs_data = (*patches_data)["renamed"];
if (renamed_funcs_data.is_array()) {
const toml::array* renamed_funcs_array = renamed_funcs_data.as_array();
// Make room for all the renamed funcs in the array.
renamed_funcs.reserve(renamed_funcs_array->size());
// Gather the renamed and place them into the array.
renamed_funcs_array->for_each([&renamed_funcs](auto&& el) {
if constexpr (toml::is_string<decltype(el)>) {
renamed_funcs.push_back(*el);
}
});
}
return renamed_funcs;
}
std::vector<N64Recomp::FunctionSize> get_func_sizes(const toml::table* patches_data) {
std::vector<N64Recomp::FunctionSize> func_sizes{};
@ -377,6 +400,9 @@ N64Recomp::Config::Config(const char* path) {
// Ignored funcs array (optional)
ignored_funcs = get_ignored_funcs(table);
// Renamed funcs array (optional)
renamed_funcs = get_renamed_funcs(table);
// Single-instruction patches (optional)
instruction_patches = get_instruction_patches(table);
@ -387,6 +413,18 @@ N64Recomp::Config::Config(const char* path) {
function_hooks = get_function_hooks(table);
}
// Use trace mode if enabled (optional)
std::optional<bool> trace_mode_opt = input_data["trace_mode"].value<bool>();
if (trace_mode_opt.has_value()) {
trace_mode = trace_mode_opt.value();
if (trace_mode) {
recomp_include += "\n#include \"trace.h\"";
}
}
else {
trace_mode = false;
}
// Function reference symbols file (optional)
std::optional<std::string> func_reference_syms_file_opt = input_data["func_reference_syms_file"].value<std::string>();
if (func_reference_syms_file_opt.has_value()) {

View file

@ -42,6 +42,7 @@ namespace N64Recomp {
bool single_file_output;
bool use_absolute_symbols;
bool unpaired_lo16_warnings;
bool trace_mode;
bool allow_exports;
bool strict_patch_mode;
std::filesystem::path elf_path;
@ -54,6 +55,7 @@ namespace N64Recomp {
std::filesystem::path output_binary_path;
std::vector<std::string> stubbed_funcs;
std::vector<std::string> ignored_funcs;
std::vector<std::string> renamed_funcs;
std::vector<InstructionPatch> instruction_patches;
std::vector<FunctionHook> function_hooks;
std::vector<FunctionSize> manual_func_sizes;

View file

@ -60,6 +60,7 @@ bool read_symbols(N64Recomp::Context& context, const ELFIO::elfio& elf_file, ELF
if (section_index < context.sections.size()) {
// Check if this symbol is the entrypoint
// TODO this never fires, the check is broken due to signedness
if (elf_config.has_entrypoint && value == elf_config.entrypoint_address && type == ELFIO::STT_FUNC) {
if (found_entrypoint_func) {
fmt::print(stderr, "Ambiguous entrypoint: {}\n", name);

View file

@ -272,12 +272,18 @@ int main(int argc, char** argv) {
std::exit(EXIT_FAILURE);
};
// TODO expose a way to dump the context from the command line.
bool dumping_context = false;
bool dumping_context;
if (argc != 2) {
fmt::print("Usage: {} [config file]\n", argv[0]);
std::exit(EXIT_SUCCESS);
if (argc >= 3) {
std::string arg2 = argv[2];
if (arg2 == "--dump-context") {
dumping_context = true;
} else {
fmt::print("Usage: {} <config file> [--dump-context]\n", argv[0]);
std::exit(EXIT_SUCCESS);
}
} else {
dumping_context = false;
}
const char* config_path = argv[1];
@ -485,10 +491,27 @@ int main(int argc, char** argv) {
// This helps prevent typos in the config file or functions renamed between versions from causing issues.
exit_failure(fmt::format("Function {} is set as ignored in the config file but does not exist!", ignored_func));
}
// Mark the function as .
// Mark the function as ignored.
context.functions[func_find->second].ignored = true;
}
// Rename any functions specified in the config file.
for (const std::string& renamed_func : config.renamed_funcs) {
// Check if the specified function exists.
auto func_find = context.functions_by_name.find(renamed_func);
if (func_find == context.functions_by_name.end()) {
// Function doesn't exist, present an error to the user instead of silently failing to rename it.
// This helps prevent typos in the config file or functions renamed between versions from causing issues.
exit_failure(fmt::format("Function {} is set as renamed in the config file but does not exist!", renamed_func));
}
// Rename the function.
N64Recomp::Function* func = &context.functions[func_find->second];
func->name = func->name + "_recomp";
}
// Propogate the trace mode parameter.
context.trace_mode = config.trace_mode;
// Apply any single-instruction patches.
for (const N64Recomp::InstructionPatch& patch : config.instruction_patches) {
// Check if the specified function exists.

View file

@ -745,6 +745,12 @@ bool N64Recomp::recompile_function(const N64Recomp::Context& context, const N64R
" int c1cs = 0;\n", // cop1 conditional signal
func.name);
if (context.trace_mode) {
fmt::print(output_file,
" TRACE_ENTRY();",
func.name);
}
// Skip analysis and recompilation of this function is stubbed.
if (!func.stubbed) {
// Use a set to sort and deduplicate labels

View file

@ -1,6 +1,6 @@
#include "n64recomp.h"
const std::unordered_set<std::string> N64Recomp::reimplemented_funcs{
const std::unordered_set<std::string> N64Recomp::reimplemented_funcs {
// OS initialize functions
"__osInitialize_common",
"osInitialize",
@ -557,7 +557,7 @@ const std::unordered_set<std::string> N64Recomp::ignored_funcs {
"kdebugserver",
};
const std::unordered_set<std::string> N64Recomp::renamed_funcs{
const std::unordered_set<std::string> N64Recomp::renamed_funcs {
// Math
"sincosf",
"sinf",