mirror of
https://github.com/open-goal/jak-project
synced 2026-05-23 15:02:01 -04:00
d3cc739e43
This attempts to get into master whatever work was done in this PR / it's earlier PR https://github.com/open-goal/jak-project/pull/3965 I don't want this work to be lost / floating around in massive PRs. However the changes are: - switch to ntsc_v1 instead of PAL as the development target, as we have done for all other games - remove most of the copied-from-jak2/3 changes as they need to be confirmed during the decompilation process not just assumed - avoids committing any changes to `game/kernel/common` as it was not clear to me if these were changes made in jak x's kernel that were not properly broken out into it's own functions. We don't want to accidentally introduce bugs into jak1-3's kernel code. - in other words, if the change in the kernel only happens in jak x...it should likely be specific to jak x's kernel, not common. --------- Co-authored-by: VodBox <dillon@vodbox.io> Co-authored-by: yodah <greenboyyodah@gmail.com>
1522 lines
57 KiB
C++
1522 lines
57 KiB
C++
#include "mips2c.h"
|
|
|
|
#include <set>
|
|
|
|
#include "common/log/log.h"
|
|
#include "common/symbols.h"
|
|
#include "common/util/print_float.h"
|
|
|
|
#include "decompiler/Disasm/InstructionMatching.h"
|
|
#include "decompiler/Function/Function.h"
|
|
#include "decompiler/ObjectFile/LinkedObjectFile.h"
|
|
|
|
/*!
|
|
* Mips2C:
|
|
* The mips2c analysis pass converts mips assembly into C code. It is a very literal translation.
|
|
* This relies on the helper functions in mips2c_private.h header.
|
|
*
|
|
* We generate a "link" function and an "execute" function. The "link" function performs symbol
|
|
* table lookups and saves the address of the slots in a cache structure used during runtime. It
|
|
* also allocates a stub function on the GOAL heap that jumps to the C++ function in the proper way.
|
|
*
|
|
* The "link" function should be called by the linker when the appropriate object file is linked.
|
|
* It should happen after GOAL linking, but before executing the top-level segment.
|
|
* This _only_ allocates a function, but doesn't set the symbol.
|
|
* You have to do that yourself, in the top level.
|
|
* This order seems weird and annoying, but it makes sure that we get the order of allocations
|
|
* right. It's likely that nothing depends on this, but better to be safe.
|
|
*
|
|
* The "execute" function is the function that should be called from GOAL.
|
|
*/
|
|
|
|
namespace decompiler {
|
|
|
|
//////////////////////
|
|
// Register Helpers
|
|
//////////////////////
|
|
|
|
Register rs7() {
|
|
return make_gpr(Reg::S7);
|
|
}
|
|
|
|
Register rr0() {
|
|
return make_gpr(Reg::R0);
|
|
}
|
|
|
|
Register rfp() {
|
|
return make_gpr(Reg::FP);
|
|
}
|
|
|
|
Register rra() {
|
|
return make_gpr(Reg::RA);
|
|
}
|
|
|
|
Register rt9() {
|
|
return make_gpr(Reg::T9);
|
|
}
|
|
|
|
Register rv0() {
|
|
return make_gpr(Reg::V0);
|
|
}
|
|
|
|
Register rsp() {
|
|
return make_gpr(Reg::SP);
|
|
}
|
|
|
|
Register make_vf(int idx) {
|
|
return Register(Reg::VF, idx);
|
|
}
|
|
|
|
/*!
|
|
* Convert a GOAL symbol name to a valid C++ variable name.
|
|
* dashes become underscores, and !/?/ * are dropped.
|
|
*/
|
|
std::string goal_to_c_name(const std::string& name) {
|
|
std::string result;
|
|
for (auto c : name) {
|
|
if (c == '!' || c == '?' || c == '*') {
|
|
continue;
|
|
}
|
|
|
|
if (c == '-') {
|
|
c = '_';
|
|
}
|
|
|
|
result.push_back(c);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/*!
|
|
* Convert a decompiler function name to a valid C++ variable name.
|
|
*/
|
|
std::string goal_to_c_function_name(const FunctionName& name) {
|
|
switch (name.kind) {
|
|
case FunctionName::FunctionKind::GLOBAL:
|
|
return goal_to_c_name(name.function_name);
|
|
case FunctionName::FunctionKind::METHOD:
|
|
return fmt::format("method_{}_{}", name.method_id, goal_to_c_name(name.type_name));
|
|
default:
|
|
ASSERT(false);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Convert a decompiler register into the name of the register constant in mips2c_private.h
|
|
*/
|
|
const char* reg_to_name(const InstructionAtom& atom) {
|
|
ASSERT(atom.is_reg());
|
|
return atom.get_reg().to_charp();
|
|
}
|
|
|
|
/*!
|
|
* A line of code in the mips2c function output. Just code + end of line comment.
|
|
*/
|
|
struct Mips2C_Line {
|
|
std::string code;
|
|
std::string comment;
|
|
|
|
Mips2C_Line() = default;
|
|
Mips2C_Line(const std::string& _code) : code(_code) {}
|
|
Mips2C_Line(const std::string& _code, const std::string& _comment)
|
|
: code(_code), comment(_comment) {}
|
|
};
|
|
|
|
/*!
|
|
* Mips2C output. Contains the execute and link function.
|
|
* This is built up in the order of MIPS instructions/labels/comments using the output_* functions.
|
|
* The output is built by write_to_string.
|
|
*/
|
|
struct Mips2C_Output {
|
|
bool jump_table = false;
|
|
/*!
|
|
* Add a label at the current line.
|
|
*/
|
|
void output_label(int block_idx) { lines.push_back(fmt::format("\nblock_{}:", block_idx)); }
|
|
|
|
void output_jump_table_block_label(int block_idx) {
|
|
lines.push_back(fmt::format("\ncase {}:", block_idx));
|
|
lines.push_back(fmt::format("next_block = {};", block_idx + 1));
|
|
}
|
|
|
|
/*!
|
|
* Add a full line comment at the current line. Includes "//" automatically
|
|
*/
|
|
void output_line_comment(const std::string& text) { lines.emplace_back("// " + text); }
|
|
|
|
/*!
|
|
* Output code and comment for an instruction.
|
|
*/
|
|
void output_instr(const std::string& instr, const std::string& comment) {
|
|
lines.emplace_back(instr, comment);
|
|
}
|
|
|
|
/*!
|
|
* Convert the output to a string.
|
|
*/
|
|
std::string write_to_string(const FunctionName& goal_func_name,
|
|
GameVersion version,
|
|
const std::string& extra = "") const {
|
|
std::string name = goal_to_c_function_name(goal_func_name);
|
|
std::string result = "//--------------------------MIPS2C---------------------\n";
|
|
result += "// clang-format off\n";
|
|
result += "#include \"game/mips2c/mips2c_private.h\"\n";
|
|
result += fmt::format("#include \"game/kernel/{}/kscheme.h\"\n", game_version_names[version]);
|
|
result += fmt::format("using ::{}::intern_from_c;\n", game_version_names[version]);
|
|
|
|
// start of namespace for this function
|
|
result += fmt::format("namespace Mips2C::{} {{\n", game_version_names[version]);
|
|
result += fmt::format("namespace {} {{\n", name);
|
|
|
|
// definition of the symbol cache.
|
|
if (!symbol_cache.empty()) {
|
|
result += "struct Cache {\n";
|
|
for (auto& sym : symbol_cache) {
|
|
result += fmt::format(" void* {}; // {}\n", goal_to_c_name(sym), sym);
|
|
}
|
|
result += "} cache;\n\n";
|
|
}
|
|
|
|
// definition of the function
|
|
// the mips2c_call function will build and pass an ExecutionContext
|
|
result += "u64 execute(void* ctxt) {\n";
|
|
result += " auto* c = (ExecutionContext*)ctxt;\n";
|
|
|
|
// the branch condition (for delay slots)
|
|
result += " bool bc = false;\n";
|
|
|
|
// the function call address (for jalr delay slots)
|
|
result += " u32 call_addr = 0;\n";
|
|
|
|
if (needs_cop1_bc) {
|
|
// the cop1 branch flag (separate from delay slot bc).
|
|
result += " bool cop1_bc = false;\n";
|
|
}
|
|
|
|
if (jump_table) {
|
|
result += "u32 next_block = 0;\n";
|
|
result += "while(true) {\n";
|
|
result += " switch(next_block) {\n";
|
|
}
|
|
|
|
// add all lines
|
|
for (auto& line : lines) {
|
|
result += " ";
|
|
result += line.code;
|
|
if (!line.comment.empty()) {
|
|
if (line.code.length() < 50) {
|
|
for (int i = 0; i < 50 - (int)line.code.length(); i++) {
|
|
result.push_back(' ');
|
|
}
|
|
}
|
|
result.append("// ");
|
|
result.append(line.comment);
|
|
}
|
|
|
|
result += '\n';
|
|
}
|
|
|
|
if (jump_table) {
|
|
result += " }\n";
|
|
result += "}\n";
|
|
}
|
|
|
|
// return!
|
|
result += "end_of_function:\n return c->gprs[v0].du64[0];\n";
|
|
result += "}\n\n";
|
|
|
|
// link function:
|
|
result += "void link() {\n";
|
|
// lookup all symbols
|
|
for (auto& sym : symbol_cache) {
|
|
switch (version) {
|
|
case GameVersion::Jak1:
|
|
case GameVersion::Jak2:
|
|
result +=
|
|
fmt::format(" cache.{} = intern_from_c(\"{}\").c();\n", goal_to_c_name(sym), sym);
|
|
break;
|
|
case GameVersion::Jak3:
|
|
case GameVersion::JakX:
|
|
result += fmt::format(" cache.{} = intern_from_c(-1, 0, \"{}\").c();\n",
|
|
goal_to_c_name(sym), sym);
|
|
break;
|
|
}
|
|
}
|
|
// this adds us to a table for lookup later, and also allocates our trampoline.
|
|
result += fmt::format(" gLinkedFunctionTable.reg(\"{}\", execute, PUT_STACK_SIZE_HERE);\n",
|
|
goal_func_name.to_string());
|
|
result += "}\n\n";
|
|
|
|
result += extra;
|
|
|
|
result += fmt::format("}} // namespace {}\n", name);
|
|
result += "} // namespace Mips2C\n";
|
|
|
|
// reminder to the user to add a callback to the link function in the linker.
|
|
result +=
|
|
fmt::format("// add {}::link to the link callback table for the object file.\n", name);
|
|
result += "// FWD DEC:\n";
|
|
result += fmt::format("namespace {} {{ extern void link(); }}\n", name);
|
|
return result;
|
|
}
|
|
|
|
/*!
|
|
* Adds name to the symbol cache, if it's not there already.
|
|
*/
|
|
void require_symbol(const std::string& name) { symbol_cache.insert(name); }
|
|
|
|
std::vector<Mips2C_Line> lines;
|
|
std::set<std::string> symbol_cache;
|
|
bool needs_cop1_bc = false;
|
|
};
|
|
|
|
/*!
|
|
* Basic block used for mips2c.
|
|
*/
|
|
struct M2C_Block {
|
|
int idx = -1; // block idx
|
|
int succ_branch = -1; // block idx if we take the branch
|
|
int succ_ft = -1; // block idx if we don't take the branch (or there is none)
|
|
std::vector<int> pred; // block idx of predecessors
|
|
|
|
int start_instr = -1; // first instruction idx
|
|
int end_instr = -1; // last instruction idx (not inclusive)
|
|
|
|
bool has_branch = false; // ends in a branch instruction?
|
|
bool branch_likely = false; // that branch is likely branch?
|
|
bool branch_always = false; // that branch is always taken?
|
|
|
|
bool has_pred(int pidx) const {
|
|
for (auto p : pred) {
|
|
if (p == pidx) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/*!
|
|
* Make second_idx be a fallthrough of first_idx.
|
|
*/
|
|
void link_fall_through(int first_idx, int second_idx, std::vector<M2C_Block>& blocks) {
|
|
auto& first = blocks.at(first_idx);
|
|
auto& second = blocks.at(second_idx);
|
|
|
|
ASSERT(first.succ_ft == -1); // don't want to overwrite something by accident.
|
|
// can only fall through to the next code in memory.
|
|
ASSERT(first_idx + 1 == second_idx);
|
|
first.succ_ft = second_idx;
|
|
|
|
if (!second.has_pred(first_idx)) {
|
|
// if a block can block fall through and branch to the same block, we want to avoid adding
|
|
// it as a pred twice. This is rare, but does happen and makes sense with likely branches
|
|
// which only run the delay slot when taken.
|
|
second.pred.push_back(first_idx);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Make second_idx be the branch destination of first_idx.
|
|
*/
|
|
void link_branch(int first_idx, int second_idx, std::vector<M2C_Block>& blocks) {
|
|
auto& first = blocks.at(first_idx);
|
|
auto& second = blocks.at(second_idx);
|
|
ASSERT(first.succ_branch == -1);
|
|
first.succ_branch = second_idx;
|
|
|
|
if (!second.has_pred(first_idx)) {
|
|
// see comment in link_fall_through
|
|
second.pred.push_back(first_idx);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Make second_idx be the fall through of a likely branch (after the delay slot)
|
|
*/
|
|
void link_fall_through_likely(int first_idx, int second_idx, std::vector<M2C_Block>& blocks) {
|
|
auto& first = blocks.at(first_idx);
|
|
auto& second = blocks.at(second_idx);
|
|
ASSERT(first.succ_ft == -1); // don't want to overwrite something by accident.
|
|
// can only fall through to the next code in memory.
|
|
ASSERT(first_idx + 2 == second_idx);
|
|
|
|
first.succ_ft = second_idx;
|
|
|
|
if (!second.has_pred(first_idx)) {
|
|
// see comment in link_fall_through
|
|
second.pred.push_back(first_idx);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Compute predecessor and successor of each block
|
|
*
|
|
* NOTE: due to likely branch delays/etc, succ_ft and succ_branch behave strangely.
|
|
*
|
|
* The succ_ft destination is always taken if has_branch is false.
|
|
* The succ_branch destination is always taken if branch_always it true.
|
|
*
|
|
* Otherwise, has_branch is true, and the succ_branch is taken if the branch condition is true.
|
|
* The succ_ft and succ_branch may be _anywhere_. succ_ft may not always be the next destination.
|
|
*/
|
|
std::vector<M2C_Block> setup_preds_and_succs(const Function& func,
|
|
const LinkedObjectFile& file,
|
|
std::unordered_set<int>& likely_delay_slot_blocks) {
|
|
// create m2c blocks
|
|
std::vector<M2C_Block> blocks;
|
|
blocks.resize(func.basic_blocks.size());
|
|
for (size_t i = 0; i < blocks.size(); i++) {
|
|
blocks[i].idx = i;
|
|
blocks[i].start_instr = func.basic_blocks[i].start_word;
|
|
blocks[i].end_instr = func.basic_blocks[i].end_word;
|
|
}
|
|
|
|
// set up succ / pred
|
|
for (int i = 0; i < int(func.basic_blocks.size()); i++) {
|
|
auto& b = func.basic_blocks[i];
|
|
if (blocks.at(i).branch_always) {
|
|
// likely branch, already set up.
|
|
ASSERT(likely_delay_slot_blocks.count(i));
|
|
continue;
|
|
} else {
|
|
ASSERT(!likely_delay_slot_blocks.count(i));
|
|
}
|
|
bool not_last = (i + 1) < int(func.basic_blocks.size());
|
|
|
|
if (b.end_word == b.start_word) {
|
|
// there's no room for a branch here, fall through to the end
|
|
if (not_last) {
|
|
link_fall_through(i, i + 1, blocks);
|
|
}
|
|
} else {
|
|
// room for at least a likely branch, try that first.
|
|
int likely_branch_idx = b.end_word - 1;
|
|
ASSERT(likely_branch_idx >= b.start_word);
|
|
auto& likely_branch_candidate = func.instructions.at(likely_branch_idx);
|
|
|
|
if (is_branch(likely_branch_candidate, true)) {
|
|
// is a likely branch
|
|
blocks.at(i).has_branch = true;
|
|
blocks.at(i).branch_likely = true;
|
|
// blocks.at(i).kind = CfgVtx::DelaySlotKind::NO_DELAY;
|
|
bool branch_always = is_always_branch(likely_branch_candidate);
|
|
|
|
// need to find block target
|
|
int block_target = -1;
|
|
int label_target = likely_branch_candidate.get_label_target();
|
|
ASSERT(label_target != -1);
|
|
const auto& label = file.labels.at(label_target);
|
|
// ASSERT(label.target_segment == seg);
|
|
ASSERT((label.offset % 4) == 0);
|
|
int offset = label.offset / 4 - func.start_word;
|
|
ASSERT(offset >= 0);
|
|
for (int j = int(func.basic_blocks.size()); j-- > 0;) {
|
|
if (func.basic_blocks[j].start_word == offset) {
|
|
block_target = j;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ASSERT(block_target != -1);
|
|
// "branch" to delay slot, which then "falls through" to the destination.
|
|
link_branch(i, i + 1, blocks);
|
|
|
|
if (branch_always) {
|
|
// don't continue to the next one
|
|
blocks.at(i).branch_always = true;
|
|
} else {
|
|
// not an always branch
|
|
if (not_last) {
|
|
// "fall through" to after the delay slot block.
|
|
// don't take the delay slot.
|
|
link_fall_through_likely(i, i + 2, blocks);
|
|
}
|
|
}
|
|
|
|
auto& delay_block = blocks.at(i + 1);
|
|
delay_block.branch_likely = false;
|
|
delay_block.branch_always = true;
|
|
delay_block.has_branch = true;
|
|
auto inserted = likely_delay_slot_blocks.insert(i + 1).second;
|
|
ASSERT(inserted);
|
|
link_branch(i + 1, block_target, blocks);
|
|
|
|
} else {
|
|
if (b.end_word - b.start_word < 2) {
|
|
// no room for a branch, just fall through
|
|
if (not_last) {
|
|
link_fall_through(i, i + 1, blocks);
|
|
}
|
|
} else {
|
|
// try as a normal branch.
|
|
int idx = b.end_word - 2;
|
|
ASSERT(idx >= b.start_word);
|
|
auto& branch_candidate = func.instructions.at(idx);
|
|
if (is_branch(branch_candidate, false)) {
|
|
blocks.at(i).has_branch = true;
|
|
blocks.at(i).branch_likely = false;
|
|
bool branch_always = is_always_branch(branch_candidate);
|
|
|
|
// need to find block target
|
|
int block_target = -1;
|
|
int label_target = branch_candidate.get_label_target();
|
|
ASSERT(label_target != -1);
|
|
const auto& label = file.labels.at(label_target);
|
|
// ASSERT(label.target_segment == seg);
|
|
ASSERT((label.offset % 4) == 0);
|
|
int offset = label.offset / 4 - func.start_word;
|
|
ASSERT(offset >= 0);
|
|
|
|
for (int j = int(func.basic_blocks.size()); j-- > 0;) {
|
|
if (func.basic_blocks[j].start_word == offset) {
|
|
block_target = j;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ASSERT(block_target != -1);
|
|
link_branch(i, block_target, blocks);
|
|
|
|
if (branch_always) {
|
|
// don't continue to the next one
|
|
blocks.at(i).branch_always = true;
|
|
} else {
|
|
// not an always branch
|
|
if (not_last) {
|
|
link_fall_through(i, i + 1, blocks);
|
|
}
|
|
}
|
|
} else {
|
|
// not a branch.
|
|
if (not_last) {
|
|
link_fall_through(i, i + 1, blocks);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return blocks;
|
|
}
|
|
|
|
/*!
|
|
* Does the given block require a label in front of it?
|
|
*/
|
|
bool block_requires_label(const Function* f,
|
|
const std::vector<M2C_Block>& blocks,
|
|
size_t block_idx) {
|
|
const auto& block = blocks[block_idx];
|
|
if (block.pred.empty()) {
|
|
// no way to get to this block??
|
|
if (block_idx != 0) {
|
|
// don't warn on the first block.
|
|
lg::warn("Mips2C function {} block {} is unreachable.", f->name(), block_idx);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (block.pred.size() == 1 && block_idx > 0 && block.pred.front() == (int)block_idx - 1 &&
|
|
blocks.at(block_idx - 1).succ_ft == (int)block_idx) {
|
|
// the only way to get to this block is to fall through, no need for a label.
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
namespace {
|
|
// hack counter for total number of unknown instruction. TODO remove
|
|
int g_unknown = 0;
|
|
} // namespace
|
|
|
|
/*!
|
|
* Complain about an unknown instruction.
|
|
*/
|
|
Mips2C_Line handle_unknown(const std::string& instr_str) {
|
|
g_unknown++;
|
|
lg::warn("mips2c unknown: {}", instr_str);
|
|
return fmt::format("// Unknown instr: {}", instr_str);
|
|
}
|
|
|
|
Mips2C_Line handle_generic_load(const Instruction& i0, const std::string& instr_str) {
|
|
if (!i0.get_src(0).is_imm()) {
|
|
// might be a load relative to a label
|
|
return handle_unknown(instr_str);
|
|
}
|
|
return {fmt::format("c->{}({}, {}, {});", i0.op_name_to_string(), reg_to_name(i0.get_dst(0)),
|
|
i0.get_src(0).get_imm(), reg_to_name(i0.get_src(1))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_lwc1(const Instruction& i0,
|
|
const std::string& instr_str,
|
|
const LinkedObjectFile* file) {
|
|
if (i0.get_src(0).is_label() && i0.get_src(1).is_reg(Register(Reg::GPR, Reg::FP))) {
|
|
auto& label = file->labels.at(i0.get_src(0).get_label());
|
|
auto& word = file->words_by_seg.at(label.target_segment).at(label.offset / 4);
|
|
ASSERT(word.kind() == LinkedWord::PLAIN_DATA);
|
|
float f;
|
|
memcpy(&f, &word.data, 4);
|
|
return {fmt::format("c->fprs[{}] = {};", reg_to_name(i0.get_dst(0)), float_to_string(f)),
|
|
instr_str};
|
|
} else {
|
|
return handle_generic_load(i0, instr_str);
|
|
}
|
|
}
|
|
|
|
Mips2C_Line handle_lw(Mips2C_Output& out,
|
|
const Instruction& i0,
|
|
const std::string& instr_str,
|
|
const LinkedObjectFile* file,
|
|
GameVersion version) {
|
|
if (i0.get_src(1).is_reg(rs7()) && i0.get_src(0).is_sym()) {
|
|
// symbol load.
|
|
out.require_symbol(i0.get_src(0).get_sym());
|
|
return {fmt::format("c->load_symbol{}({}, cache.{});", version == GameVersion::Jak1 ? "" : "2",
|
|
reg_to_name(i0.get_dst(0)), goal_to_c_name(i0.get_src(0).get_sym())),
|
|
instr_str};
|
|
}
|
|
if (i0.get_src(1).is_reg(rfp()) && i0.get_src(0).is_label()) {
|
|
const auto& label = file->labels.at(i0.get_src(0).get_label());
|
|
ASSERT((label.offset % 4) == 0);
|
|
const auto& word = file->words_by_seg.at(label.target_segment).at(label.offset / 4);
|
|
ASSERT(word.kind() == LinkedWord::PLAIN_DATA);
|
|
u32 val_u32 = word.data;
|
|
float val_float;
|
|
memcpy(&val_float, &val_u32, 4);
|
|
std::string comment = fmt::format("{} {}", instr_str, float_to_string(val_float));
|
|
return {fmt::format("c->lw_float_constant({}, 0x{:08x});", reg_to_name(i0.get_dst(0)), val_u32),
|
|
comment};
|
|
} else {
|
|
// fall back to standard loads
|
|
return handle_generic_load(i0, instr_str);
|
|
}
|
|
}
|
|
|
|
Mips2C_Line handle_generic_store(Mips2C_Output& /*out*/,
|
|
const Instruction& i0,
|
|
const std::string& instr_str) {
|
|
return {fmt::format("c->{}({}, {}, {});", i0.op_name_to_string(), reg_to_name(i0.get_src(0)),
|
|
i0.get_src(1).get_imm(), reg_to_name(i0.get_src(2))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_generic_op2_u16(const Instruction& i0, const std::string& instr_str) {
|
|
return {fmt::format("c->{}({}, {}, {});", i0.op_name_to_string(), reg_to_name(i0.get_dst(0)),
|
|
reg_to_name(i0.get_src(0)), i0.get_src(1).get_imm()),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_daddiu(Mips2C_Output& out,
|
|
const Instruction& i0,
|
|
const std::string& instr_str,
|
|
GameVersion version) {
|
|
if (i0.get_src(1).is_label()) {
|
|
return {instr_str, instr_str};
|
|
} else if (i0.get_src(0).is_reg(rs7()) && i0.get_src(1).is_sym("#t")) {
|
|
return {fmt::format("c->{}({}, {}, {});", i0.op_name_to_string(), reg_to_name(i0.get_dst(0)),
|
|
reg_to_name(i0.get_src(0)), true_symbol_offset(version)),
|
|
instr_str};
|
|
} else if (i0.get_src(0).is_reg(rs7()) && i0.get_src(1).is_sym()) {
|
|
out.require_symbol(i0.get_src(1).get_sym());
|
|
return {fmt::format("c->load_symbol_addr({}, cache.{});", reg_to_name(i0.get_dst(0)),
|
|
goal_to_c_name(i0.get_src(1).get_sym())),
|
|
instr_str};
|
|
} else {
|
|
return handle_generic_op2_u16(i0, instr_str);
|
|
}
|
|
}
|
|
|
|
Mips2C_Line handle_sw(Mips2C_Output& out,
|
|
const Instruction& i0,
|
|
const std::string& instr_str,
|
|
GameVersion version) {
|
|
if (i0.get_src(1).is_sym() && i0.get_src(2).is_reg(rs7())) {
|
|
out.require_symbol(i0.get_src(1).get_sym());
|
|
return {fmt::format("c->store_symbol{}({}, cache.{});", version == GameVersion::Jak1 ? "" : "2",
|
|
reg_to_name(i0.get_src(0)), goal_to_c_name(i0.get_src(1).get_sym())),
|
|
instr_str};
|
|
return handle_unknown(instr_str);
|
|
// auto name = i0.get_src(1).get_sym();
|
|
// // store into symbol table!
|
|
// SimpleAtom val;
|
|
// if (i0.get_src(0).is_reg(rs7())) {
|
|
// // store a false
|
|
// val = SimpleAtom::make_sym_val("#f");
|
|
// } else if (i0.get_src(0).is_reg(rr0())) {
|
|
// // store a 0
|
|
// val = SimpleAtom::make_int_constant(0);
|
|
// } else {
|
|
// // store a register.
|
|
// val = make_src_atom(i0.get_src(0).get_reg(), idx);
|
|
// }
|
|
// return std::make_unique<StoreOp>(4, StoreOp::Kind::INTEGER,
|
|
// SimpleAtom::make_sym_val(name).as_expr(), val, idx);
|
|
} else {
|
|
return handle_generic_store(out, i0, instr_str);
|
|
}
|
|
}
|
|
|
|
std::string dest_to_char(u8 x) {
|
|
if (x == 0) {
|
|
return "NONE";
|
|
}
|
|
std::string dest;
|
|
if (x & 8)
|
|
dest.push_back('x');
|
|
if (x & 4)
|
|
dest.push_back('y');
|
|
if (x & 2)
|
|
dest.push_back('z');
|
|
if (x & 1)
|
|
dest.push_back('w');
|
|
return dest;
|
|
}
|
|
|
|
Mips2C_Line handle_generic_op3_bc_mask(const Instruction& i0,
|
|
const std::string& instr_str,
|
|
const std::string& op_name) {
|
|
return {fmt::format("c->{}(DEST::{}, BC::{}, {}, {}, {});", op_name, dest_to_char(i0.cop2_dest),
|
|
i0.cop2_bc_to_char(), reg_to_name(i0.get_dst(0)), reg_to_name(i0.get_src(0)),
|
|
reg_to_name(i0.get_src(1))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_generic_op3_mask(const Instruction& i0,
|
|
const std::string& instr_str,
|
|
const std::string& op_name) {
|
|
return {fmt::format("c->{}(DEST::{}, {}, {}, {});", op_name, dest_to_char(i0.cop2_dest),
|
|
reg_to_name(i0.get_dst(0)), reg_to_name(i0.get_src(0)),
|
|
reg_to_name(i0.get_src(1))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_generic_op3(const Instruction& i0,
|
|
const std::string& instr_str,
|
|
const std::optional<std::string>& name_override) {
|
|
return {fmt::format("c->{}({}, {}, {});", name_override ? *name_override : i0.op_name_to_string(),
|
|
reg_to_name(i0.get_dst(0)), reg_to_name(i0.get_src(0)),
|
|
reg_to_name(i0.get_src(1))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_generic_op2_mask(const Instruction& i0,
|
|
const std::string& instr_str,
|
|
const std::string& op_name) {
|
|
return {fmt::format("c->{}(DEST::{}, {}, {});", op_name, dest_to_char(i0.cop2_dest),
|
|
reg_to_name(i0.get_dst(0)), reg_to_name(i0.get_src(0))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_generic_op2(const Instruction& i0,
|
|
const std::string& instr_str,
|
|
const std::string& op_name) {
|
|
return {fmt::format("c->{}({}, {});", op_name, reg_to_name(i0.get_dst(0)),
|
|
reg_to_name(i0.get_src(0))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_div_divu(const Instruction& i0,
|
|
const std::string& instr_str,
|
|
const std::string& op_name) {
|
|
return {fmt::format("c->{}({}, {});", op_name, reg_to_name(i0.get_src(0)),
|
|
reg_to_name(i0.get_src(1))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_generic_op1(const Instruction& i0,
|
|
const std::string& instr_str,
|
|
const std::string& op_name) {
|
|
return {fmt::format("c->{}({});", op_name, reg_to_name(i0.get_dst(0))), instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_plain_op(const Instruction& /*i0*/,
|
|
const std::string& instr_str,
|
|
const std::string& op_name) {
|
|
return {fmt::format("c->{}();", op_name), instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_or(const Instruction& i0, const std::string& instr_str) {
|
|
if (is_gpr_3(i0, InstructionKind::OR, {}, rs7(), rr0())) {
|
|
// set reg_dest to #f : or reg_dest, s7, r0
|
|
return {
|
|
fmt::format("c->mov64({}, {});", reg_to_name(i0.get_dst(0)), reg_to_name(i0.get_src(0))),
|
|
instr_str};
|
|
} else if (is_gpr_3(i0, InstructionKind::OR, {}, rr0(), rr0())) {
|
|
// set reg_dest to 0 : or reg_dest, r0, r0
|
|
return {fmt::format("c->gprs[{}].du64[0] = 0;", reg_to_name(i0.get_dst(0))), instr_str};
|
|
} else if (is_gpr_3(i0, InstructionKind::OR, {}, {}, rr0())) {
|
|
// set dst to src : or dst, src, r0
|
|
return {
|
|
fmt::format("c->mov64({}, {});", reg_to_name(i0.get_dst(0)), reg_to_name(i0.get_src(0))),
|
|
instr_str};
|
|
} else {
|
|
// actually do a logical OR of two registers: or a0, a1, a2
|
|
return handle_generic_op3(i0, instr_str, "or_");
|
|
}
|
|
}
|
|
|
|
Mips2C_Line handle_sll(const Instruction& i0, const std::string& instr_str) {
|
|
if (is_nop(i0)) {
|
|
return {"// nop", instr_str};
|
|
} else {
|
|
return handle_generic_op2_u16(i0, instr_str);
|
|
}
|
|
}
|
|
|
|
Mips2C_Line handle_vmula_bc(const Instruction& i0, const std::string& instr_str) {
|
|
return {fmt::format("c->vmula_bc(DEST::{}, BC::{}, {}, {});", dest_to_char(i0.cop2_dest),
|
|
i0.cop2_bc_to_char(), reg_to_name(i0.get_src(0)), reg_to_name(i0.get_src(1))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_vmadda_bc(const Instruction& i0, const std::string& instr_str) {
|
|
return {fmt::format("c->vmadda_bc(DEST::{}, BC::{}, {}, {});", dest_to_char(i0.cop2_dest),
|
|
i0.cop2_bc_to_char(), reg_to_name(i0.get_src(0)), reg_to_name(i0.get_src(1))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_vmadda(const Instruction& i0, const std::string& instr_str) {
|
|
return {fmt::format("c->vmadda(DEST::{}, {}, {});", dest_to_char(i0.cop2_dest),
|
|
reg_to_name(i0.get_src(0)), reg_to_name(i0.get_src(1))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_vmsuba(const Instruction& i0, const std::string& instr_str) {
|
|
return {fmt::format("c->vmsuba(DEST::{}, {}, {});", dest_to_char(i0.cop2_dest),
|
|
reg_to_name(i0.get_src(0)), reg_to_name(i0.get_src(1))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_vmula(const Instruction& i0, const std::string& instr_str) {
|
|
return {fmt::format("c->vmula(DEST::{}, {}, {});", dest_to_char(i0.cop2_dest),
|
|
reg_to_name(i0.get_src(0)), reg_to_name(i0.get_src(1))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_vadda_bc(const Instruction& i0, const std::string& instr_str) {
|
|
return {fmt::format("c->vadda_bc(DEST::{}, BC::{}, {}, {});", dest_to_char(i0.cop2_dest),
|
|
i0.cop2_bc_to_char(), reg_to_name(i0.get_src(0)), reg_to_name(i0.get_src(1))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_vmsuba_bc(const Instruction& i0, const std::string& instr_str) {
|
|
return {fmt::format("c->vmsuba_bc(DEST::{}, BC::{}, {}, {});", dest_to_char(i0.cop2_dest),
|
|
i0.cop2_bc_to_char(), reg_to_name(i0.get_src(0)), reg_to_name(i0.get_src(1))),
|
|
instr_str};
|
|
}
|
|
|
|
std::string reg64_or_zero(const InstructionAtom& atom) {
|
|
if (atom.is_reg(Register(Reg::GPR, Reg::R0))) {
|
|
return "0";
|
|
} else {
|
|
return fmt::format("c->sgpr64({})", atom.get_reg().to_string());
|
|
}
|
|
}
|
|
|
|
Mips2C_Line handle_branch_reg2(const Instruction& i0,
|
|
const std::string& instr_str,
|
|
const std::string& op_name) {
|
|
return {fmt::format("bc = {} {} {};", reg64_or_zero(i0.get_src(0)), op_name,
|
|
reg64_or_zero(i0.get_src(1))),
|
|
instr_str};
|
|
}
|
|
|
|
Mips2C_Line handle_non_likely_branch_bc(const Instruction& i0, const std::string& instr_str) {
|
|
switch (i0.kind) {
|
|
case InstructionKind::BNE:
|
|
return handle_branch_reg2(i0, instr_str, "!=");
|
|
case InstructionKind::BEQ:
|
|
return handle_branch_reg2(i0, instr_str, "==");
|
|
case InstructionKind::BLTZ:
|
|
return {fmt::format("bc = ((s64){}) < 0;", reg64_or_zero(i0.get_src(0))), instr_str};
|
|
case InstructionKind::BGTZ:
|
|
return {fmt::format("bc = ((s64){}) > 0;", reg64_or_zero(i0.get_src(0))), instr_str};
|
|
case InstructionKind::BGEZ:
|
|
return {fmt::format("bc = ((s64){}) >= 0;", reg64_or_zero(i0.get_src(0))), instr_str};
|
|
case InstructionKind::BLEZ:
|
|
return {fmt::format("bc = ((s64){}) <= 0;", reg64_or_zero(i0.get_src(0))), instr_str};
|
|
case InstructionKind::BC1F:
|
|
return {fmt::format("bc = !cop1_bc;"), instr_str};
|
|
case InstructionKind::BC1T:
|
|
return {fmt::format("bc = cop1_bc;"), instr_str};
|
|
default:
|
|
return handle_unknown(instr_str);
|
|
}
|
|
}
|
|
|
|
Mips2C_Line handle_likely_branch_bc(const Instruction& i0, const std::string& instr_str) {
|
|
switch (i0.kind) {
|
|
case InstructionKind::BLTZL:
|
|
return {fmt::format("((s64){}) < 0", reg64_or_zero(i0.get_src(0))), instr_str};
|
|
case InstructionKind::BGEZL:
|
|
return {fmt::format("((s64){}) >= 0", reg64_or_zero(i0.get_src(0))), instr_str};
|
|
case InstructionKind::BGTZL:
|
|
return {fmt::format("((s64){}) > 0", reg64_or_zero(i0.get_src(0))), instr_str};
|
|
case InstructionKind::BNEL:
|
|
return {fmt::format("((s64){}) != ((s64){})", reg64_or_zero(i0.get_src(0)),
|
|
reg64_or_zero(i0.get_src(1))),
|
|
instr_str};
|
|
case InstructionKind::BEQL:
|
|
return {fmt::format("((s64){}) == ((s64){})", reg64_or_zero(i0.get_src(0)),
|
|
reg64_or_zero(i0.get_src(1))),
|
|
instr_str};
|
|
case InstructionKind::BC1TL:
|
|
return {"cop1_bc", instr_str};
|
|
case InstructionKind::BC1FL:
|
|
return {"!cop1_bc", instr_str};
|
|
default:
|
|
return handle_unknown(instr_str);
|
|
}
|
|
}
|
|
|
|
Mips2C_Line handle_vdiv(const Instruction& i0, const std::string& instr_string) {
|
|
return {fmt::format("c->vdiv({}, BC::{}, {}, BC::{});", reg_to_name(i0.get_src(0)),
|
|
"xyzw"[i0.get_src(1).get_vf_field()], reg_to_name(i0.get_src(2)),
|
|
"xyzw"[i0.get_src(3).get_vf_field()]),
|
|
instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_vrsqrt(const Instruction& i0, const std::string& instr_string) {
|
|
return {fmt::format("c->vrsqrt({}, BC::{}, {}, BC::{});", reg_to_name(i0.get_src(0)),
|
|
"xyzw"[i0.get_src(1).get_vf_field()], reg_to_name(i0.get_src(2)),
|
|
"xyzw"[i0.get_src(3).get_vf_field()]),
|
|
instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_vsqrt(const Instruction& i0, const std::string& instr_string) {
|
|
return {fmt::format("c->vsqrt({}, BC::{});", reg_to_name(i0.get_src(0)),
|
|
"xyzw"[i0.get_src(1).get_vf_field()]),
|
|
instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_vrxor(const Instruction& i0, const std::string& instr_string) {
|
|
return {fmt::format("c->vrxor({}, BC::{});", reg_to_name(i0.get_src(0)), i0.cop2_bc_to_char()),
|
|
instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_vrget(const Instruction& i0, const std::string& instr_string) {
|
|
return {fmt::format("c->vrget(DEST::{}, {});", dest_to_char(i0.cop2_dest),
|
|
reg_to_name(i0.get_dst(0))),
|
|
instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_vrnext(const Instruction& i0, const std::string& instr_string) {
|
|
return {fmt::format("c->vrnext(DEST::{}, {});", dest_to_char(i0.cop2_dest),
|
|
reg_to_name(i0.get_dst(0))),
|
|
instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_por(const Instruction& i0, const std::string& instr_string) {
|
|
if (is_gpr_3(i0, InstructionKind::POR, {}, {}, rr0())) {
|
|
return {fmt::format("c->mov128_gpr_gpr({}, {});", reg_to_name(i0.get_dst(0)),
|
|
reg_to_name(i0.get_src(0))),
|
|
instr_string};
|
|
} else {
|
|
return handle_generic_op3(i0, instr_string, {});
|
|
}
|
|
}
|
|
|
|
Mips2C_Line handle_vopmula(const Instruction& i0, const std::string& instr_string) {
|
|
return {
|
|
fmt::format("c->vopmula({}, {});", reg_to_name(i0.get_src(0)), reg_to_name(i0.get_src(1))),
|
|
instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_vopmsub(const Instruction& i0, const std::string& instr_string) {
|
|
return {fmt::format("c->vopmsub({}, {}, {});", reg_to_name(i0.get_dst(0)),
|
|
reg_to_name(i0.get_src(0)), reg_to_name(i0.get_src(1))),
|
|
instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_lui(const Instruction& i0,
|
|
const std::string& instr_string,
|
|
Mips2C_Output& op,
|
|
GameVersion version) {
|
|
if (i0.get_src(0).get_imm() == 0x7000) {
|
|
op.require_symbol("*fake-scratchpad-data*");
|
|
return {fmt::format("get_fake_spad_addr{}({}, cache.fake_scratchpad_data, 0, c);",
|
|
version == GameVersion::Jak1 ? "" : "2", reg_to_name(i0.get_dst(0))),
|
|
instr_string};
|
|
} else {
|
|
return {fmt::format("c->lui({}, {});", reg_to_name(i0.get_dst(0)), i0.get_src(0).get_imm()),
|
|
instr_string};
|
|
}
|
|
}
|
|
|
|
Mips2C_Line handle_clts(const Instruction& i0, const std::string& instr_string) {
|
|
return {fmt::format("cop1_bc = c->fprs[{}] < c->fprs[{}];", reg_to_name(i0.get_src(0)),
|
|
reg_to_name(i0.get_src(1))),
|
|
instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_cles(const Instruction& i0, const std::string& instr_string) {
|
|
return {fmt::format("cop1_bc = c->fprs[{}] <= c->fprs[{}];", reg_to_name(i0.get_src(0)),
|
|
reg_to_name(i0.get_src(1))),
|
|
instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_ceqs(const Instruction& i0, const std::string& instr_string) {
|
|
return {fmt::format("cop1_bc = c->fprs[{}] == c->fprs[{}];", reg_to_name(i0.get_src(0)),
|
|
reg_to_name(i0.get_src(1))),
|
|
instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_pmfhl_lh(const Instruction& i0, const std::string& instr_string) {
|
|
return {fmt::format("c->pmfhl_lh({});", reg_to_name(i0.get_dst(0))), instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_ctc2(const Instruction& i0, const std::string& instr_string) {
|
|
return {fmt::format("{} = c->gpr_src({}).du16[0];", reg_to_name(i0.get_dst(0)),
|
|
reg_to_name(i0.get_src(0))),
|
|
instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_cfc2(const Instruction& i0, const std::string& instr_string) {
|
|
return {fmt::format("c->gprs[{}].du64[0] = {};", reg_to_name(i0.get_dst(0)),
|
|
reg_to_name(i0.get_src(0))),
|
|
instr_string};
|
|
}
|
|
|
|
Mips2C_Line handle_normal_instr(Mips2C_Output& output,
|
|
const Instruction& i0,
|
|
const std::string& instr_str,
|
|
int& unknown_count,
|
|
const LinkedObjectFile* file,
|
|
GameVersion version) {
|
|
switch (i0.kind) {
|
|
case InstructionKind::CTC2:
|
|
return handle_ctc2(i0, instr_str);
|
|
case InstructionKind::CFC2:
|
|
return handle_cfc2(i0, instr_str);
|
|
case InstructionKind::LW:
|
|
return handle_lw(output, i0, instr_str, file, version);
|
|
case InstructionKind::LB:
|
|
case InstructionKind::LWL:
|
|
case InstructionKind::LWR:
|
|
case InstructionKind::LDR:
|
|
case InstructionKind::LDL:
|
|
case InstructionKind::LBU:
|
|
case InstructionKind::LWU:
|
|
case InstructionKind::LQ:
|
|
case InstructionKind::LQC2:
|
|
case InstructionKind::LH:
|
|
case InstructionKind::LHU:
|
|
case InstructionKind::LD:
|
|
return handle_generic_load(i0, instr_str);
|
|
case InstructionKind::LWC1:
|
|
return handle_lwc1(i0, instr_str, file);
|
|
case InstructionKind::SQ:
|
|
case InstructionKind::SQC2:
|
|
case InstructionKind::SH:
|
|
case InstructionKind::SD:
|
|
case InstructionKind::SB:
|
|
case InstructionKind::SWC1:
|
|
return handle_generic_store(output, i0, instr_str);
|
|
case InstructionKind::VADD_BC:
|
|
return handle_generic_op3_bc_mask(i0, instr_str, "vadd_bc");
|
|
case InstructionKind::VMINI_BC:
|
|
return handle_generic_op3_bc_mask(i0, instr_str, "vmini_bc");
|
|
case InstructionKind::VMAX_BC:
|
|
return handle_generic_op3_bc_mask(i0, instr_str, "vmax_bc");
|
|
case InstructionKind::VSUB_BC:
|
|
return handle_generic_op3_bc_mask(i0, instr_str, "vsub_bc");
|
|
case InstructionKind::VMUL_BC:
|
|
return handle_generic_op3_bc_mask(i0, instr_str, "vmul_bc");
|
|
case InstructionKind::VMADD:
|
|
return handle_generic_op3_mask(i0, instr_str, "vmadd");
|
|
case InstructionKind::VMSUB:
|
|
return handle_generic_op3_mask(i0, instr_str, "vmsub");
|
|
case InstructionKind::VMUL:
|
|
return handle_generic_op3_mask(i0, instr_str, "vmul");
|
|
case InstructionKind::VADD:
|
|
return handle_generic_op3_mask(i0, instr_str, "vadd");
|
|
case InstructionKind::VSUB:
|
|
return handle_generic_op3_mask(i0, instr_str, "vsub");
|
|
case InstructionKind::VMINI:
|
|
return handle_generic_op3_mask(i0, instr_str, "vmini");
|
|
case InstructionKind::VMAX:
|
|
return handle_generic_op3_mask(i0, instr_str, "vmax");
|
|
case InstructionKind::OR:
|
|
return handle_or(i0, instr_str);
|
|
case InstructionKind::SW:
|
|
return handle_sw(output, i0, instr_str, version);
|
|
case InstructionKind::VMOVE:
|
|
return handle_generic_op2_mask(i0, instr_str, "vmove");
|
|
case InstructionKind::VITOF0:
|
|
return handle_generic_op2_mask(i0, instr_str, "vitof0");
|
|
case InstructionKind::VITOF12:
|
|
return handle_generic_op2_mask(i0, instr_str, "vitof12");
|
|
case InstructionKind::VITOF15:
|
|
return handle_generic_op2_mask(i0, instr_str, "vitof15");
|
|
case InstructionKind::VFTOI0:
|
|
return handle_generic_op2_mask(i0, instr_str, "vftoi0");
|
|
case InstructionKind::VFTOI4:
|
|
return handle_generic_op2_mask(i0, instr_str, "vftoi4");
|
|
case InstructionKind::VFTOI12:
|
|
return handle_generic_op2_mask(i0, instr_str, "vftoi12");
|
|
case InstructionKind::VABS:
|
|
return handle_generic_op2_mask(i0, instr_str, "vabs");
|
|
case InstructionKind::VADDQ:
|
|
return handle_generic_op2_mask(i0, instr_str, "vaddq");
|
|
case InstructionKind::VMR32:
|
|
return handle_generic_op2_mask(i0, instr_str, "vmr32");
|
|
case InstructionKind::ANDI:
|
|
case InstructionKind::ORI:
|
|
case InstructionKind::XORI:
|
|
case InstructionKind::SRA:
|
|
case InstructionKind::DSLL:
|
|
case InstructionKind::DSLL32:
|
|
case InstructionKind::DSRA:
|
|
case InstructionKind::DSRA32:
|
|
case InstructionKind::DSRL32:
|
|
case InstructionKind::DSRL:
|
|
case InstructionKind::SRL:
|
|
case InstructionKind::PSRAW:
|
|
case InstructionKind::PSRAH:
|
|
case InstructionKind::PSRLH:
|
|
case InstructionKind::PSLLW:
|
|
case InstructionKind::PSLLH:
|
|
return handle_generic_op2_u16(i0, instr_str);
|
|
case InstructionKind::SLL:
|
|
return handle_sll(i0, instr_str);
|
|
case InstructionKind::DADDU:
|
|
case InstructionKind::DSUBU:
|
|
case InstructionKind::ADDU:
|
|
case InstructionKind::PEXTLH:
|
|
case InstructionKind::PEXTLB:
|
|
case InstructionKind::MOVN:
|
|
case InstructionKind::PEXTUW:
|
|
case InstructionKind::PEXTUH:
|
|
case InstructionKind::PEXTLW:
|
|
case InstructionKind::PCPYUD:
|
|
case InstructionKind::PCPYLD:
|
|
case InstructionKind::PPACH:
|
|
case InstructionKind::PINTEH:
|
|
case InstructionKind::PCGTW:
|
|
case InstructionKind::PPACB:
|
|
case InstructionKind::PADDW:
|
|
case InstructionKind::PADDB:
|
|
case InstructionKind::PEXTUB:
|
|
case InstructionKind::PMULTH:
|
|
case InstructionKind::PMADDH:
|
|
case InstructionKind::PADDH:
|
|
case InstructionKind::PSUBH:
|
|
case InstructionKind::PMINH:
|
|
case InstructionKind::MOVZ:
|
|
case InstructionKind::MULT3:
|
|
case InstructionKind::MULTU3:
|
|
case InstructionKind::PMINW:
|
|
case InstructionKind::PMAXW:
|
|
case InstructionKind::PMAXH:
|
|
case InstructionKind::SUBU:
|
|
case InstructionKind::SLT:
|
|
case InstructionKind::SLTU:
|
|
case InstructionKind::DSRAV:
|
|
case InstructionKind::DSLLV:
|
|
case InstructionKind::DSRLV:
|
|
case InstructionKind::SLLV:
|
|
case InstructionKind::PAND:
|
|
case InstructionKind::PCEQB:
|
|
case InstructionKind::PPACW:
|
|
case InstructionKind::PCEQW:
|
|
return handle_generic_op3(i0, instr_str, {});
|
|
case InstructionKind::MULS:
|
|
return handle_generic_op3(i0, instr_str, "muls");
|
|
case InstructionKind::DIVS:
|
|
return handle_generic_op3(i0, instr_str, "divs");
|
|
case InstructionKind::ADDS:
|
|
return handle_generic_op3(i0, instr_str, "adds");
|
|
case InstructionKind::SUBS:
|
|
return handle_generic_op3(i0, instr_str, "subs");
|
|
case InstructionKind::MINS:
|
|
return handle_generic_op3(i0, instr_str, "mins");
|
|
case InstructionKind::MAXS:
|
|
return handle_generic_op3(i0, instr_str, "maxs");
|
|
case InstructionKind::XOR:
|
|
return handle_generic_op3(i0, instr_str, "xor_");
|
|
case InstructionKind::AND:
|
|
return handle_generic_op3(i0, instr_str, "and_"); // and isn't allowed in C++
|
|
case InstructionKind::NOR:
|
|
return handle_generic_op3(i0, instr_str, "nor"); // and isn't allowed in C++
|
|
case InstructionKind::DADDIU:
|
|
return handle_daddiu(output, i0, instr_str, version);
|
|
case InstructionKind::ADDIU:
|
|
case InstructionKind::SLTI:
|
|
case InstructionKind::SLTIU:
|
|
return handle_generic_op2_u16(i0, instr_str);
|
|
case InstructionKind::QMTC2:
|
|
return handle_generic_op2(i0, instr_str, "mov128_vf_gpr");
|
|
case InstructionKind::QMFC2:
|
|
return handle_generic_op2(i0, instr_str, "mov128_gpr_vf");
|
|
case InstructionKind::VMULA_BC:
|
|
return handle_vmula_bc(i0, instr_str);
|
|
case InstructionKind::VMADDA_BC:
|
|
return handle_vmadda_bc(i0, instr_str);
|
|
case InstructionKind::VADDA_BC:
|
|
return handle_vadda_bc(i0, instr_str);
|
|
case InstructionKind::VMSUBA_BC:
|
|
return handle_vmsuba_bc(i0, instr_str);
|
|
case InstructionKind::VMADDA:
|
|
return handle_vmadda(i0, instr_str);
|
|
case InstructionKind::VMSUBA:
|
|
return handle_vmsuba(i0, instr_str);
|
|
case InstructionKind::VMULA:
|
|
return handle_vmula(i0, instr_str);
|
|
case InstructionKind::VMADD_BC:
|
|
return handle_generic_op3_bc_mask(i0, instr_str, "vmadd_bc");
|
|
case InstructionKind::VMSUB_BC:
|
|
return handle_generic_op3_bc_mask(i0, instr_str, "vmsub_bc");
|
|
case InstructionKind::VDIV:
|
|
return handle_vdiv(i0, instr_str);
|
|
case InstructionKind::VSQRT:
|
|
return handle_vsqrt(i0, instr_str);
|
|
case InstructionKind::VRSQRT:
|
|
return handle_vrsqrt(i0, instr_str);
|
|
case InstructionKind::VRXOR:
|
|
return handle_vrxor(i0, instr_str);
|
|
case InstructionKind::VRGET:
|
|
return handle_vrget(i0, instr_str);
|
|
case InstructionKind::VRNEXT:
|
|
return handle_vrnext(i0, instr_str);
|
|
case InstructionKind::POR:
|
|
return handle_por(i0, instr_str);
|
|
case InstructionKind::VMULQ:
|
|
return handle_generic_op2_mask(i0, instr_str, "vmulq");
|
|
case InstructionKind::VNOP:
|
|
return {"// nop", instr_str};
|
|
case InstructionKind::MFC1:
|
|
return handle_generic_op2(i0, instr_str, "mfc1");
|
|
case InstructionKind::MTC1:
|
|
return handle_generic_op2(i0, instr_str, "mtc1");
|
|
case InstructionKind::NEGS:
|
|
return handle_generic_op2(i0, instr_str, "negs");
|
|
case InstructionKind::ABSS:
|
|
return handle_generic_op2(i0, instr_str, "abss");
|
|
case InstructionKind::MOVS:
|
|
return handle_generic_op2(i0, instr_str, "movs");
|
|
case InstructionKind::CVTWS:
|
|
return handle_generic_op2(i0, instr_str, "cvtws");
|
|
case InstructionKind::CVTSW:
|
|
return handle_generic_op2(i0, instr_str, "cvtsw");
|
|
case InstructionKind::PEXEW:
|
|
return handle_generic_op2(i0, instr_str, "pexew");
|
|
case InstructionKind::PEXCW:
|
|
return handle_generic_op2(i0, instr_str, "pexcw");
|
|
case InstructionKind::SQRTS:
|
|
return handle_generic_op2(i0, instr_str, "sqrts");
|
|
case InstructionKind::PLZCW:
|
|
return handle_generic_op2(i0, instr_str, "plzcw");
|
|
case InstructionKind::PCPYH:
|
|
return handle_generic_op2(i0, instr_str, "pcpyh");
|
|
case InstructionKind::PROT3W:
|
|
return handle_generic_op2(i0, instr_str, "prot3w");
|
|
case InstructionKind::LUI:
|
|
return handle_lui(i0, instr_str, output, version);
|
|
case InstructionKind::CLTS:
|
|
output.needs_cop1_bc = true;
|
|
return handle_clts(i0, instr_str);
|
|
case InstructionKind::CLES:
|
|
output.needs_cop1_bc = true;
|
|
return handle_cles(i0, instr_str);
|
|
case InstructionKind::CEQS:
|
|
output.needs_cop1_bc = true;
|
|
return handle_ceqs(i0, instr_str);
|
|
case InstructionKind::VWAITQ:
|
|
return handle_plain_op(i0, instr_str, "vwaitq");
|
|
case InstructionKind::VOPMULA:
|
|
return handle_vopmula(i0, instr_str);
|
|
case InstructionKind::VOPMSUB:
|
|
return handle_vopmsub(i0, instr_str);
|
|
case InstructionKind::PMFHL_LH:
|
|
return handle_pmfhl_lh(i0, instr_str);
|
|
case InstructionKind::DIV:
|
|
return handle_div_divu(i0, instr_str, "div");
|
|
case InstructionKind::DIVU:
|
|
return handle_div_divu(i0, instr_str, "divu");
|
|
case InstructionKind::MFHI:
|
|
return handle_generic_op1(i0, instr_str, "mfhi");
|
|
case InstructionKind::MFLO:
|
|
return handle_generic_op1(i0, instr_str, "mflo");
|
|
default:
|
|
unknown_count++;
|
|
return handle_unknown(instr_str);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
struct JumpTableBlock {
|
|
int idx = -1;
|
|
int start_instr = -1;
|
|
int end_instr = -1; // not inclusive
|
|
int succ_branch = -1; // block idx if we take the branch
|
|
int succ_ft = -1; // block idx if we don't take the branch (or there is none)
|
|
bool has_branch = false; // ends in a branch instruction?
|
|
bool branch_likely = false; // that branch is likely branch?
|
|
bool branch_always = false; // that branch is always taken?
|
|
};
|
|
|
|
void run_mips2c_jump_table(Function* f,
|
|
const std::vector<int>& jump_table_locations,
|
|
GameVersion version) {
|
|
lg::info("mips2c-jump on {}", f->name());
|
|
u32 magic_code = std::hash<std::string>()(f->name());
|
|
std::unordered_map<int, int> loc_to_block;
|
|
for (size_t bb_idx = 0; bb_idx < f->basic_blocks.size(); bb_idx++) {
|
|
loc_to_block[f->basic_blocks[bb_idx].start_word] = bb_idx;
|
|
}
|
|
|
|
auto* file = f->ir2.env.file;
|
|
std::unordered_set<int> likely_delay_blocks;
|
|
auto blocks = setup_preds_and_succs(*f, *file, likely_delay_blocks);
|
|
Mips2C_Output output;
|
|
output.jump_table = true;
|
|
int unknown_count = 0;
|
|
|
|
for (size_t block_idx = 0; block_idx < blocks.size(); block_idx++) {
|
|
const auto& block = blocks[block_idx];
|
|
|
|
if (likely_delay_blocks.count(block_idx)) {
|
|
continue;
|
|
}
|
|
|
|
output.output_jump_table_block_label(block_idx);
|
|
|
|
for (int i = block.start_instr; i < block.end_instr; i++) {
|
|
size_t old_line_count = output.lines.size();
|
|
auto& instr = f->instructions.at(i);
|
|
auto instr_str = instr.to_string(file->labels);
|
|
|
|
if (is_branch(instr, {})) {
|
|
if (block.branch_likely) {
|
|
auto branch_line = handle_likely_branch_bc(instr, instr_str);
|
|
output.lines.emplace_back(fmt::format("if ({}) {{", branch_line.code),
|
|
branch_line.comment);
|
|
// next block should be the delay slot
|
|
ASSERT((int)block_idx + 1 == block.succ_branch);
|
|
auto& delay_block = blocks.at(block.succ_branch);
|
|
ASSERT(delay_block.end_instr - delay_block.start_instr == 1); // only 1 instr.
|
|
auto& delay_instr = f->instructions.at(delay_block.start_instr);
|
|
auto delay_instr_str = delay_instr.to_string(file->labels);
|
|
auto delay_instr_line = handle_normal_instr(output, delay_instr, delay_instr_str,
|
|
unknown_count, file, version);
|
|
output.lines.emplace_back(fmt::format(" {}", delay_instr_line.code),
|
|
delay_instr_line.comment);
|
|
ASSERT(delay_block.succ_ft == -1);
|
|
output.lines.emplace_back(fmt::format(" next_block = {};", delay_block.succ_branch), "");
|
|
output.lines.emplace_back("break;");
|
|
output.lines.emplace_back("}", "");
|
|
} else {
|
|
if (is_always_branch(instr)) {
|
|
// skip the branch ins.
|
|
output.lines.emplace_back("//" + instr_str, instr_str);
|
|
// then the delay slot
|
|
ASSERT(i + 1 < block.end_instr);
|
|
i++;
|
|
auto& delay_i = f->instructions.at(i);
|
|
auto delay_i_str = delay_i.to_string(file->labels);
|
|
output.lines.push_back(
|
|
handle_normal_instr(output, delay_i, delay_i_str, unknown_count, file, version));
|
|
ASSERT(i + 1 == block.end_instr);
|
|
// then the goto
|
|
output.lines.emplace_back(fmt::format("next_block = {};", block.succ_branch),
|
|
"branch always\n");
|
|
output.lines.emplace_back("break;");
|
|
|
|
} else {
|
|
// set the branch condition
|
|
output.lines.push_back(handle_non_likely_branch_bc(instr, instr_str));
|
|
// then the delay slot
|
|
ASSERT(i + 1 < block.end_instr);
|
|
i++;
|
|
auto& delay_i = f->instructions.at(i);
|
|
auto delay_i_str = delay_i.to_string(file->labels);
|
|
output.lines.push_back(
|
|
handle_normal_instr(output, delay_i, delay_i_str, unknown_count, file, version));
|
|
ASSERT(i + 1 == block.end_instr);
|
|
// then the goto
|
|
output.lines.emplace_back(
|
|
fmt::format("if (bc) {{next_block = {};}}", block.succ_branch),
|
|
"branch non-likely\n");
|
|
output.lines.emplace_back("break;");
|
|
}
|
|
}
|
|
} else if (is_jr_ra(instr)) {
|
|
// skip
|
|
output.lines.emplace_back("//" + instr_str, instr_str);
|
|
// then the delay slot
|
|
ASSERT(i + 1 < block.end_instr);
|
|
i++;
|
|
auto& delay_i = f->instructions.at(i);
|
|
auto delay_i_str = delay_i.to_string(file->labels);
|
|
output.lines.push_back(
|
|
handle_normal_instr(output, delay_i, delay_i_str, unknown_count, file, version));
|
|
|
|
// then the goto
|
|
output.lines.emplace_back(fmt::format("goto end_of_function;", block.succ_branch),
|
|
"return\n");
|
|
} else if (instr.kind == InstructionKind::JALR) {
|
|
ASSERT(instr.get_dst(0).is_reg(Register(Reg::GPR, Reg::RA)));
|
|
ASSERT(i < block.end_instr - 1);
|
|
output.lines.emplace_back(
|
|
fmt::format("call_addr = c->gprs[{}].du32[0];", reg_to_name(instr.get_src(0))),
|
|
"function call:");
|
|
i++;
|
|
auto& delay_i = f->instructions.at(i);
|
|
auto delay_i_str = delay_i.to_string(file->labels);
|
|
output.lines.push_back(
|
|
handle_normal_instr(output, delay_i, delay_i_str, unknown_count, file, version));
|
|
output.lines.emplace_back("c->jalr(call_addr);", instr_str);
|
|
} else if (instr.kind == InstructionKind::JR) {
|
|
// special case for jr's to handle the jump tableing.
|
|
output.lines.emplace_back(fmt::format("next_block = 0x{:x} ^ c->gprs[{}].du32[0];",
|
|
magic_code, reg_to_name(instr.get_src(0))),
|
|
instr_str);
|
|
output.lines.emplace_back(fmt::format("ASSERT(next_block < {});", f->basic_blocks.size()));
|
|
output.lines.emplace_back("break;");
|
|
|
|
} else {
|
|
output.lines.push_back(
|
|
handle_normal_instr(output, instr, instr_str, unknown_count, file, version));
|
|
}
|
|
|
|
ASSERT(output.lines.size() > old_line_count);
|
|
}
|
|
}
|
|
|
|
std::string jump_loc_table =
|
|
fmt::format("u32 jump_table_vals[{}] = {{\n", jump_table_locations.size());
|
|
for (auto loc : jump_table_locations) {
|
|
auto block = loc_to_block.at(loc + 1);
|
|
jump_loc_table +=
|
|
fmt::format(" 0x{:x}, // = {} ^ {}\n", ((u32)block) ^ magic_code, block, magic_code);
|
|
}
|
|
jump_loc_table += "};\n\n";
|
|
|
|
f->mips2c_output = output.write_to_string(f->guessed_name, version, jump_loc_table);
|
|
if (g_unknown > 0) {
|
|
lg::error("Mips to C pass in {} hit {} unknown instructions", f->name(), g_unknown);
|
|
}
|
|
}
|
|
|
|
void run_mips2c(Function* f, GameVersion version) {
|
|
g_unknown = 0;
|
|
auto* file = f->ir2.env.file;
|
|
std::unordered_set<int> likely_delay_blocks;
|
|
auto blocks = setup_preds_and_succs(*f, *file, likely_delay_blocks);
|
|
Mips2C_Output output;
|
|
int unknown_count = 0;
|
|
|
|
for (size_t block_idx = 0; block_idx < blocks.size(); block_idx++) {
|
|
const auto& block = blocks[block_idx];
|
|
|
|
if (likely_delay_blocks.count(block_idx)) {
|
|
continue;
|
|
}
|
|
|
|
if (block_requires_label(f, blocks, block_idx)) {
|
|
output.output_label(block_idx);
|
|
}
|
|
|
|
for (int i = block.start_instr; i < block.end_instr; i++) {
|
|
size_t old_line_count = output.lines.size();
|
|
auto& instr = f->instructions.at(i);
|
|
auto instr_str = instr.to_string(file->labels);
|
|
|
|
if (is_branch(instr, {})) {
|
|
if (block.branch_likely) {
|
|
auto branch_line = handle_likely_branch_bc(instr, instr_str);
|
|
output.lines.emplace_back(fmt::format("if ({}) {{", branch_line.code),
|
|
branch_line.comment);
|
|
// next block should be the delay slot
|
|
ASSERT((int)block_idx + 1 == block.succ_branch);
|
|
auto& delay_block = blocks.at(block.succ_branch);
|
|
ASSERT(delay_block.end_instr - delay_block.start_instr == 1); // only 1 instr.
|
|
auto& delay_instr = f->instructions.at(delay_block.start_instr);
|
|
auto delay_instr_str = delay_instr.to_string(file->labels);
|
|
auto delay_instr_line = handle_normal_instr(output, delay_instr, delay_instr_str,
|
|
unknown_count, file, version);
|
|
output.lines.emplace_back(fmt::format(" {}", delay_instr_line.code),
|
|
delay_instr_line.comment);
|
|
ASSERT(delay_block.succ_ft == -1);
|
|
output.lines.emplace_back(fmt::format(" goto block_{};", delay_block.succ_branch), "");
|
|
output.lines.emplace_back("}", "");
|
|
} else {
|
|
if (is_always_branch(instr)) {
|
|
// skip the branch ins.
|
|
output.lines.emplace_back("//" + instr_str, instr_str);
|
|
// then the delay slot
|
|
ASSERT(i + 1 < block.end_instr);
|
|
i++;
|
|
auto& delay_i = f->instructions.at(i);
|
|
auto delay_i_str = delay_i.to_string(file->labels);
|
|
output.lines.push_back(
|
|
handle_normal_instr(output, delay_i, delay_i_str, unknown_count, file, version));
|
|
ASSERT(i + 1 == block.end_instr);
|
|
// then the goto
|
|
output.lines.emplace_back(fmt::format("goto block_{};", block.succ_branch),
|
|
"branch always\n");
|
|
} else {
|
|
// set the branch condition
|
|
output.lines.push_back(handle_non_likely_branch_bc(instr, instr_str));
|
|
// then the delay slot
|
|
if (!(i + 1 < block.end_instr)) {
|
|
output.lines.emplace_back("DANGER jump to delay slot, this MUST be fixed manually!",
|
|
"");
|
|
lg::warn("Delay slot weirdness in {}, block {}", f->name(), block_idx);
|
|
}
|
|
i++;
|
|
auto& delay_i = f->instructions.at(i);
|
|
auto delay_i_str = delay_i.to_string(file->labels);
|
|
output.lines.push_back(
|
|
handle_normal_instr(output, delay_i, delay_i_str, unknown_count, file, version));
|
|
// ASSERT(i + 1 == block.end_instr);
|
|
// then the goto
|
|
output.lines.emplace_back(fmt::format("if (bc) {{goto block_{};}}", block.succ_branch),
|
|
"branch non-likely\n");
|
|
}
|
|
}
|
|
} else if (is_jr_ra(instr)) {
|
|
// skip
|
|
output.lines.emplace_back("//" + instr_str, instr_str);
|
|
// then the delay slot
|
|
ASSERT(i + 1 < block.end_instr);
|
|
i++;
|
|
auto& delay_i = f->instructions.at(i);
|
|
auto delay_i_str = delay_i.to_string(file->labels);
|
|
output.lines.push_back(
|
|
handle_normal_instr(output, delay_i, delay_i_str, unknown_count, file, version));
|
|
|
|
// then the goto
|
|
output.lines.emplace_back(fmt::format("goto end_of_function;", block.succ_branch),
|
|
"return\n");
|
|
} else if (instr.kind == InstructionKind::JALR) {
|
|
ASSERT(instr.get_dst(0).is_reg(Register(Reg::GPR, Reg::RA)));
|
|
ASSERT(i < block.end_instr - 1);
|
|
output.lines.emplace_back(
|
|
fmt::format("call_addr = c->gprs[{}].du32[0];", reg_to_name(instr.get_src(0))),
|
|
"function call:");
|
|
i++;
|
|
auto& delay_i = f->instructions.at(i);
|
|
auto delay_i_str = delay_i.to_string(file->labels);
|
|
output.lines.push_back(
|
|
handle_normal_instr(output, delay_i, delay_i_str, unknown_count, file, version));
|
|
output.lines.emplace_back("c->jalr(call_addr);", instr_str);
|
|
} else {
|
|
output.lines.push_back(
|
|
handle_normal_instr(output, instr, instr_str, unknown_count, file, version));
|
|
}
|
|
|
|
ASSERT(output.lines.size() > old_line_count);
|
|
}
|
|
}
|
|
|
|
f->mips2c_output = output.write_to_string(f->guessed_name, version);
|
|
if (g_unknown > 0) {
|
|
lg::error("Mips to C pass in {} hit {} unknown instructions", f->name(), g_unknown);
|
|
}
|
|
}
|
|
} // namespace decompiler
|