Implement indirect calls and fix signed branch conditions in live recompiler

This commit is contained in:
Mr-Wiseguy 2024-10-07 21:39:39 -04:00
parent 898de82da7
commit dc2a5b6a46
3 changed files with 82 additions and 19 deletions

View file

@ -48,7 +48,7 @@ struct N64Recomp::LiveGeneratorContext {
sljit_jump* cur_branch_jump;
};
N64Recomp::LiveGenerator::LiveGenerator(size_t num_funcs) : compiler(compiler) {
N64Recomp::LiveGenerator::LiveGenerator(size_t num_funcs, const LiveGeneratorInputs& inputs) : compiler(compiler), inputs(inputs) {
compiler = sljit_create_compiler(NULL);
context = std::make_unique<LiveGeneratorContext>();
context->func_labels.resize(num_funcs);
@ -102,6 +102,9 @@ N64Recomp::LiveGeneratorOutput::~LiveGeneratorOutput() {
if (code != nullptr) {
sljit_free_code(code, nullptr);
}
for (const char* literal : string_literals) {
delete[] literal;
}
}
constexpr int get_gpr_context_offset(int gpr_index) {
@ -710,11 +713,39 @@ void N64Recomp::LiveGenerator::emit_function_end() const {
}
void N64Recomp::LiveGenerator::emit_function_call_lookup(uint32_t addr) const {
assert(false);
// Load the address immediate into the first argument.
sljit_emit_op1(compiler, SLJIT_MOV32, SLJIT_R0, 0, SLJIT_IMM, int32_t(addr));
// Call get_function.
sljit_emit_icall(compiler, SLJIT_CALL, SLJIT_ARGS1(P, 32), SLJIT_IMM, sljit_sw(inputs.get_function));
// Copy the return value into R2 so that it can be used for icall
sljit_emit_op1(compiler, SLJIT_MOV, SLJIT_R2, 0, SLJIT_R0, 0);
// Load rdram and ctx into R0 and R1.
sljit_emit_op2(compiler, SLJIT_ADD, SLJIT_R0, 0, Registers::rdram, 0, SLJIT_IMM, 0xFFFFFFFF80000000);
sljit_emit_op1(compiler, SLJIT_MOV, SLJIT_R1, 0, Registers::ctx, 0);
// Call the function.
sljit_emit_icall(compiler, SLJIT_CALL, SLJIT_ARGS2V(P, P), SLJIT_R2, 0);
}
void N64Recomp::LiveGenerator::emit_function_call_by_register(int reg) const {
assert(false);
// Load the register's value into the first argument.
sljit_emit_op1(compiler, SLJIT_MOV32, SLJIT_R0, 0, SLJIT_MEM1(Registers::ctx), get_gpr_context_offset(reg));
// Call get_function.
sljit_emit_icall(compiler, SLJIT_CALL, SLJIT_ARGS1(P, 32), SLJIT_IMM, sljit_sw(inputs.get_function));
// Copy the return value into R2 so that it can be used for icall
sljit_emit_op1(compiler, SLJIT_MOV, SLJIT_R2, 0, SLJIT_R0, 0);
// Load rdram and ctx into R0 and R1.
sljit_emit_op2(compiler, SLJIT_ADD, SLJIT_R0, 0, Registers::rdram, 0, SLJIT_IMM, 0xFFFFFFFF80000000);
sljit_emit_op1(compiler, SLJIT_MOV, SLJIT_R1, 0, Registers::ctx, 0);
// Call the function.
sljit_emit_icall(compiler, SLJIT_CALL, SLJIT_ARGS2V(P, P), SLJIT_R2, 0);
}
void N64Recomp::LiveGenerator::emit_function_call_reference_symbol(const Context& context, uint16_t section_index, size_t symbol_index) const {
@ -767,8 +798,12 @@ void N64Recomp::LiveGenerator::emit_branch_condition(const ConditionalBranchOp&
// Make sure there's no pending jump.
assert(context->cur_branch_jump == nullptr);
// Branch conditions do not allow unary ops, except for ToS64 on the first operand to indicate the branch comparison is signed.
assert(op.operands.operand_operations[0] == UnaryOpType::None || op.operands.operand_operations[0] == UnaryOpType::ToS64);
assert(op.operands.operand_operations[1] == UnaryOpType::None);
sljit_s32 condition_type;
bool cmp_signed = op.operands.operand_operations[1] == UnaryOpType::ToS64;
bool cmp_signed = op.operands.operand_operations[0] == UnaryOpType::ToS64;
// Comparisons need to be inverted to account for the fact that the generator is expected to generate a code block that only runs if
// the condition is met, meaning the branch should be taken if the condition isn't met.
switch (op.comparison) {
@ -780,34 +815,34 @@ void N64Recomp::LiveGenerator::emit_branch_condition(const ConditionalBranchOp&
break;
case BinaryOpType::GreaterEq:
if (cmp_signed) {
condition_type = SLJIT_LESS;
condition_type = SLJIT_SIG_LESS;
}
else {
condition_type = SLJIT_SIG_LESS;
condition_type = SLJIT_LESS;
}
break;
case BinaryOpType::Greater:
if (cmp_signed) {
condition_type = SLJIT_LESS_EQUAL;
condition_type = SLJIT_SIG_LESS_EQUAL;
}
else {
condition_type = SLJIT_SIG_LESS_EQUAL;
condition_type = SLJIT_LESS_EQUAL;
}
break;
case BinaryOpType::LessEq:
if (cmp_signed) {
condition_type = SLJIT_GREATER;
condition_type = SLJIT_SIG_GREATER;
}
else {
condition_type = SLJIT_SIG_GREATER;
condition_type = SLJIT_GREATER;
}
break;
case BinaryOpType::Less:
if (cmp_signed) {
condition_type = SLJIT_GREATER_EQUAL;
condition_type = SLJIT_SIG_GREATER_EQUAL;
}
else {
condition_type = SLJIT_SIG_GREATER_EQUAL;
condition_type = SLJIT_GREATER_EQUAL;
}
break;
default:
@ -821,10 +856,6 @@ void N64Recomp::LiveGenerator::emit_branch_condition(const ConditionalBranchOp&
get_operand_values(op.operands.operands[0], ctx, src1, src1w);
get_operand_values(op.operands.operands[1], ctx, src2, src2w);
// Branch conditions do not allow unary ops, except for ToS64 on the first operand to indicate the branch comparison is signed.
assert(op.operands.operand_operations[0] == UnaryOpType::None || op.operands.operand_operations[1] == UnaryOpType::ToS64);
assert(op.operands.operand_operations[1] == UnaryOpType::None);
// Create a compare jump and track it as the pending branch jump.
context->cur_branch_jump = sljit_emit_cmp(compiler, condition_type, src1, src1w, src2, src2w);
}

View file

@ -65,6 +65,18 @@ struct TestStats {
uint64_t code_size;
};
void write1(uint8_t* rdram, recomp_context* ctx) {
MEM_B(0, ctx->r4) = 1;
}
recomp_func_t* test_get_function(int32_t vram) {
if (vram == 0x80100000) {
return write1;
}
assert(false);
return nullptr;
}
TestStats run_test(const std::filesystem::path& tests_dir, const std::string& test_name) {
std::filesystem::path input_path = tests_dir / (test_name + "_data.bin");
std::filesystem::path data_dump_path = tests_dir / (test_name + "_data_out.bin");
@ -185,8 +197,12 @@ TestStats run_test(const std::filesystem::path& tests_dir, const std::string& te
auto before_codegen = std::chrono::system_clock::now();
N64Recomp::LiveGeneratorInputs generator_inputs {
.get_function = test_get_function,
};
// Create the sljit compiler and the generator.
N64Recomp::LiveGenerator generator{ context.functions.size() };
N64Recomp::LiveGenerator generator{ context.functions.size(), generator_inputs };
for (size_t func_index = 0; func_index < context.functions.size(); func_index++) {
std::ostringstream dummy_ostream{};

View file

@ -27,13 +27,28 @@ namespace N64Recomp {
}
~LiveGeneratorOutput();
bool good = false;
std::vector<recomp_func_t*> functions;
// Storage for string literals referenced by recompiled code. These must be manually allocated to prevent
// them from moving, as the referenced address is baked into the recompiled code.
std::vector<const char*> string_literals;
// 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;
};
struct LiveGeneratorInputs {
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);
};
class LiveGenerator final : public Generator {
public:
LiveGenerator(size_t num_funcs);
LiveGenerator(size_t num_funcs, const LiveGeneratorInputs& inputs);
~LiveGenerator();
LiveGeneratorOutput finish();
void process_binary_op(const BinaryOp& op, const InstructionContext& ctx) const final;
@ -72,6 +87,7 @@ namespace N64Recomp {
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;
sljit_compiler* compiler;
LiveGeneratorInputs inputs;
mutable std::unique_ptr<LiveGeneratorContext> context;
};