247 lines
8.6 KiB
Python
247 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
|
"""rtobj.py — runtime-capture of soothe2 DSP object heap (v2: periodic heap scan).
|
|
|
|
Spawns `reaper -nosplash -renderproject <rpp>`, locates the yabridge-host process
|
|
with soothe2 mapped, computes modbase, and PERIODICALLY scans ONLY anonymous rw
|
|
regions (heaps) for the Soothe2Module vtable pointer, verifying by ctor fields.
|
|
On a valid object it dumps twin-state / step-5 window / level weights / mask /
|
|
warp / freqaxis / setters into OUTDIR.
|
|
"""
|
|
import subprocess, os, glob, sys, time, struct, json
|
|
|
|
VPTR_RVA = 0x24abb80
|
|
VPTR_N = 0x60
|
|
N_BINS = 342
|
|
N_DUMP = 2048
|
|
|
|
|
|
def mem_read(pid, addr, n):
|
|
mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
|
|
try:
|
|
return os.pread(mem, n, addr)
|
|
except Exception:
|
|
return None
|
|
finally:
|
|
os.close(mem)
|
|
|
|
|
|
def find_host(proc, wait=120):
|
|
t0 = time.time()
|
|
while time.time() - t0 < wait:
|
|
for p in glob.glob('/proc/[0-9]*'):
|
|
try:
|
|
pid = int(os.path.basename(p))
|
|
cmd = open(f'/proc/{pid}/cmdline', 'rb').read().replace(b'\0', b' ').decode('utf8', 'replace')
|
|
maps = open(f'/proc/{pid}/maps').read()
|
|
except Exception:
|
|
continue
|
|
if 'yabridge-host' in cmd and 'soothe2' in maps:
|
|
return pid
|
|
time.sleep(0.05)
|
|
return None
|
|
|
|
|
|
def soothe2_base(host):
|
|
try:
|
|
maps = open(f'/proc/{host}/maps').read()
|
|
except Exception:
|
|
return None
|
|
for line in maps.splitlines():
|
|
if 'soothe2' not in line:
|
|
continue
|
|
rlo = int(line.split()[0].split('-')[0], 16)
|
|
hdr = mem_read(host, rlo, 0x1000)
|
|
if hdr and hdr[:2] == b'MZ':
|
|
x = struct.unpack('<I', hdr[0x3c:0x40])[0]
|
|
if hdr[x:x + 4] == b'PE\x00\x00':
|
|
return rlo
|
|
return None
|
|
|
|
|
|
def heap_regions(pid, excl_start=None, excl_end=None):
|
|
out = []
|
|
try:
|
|
for line in open(f'/proc/{pid}/maps').read().splitlines():
|
|
p = line.split()
|
|
if len(p) < 2:
|
|
continue
|
|
path = p[5] if len(p) > 5 else ''
|
|
if 'r' not in p[1]:
|
|
continue
|
|
lo, hi = (int(x, 16) for x in p[0].split('-'))
|
|
# skip the image file-back regions of the plugin itself
|
|
if excl_start is not None and excl_start <= lo < excl_end:
|
|
continue
|
|
if 'soothe2' in line:
|
|
continue
|
|
out.append((lo, hi))
|
|
except Exception:
|
|
pass
|
|
return out
|
|
|
|
|
|
def read_vslots(host, base):
|
|
"""vtable pointer values = addresses of the .data vtable-slots themselves
|
|
(object's first qword points here), range [vstart, vstart+VPTR_N)."""
|
|
vstart = base + VPTR_RVA
|
|
slots = [vstart + off for off in range(0, VPTR_N, 8)]
|
|
return slots
|
|
|
|
|
|
def scan_objects(host, slots, excl_start=None, excl_end=None):
|
|
"""Find reg addrs whose qword content == any vtable slot (object vptr)."""
|
|
hits = []
|
|
mem = os.open(f'/proc/{host}/mem', os.O_RDONLY)
|
|
pats = [struct.pack('<Q', s) for s in slots]
|
|
try:
|
|
for lo, hi in heap_regions(host, excl_start, excl_end):
|
|
try:
|
|
d = os.pread(mem, hi - lo, lo)
|
|
except Exception:
|
|
continue
|
|
if not d:
|
|
continue
|
|
for p in pats:
|
|
i = 0
|
|
while True:
|
|
i = d.find(p, i)
|
|
if i < 0:
|
|
break
|
|
hits.append(lo + i)
|
|
i += 1
|
|
finally:
|
|
os.close(mem)
|
|
return hits
|
|
|
|
|
|
def f32(d, off):
|
|
return struct.unpack_from('<f', d, off)[0] if len(d) >= off + 4 else None
|
|
|
|
|
|
def verify(host, base):
|
|
d = mem_read(host, base, 0x541000)
|
|
if d is None or len(d) < 0x540900:
|
|
return None
|
|
f = {
|
|
'0x24': f32(d, 0x24),
|
|
'0x540874': f32(d, 0x540874),
|
|
'0x54087c': f32(d, 0x54087c),
|
|
'0x540880': f32(d, 0x540880),
|
|
'0x540884': f32(d, 0x540884),
|
|
'0x540888': f32(d, 0x540888),
|
|
'0x54088c': f32(d, 0x54088c),
|
|
'0x540894': f32(d, 0x540894),
|
|
}
|
|
ok = (f['0x24'] == 44100.0 and f['0x540874'] == 1.0 and f['0x540894'] == 1.0)
|
|
return f if ok else None
|
|
|
|
|
|
def dump_floats(host, addr, count, tag, od, fmt=lambda v: f'{v:.17g}'):
|
|
d = mem_read(host, addr, count * 4)
|
|
if not d:
|
|
return
|
|
open(f'{tag}.bin', 'wb').write(d)
|
|
n = len(d) // 4
|
|
vals = struct.unpack('<%df' % n, d[:n * 4])
|
|
with open(f'{od}/{tag}.txt', 'w') as f:
|
|
for i, v in enumerate(vals):
|
|
f.write(f'{i}: {fmt(v)}\n')
|
|
|
|
|
|
def main():
|
|
RPP = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual300.rpp'
|
|
OD = sys.argv[2] if len(sys.argv) > 2 else '/tmp/rtobj_dual300'
|
|
os.makedirs(OD, exist_ok=True)
|
|
|
|
os.system("pkill -9 -x reaper 2>/dev/null; pkill -9 -f '[y]abridge' 2>/dev/null; sleep 1")
|
|
proc = subprocess.Popen(
|
|
['/usr/bin/reaper', '-nosplash', '-ignoreerrors', '-renderproject', RPP],
|
|
stdout=open(f'{OD}/reaper.log', 'w'), stderr=subprocess.STDOUT)
|
|
print(f'reaper pid={proc.pid}', flush=True)
|
|
|
|
host = find_host(proc)
|
|
if not host:
|
|
proc.kill(); sys.exit('no host')
|
|
base = soothe2_base(host)
|
|
print(f'host={host} modbase=0x{base:x}', flush=True)
|
|
|
|
slots = read_vslots(host, base)
|
|
print(f'vtable slots: {[hex(s) for s in slots]}', flush=True)
|
|
|
|
# periodic scan while render alive
|
|
got = None
|
|
t0 = time.time()
|
|
last_host = host
|
|
nscan = 0
|
|
while time.time() - t0 < 300 and not got:
|
|
if proc.poll() is not None:
|
|
print('render finished', flush=True)
|
|
break
|
|
if not os.path.exists(f'/proc/{last_host}'):
|
|
print('host died, re-finding...', flush=True)
|
|
last_host = find_host(proc, wait=10)
|
|
if not last_host:
|
|
break
|
|
base = soothe2_base(last_host)
|
|
if not base:
|
|
break
|
|
slots = read_vslots(last_host, base)
|
|
t1 = time.time()
|
|
hits = scan_objects(last_host, slots, excl_start=base, excl_end=base + 0x7000000)
|
|
if not hits:
|
|
# fallback: no vptr-of-this module found anywhere -> try ctor as marker too
|
|
hits = scan_objects(last_host, [base + 0x529610], excl_start=base, excl_end=base + 0x7000000)
|
|
if hits:
|
|
print(f' (ctor-marker fallback hits={len(hits)})', flush=True)
|
|
for a in hits:
|
|
f = verify(last_host, a)
|
|
if f:
|
|
got = (a, f)
|
|
break
|
|
nscan += 1
|
|
if not got:
|
|
dt = time.time() - t1
|
|
print(f' scan#{nscan} t={dt:.2f}s no object', flush=True)
|
|
time.sleep(0.25)
|
|
else:
|
|
break
|
|
host = last_host
|
|
if not got:
|
|
print('NO VALID DSP OBJECT FOUND', flush=True)
|
|
proc.kill(); sys.exit(2)
|
|
|
|
addr, fields = got
|
|
tag = os.path.basename(f'obj_{addr:x}')
|
|
meta = {'base': addr, 'modbase': base, 'rpp': RPP, 'host': host, 'fields': fields}
|
|
open(f'{OD}/{tag}.json', 'w').write(json.dumps(meta, indent=2))
|
|
print(f'OBJECT 0x{addr:x} fields={fields}', flush=True)
|
|
|
|
dump_floats(host, addr + 0x28, 0x200, f'{tag}.twin_order_f', OD, fmt=lambda v: f'{v:.9g}')
|
|
dump_floats(host, addr + 0x40, 0x2ac, f'{tag}.twin_state_A', OD)
|
|
dump_floats(host, addr + 0x58, 0x2ac, f'{tag}.twin_state_B', OD)
|
|
dump_floats(host, addr + 0x540658, N_DUMP, f'{tag}.window_540658', OD, fmt=lambda v: f'{v:.9g}')
|
|
for off, nm in ((0x5406b8, 'w_b8'), (0x5406c8, 'w_c8'),
|
|
(0x5406d8, 'w_d8'), (0x5406e8, 'w_e8')):
|
|
dump_floats(host, addr + off, N_DUMP, f'{tag}.{nm}', OD, fmt=lambda v: f'{v:.9g}')
|
|
dump_floats(host, addr + 0x5406a8, 0x200, f'{tag}.warp_5406a8', OD, fmt=lambda v: f'{v:.9g}')
|
|
dump_floats(host, addr + 0x540698, 0x200, f'{tag}.freqaxis_540698', OD, fmt=lambda v: f'{v:.9g}')
|
|
dump_floats(host, addr + 0x5407c8, N_DUMP, f'{tag}.maskacc_5407c8', OD, fmt=lambda v: f'{v:.9g}')
|
|
|
|
# scalar fields
|
|
for off, nm in ((0x1a0, 'n1a0'), (0x1ac, 'n1ac'), (0x1a4, 'n1a4'),
|
|
(0x540868, 'n540868'), (0x54086c, 'n54086c'),
|
|
(0x540870, 's540870'), (0x540878, 's540878'), (0x54087c, 's54087c'),
|
|
(0x540880, 's540880'), (0x540884, 's540884'),
|
|
(0x540888, 's540888'), (0x54088c, 's54088c'),
|
|
(0x540890, 's540890'), (0x540894, 's540894')):
|
|
d = mem_read(host, addr + off, 4)
|
|
if d:
|
|
v = struct.unpack('<f', d)[0] if not nm.startswith('n') else struct.unpack('<I', d)[0]
|
|
open(f'{OD}/{tag}.{nm}.txt', 'w').write(f'{v!r}\n')
|
|
|
|
print('capture done', flush=True)
|
|
proc.wait(timeout=30)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main() |