Files
Valiantsin DzerakhandClaude Opus 5.5 5e584ea966 Merge pull request #51: Load Atmosphere cheat files and switch their cheats from a Cheats tab in the menu (conongnang)
Merged onto the mods folder (task/mods1); the Cheats tab sits after Mods. The cheats' menu text and the cheat file's
place are reworked for the mods folder in the commits that follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:31:17 +03:00

159 lines
7.9 KiB
Python

"""Code patch table for the generator: guest instructions replaced in the lifted C, switchable at run time.
Lifted code cannot be byte-patched when the game runs, so a patch is applied when the C is generated. A patch file
(gen.py --patches) has two kinds of lines; `#` starts a comment, fields are separated by whitespace:
set <name> <env> <default> a patch set: a run-time switch <env>=0/1, <default> when it is unset
site <set> <module> <address> <original> <replacement> [<replacement> ...]
spin <module> <head> <back-edge> [<back-edge> ...]
<address> is a module offset (hex), <original> the instruction word the module has there (checked; a mismatch stops
the generator), each <replacement> an instruction word (hex) or `@page:x<n>`, which is `adrp x<n>, <patch page>`
encoded for the site's address. The patch page is the first 4 KiB page after main's image (layout.py leaves the rest
of the 2 MiB-aligned module slot free); the runtime commits it and owns its contents. Words in use, by byte offset:
+0..15 the frame-rate step (runtime/frame_rate.c), +16..55 the output, swapchain and scene sizes
(runtime/resolution.c res_init), +56..59 the camera's aspect ratio (runtime/resolution.c res_init, set aspect), +60..63
the field-of-view factor (runtime/resolution.c res_fov_live, set fov), +92..95 the sun shadow map size
(runtime/resolution.c shadow_init, set shadow). A new set takes words outside these.
The replacement words are lifted at the site's address in place of the original instruction, and the generated C reads
`if (rt_patch_on[<set>]) { replacement } else { original }`: one byte load and a branch per executed site. Several
sets may patch one address (cheats that write different values there): `if (a) {..} else if (b) {..} else {..}`, the
first set in file order wins. The original may be any instruction but br, blr and ret; a replacement's last word may be
a bl (lifted with the site's return address), a b or b.cond / cbz / tbz (its target joins the function's labels when
inside it) or a ret; earlier words must not branch. Every alternative of a site writes the same flags. The generator
writes patches.h (set names, switches, the page address) next to layout.h for the runtime.
A `spin` line names a spin wait found by tools/spin_waits.py: a loop with head <head> that only another thread can end,
and the direct branches back to it (module offsets). Each back-edge (checked: a b, b.cond, cbz / cbnz or tbz / tbnz to
<head>) is lifted as `rt_spin_gate(c, <head>); goto <head>`, which keeps every register and only adds time
(runtime/kernel.c kern_spin_gate).
"""
from pathlib import Path
PAGE = 0x1000
class PatchError(ValueError):
pass
def adrp(pc, target, rd):
imm = (target >> 12) - (pc >> 12)
if not -(1 << 20) <= imm < (1 << 20):
raise PatchError(f'{pc:#x}: patch page {target:#x} out of ADRP range')
imm &= 0x1FFFFF
return 0x90000000 | (imm & 3) << 29 | (imm >> 2) << 5 | rd
class Table:
"""sets: [(name, env, default)]; sites: {module: {offset: [(set index, [word | ('page', reg)], original)]}} (the
alternatives of a site in file order); page."""
def __init__(self, sets=(), sites=None, page=0, spins=None):
self.sets = list(sets)
self.sites = sites or {}
self.page = page
self.spins = spins or {} # {module: {back-edge offset: head offset}}
def spins_for(self, name, base):
"""{absolute back-edge address: absolute head address}."""
return {base + b: base + h for b, h in self.spins.get(name, {}).items()}
def for_module(self, name, base):
"""{absolute address: [(set index, [words])]} with @page resolved."""
out = {}
for off, alts in self.sites.get(name, {}).items():
a = base + off
out[a] = [(k, [adrp(a, self.page, w[1]) if isinstance(w, tuple) else w for w in repl])
for k, repl, _ in alts]
return out
def load(path, mods):
"""Table from a patch file; mods is layout() output [(name, base, size)]."""
sets, index, sites, spins = [], {}, {}, {}
bases = {n: (b, s) for n, b, s in mods}
for no, raw in enumerate(Path(path).read_text().splitlines(), 1):
f = raw.split('#', 1)[0].split()
if not f:
continue
where = f'{path}:{no}'
if f[0] == 'set' and len(f) == 4:
if f[1] in index:
raise PatchError(f'{where}: set {f[1]} defined twice')
index[f[1]] = len(sets)
sets.append((f[1], f[2], int(f[3])))
elif f[0] == 'site' and len(f) >= 6:
if f[1] not in index:
raise PatchError(f'{where}: unknown set {f[1]}')
if f[2] not in bases:
raise PatchError(f'{where}: module {f[2]} not in the layout')
off, orig = int(f[3], 16), int(f[4], 16)
repl = []
for w in f[5:]:
if w.startswith('@page:x'):
repl.append(('page', int(w[7:])))
else:
repl.append(int(w, 16))
alts = sites.setdefault(f[2], {}).setdefault(off, [])
if any(k == index[f[1]] for k, _, _ in alts):
raise PatchError(f'{where}: {f[2]}+{off:#x} patched twice by set {f[1]}')
if alts and alts[0][2] != orig:
raise PatchError(f'{where}: {f[2]}+{off:#x} expects {orig:08x}, an earlier line {alts[0][2]:08x}')
alts.append((index[f[1]], repl, orig))
elif f[0] == 'spin' and len(f) >= 4:
if f[1] not in bases:
raise PatchError(f'{where}: module {f[1]} not in the layout')
mod, head = spins.setdefault(f[1], {}), int(f[2], 16)
for b in f[3:]:
b = int(b, 16)
if b in mod:
raise PatchError(f'{where}: {f[1]}+{b:#x} is a back-edge twice')
mod[b] = head
else:
raise PatchError(f'{where}: cannot parse {raw!r}')
base, size = bases['main'][0], bases['main'][1]
page = (base + size + PAGE - 1) & ~(PAGE - 1)
nxt = min((b for _, b, _ in mods if b > base), default=None)
if nxt is not None and page + PAGE > nxt:
raise PatchError(f'no room for the patch page after main ({page:#x})')
return Table(sets, sites, page, spins)
def check(table, mod, name):
"""Stops on a site whose original word differs from the module's."""
for off, [(_, _, want), *_] in table.sites.get(name, {}).items():
a = mod.base + off
if not mod.in_text(a):
raise PatchError(f'{name}+{off:#x}: patch site outside .text')
have = mod.word(a)
if have != want:
raise PatchError(f'{name}+{off:#x}: module has {have:08x}, the patch expects {want:08x}')
for b, head in table.spins.get(name, {}).items():
a = mod.base + b
if not mod.in_text(a) or branch_target(mod.word(a), a) != mod.base + head:
raise PatchError(f'{name}+{b:#x}: not a direct branch to the spin wait head {name}+{head:#x}')
def branch_target(w, a):
"""Target of the direct branch w at a (b, b.cond, cbz / cbnz, tbz / tbnz), or None."""
if w >> 26 == 0x05:
return a + ((((w & 0x3FFFFFF) ^ 0x2000000) - 0x2000000) << 2)
if w & 0xFF000010 == 0x54000000 or w & 0x7E000000 == 0x34000000:
return a + ((((w >> 5) & 0x7FFFF) ^ 0x40000) - 0x40000 << 2)
if w & 0x7E000000 == 0x36000000:
return a + ((((w >> 5) & 0x3FFF) ^ 0x2000) - 0x2000 << 2)
return None
def header_text(table, marker):
lines = [f'// {marker}: code patch sets, generated by recomp/gen.py --patches. Do not edit or commit.',
'#pragma once', '', f'#define RT_PATCH_COUNT {len(table.sets)}',
f'#define RT_PATCH_PAGE {table.page:#x}ull',
'// X(index, name, env switch, default)',
'#define RT_PATCH_SETS(X) ' + ' '.join(f'X({i}, {n}, "{e}", {d})' for i, (n, e, d) in
enumerate(table.sets)), '']
return '\n'.join(lines)