Implement live recompiler (#114)

This commit implements the "live recompiler", which is another backend for the recompiler that generates platform-specific assembly at runtime. This is still static recompilation as opposed to dynamic recompilation, as it still requires information about the binary to recompile and leverages the same static analysis that the C recompiler uses. However, similarly to dynamic recompilation it's aimed at recompiling binaries at runtime, mainly for modding purposes.

The live recompiler leverages a library called sljit to generate platform-specific code. This library provides an API that's implemented on several platforms, including the main targets of this component: x86_64 and ARM64.

Performance is expected to be slower than the C recompiler, but should still be plenty fast enough for running large amounts of recompiled code without an issue. Considering these ROMs can often be run through an interpreter and still hit their full speed, performance should not be a concern for running native code even if it's less optimal than the C recompiler's codegen.

As mentioned earlier, the main use of the live recompiler will be for loading mods in the N64Recomp runtime. This makes it so that modders don't need to ship platform-specific binaries for their mods, and allows fixing bugs with recompilation down the line without requiring modders to update their binaries.

This PR also includes a utility for testing the live recompiler. It accepts binaries in a custom format which contain the instructions, input data, and target data. Documentation for the test format as well as most of the tests that were used to validate the live recompiler can be found here. The few remaining tests were hacked together binaries that I put together very hastily, so they need to be cleaned up and will probably be uploaded at a later date. The only test in that suite that doesn't currently succeed is the div test, due to unknown behavior when the two operands aren't properly sign extended to 64 bits. This has no bearing on practical usage, since the inputs will always be sign extended as expected.
This commit is contained in:
Wiseguy 2024-12-31 16:11:40 -05:00 committed by GitHub
parent 0d0e93e979
commit 66062a06e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 3452 additions and 385 deletions

View file

@ -0,0 +1,597 @@
#ifndef __RECOMP_PORT__
#define __RECOMP_PORT__
#include <span>
#include <string_view>
#include <cstdint>
#include <utility>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <filesystem>
#ifdef _MSC_VER
inline uint32_t byteswap(uint32_t val) {
return _byteswap_ulong(val);
}
#else
constexpr uint32_t byteswap(uint32_t val) {
return __builtin_bswap32(val);
}
#endif
namespace N64Recomp {
struct Function {
uint32_t vram;
uint32_t rom;
std::vector<uint32_t> words;
std::string name;
uint16_t section_index;
bool ignored;
bool reimplemented;
bool stubbed;
std::unordered_map<int32_t, std::string> function_hooks;
Function(uint32_t vram, uint32_t rom, std::vector<uint32_t> words, std::string name, uint16_t section_index, bool ignored = false, bool reimplemented = false, bool stubbed = false)
: vram(vram), rom(rom), words(std::move(words)), name(std::move(name)), section_index(section_index), ignored(ignored), reimplemented(reimplemented), stubbed(stubbed) {}
Function() = default;
};
struct JumpTable {
uint32_t vram;
uint32_t addend_reg;
uint32_t rom;
uint32_t lw_vram;
uint32_t addu_vram;
uint32_t jr_vram;
uint16_t section_index;
std::vector<uint32_t> entries;
JumpTable(uint32_t vram, uint32_t addend_reg, uint32_t rom, uint32_t lw_vram, uint32_t addu_vram, uint32_t jr_vram, uint16_t section_index, std::vector<uint32_t>&& entries)
: vram(vram), addend_reg(addend_reg), rom(rom), lw_vram(lw_vram), addu_vram(addu_vram), jr_vram(jr_vram), section_index(section_index), entries(std::move(entries)) {}
};
enum class RelocType : uint8_t {
R_MIPS_NONE = 0,
R_MIPS_16,
R_MIPS_32,
R_MIPS_REL32,
R_MIPS_26,
R_MIPS_HI16,
R_MIPS_LO16,
R_MIPS_GPREL16,
};
struct Reloc {
uint32_t address;
uint32_t target_section_offset;
uint32_t symbol_index; // Only used for reference symbols and special section symbols
uint16_t target_section;
RelocType type;
bool reference_symbol;
};
// Special section indices.
constexpr uint16_t SectionAbsolute = (uint16_t)-2;
constexpr uint16_t SectionImport = (uint16_t)-3; // Imported symbols for mods
constexpr uint16_t SectionEvent = (uint16_t)-4;
// Special section names.
constexpr std::string_view PatchSectionName = ".recomp_patch";
constexpr std::string_view ForcedPatchSectionName = ".recomp_force_patch";
constexpr std::string_view ExportSectionName = ".recomp_export";
constexpr std::string_view EventSectionName = ".recomp_event";
constexpr std::string_view ImportSectionPrefix = ".recomp_import.";
constexpr std::string_view CallbackSectionPrefix = ".recomp_callback.";
// Special dependency names.
constexpr std::string_view DependencySelf = ".";
constexpr std::string_view DependencyBaseRecomp = "*";
struct Section {
uint32_t rom_addr = 0;
uint32_t ram_addr = 0;
uint32_t size = 0;
uint32_t bss_size = 0; // not populated when using a symbol toml
std::vector<uint32_t> function_addrs; // only used by the CLI (to find the size of static functions)
std::vector<Reloc> relocs;
std::string name;
uint16_t bss_section_index = (uint16_t)-1;
bool executable = false;
bool relocatable = false; // TODO is this needed? relocs being non-empty should be an equivalent check.
bool has_mips32_relocs = false;
};
struct ReferenceSection {
uint32_t rom_addr;
uint32_t ram_addr;
uint32_t size;
bool relocatable;
};
struct ReferenceSymbol {
std::string name;
uint16_t section_index;
uint32_t section_offset;
bool is_function;
};
struct ElfParsingConfig {
std::string bss_section_suffix;
// Functions with manual size overrides
std::unordered_map<std::string, size_t> manually_sized_funcs;
// The section names that were specified as relocatable
std::unordered_set<std::string> relocatable_sections;
bool has_entrypoint;
int32_t entrypoint_address;
bool use_absolute_symbols;
bool unpaired_lo16_warnings;
bool all_sections_relocatable;
};
struct DataSymbol {
uint32_t vram;
std::string name;
DataSymbol(uint32_t vram, std::string&& name) : vram(vram), name(std::move(name)) {}
};
using DataSymbolMap = std::unordered_map<uint16_t, std::vector<DataSymbol>>;
extern const std::unordered_set<std::string> reimplemented_funcs;
extern const std::unordered_set<std::string> ignored_funcs;
extern const std::unordered_set<std::string> renamed_funcs;
struct ImportSymbol {
ReferenceSymbol base;
size_t dependency_index;
};
struct DependencyEvent {
size_t dependency_index;
std::string event_name;
};
struct EventSymbol {
ReferenceSymbol base;
};
struct Callback {
size_t function_index;
size_t dependency_event_index;
};
struct SymbolReference {
// Reference symbol section index, or one of the special section indices such as SectionImport.
uint16_t section_index;
size_t symbol_index;
};
enum class ReplacementFlags : uint32_t {
Force = 1 << 0,
};
inline ReplacementFlags operator&(ReplacementFlags lhs, ReplacementFlags rhs) { return ReplacementFlags(uint32_t(lhs) & uint32_t(rhs)); }
inline ReplacementFlags operator|(ReplacementFlags lhs, ReplacementFlags rhs) { return ReplacementFlags(uint32_t(lhs) | uint32_t(rhs)); }
struct FunctionReplacement {
uint32_t func_index;
uint32_t original_section_vrom;
uint32_t original_vram;
ReplacementFlags flags;
};
class Context {
private:
//// Reference symbols (used for populating relocations for patches)
// A list of the sections that contain the reference symbols.
std::vector<ReferenceSection> reference_sections;
// A list of the reference symbols.
std::vector<ReferenceSymbol> reference_symbols;
// Mapping of symbol name to reference symbol index.
std::unordered_map<std::string, SymbolReference> reference_symbols_by_name;
// Whether all reference sections should be treated as relocatable (used in live recompilation).
bool all_reference_sections_relocatable = false;
public:
std::vector<Section> sections;
std::vector<Function> functions;
// A list of the list of each function (by index in `functions`) in a given section
std::vector<std::vector<size_t>> section_functions;
// A mapping of vram address to every function with that address.
std::unordered_map<uint32_t, std::vector<size_t>> functions_by_vram;
// A mapping of bss section index to the corresponding non-bss section index.
std::unordered_map<uint16_t, uint16_t> bss_section_to_section;
// The target ROM being recompiled, TODO move this outside of the context to avoid making a copy for mod contexts.
// Used for reading relocations and for the output binary feature.
std::vector<uint8_t> rom;
// Whether reference symbols should be validated when emitting function calls during recompilation.
bool skip_validating_reference_symbols = true;
//// Only used by the CLI, TODO move this to a struct in the internal headers.
// A mapping of function name to index in the functions vector
std::unordered_map<std::string, size_t> functions_by_name;
//// Mod dependencies and their symbols
//// Imported values
// Mapping of dependency name to dependency index.
std::unordered_map<std::string, size_t> dependencies_by_name;
// List of symbols imported from dependencies.
std::vector<ImportSymbol> import_symbols;
// List of events imported from dependencies.
std::vector<DependencyEvent> dependency_events;
// Mappings of dependency event name to the index in dependency_events, all indexed by dependency.
std::vector<std::unordered_map<std::string, size_t>> dependency_events_by_name;
// Mappings of dependency import name to index in import_symbols, all indexed by dependency.
std::vector<std::unordered_map<std::string, size_t>> dependency_imports_by_name;
//// Exported values
// List of function replacements, which contains the original function to replace and the function index to replace it with.
std::vector<FunctionReplacement> replacements;
// Indices of every exported function.
std::vector<size_t> exported_funcs;
// List of callbacks, which contains the function for the callback and the dependency event it attaches to.
std::vector<Callback> callbacks;
// 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.
bool read_data_reference_syms(const std::filesystem::path& data_syms_file_path);
static bool from_symbol_file(const std::filesystem::path& symbol_file_path, std::vector<uint8_t>&& rom, Context& out, bool with_relocs);
static bool from_elf_file(const std::filesystem::path& elf_file_path, Context& out, const ElfParsingConfig& flags, bool for_dumping_context, DataSymbolMap& data_syms_out, bool& found_entrypoint_out);
Context() = default;
bool add_dependency(const std::string& id) {
if (dependencies_by_name.contains(id)) {
return false;
}
size_t dependency_index = dependencies_by_name.size();
dependencies_by_name.emplace(id, dependency_index);
dependency_events_by_name.resize(dependencies_by_name.size());
dependency_imports_by_name.resize(dependencies_by_name.size());
return true;
}
bool add_dependencies(const std::vector<std::string>& new_dependencies) {
dependencies_by_name.reserve(dependencies_by_name.size() + new_dependencies.size());
// Check if any of the dependencies already exist and fail if so.
for (const std::string& dep : new_dependencies) {
if (dependencies_by_name.contains(dep)) {
return false;
}
}
for (const std::string& dep : new_dependencies) {
size_t dependency_index = dependencies_by_name.size();
dependencies_by_name.emplace(dep, dependency_index);
}
dependency_events_by_name.resize(dependencies_by_name.size());
dependency_imports_by_name.resize(dependencies_by_name.size());
return true;
}
bool find_dependency(const std::string& mod_id, size_t& dependency_index) {
auto find_it = dependencies_by_name.find(mod_id);
if (find_it != dependencies_by_name.end()) {
dependency_index = find_it->second;
}
else {
// Handle special dependency names.
if (mod_id == DependencySelf || mod_id == DependencyBaseRecomp) {
add_dependency(mod_id);
dependency_index = dependencies_by_name[mod_id];
}
else {
return false;
}
}
return true;
}
size_t find_function_by_vram_section(uint32_t vram, size_t section_index) const {
auto find_it = functions_by_vram.find(vram);
if (find_it == functions_by_vram.end()) {
return (size_t)-1;
}
for (size_t function_index : find_it->second) {
if (functions[function_index].section_index == section_index) {
return function_index;
}
}
return (size_t)-1;
}
bool has_reference_symbols() const {
return !reference_symbols.empty() || !import_symbols.empty() || !event_symbols.empty();
}
bool is_regular_reference_section(uint16_t section_index) const {
return section_index != SectionImport && section_index != SectionEvent;
}
bool find_reference_symbol(const std::string& symbol_name, SymbolReference& ref_out) const {
auto find_sym_it = reference_symbols_by_name.find(symbol_name);
// Check if the symbol was found.
if (find_sym_it == reference_symbols_by_name.end()) {
return false;
}
ref_out = find_sym_it->second;
return true;
}
bool reference_symbol_exists(const std::string& symbol_name) const {
SymbolReference dummy_ref;
return find_reference_symbol(symbol_name, dummy_ref);
}
bool find_regular_reference_symbol(const std::string& symbol_name, SymbolReference& ref_out) const {
SymbolReference ref_found;
if (!find_reference_symbol(symbol_name, ref_found)) {
return false;
}
// Ignore reference symbols in special sections.
if (!is_regular_reference_section(ref_found.section_index)) {
return false;
}
ref_out = ref_found;
return true;
}
const ReferenceSymbol& get_reference_symbol(uint16_t section_index, size_t symbol_index) const {
if (section_index == SectionImport) {
return import_symbols[symbol_index].base;
}
else if (section_index == SectionEvent) {
return event_symbols[symbol_index].base;
}
return reference_symbols[symbol_index];
}
size_t num_regular_reference_symbols() {
return reference_symbols.size();
}
const ReferenceSymbol& get_regular_reference_symbol(size_t index) const {
return reference_symbols[index];
}
const ReferenceSymbol& get_reference_symbol(const SymbolReference& ref) const {
return get_reference_symbol(ref.section_index, ref.symbol_index);
}
bool is_reference_section_relocatable(uint16_t section_index) const {
if (all_reference_sections_relocatable) {
return true;
}
if (section_index == SectionAbsolute) {
return false;
}
else if (section_index == SectionImport || section_index == SectionEvent) {
return true;
}
return reference_sections[section_index].relocatable;
}
bool add_reference_symbol(const std::string& symbol_name, uint16_t section_index, uint32_t vram, bool is_function) {
uint32_t section_vram;
if (section_index == SectionAbsolute) {
section_vram = 0;
}
else if (section_index < reference_sections.size()) {
section_vram = reference_sections[section_index].ram_addr;
}
// Invalid section index.
else {
return false;
}
// TODO Check if reference_symbols_by_name already contains the name and show a conflict error if so.
reference_symbols_by_name.emplace(symbol_name, N64Recomp::SymbolReference{
.section_index = section_index,
.symbol_index = reference_symbols.size()
});
reference_symbols.emplace_back(N64Recomp::ReferenceSymbol{
.name = symbol_name,
.section_index = section_index,
.section_offset = vram - section_vram,
.is_function = is_function
});
return true;
}
void add_import_symbol(const std::string& symbol_name, size_t dependency_index) {
// TODO Check if dependency_imports_by_name[dependency_index] already contains the name and show a conflict error if so.
dependency_imports_by_name[dependency_index][symbol_name] = import_symbols.size();
import_symbols.emplace_back(
N64Recomp::ImportSymbol {
.base = N64Recomp::ReferenceSymbol {
.name = symbol_name,
.section_index = N64Recomp::SectionImport,
.section_offset = 0,
.is_function = true
},
.dependency_index = dependency_index,
}
);
}
bool find_import_symbol(const std::string& symbol_name, size_t dependency_index, SymbolReference& ref_out) const {
if (dependency_index >= dependencies_by_name.size()) {
return false;
}
auto find_it = dependency_imports_by_name[dependency_index].find(symbol_name);
if (find_it == dependency_imports_by_name[dependency_index].end()) {
return false;
}
ref_out.section_index = SectionImport;
ref_out.symbol_index = find_it->second;
return true;
}
void add_event_symbol(const std::string& symbol_name) {
// TODO Check if reference_symbols_by_name already contains the name and show a conflict error if so.
reference_symbols_by_name[symbol_name] = N64Recomp::SymbolReference {
.section_index = N64Recomp::SectionEvent,
.symbol_index = event_symbols.size()
};
event_symbols.emplace_back(
N64Recomp::EventSymbol {
.base = N64Recomp::ReferenceSymbol {
.name = symbol_name,
.section_index = N64Recomp::SectionEvent,
.section_offset = 0,
.is_function = true
}
}
);
}
bool find_event_symbol(const std::string& symbol_name, SymbolReference& ref_out) const {
SymbolReference ref_found;
if (!find_reference_symbol(symbol_name, ref_found)) {
return false;
}
// Ignore reference symbols that aren't in the event section.
if (ref_found.section_index != SectionEvent) {
return false;
}
ref_out = ref_found;
return true;
}
bool add_dependency_event(const std::string& event_name, size_t dependency_index, size_t& dependency_event_index) {
if (dependency_index >= dependencies_by_name.size()) {
return false;
}
// Prevent adding the same event to a dependency twice. This isn't an error, since a mod could register
// multiple callbacks to the same event.
auto find_it = dependency_events_by_name[dependency_index].find(event_name);
if (find_it != dependency_events_by_name[dependency_index].end()) {
dependency_event_index = find_it->second;
return true;
}
dependency_event_index = dependency_events.size();
dependency_events.emplace_back(DependencyEvent{
.dependency_index = dependency_index,
.event_name = event_name
});
dependency_events_by_name[dependency_index][event_name] = dependency_event_index;
return true;
}
bool add_callback(size_t dependency_event_index, size_t function_index) {
callbacks.emplace_back(Callback{
.function_index = function_index,
.dependency_event_index = dependency_event_index
});
return true;
}
uint32_t get_reference_section_vram(uint16_t section_index) const {
if (section_index == N64Recomp::SectionAbsolute) {
return 0;
}
else if (!is_regular_reference_section(section_index)) {
return 0;
}
else {
return reference_sections[section_index].ram_addr;
}
}
uint32_t get_reference_section_rom(uint16_t section_index) const {
if (section_index == N64Recomp::SectionAbsolute) {
return (uint32_t)-1;
}
else if (!is_regular_reference_section(section_index)) {
return (uint32_t)-1;
}
else {
return reference_sections[section_index].rom_addr;
}
}
void copy_reference_sections_from(const Context& rhs) {
reference_sections = rhs.reference_sections;
}
void set_all_reference_sections_relocatable() {
all_reference_sections_relocatable = true;
}
};
class Generator;
bool recompile_function(const Context& context, size_t function_index, std::ostream& output_file, std::span<std::vector<uint32_t>> static_funcs, bool tag_reference_relocs);
bool recompile_function_custom(Generator& generator, const Context& context, size_t function_index, std::ostream& output_file, std::span<std::vector<uint32_t>> static_funcs_out, bool tag_reference_relocs);
enum class ModSymbolsError {
Good,
NotASymbolFile,
UnknownSymbolFileVersion,
CorruptSymbolFile,
FunctionOutOfBounds,
};
ModSymbolsError parse_mod_symbols(std::span<const char> data, std::span<const uint8_t> binary, const std::unordered_map<uint32_t, uint16_t>& sections_by_vrom, Context& context_out);
std::vector<uint8_t> symbols_to_bin_v1(const Context& mod_context);
inline bool validate_mod_id(std::string_view str) {
// Disallow empty ids.
if (str.size() == 0) {
return false;
}
// Allow special dependency ids.
if (str == N64Recomp::DependencySelf || str == N64Recomp::DependencyBaseRecomp) {
return true;
}
// These following rules basically describe C identifiers. There's no specific reason to enforce them besides colon (currently),
// so this is just to prevent "weird" mod ids.
// Check the first character, which must be alphabetical or an underscore.
if (!isalpha(str[0]) && str[0] != '_') {
return false;
}
// Check the remaining characters, which can be alphanumeric or underscore.
for (char c : str.substr(1)) {
if (!isalnum(c) && c != '_') {
return false;
}
}
return true;
}
inline bool validate_mod_id(const std::string& str) {
return validate_mod_id(std::string_view{str});
}
}
#endif

View file

@ -0,0 +1,109 @@
#ifndef __GENERATOR_H__
#define __GENERATOR_H__
#include "recompiler/context.h"
#include "operations.h"
namespace N64Recomp {
struct InstructionContext {
int rd;
int rs;
int rt;
int sa;
int fd;
int fs;
int ft;
int cop1_cs;
uint16_t imm16;
bool reloc_tag_as_reference;
RelocType reloc_type;
uint32_t reloc_section_index;
uint32_t reloc_target_section_offset;
};
class Generator {
public:
virtual void process_binary_op(const BinaryOp& op, const InstructionContext& ctx) const = 0;
virtual void process_unary_op(const UnaryOp& op, const InstructionContext& ctx) const = 0;
virtual void process_store_op(const StoreOp& op, const InstructionContext& ctx) const = 0;
virtual void emit_function_start(const std::string& function_name, size_t func_index) const = 0;
virtual void emit_function_end() const = 0;
virtual void emit_function_call_lookup(uint32_t addr) const = 0;
virtual void emit_function_call_by_register(int reg) const = 0;
// target_section_offset can each be deduced from symbol_index if the full context is available,
// but for live recompilation the reference symbol list is unavailable so it's still provided.
virtual void emit_function_call_reference_symbol(const Context& context, uint16_t section_index, size_t symbol_index, uint32_t target_section_offset) const = 0;
virtual void emit_function_call(const Context& context, size_t function_index) const = 0;
virtual void emit_named_function_call(const std::string& function_name) const = 0;
virtual void emit_goto(const std::string& target) const = 0;
virtual void emit_label(const std::string& label_name) const = 0;
virtual void emit_jtbl_addend_declaration(const JumpTable& jtbl, int reg) const = 0;
virtual void emit_branch_condition(const ConditionalBranchOp& op, const InstructionContext& ctx) const = 0;
virtual void emit_branch_close() const = 0;
virtual void emit_switch(const Context& recompiler_context, const JumpTable& jtbl, int reg) const = 0;
virtual void emit_case(int case_index, const std::string& target_label) const = 0;
virtual void emit_switch_error(uint32_t instr_vram, uint32_t jtbl_vram) const = 0;
virtual void emit_switch_close() const = 0;
virtual void emit_return() const = 0;
virtual void emit_check_fr(int fpr) const = 0;
virtual void emit_check_nan(int fpr, bool is_double) const = 0;
virtual void emit_cop0_status_read(int reg) const = 0;
virtual void emit_cop0_status_write(int reg) const = 0;
virtual void emit_cop1_cs_read(int reg) const = 0;
virtual void emit_cop1_cs_write(int reg) const = 0;
virtual void emit_muldiv(InstrId instr_id, int reg1, int reg2) const = 0;
virtual void emit_syscall(uint32_t instr_vram) const = 0;
virtual void emit_do_break(uint32_t instr_vram) const = 0;
virtual void emit_pause_self() const = 0;
virtual void emit_trigger_event(uint32_t event_index) const = 0;
virtual void emit_comment(const std::string& comment) const = 0;
};
class CGenerator final : Generator {
public:
CGenerator(std::ostream& output_file) : output_file(output_file) {};
void process_binary_op(const BinaryOp& op, const InstructionContext& ctx) const final;
void process_unary_op(const UnaryOp& op, const InstructionContext& ctx) const final;
void process_store_op(const StoreOp& op, const InstructionContext& ctx) const final;
void emit_function_start(const std::string& function_name, size_t func_index) const final;
void emit_function_end() const final;
void emit_function_call_lookup(uint32_t addr) const final;
void emit_function_call_by_register(int reg) const final;
void emit_function_call_reference_symbol(const Context& context, uint16_t section_index, size_t symbol_index, uint32_t target_section_offset) const final;
void emit_function_call(const Context& context, size_t function_index) const final;
void emit_named_function_call(const std::string& function_name) const final;
void emit_goto(const std::string& target) const final;
void emit_label(const std::string& label_name) const final;
void emit_jtbl_addend_declaration(const JumpTable& jtbl, int reg) const final;
void emit_branch_condition(const ConditionalBranchOp& op, const InstructionContext& ctx) const final;
void emit_branch_close() const final;
void emit_switch(const Context& recompiler_context, const JumpTable& jtbl, int reg) const final;
void emit_case(int case_index, const std::string& target_label) const final;
void emit_switch_error(uint32_t instr_vram, uint32_t jtbl_vram) const final;
void emit_switch_close() const final;
void emit_return() const final;
void emit_check_fr(int fpr) const final;
void emit_check_nan(int fpr, bool is_double) const final;
void emit_cop0_status_read(int reg) const final;
void emit_cop0_status_write(int reg) const final;
void emit_cop1_cs_read(int reg) const final;
void emit_cop1_cs_write(int reg) const final;
void emit_muldiv(InstrId instr_id, int reg1, int reg2) const final;
void emit_syscall(uint32_t instr_vram) const final;
void emit_do_break(uint32_t instr_vram) const final;
void emit_pause_self() const final;
void emit_trigger_event(uint32_t event_index) const final;
void emit_comment(const std::string& comment) const final;
private:
void get_operand_string(Operand operand, UnaryOpType operation, const InstructionContext& context, std::string& operand_string) const;
void get_binary_expr_string(BinaryOpType type, const BinaryOperands& operands, const InstructionContext& ctx, const std::string& output, std::string& expr_string) const;
void get_notation(BinaryOpType op_type, std::string& func_string, std::string& infix_string) const;
std::ostream& output_file;
};
}
#endif

View file

@ -0,0 +1,141 @@
#ifndef __LIVE_RECOMPILER_H__
#define __LIVE_RECOMPILER_H__
#include <unordered_map>
#include "recompiler/generator.h"
#include "recomp.h"
struct sljit_compiler;
namespace N64Recomp {
struct LiveGeneratorContext;
struct ReferenceJumpDetails {
uint16_t section;
uint32_t section_offset;
};
struct LiveGeneratorOutput {
LiveGeneratorOutput() = default;
LiveGeneratorOutput(const LiveGeneratorOutput& rhs) = delete;
LiveGeneratorOutput(LiveGeneratorOutput&& rhs) { *this = std::move(rhs); }
LiveGeneratorOutput& operator=(const LiveGeneratorOutput& rhs) = delete;
LiveGeneratorOutput& operator=(LiveGeneratorOutput&& rhs) {
good = rhs.good;
string_literals = std::move(rhs.string_literals);
jump_tables = std::move(rhs.jump_tables);
code = rhs.code;
code_size = rhs.code_size;
functions = std::move(rhs.functions);
reference_symbol_jumps = std::move(rhs.reference_symbol_jumps);
import_jumps_by_index = std::move(rhs.import_jumps_by_index);
executable_offset = rhs.executable_offset;
rhs.good = false;
rhs.code = nullptr;
rhs.code_size = 0;
rhs.reference_symbol_jumps.clear();
rhs.executable_offset = 0;
return *this;
}
~LiveGeneratorOutput();
size_t num_reference_symbol_jumps() const;
void set_reference_symbol_jump(size_t jump_index, recomp_func_t* func);
ReferenceJumpDetails get_reference_symbol_jump_details(size_t jump_index);
void populate_import_symbol_jumps(size_t import_index, recomp_func_t* func);
bool good = false;
// Storage for string literals referenced by recompiled code. These are allocated as unique_ptr arrays
// to prevent them from moving, as the referenced address is baked into the recompiled code.
std::vector<std::unique_ptr<char[]>> string_literals;
// Storage for jump tables referenced by recompiled code (vector of arrays of pointers). These are also
// allocated as unique_ptr arrays for the same reason as strings.
std::vector<std::unique_ptr<void*[]>> jump_tables;
// Recompiled code.
void* code;
// Size of the recompiled code.
size_t code_size;
// Pointers to each individual function within the recompiled code.
std::vector<recomp_func_t*> functions;
private:
// List of jump details and the corresponding jump instruction address. These jumps get populated after recompilation is complete
// during dependency resolution.
std::vector<std::pair<ReferenceJumpDetails, void*>> reference_symbol_jumps;
// Mapping of import symbol index to any jumps to that import symbol.
std::unordered_multimap<size_t, void*> import_jumps_by_index;
// sljit executable offset.
int64_t executable_offset;
friend class LiveGenerator;
};
struct LiveGeneratorInputs {
uint32_t base_event_index;
void (*cop0_status_write)(recomp_context* ctx, gpr value);
gpr (*cop0_status_read)(recomp_context* ctx);
void (*switch_error)(const char* func, uint32_t vram, uint32_t jtbl);
void (*do_break)(uint32_t vram);
recomp_func_t* (*get_function)(int32_t vram);
void (*syscall_handler)(uint8_t* rdram, recomp_context* ctx, int32_t instruction_vram);
void (*pause_self)(uint8_t* rdram);
void (*trigger_event)(uint8_t* rdram, recomp_context* ctx, uint32_t event_index);
int32_t *reference_section_addresses;
int32_t *local_section_addresses;
};
class LiveGenerator final : public Generator {
public:
LiveGenerator(size_t num_funcs, const LiveGeneratorInputs& inputs);
~LiveGenerator();
// Prevent moving or copying.
LiveGenerator(const LiveGenerator& rhs) = delete;
LiveGenerator(LiveGenerator&& rhs) = delete;
LiveGenerator& operator=(const LiveGenerator& rhs) = delete;
LiveGenerator& operator=(LiveGenerator&& rhs) = delete;
LiveGeneratorOutput finish();
void process_binary_op(const BinaryOp& op, const InstructionContext& ctx) const final;
void process_unary_op(const UnaryOp& op, const InstructionContext& ctx) const final;
void process_store_op(const StoreOp& op, const InstructionContext& ctx) const final;
void emit_function_start(const std::string& function_name, size_t func_index) const final;
void emit_function_end() const final;
void emit_function_call_lookup(uint32_t addr) const final;
void emit_function_call_by_register(int reg) const final;
void emit_function_call_reference_symbol(const Context& context, uint16_t section_index, size_t symbol_index, uint32_t target_section_offset) const final;
void emit_function_call(const Context& context, size_t function_index) const final;
void emit_named_function_call(const std::string& function_name) const final;
void emit_goto(const std::string& target) const final;
void emit_label(const std::string& label_name) const final;
void emit_jtbl_addend_declaration(const JumpTable& jtbl, int reg) const final;
void emit_branch_condition(const ConditionalBranchOp& op, const InstructionContext& ctx) const final;
void emit_branch_close() const final;
void emit_switch(const Context& recompiler_context, const JumpTable& jtbl, int reg) const final;
void emit_case(int case_index, const std::string& target_label) const final;
void emit_switch_error(uint32_t instr_vram, uint32_t jtbl_vram) const final;
void emit_switch_close() const final;
void emit_return() const final;
void emit_check_fr(int fpr) const final;
void emit_check_nan(int fpr, bool is_double) const final;
void emit_cop0_status_read(int reg) const final;
void emit_cop0_status_write(int reg) const final;
void emit_cop1_cs_read(int reg) const final;
void emit_cop1_cs_write(int reg) const final;
void emit_muldiv(InstrId instr_id, int reg1, int reg2) const final;
void emit_syscall(uint32_t instr_vram) const final;
void emit_do_break(uint32_t instr_vram) const final;
void emit_pause_self() const final;
void emit_trigger_event(uint32_t event_index) const final;
void emit_comment(const std::string& comment) const final;
private:
void get_operand_string(Operand operand, UnaryOpType operation, const InstructionContext& context, std::string& operand_string) const;
void get_binary_expr_string(BinaryOpType type, const BinaryOperands& operands, const InstructionContext& ctx, const std::string& output, std::string& expr_string) const;
void get_notation(BinaryOpType op_type, std::string& func_string, std::string& infix_string) const;
// Loads the relocated address specified by the instruction context into the target register.
void load_relocated_address(const InstructionContext& ctx, int reg) const;
sljit_compiler* compiler;
LiveGeneratorInputs inputs;
mutable std::unique_ptr<LiveGeneratorContext> context;
mutable bool errored;
};
void live_recompiler_init();
bool recompile_function_live(LiveGenerator& generator, const Context& context, size_t function_index, std::ostream& output_file, std::span<std::vector<uint32_t>> static_funcs_out, bool tag_reference_relocs);
}
#endif

View file

@ -0,0 +1,213 @@
#ifndef __OPERATIONS_H__
#define __OPERATIONS_H__
#include <unordered_map>
#include "rabbitizer.hpp"
namespace N64Recomp {
using InstrId = rabbitizer::InstrId::UniqueId;
using Cop0Reg = rabbitizer::Registers::Cpu::Cop0;
enum class StoreOpType {
SD,
SDL,
SDR,
SW,
SWL,
SWR,
SH,
SB,
SDC1,
SWC1
};
enum class UnaryOpType {
None,
ToS32,
ToU32,
ToS64,
ToU64,
Lui,
Mask5, // Mask to 5 bits
Mask6, // Mask to 5 bits
ToInt32, // Functionally equivalent to ToS32, only exists for parity with old codegen
NegateFloat,
NegateDouble,
AbsFloat,
AbsDouble,
SqrtFloat,
SqrtDouble,
ConvertSFromW,
ConvertWFromS,
ConvertDFromW,
ConvertWFromD,
ConvertDFromS,
ConvertSFromD,
ConvertDFromL,
ConvertLFromD,
ConvertSFromL,
ConvertLFromS,
TruncateWFromS,
TruncateWFromD,
TruncateLFromS,
TruncateLFromD,
RoundWFromS,
RoundWFromD,
RoundLFromS,
RoundLFromD,
CeilWFromS,
CeilWFromD,
CeilLFromS,
CeilLFromD,
FloorWFromS,
FloorWFromD,
FloorLFromS,
FloorLFromD
};
enum class BinaryOpType {
// Addition/subtraction
Add32,
Sub32,
Add64,
Sub64,
// Float arithmetic
AddFloat,
AddDouble,
SubFloat,
SubDouble,
MulFloat,
MulDouble,
DivFloat,
DivDouble,
// Bitwise
And64,
Or64,
Nor64,
Xor64,
Sll32,
Sll64,
Srl32,
Srl64,
Sra32,
Sra64,
// Comparisons
Equal,
NotEqual,
Less,
LessEq,
Greater,
GreaterEq,
EqualFloat,
LessFloat,
LessEqFloat,
EqualDouble,
LessDouble,
LessEqDouble,
// Loads
LD,
LW,
LWU,
LH,
LHU,
LB,
LBU,
LDL,
LDR,
LWL,
LWR,
// Fixed result
True,
False,
COUNT,
};
enum class Operand {
Rd, // GPR
Rs, // GPR
Rt, // GPR
Fd, // FPR
Fs, // FPR
Ft, // FPR
FdDouble, // Double float in fd FPR
FsDouble, // Double float in fs FPR
FtDouble, // Double float in ft FPR
// Raw low 32-bit values of FPRs with handling for mips3 float mode behavior
FdU32L,
FsU32L,
FtU32L,
// Raw high 32-bit values of FPRs with handling for mips3 float mode behavior
FdU32H,
FsU32H,
FtU32H,
// Raw 64-bit values of FPRs
FdU64,
FsU64,
FtU64,
ImmU16, // 16-bit immediate, unsigned
ImmS16, // 16-bit immediate, signed
Sa, // Shift amount
Sa32, // Shift amount plus 32
Cop1cs, // Coprocessor 1 Condition Signal
Hi,
Lo,
Zero,
Base = Rs, // Alias for Rs for loads
};
struct StoreOp {
StoreOpType type;
Operand value_input;
};
struct UnaryOp {
UnaryOpType operation;
Operand output;
Operand input;
// Whether the FR bit needs to be checked for odd float registers for this instruction.
bool check_fr = false;
// Whether the input need to be checked for being NaN.
bool check_nan = false;
};
struct BinaryOperands {
// Operation to apply to each operand before applying the binary operation to them.
UnaryOpType operand_operations[2];
// The source of the input operands.
Operand operands[2];
};
struct BinaryOp {
// The type of binary operation this represents.
BinaryOpType type;
// The output operand.
Operand output;
// The input operands.
BinaryOperands operands;
// Whether the FR bit needs to be checked for odd float registers for this instruction.
bool check_fr = false;
// Whether the inputs need to be checked for being NaN.
bool check_nan = false;
};
struct ConditionalBranchOp {
// The type of binary operation to use for this compare
BinaryOpType comparison;
// The input operands.
BinaryOperands operands;
// Whether this jump should link for returns.
bool link;
// Whether this jump has "likely" behavior (doesn't execute the delay slot if skipped).
bool likely;
};
extern const std::unordered_map<InstrId, UnaryOp> unary_ops;
extern const std::unordered_map<InstrId, BinaryOp> binary_ops;
extern const std::unordered_map<InstrId, ConditionalBranchOp> conditional_branch_ops;
extern const std::unordered_map<InstrId, StoreOp> store_ops;
}
#endif