diff --git a/docs/difficulty.resident.md b/docs/difficulty.resident.md index 4d13a5cc8f..36ccb3285d 100644 --- a/docs/difficulty.resident.md +++ b/docs/difficulty.resident.md @@ -1,19 +1,17 @@ # Unmatched difficulty inventory (generated by tools/difficulty.py — harvest queue) -unmatched functions : 56 +unmatched functions : 54 trivial (<=5 ins) : 1 -non-jtbl leaves : 12 (best harvest targets) +non-jtbl leaves : 11 (best harvest targets) jump-table funcs : 5 (deferred — need the rodata-island workflow, Task 2') -## Easiest 56 unmatched (score asc) — the work queue +## Easiest 54 unmatched (score asc) — the work queue | score | name | nins | br | calls | jtbl | leaf | |---|---|---|---|---|---|---| | 4 | func_800CFBE8 | 4 | 0 | 0 | - | Y | -| 7 | func_800CFBCC | 7 | 0 | 0 | - | Y | | 15 | func_800D1028 | 11 | 0 | 2 | - | - | | 21 | func_800D2E20 | 19 | 0 | 1 | - | - | | 23 | func_800D05E8 | 18 | 1 | 1 | - | - | -| 25 | func_800D1938 | 19 | 0 | 3 | - | - | | 26 | func_800D0EC4 | 18 | 2 | 1 | - | - | | 27 | func_800CEF5C | 21 | 0 | 3 | - | - | | 29 | func_800D0F8C | 21 | 2 | 1 | - | - | diff --git a/docs/progress.resident.md b/docs/progress.resident.md index c769ce3ea0..fdc6efc6a0 100644 --- a/docs/progress.resident.md +++ b/docs/progress.resident.md @@ -1,18 +1,18 @@ # BFM matching progress (generated by tools/progress.py — authoritative) -REAL substantive matches : 87 <- the Gen1-exit >=25 bar counts THIS +REAL substantive matches : 89 <- the Gen1-exit >=25 bar counts THIS LINKED real PsyQ objects : 0 <- byte-identical via linked SDK objects NON_MATCHING (near-miss) : 0 splat-auto empty no-ops : 2 -INCLUDE_ASM stubs : 56 +INCLUDE_ASM stubs : 54 data blobs (excluded) : 0 ---------------------------------------- matchable functions : 145 -REAL / matchable : 87 / 145 = 60.00% -byte-identical/ matchable: 89 / 145 = 61.38% (REAL+LINKED+empties) +REAL / matchable : 89 / 145 = 61.38% +byte-identical/ matchable: 91 / 145 = 62.76% (REAL+LINKED+empties) LINKED subsegs: (0 fns) -REAL matches: DsMix func_800CEDFC func_800CEE40 func_800CEE84 func_800CEF04 func_800CEF34 func_800CEFB0 func_800CF370 func_800CF398 func_800CF47C func_800CF4D4 func_800CF510 func_800CF5D4 func_800CF714 func_800CF764 func_800CF7A4 func_800CF804 func_800CF818 func_800CF82C func_800CF840 func_800CF854 func_800CF864 func_800CF8B4 func_800CF8CC func_800CF94C func_800CFBBC func_800CFBF8 func_800CFD68 func_800CFDA4 func_800CFDE8 func_800D02C0 func_800D0630 func_800D065C func_800D0694 func_800D06CC func_800D0C48 func_800D0C74 func_800D0CA0 func_800D0F0C func_800D0F4C func_800D0FE0 func_800D1000 func_800D1054 func_800D1078 func_800D109C func_800D10C8 func_800D1714 func_800D1724 func_800D1734 func_800D1744 func_800D18DC func_800D19DC func_800D1B10 func_800D1D24 func_800D1D94 func_800D1F58 func_800D1F90 func_800D222C func_800D22E4 func_800D2318 func_800D2468 func_800D25FC func_800D2624 func_800D2DAC func_800D2DFC func_800D2EE8 func_800D2F0C func_800D2F48 func_800D2F8C func_800D2FB0 func_800D2FEC func_800D30A8 func_800D30C8 func_800D3104 func_800D3120 func_800D319C func_800D31C0 func_800D31FC func_800D3238 func_800D3274 func_800D32D4 func_800D3328 func_800D3348 func_800D3384 func_800D3398 func_800D33B8 func_800D33E0 +REAL matches: DsMix func_800CEDFC func_800CEE40 func_800CEE84 func_800CEF04 func_800CEF34 func_800CEFB0 func_800CF370 func_800CF398 func_800CF47C func_800CF4D4 func_800CF510 func_800CF5D4 func_800CF714 func_800CF764 func_800CF7A4 func_800CF804 func_800CF818 func_800CF82C func_800CF840 func_800CF854 func_800CF864 func_800CF8B4 func_800CF8CC func_800CF94C func_800CFBBC func_800CFBCC func_800CFBF8 func_800CFD68 func_800CFDA4 func_800CFDE8 func_800D02C0 func_800D0630 func_800D065C func_800D0694 func_800D06CC func_800D0C48 func_800D0C74 func_800D0CA0 func_800D0F0C func_800D0F4C func_800D0FE0 func_800D1000 func_800D1054 func_800D1078 func_800D109C func_800D10C8 func_800D1714 func_800D1724 func_800D1734 func_800D1744 func_800D18DC func_800D1938 func_800D19DC func_800D1B10 func_800D1D24 func_800D1D94 func_800D1F58 func_800D1F90 func_800D222C func_800D22E4 func_800D2318 func_800D2468 func_800D25FC func_800D2624 func_800D2DAC func_800D2DFC func_800D2EE8 func_800D2F0C func_800D2F48 func_800D2F8C func_800D2FB0 func_800D2FEC func_800D30A8 func_800D30C8 func_800D3104 func_800D3120 func_800D319C func_800D31C0 func_800D31FC func_800D3238 func_800D3274 func_800D32D4 func_800D3328 func_800D3348 func_800D3384 func_800D3398 func_800D33B8 func_800D33E0 NON_MATCHING: build SHA1: 8e17e02ff8954d07c979449198f7e1645046b353 (byte-identical) diff --git a/src/resident/resident.c b/src/resident/resident.c index d42cffda37..799671b47c 100644 --- a/src/resident/resident.c +++ b/src/resident/resident.c @@ -305,7 +305,24 @@ void func_800CFBBC(void) { D_80114E70 = 0; } -INCLUDE_ASM("asm/resident/nonmatchings/resident", func_800CFBCC); +/* func_800CFBCC — trivial two-halfword setter. + * addiu v0,zero,0x1 ; sh v0,D_80114E70 ; sh a0,D_80114E74 ; jr ra + * + * D_80114E70 is the SAME global already matched as `u16` in src/resident/resident.c + * (func_800CFBBC sets D_80114E70 = 0). Both stores are `sh` (halfword) so the global / + * arg signedness does NOT change the emitted store — width is fixed at 16. Kept u16 to + * mirror the proven type. Stores are in source order (E70 first, E74 second), with the + * `1` materialised once into v0. + * + * Self-contained per drafts3 rules: NO #include; typedef inlined. + */ +extern u16 D_80114E70; +extern u16 D_80114E74; + +void func_800CFBCC(u16 arg0) { + D_80114E70 = 1; + D_80114E74 = arg0; +} INCLUDE_ASM("asm/resident/nonmatchings/resident", func_800CFBE8); @@ -675,7 +692,43 @@ void func_800D18DC(void) { local.fn[idx](&D_800AE6A8); } -INCLUDE_ASM("asm/resident/nonmatchings/resident", func_800D1938); +/* ANALYSIS (asm 0x4C): + * s0 = arg0 (saved in func_800D1F58's delay slot) + * func_800D1F58(); // void, no args set -> a0 still holds arg0 (incidental) + * func_801285D4(); // void, nop in delay slot + * v0 = (u8)field 0x14 (lbu) + * a0 = arg0 + * field 0x15 = 0 (sb) + * v0 = v0 + 1 + * field 0x14 = (u8)v0 (sb, in jal delay slot) + * func_800D1984(arg0); + * return 0; // addu v0,zero,zero -> plain s32 0 + * + * field0x14 read with lbu (u8), the +1 result stored back with sb (so the store + * truncates to 8 bits naturally). field0x15 is a u8 store of 0. The store order + * in asm is: sb 0x15 (=0), then sb 0x14 (=field+1) -- but the 0x14 store sits in + * the func_800D1984 delay slot. Writing field0x15=0 first then field0x14++ keeps + * gcc's natural schedule (the 0x14 store hoists into the call delay slot). + */ + +typedef struct { + u8 pad[0x14]; + u8 f14; /* 0x14 */ + u8 f15; /* 0x15 */ +} S800D1938; + +extern void func_800D1F58(void); +extern void func_801285D4(void); +extern void func_800D1984(S800D1938 *arg0); + +s32 func_800D1938(S800D1938 *arg0) { + func_800D1F58(); + func_801285D4(); + arg0->f15 = 0; + arg0->f14 = arg0->f14 + 1; + func_800D1984(arg0); + return 0; +} INCLUDE_ASM("asm/resident/nonmatchings/resident", func_800D1984); diff --git a/tools/harvest_verify.py b/tools/harvest_verify.py index c70d4c7697..3ba00bc5cb 100644 --- a/tools/harvest_verify.py +++ b/tools/harvest_verify.py @@ -28,6 +28,15 @@ a = ap.parse_args() STUB = 'INCLUDE_ASM("' + a.asm_subdir + '", {fn});' # {fn} filled per function +# Some drafts inline `typedef unsigned char u8;` etc. ("self-contained") -> when placed in a +# .c that already #includes common.h, gcc-2.7.2 (C89) errors on the redefinition. Strip those +# lines so common.h provides the types (a compile error is NOT a byte mismatch). +_TD = re.compile(r'^[ \t]*typedef\b.*\b(u8|u16|u32|u64|s8|s16|s32|s64|f32|f64)[ \t]*;[ \t]*\n', re.M) + + +def strip_typedefs(c): + return _TD.sub('', c) + def sha1(path): return hashlib.sha1(open(path, 'rb').read()).hexdigest() if os.path.exists(path) else None @@ -56,7 +65,7 @@ for cf in sorted(glob.glob(a.drafts + '/*.c')): w = open(cp).read().strip().lower().split() if w and w[0] in ('high', 'medium', 'low'): conf = w[0] - drafts[fn] = {'c': open(cf).read(), 'conf': conf} + drafts[fn] = {'c': strip_typedefs(open(cf).read()), 'conf': conf} order = {'high': 0, 'medium': 1, 'low': 2} items = sorted(drafts, key=lambda fn: (order[drafts[fn]['conf']], len(drafts[fn]['c']))) diff --git a/tools/match_one.py b/tools/match_one.py new file mode 100644 index 0000000000..06a6b9807f --- /dev/null +++ b/tools/match_one.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Per-function matching check: compile ONE function's C standalone (the pinned triple), +mask relocations (jal/j 26-bit + HI16/LO16 immediates, like psyq_identify), and compare to +the target bytes embedded in its splat .s. Fully isolated (own temp dir) so many run in +PARALLEL with no shared build -- a real asm-differ loop for an agent to iterate against. + +Prints "MATCH (N ins)" on a byte-exact (relocation-masked) match, else a per-instruction diff. +gcc-2.7.2 -O2 compiles each global function independently (no cross-TU inlining of externs), +so a standalone match == the whole-binary byte-gate for that function. + + python3 tools/match_one.py func_800D1DB0 # uses .run/drafts3|2|/.c + python3 tools/match_one.py func_800D1DB0 --c /path/to.c # explicit C file +""" +import subprocess, re, sys, os, struct, argparse + +ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) +ap.add_argument('fn') +ap.add_argument('--c', help='C file (externs + the function def). Default: search .run/drafts3|2|/') +ap.add_argument('--asm-subdir', default='asm/resident/nonmatchings/resident') +ap.add_argument('--work', default='.run/match') +a = ap.parse_args() + +CPP = 'mipsel-linux-gnu-cpp'; CC1 = 'tools/bin/gcc-2.7.2-psx/cc1' +MASPSX = 'tools/maspsx/maspsx.py'; AS = 'mipsel-linux-gnu-as'; PY = '.venv/bin/python' +OBJDUMP = 'mipsel-linux-gnu-objdump' +CPPFLAGS = '-lang-c -Iinclude -undef -Wall -fno-builtin -Dmips -D__GNUC__=2 -D__OPTIMIZE__ -Dpsx -D_PSYQ -D_MIPSEL -D_LANGUAGE_C'.split() +CC1FLAGS = '-quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker'.split() +ASFLAGS = '-Iinclude -march=r3000 -mtune=r3000 -no-pad-sections -O1 -G0'.split() + +cfile = a.c +if not cfile: + for d in ('.run/drafts3', '.run/drafts2', '.run/drafts'): + if os.path.exists('%s/%s.c' % (d, a.fn)): + cfile = '%s/%s.c' % (d, a.fn); break +if not cfile or not os.path.exists(cfile): + print('FAIL: no C file for', a.fn); sys.exit(2) + +wd = '%s/%s' % (a.work, a.fn); os.makedirs(wd, exist_ok=True) +src = open(cfile).read() +# strip inline scalar-typedef redefinitions (common.h provides them; C89 rejects the dup) +src = re.sub(r'^[ \t]*typedef\b.*\b(u8|u16|u32|u64|s8|s16|s32|s64|f32|f64)[ \t]*;[ \t]*\n', '', src, flags=re.M) +if '#include "common.h"' not in src: + src = '#include "common.h"\n' + src +open('%s/t.c' % wd, 'w').write(src) + + +def pipe(cmd, data=None): + return subprocess.run(cmd, input=data, capture_output=True) + + +p = pipe([CPP] + CPPFLAGS + ['%s/t.c' % wd]) +if p.returncode: print('CPP FAIL\n' + p.stderr.decode()[-1500:]); sys.exit(1) +p = pipe([CC1] + CC1FLAGS, p.stdout) +if p.returncode: print('CC1 FAIL\n' + p.stderr.decode()[-1800:]); sys.exit(1) +p = pipe([PY, MASPSX, '--aspsx-version=2.56', '--expand-div'], p.stdout) +if p.returncode: print('MASPSX FAIL\n' + p.stderr.decode()[-1500:]); sys.exit(1) +p = pipe([AS] + ASFLAGS + ['-o', '%s/t.o' % wd], p.stdout) +if p.returncode: print('AS FAIL\n' + p.stderr.decode()[-1500:]); sys.exit(1) + + +def my_insns(obj, fn): + """objdump the function; return (words, masks, mnemonics). mask=0 on jal/j 26-bit, + 0xFFFF0000 on HI16/LO16 reloc immediates, else 0xFFFFFFFF.""" + d = subprocess.run([OBJDUMP, '-dr', '-j', '.text', obj], capture_output=True, text=True).stdout + words, masks, mnem = [], [], [] + infn = False + for line in d.splitlines(): + h = re.match(r'^[0-9a-f]+ <([^>]+)>:', line) + if h: + infn = (h.group(1) == fn); continue + if not infn: + continue + mi = re.match(r'\s+([0-9a-f]+):\s+([0-9a-f]{8})\s+(.*)', line) + if mi: + w = int(mi.group(2), 16) + words.append(w); mnem.append(mi.group(3).strip()) + masks.append(0 if (w >> 26) in (2, 3) else 0xFFFFFFFF) + elif 'R_MIPS' in line and words: + masks[-1] = 0 if '_26' in line else 0xFFFF0000 + return words, masks, mnem + + +def target_insns(fn): + """parse the splat .s: each /* off vaddr LEHEX */ mnemonic line -> (word, mnemonic).""" + words, mnem = [], [] + for line in open('%s/%s.s' % (a.asm_subdir, fn)): + mi = re.match(r'\s*/\*\s*[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s+([0-9A-Fa-f]{8})\s*\*/\s+(.*)', line) + if mi: + words.append(struct.unpack('