mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 02:32:02 -04:00
code_generator: resolve computed-stride jumps into unrolled runs (Duff's device)
A `jr R` with `R = base + index * stride` and no table in memory is how unrolled copy loops enter their body in the middle (Duff's device). The bounded-jump-table resolver only knows the `sltiu/beq; sll; addu; lw; jr` chain, so the emitter fell back to a bare CPS tail-transfer; the trampoline then found no dispatch key at the interior PC (straight-line code, no block leader) and the dirty-RAM interpreter ran the rest of the copy on every call. On Breath of Fire III that is the file decompressor's literal-copy loop (`0x80164D1C`, 32 groups of `lbu/nop/sb`), which fires on every load and was the whole non-kernel interpreted residual of a play session once the kernel patch ranges (#346) and the call vectors (#381) ran native. resolve_computed_stride_jump walks the chain back from the jr through nearest definitions in straight-line code: `addu R, B, S` with S from `sll S, I, k` or the two-sll sum `sll P,I,a; sll Q,I,b; addu S,P,Q`, and then requires the code after the delay slot to be N >= 2 shape-identical groups of `stride` bytes (same opcodes and register fields, immediates free, no control flow inside a group). Targets are run_start + k * stride for k = 0..N. The base register's value is never assumed: the emitter switches on the actual runtime target with the CPS tail-transfer as the default, so the recovered cases can only add native entries, never redirect a jump that lands elsewhere. Wired into both code-generator sites (interior labels + the switch) and the exact-entry discovery walker. On the title's boot EXE it matches exactly one site, the decompressor loop (33 entries); every other jr is untouched. Adds recompiler/tests/test_computed_stride_jump_codegen.py (synthetic PS-EXE with the idiom; asserts the switch, every case and the interior labels), registered with CTest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1133,6 +1133,162 @@ bool resolve_exact_bounded_jump_table(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool resolve_computed_stride_jump(
|
||||
const PS1Executable& exe,
|
||||
uint32_t entry,
|
||||
uint32_t hard_cap,
|
||||
uint32_t jr_pc,
|
||||
uint32_t jr_rs,
|
||||
ExactJumpTable& table,
|
||||
ExactAddressMapper runtime_to_image) {
|
||||
table = {};
|
||||
if (entry >= hard_cap || jr_pc < entry || jr_pc + 8u >= hard_cap ||
|
||||
(entry & 3u) != 0u || (hard_cap & 3u) != 0u ||
|
||||
(jr_pc & 3u) != 0u || jr_rs == 0u || jr_rs == 31u) {
|
||||
return false;
|
||||
}
|
||||
auto read = [&](uint32_t pc) { return exe.read_word(pc); };
|
||||
auto jr_word = read(jr_pc);
|
||||
if (!jr_word.has_value() ||
|
||||
*jr_word != ((jr_rs & 0x1Fu) << 21u | 0x08u)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Nearest definition of `reg` strictly before `use_pc`, walking back at
|
||||
// most `window` instructions through straight-line code only. A control
|
||||
// transfer (or its delay slot) in the way ends the search: the chain
|
||||
// must be one basic block so every path to the jr computes the same
|
||||
// shape.
|
||||
auto nearest_def = [&](uint32_t reg, uint32_t use_pc,
|
||||
uint32_t window) -> std::optional<uint32_t> {
|
||||
if (reg == 0u) return std::nullopt;
|
||||
uint32_t pc = use_pc;
|
||||
for (uint32_t n = 0; n < window && pc >= entry + 4u; ++n) {
|
||||
pc -= 4u;
|
||||
auto w = read(pc);
|
||||
if (!w.has_value()) return std::nullopt;
|
||||
if (exact_classify_cf(pc, *w).kind != ExactCfKind::Normal)
|
||||
return std::nullopt;
|
||||
if (exact_instruction_writes_gpr(*w, reg)) return pc;
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
auto fields = [&](uint32_t w, uint32_t& op, uint32_t& rs, uint32_t& rt,
|
||||
uint32_t& rd, uint32_t& sa, uint32_t& fn) {
|
||||
op = (w >> 26) & 0x3Fu; rs = (w >> 21) & 0x1Fu; rt = (w >> 16) & 0x1Fu;
|
||||
rd = (w >> 11) & 0x1Fu; sa = (w >> 6) & 0x1Fu; fn = w & 0x3Fu;
|
||||
};
|
||||
auto is_addu = [&](uint32_t w, uint32_t& rd, uint32_t& rs, uint32_t& rt) {
|
||||
uint32_t op, sa, fn;
|
||||
fields(w, op, rs, rt, rd, sa, fn);
|
||||
return op == 0u && fn == 0x21u && sa == 0u && rd != 0u;
|
||||
};
|
||||
// sll rd, rt, sa -> (rd, index rt, shift)
|
||||
auto is_sll = [&](uint32_t w, uint32_t& rd, uint32_t& idx, uint32_t& sh) {
|
||||
uint32_t op, rs, fn;
|
||||
fields(w, op, rs, idx, rd, sh, fn);
|
||||
return op == 0u && fn == 0u && rs == 0u && rd != 0u && idx != 0u && w != 0u;
|
||||
};
|
||||
|
||||
// 1. jr_rs = addu B, S (either order), nearest definition before the jr.
|
||||
auto addu_pc = nearest_def(jr_rs, jr_pc, 8u);
|
||||
if (!addu_pc.has_value()) return false;
|
||||
uint32_t a_rd, a_rs, a_rt;
|
||||
if (!is_addu(*read(*addu_pc), a_rd, a_rs, a_rt) || a_rs == a_rt ||
|
||||
a_rs == 0u || a_rt == 0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. One operand is the scaled index: sll S,I,k or the two-sll sum.
|
||||
// Try each operand as the scaled register; the other is the base.
|
||||
auto scaled_stride = [&](uint32_t s_reg, uint32_t use_pc) -> uint32_t {
|
||||
auto def = nearest_def(s_reg, use_pc, 8u);
|
||||
if (!def.has_value()) return 0u;
|
||||
uint32_t w = *read(*def);
|
||||
uint32_t rd, idx, sh;
|
||||
if (is_sll(w, rd, idx, sh)) {
|
||||
return (sh >= 2u && sh <= 8u) ? (1u << sh) : 0u;
|
||||
}
|
||||
uint32_t s_rd, p, q;
|
||||
if (!is_addu(w, s_rd, p, q) || p == q || p == 0u || q == 0u) return 0u;
|
||||
auto p_def = nearest_def(p, *def, 8u);
|
||||
auto q_def = nearest_def(q, *def, 8u);
|
||||
if (!p_def.has_value() || !q_def.has_value()) return 0u;
|
||||
uint32_t p_rd, p_idx, p_sh, q_rd, q_idx, q_sh;
|
||||
if (!is_sll(*read(*p_def), p_rd, p_idx, p_sh) ||
|
||||
!is_sll(*read(*q_def), q_rd, q_idx, q_sh) ||
|
||||
p_idx != q_idx || p_sh == q_sh || p_sh > 8u || q_sh > 8u) {
|
||||
return 0u;
|
||||
}
|
||||
// The index must reach both shifts unchanged: no write to it between
|
||||
// the earlier sll and the later one.
|
||||
uint32_t lo = std::min(*p_def, *q_def), hi = std::max(*p_def, *q_def);
|
||||
for (uint32_t pc = lo + 4u; pc < hi; pc += 4u) {
|
||||
if (exact_instruction_writes_gpr(*read(pc), p_idx)) return 0u;
|
||||
}
|
||||
return (1u << p_sh) + (1u << q_sh);
|
||||
};
|
||||
uint32_t stride = scaled_stride(a_rt, *addu_pc);
|
||||
if (stride == 0u) stride = scaled_stride(a_rs, *addu_pc);
|
||||
if (stride == 0u || (stride & 3u) != 0u || stride > 256u) return false;
|
||||
|
||||
// 3. The run: N >= 2 shape-identical groups right after the delay slot.
|
||||
// Same opcode/register fields per position; immediates free; no
|
||||
// control flow inside group 0 (so every group is straight-line).
|
||||
const uint32_t run_start = jr_pc + 8u;
|
||||
const uint32_t words_per_group = stride / 4u;
|
||||
if (run_start + stride > hard_cap) return false;
|
||||
auto shape = [](uint32_t w) -> uint32_t {
|
||||
uint32_t op = (w >> 26) & 0x3Fu;
|
||||
// I-type: keep opcode + rs + rt, drop the immediate. J-type and
|
||||
// SPECIAL/REGIMM/COP forms compare whole.
|
||||
if (op == 0u || op == 1u || op == 2u || op == 3u || op == 0x10u ||
|
||||
op == 0x12u) {
|
||||
return w;
|
||||
}
|
||||
return w & 0xFFFF0000u;
|
||||
};
|
||||
std::vector<uint32_t> group0;
|
||||
bool all_nop = true;
|
||||
for (uint32_t i = 0; i < words_per_group; ++i) {
|
||||
auto w = read(run_start + i * 4u);
|
||||
if (!w.has_value()) return false;
|
||||
if (exact_classify_cf(run_start + i * 4u, *w).kind != ExactCfKind::Normal)
|
||||
return false;
|
||||
if (*w != 0u) all_nop = false;
|
||||
group0.push_back(*w);
|
||||
}
|
||||
if (all_nop) return false;
|
||||
uint32_t groups = 1;
|
||||
for (;;) {
|
||||
uint32_t g = run_start + groups * stride;
|
||||
if (g + stride > hard_cap) break;
|
||||
bool same = true;
|
||||
for (uint32_t i = 0; i < words_per_group && same; ++i) {
|
||||
auto w = read(g + i * 4u);
|
||||
same = w.has_value() && shape(*w) == shape(group0[i]);
|
||||
}
|
||||
if (!same) break;
|
||||
++groups;
|
||||
}
|
||||
if (groups < 2u) return false;
|
||||
|
||||
auto mapped = [&](uint32_t addr) {
|
||||
return runtime_to_image ? runtime_to_image(addr, exe) : addr;
|
||||
};
|
||||
ExactJumpTable resolved;
|
||||
resolved.table_base = run_start;
|
||||
resolved.stride = stride;
|
||||
for (uint32_t k = 0; k <= groups; ++k) {
|
||||
uint32_t t = run_start + k * stride;
|
||||
if (t >= hard_cap) break;
|
||||
resolved.targets.emplace_back(t, mapped(t));
|
||||
}
|
||||
resolved.table_count = static_cast<uint32_t>(resolved.targets.size());
|
||||
table = std::move(resolved);
|
||||
return true;
|
||||
}
|
||||
|
||||
FunctionAnalysisResult FunctionAnalyzer::analyze_exact_entries(
|
||||
const std::vector<uint32_t>& entries,
|
||||
const std::vector<std::pair<uint32_t, uint32_t>>& producer_ranges,
|
||||
@@ -1304,7 +1460,9 @@ FunctionAnalysisResult FunctionAnalyzer::analyze_exact_entries(
|
||||
}
|
||||
if (resolve_exact_bounded_jump_table(
|
||||
exe_, entry, hard_cap, pc, jr_rs, table,
|
||||
nullptr, producer_lo, producer_hi)) {
|
||||
nullptr, producer_lo, producer_hi) ||
|
||||
resolve_computed_stride_jump(
|
||||
exe_, entry, hard_cap, pc, jr_rs, table)) {
|
||||
bool all_owned = std::all_of(
|
||||
table.targets.begin(), table.targets.end(),
|
||||
[&](const auto& target_pair) {
|
||||
|
||||
Reference in New Issue
Block a user